Merge origin/master: web permission sandbox, default pi-ai providers

This commit is contained in:
Turtle
2026-07-29 14:29:32 +08:00
parent 42e3cceb64
commit e7c0a5b794
147 changed files with 6770 additions and 195 deletions

View File

@@ -39,15 +39,15 @@ Swappable LLM, bash, filesystem, and other capability providers remain in the le
| `toolTasks` | owner defaults | Background-task control-tool config, or `false` |
| `goals` | owner defaults | Persisted goal-domain and model-tool config; `false` removes the goal stack and `/goal` producer |
| `workspaceContext` | required | Workspace-instruction config, or `false` |
| `persistenceRoot` | `./.sessions` | JSONL persistence root and parent of the derived `session-query.db` index |
| `persistenceRoot` | `./.sessions` (launcher boot slot overrides) | JSONL persistence root and parent of the derived `session-query.db` index |
| `persistenceCompression` | `'zstd'` | JSONL artifact encoding (`'zstd'` or raw `'none'`) |
| `sessionReferences` | service defaults | Cross-session candidate and snapshot limits routed to `dsh-session-reference` |
| `welcome` | `ready.` | TUI subtitle |
| `resumeCommand` | — | Exit and no-host fallback command template; the selector itself uses session query and host handoff |
| `ui` | owner defaults | TUI presentation settings such as reasoning, color, and card height |
| `resumeSessionId` | — | Exact persisted session to resume |
Fresh runs mint a `main-session-<uuid>` session id and pass it to both the TUI and configured agent. Resumed runs bind both components to `resumeSessionId`. The TUI mounts before the spine so it can render a matching config-start failure instead of leaving a blank terminal. The app composes persistence and session query for `/resume`; an embedding host may additionally provide `tuiResumeHost` for in-place process handoff.
Session identity is launcher-owned rather than configurable: a launcher provides `MAIN_SESSION_ID_KEY` on the boot context, and this app binds both the TUI and the configured agent to that id, loading persisted history only when the launcher also set `resume`. With no such slot the app mints a `main-session-<uuid>` and creates it fresh. The TUI mounts before the spine so it can render a matching config-start failure instead of leaving a blank terminal. The app composes persistence and session query for `/resume`; a launcher may additionally provide `tuiResumeHost` for in-place process handoff and `TUI_GOODBYE_MESSAGE_KEY` for the line printed on exit.
`persistenceRoot` defaults to project-local `./.sessions`: an app bundle must not assume the user's shared session store. A launcher that wants one store across every cwd states that policy through the `SESSIONS_ROOT_KEY` boot slot (`ctx.provide` before any Loader entry mounts) — the dsh CLI provides its Harness-home root there, so its `/resume` lists sessions from every workspace. Precedence is explicit config, then the launcher slot, then the project-local default.
## Front door

View File

@@ -30,20 +30,20 @@
"@cordisjs/plugin-loader": "^1.0.0-rc.5",
"@deepseek-ai/dsh-agent": "^0.0.1",
"@deepseek-ai/dsh-agent-loop": "^0.0.1",
"@deepseek-ai/dsh-commands": "^0.0.1",
"@deepseek-ai/dsh-command-goal": "^0.0.1",
"@deepseek-ai/dsh-agent-spine-demo": "^0.0.1",
"@deepseek-ai/dsh-command-goal": "^0.0.1",
"@deepseek-ai/dsh-commands": "^0.0.1",
"@deepseek-ai/dsh-invariants": "^0.0.1",
"@deepseek-ai/dsh-llm": "^0.0.1",
"@deepseek-ai/dsh-session": "^0.0.1",
"@deepseek-ai/dsh-session-checkpoint-policy": "^0.0.1",
"@deepseek-ai/dsh-session-persistence-jsonl": "^0.0.1",
"@deepseek-ai/dsh-session-query": "^0.0.1",
"@deepseek-ai/dsh-session-query-sqlite": "^0.0.1",
"@deepseek-ai/dsh-session-reference": "^0.0.1",
"@deepseek-ai/dsh-session-persistence-jsonl": "^0.0.1",
"@deepseek-ai/dsh-tui": "^0.0.1",
"@deepseek-ai/dsh-tool-ask-user": "^0.0.1",
"@deepseek-ai/dsh-tools": "^0.0.1",
"@deepseek-ai/dsh-tui": "^0.0.1",
"@deepseek-ai/dsh-user-interaction": "^0.0.1",
"@deepseek-ai/dsh-workspace-context": "^0.0.1",
"cordis": "^4.0.0-rc.7",
@@ -53,21 +53,21 @@
"@cordisjs/plugin-loader": "workspace:^",
"@deepseek-ai/dsh-agent": "workspace:^",
"@deepseek-ai/dsh-agent-loop": "workspace:^",
"@deepseek-ai/dsh-commands": "workspace:^",
"@deepseek-ai/dsh-command-goal": "workspace:^",
"@deepseek-ai/dsh-agent-spine-demo": "workspace:^",
"@deepseek-ai/dsh-command-goal": "workspace:^",
"@deepseek-ai/dsh-commands": "workspace:^",
"@deepseek-ai/dsh-invariants": "workspace:^",
"@deepseek-ai/dsh-llm": "workspace:^",
"@deepseek-ai/dsh-session": "workspace:^",
"@deepseek-ai/dsh-session-checkpoint-policy": "workspace:^",
"@deepseek-ai/dsh-session-persistence-jsonl": "workspace:^",
"@deepseek-ai/dsh-session-query": "workspace:^",
"@deepseek-ai/dsh-session-query-sqlite": "workspace:^",
"@deepseek-ai/dsh-session-reference": "workspace:^",
"@deepseek-ai/dsh-session-persistence-jsonl": "workspace:^",
"@deepseek-ai/dsh-system-prompt": "workspace:^",
"@deepseek-ai/dsh-tui": "workspace:^",
"@deepseek-ai/dsh-tool-ask-user": "workspace:^",
"@deepseek-ai/dsh-tools": "workspace:^",
"@deepseek-ai/dsh-tui": "workspace:^",
"@deepseek-ai/dsh-user-interaction": "workspace:^",
"@deepseek-ai/dsh-workspace-context": "workspace:^",
"cordis": "^4.0.0-rc.7",

View File

@@ -30,6 +30,11 @@ import * as toolAskUser from '@deepseek-ai/dsh-tool-ask-user'
import * as uiTui from '@deepseek-ai/dsh-tui'
export const name = 'tui-demo'
// The bundle's own fallback stays project-local: a plugin must never assume
// the user's shared session store. The dsh launcher's SESSIONS_ROOT_KEY slot
// (opaque here — the CLI resolves it to DSH_HOME/sessions) carries any
// shared-store policy, and explicit config wins over both.
const DEFAULT_PERSISTENCE_ROOT = './.sessions'
// Each front door keeps a complete Loader contract so its deployment config is
@@ -53,7 +58,12 @@ export interface Config {
dshHome?: string
/** Fallback session-title limits forwarded through agent-spine-demo. */
sessionTitle?: NonNullable<agentCore.Config['sessionTitle']>
/** Directory for JSONL sessions and the derived query index. Defaults to `./.sessions`. */
/**
* Directory for JSONL sessions and the derived query index. Precedence:
* this explicit config, then the launcher's opaque `SESSIONS_ROOT_KEY` boot
* slot (the dsh CLI resolves it to `DSH_HOME/sessions`), then a project-local
* `./.sessions` fallback — the bundle itself never assumes a global store.
*/
persistenceRoot?: string
/** JSONL artifact encoding; defaults to checksummed Zstandard frames. */
persistenceCompression?: JsonlCompression
@@ -61,13 +71,6 @@ export interface Config {
sessionReferences?: SessionReferenceConfig
/** TUI transcript's optional first line; absent renders nothing on start. */
welcome?: string
/**
* Shell command template the TUI prints on exit and lists under `/resume`,
* with `{session}` replaced by the live session id (forwarded to the front
* door). Set it to a command that resumes the session, e.g.
* `dsh --resume {session}`.
*/
resumeCommand?: string
/** Full-screen TUI presentation settings. */
ui?: uiTui.TuiConfig
/** Skill registry, local-provider, and model-facing consumer config. */
@@ -78,8 +81,6 @@ export interface Config {
toolTasks?: NonNullable<agentCore.Config['toolTasks']>
/** Persisted same-session goals; owner defaults enable them, or false disables the stack and command. */
goals?: agentCore.GoalConfig | false
/** Persisted session id to resume instead of creating a fresh session. */
resumeSessionId?: string
/** Controls automatic AGENTS.md/CLAUDE.md loading; configure a byte budget or set `false`. */
workspaceContext: agentCore.Config['workspaceContext']
}
@@ -94,33 +95,38 @@ export const Config: z<Config> = z.object({
tools: ToolRegistry.Config,
dshHome: z.string(),
sessionTitle: agentCore.SessionTitleConfigSchema,
persistenceRoot: z.string().default(DEFAULT_PERSISTENCE_ROOT),
// No schema default: schemastery would materialize it before composeTuiApp
// runs, shadowing the launcher's SESSIONS_ROOT_KEY slot for a Loader mount.
persistenceRoot: z.string(),
persistenceCompression: JsonlCompressionSchema,
sessionReferences: SessionReferenceService.Config,
welcome: z.string(),
resumeCommand: z.string(),
ui: uiTui.TuiConfigSchema,
skills: agentCore.SkillConfigSchema,
toolBash: agentCore.ToolBashConfigSchema,
toolTasks: z.union([z.const(false), agentCore.ToolTasksConfigSchema]),
goals: z.union([z.const(false), agentCore.GoalConfigSchema]),
resumeSessionId: z.string(),
workspaceContext: z.union([z.const(false), workspaceContext.Config]).required(),
})
/* jscpd:ignore-end */
/**
* Compose the spine, TUI, JSONL persistence, and user-question tool around one
* exact fresh or resumed session identity. The TUI subscribes to startup
* failures before the spine creates the agent.
* exact fresh or resumed session identity, taken from the launcher's
* {@link uiTui.MAIN_SESSION_ID_KEY} slot. The TUI subscribes to startup failures
* before the spine creates the agent.
* @param ctx - context receiving the app's child plugins.
* @param config - validated app configuration.
*/
export function composeTuiApp(ctx: Context, config: Config): void {
const resumeSessionId = config.resumeSessionId === '' ? undefined : config.resumeSessionId
const sessionId = SessionId(resumeSessionId ?? `main-session-${randomUUID()}`)
// The launcher, not the deployment config, owns `main`'s session identity: it
// reaches a Loader-mounted bundle only through this context slot. A launcher
// that supplies an id knows whether that session already exists, so it also
// states whether to load persisted history. No launcher means mint one here.
const identity = ctx.get(uiTui.MAIN_SESSION_ID_KEY)
const sessionId = SessionId(identity?.id ?? `main-session-${randomUUID()}`)
const goals = config.goals ?? {}
const persistenceRoot = config.persistenceRoot ?? DEFAULT_PERSISTENCE_ROOT
const persistenceRoot = config.persistenceRoot ?? ctx.get(uiTui.SESSIONS_ROOT_KEY) ?? DEFAULT_PERSISTENCE_ROOT
ctx.plugin(CommandService)
if (goals !== false) ctx.plugin(commandGoal)
ctx.plugin(SessionPersistenceJsonl, {
@@ -135,7 +141,6 @@ export function composeTuiApp(ctx: Context, config: Config): void {
ctx.plugin(uiTui, {
...config.ui,
...config.welcome === undefined ? {} : { welcome: config.welcome },
...config.resumeCommand === undefined ? {} : { resumeCommand: config.resumeCommand },
sessionId,
})
ctx.plugin(agentCore, {
@@ -146,7 +151,9 @@ export function composeTuiApp(ctx: Context, config: Config): void {
provider: config.provider,
model: config.model,
cwd: process.cwd(),
...resumeSessionId === undefined ? { sessionId } : { resumeSessionId: sessionId },
// `resumeSessionId` requires existing persisted history and rejects a
// missing log, so only a launcher that asked to resume takes that path.
...identity?.resume === true ? { resumeSessionId: sessionId } : { sessionId },
}],
})
ctx.plugin(toolAskUser)

View File

@@ -3,6 +3,8 @@ import { join } from 'node:path'
import { Context } from 'cordis'
import Loader from '@cordisjs/plugin-loader'
import { TOOL_ORDER_REST } from '@deepseek-ai/dsh-system-prompt'
import { SessionId } from '@deepseek-ai/dsh-session'
import { MAIN_SESSION_ID_KEY, SESSIONS_ROOT_KEY, type MainSessionIdentity } from '@deepseek-ai/dsh-tui'
import * as tuiAgent from '../src/index.ts'
interface PluginCall {
@@ -10,12 +12,21 @@ interface PluginCall {
readonly config: unknown
}
function recordingContext(): { readonly ctx: Context; readonly calls: PluginCall[] } {
/**
* Record the composed plugin tree. `identity` stands in for the launcher-owned
* {@link MAIN_SESSION_ID_KEY} slot; omitting it means no launcher chose a session.
*/
function recordingContext(
identity?: MainSessionIdentity,
sessionsRoot?: string,
): { readonly ctx: Context; readonly calls: PluginCall[] } {
const calls: PluginCall[] = []
const ctx = {
plugin(plugin: { name?: string }, config?: unknown) {
calls.push({ name: plugin.name ?? '', config })
},
get: (key: string) => key === MAIN_SESSION_ID_KEY ? identity
: key === SESSIONS_ROOT_KEY ? sessionsRoot : undefined,
} as unknown as Context
return { ctx, calls }
}
@@ -39,7 +50,6 @@ describe('dsh-tui-demo app', () => {
maxReferenceBytes: 1234,
},
welcome: 'TUI ready',
resumeCommand: 'dsh --resume {session}',
ui: { theme: { color: false }, maxToolOutputLines: 3 },
skills: { tool: { catalogDescriptionMaxLength: 8 } },
toolBash: { enableRunInBackground: false },
@@ -71,7 +81,6 @@ describe('dsh-tui-demo app', () => {
const tuiConfig = calls[8]?.config as { sessionId: string }
expect(tuiConfig).toMatchObject({
welcome: 'TUI ready',
resumeCommand: 'dsh --resume {session}',
theme: { color: false },
maxToolOutputLines: 3,
})
@@ -100,16 +109,46 @@ describe('dsh-tui-demo app', () => {
})
})
it('resumes the configured session and applies runtime defaults', () => {
const { ctx, calls } = recordingContext()
it('uses the launcher sessions-root slot through schema-normalized config', () => {
// The Loader normalizes config through the schemastery Config BEFORE apply
// runs. A schema .default() on persistenceRoot would materialize here and
// permanently shadow the launcher slot — the regression this test pins.
const normalized = tuiAgent.Config({
provider: 'mock',
model: 'mock-model',
workspaceContext: false,
} as never)
expect(normalized.persistenceRoot).toBeUndefined()
const { ctx, calls } = recordingContext(undefined, '/launcher/sessions')
tuiAgent.composeTuiApp(ctx, normalized)
expect(calls[2]?.config).toMatchObject({ root: '/launcher/sessions' })
expect(calls[4]?.config).toEqual({ path: join('/launcher/sessions', 'session-query.db') })
})
it('lets an explicit persistenceRoot win over the launcher slot', () => {
const { ctx, calls } = recordingContext(undefined, '/launcher/sessions')
tuiAgent.composeTuiApp(ctx, {
provider: 'mock',
model: 'mock-model',
persistenceRoot: '/explicit/root',
workspaceContext: false,
})
expect(calls[2]?.config).toEqual({ root: '/explicit/root' })
})
it('loads persisted history for a launcher-selected resume identity', () => {
// The bundle default stays project-local: shared-store policy is the
// launcher's, which patches `persistenceRoot` itself (the dsh CLI does).
const { ctx, calls } = recordingContext({ id: SessionId('persisted-session'), resume: true })
tuiAgent.composeTuiApp(ctx, {
provider: 'mock',
model: 'mock-model',
resumeSessionId: 'persisted-session',
workspaceContext: false,
})
expect(calls[2]?.config).toEqual({ root: './.sessions' })
expect(calls[4]?.config).toEqual({ path: join('./.sessions', 'session-query.db') })
expect(calls[5]?.config).toEqual({})
// No configured welcome forwards none: the TUI banner sweeps in without a subtitle.
expect(calls[8]?.config).toEqual({ sessionId: 'persisted-session' })
@@ -119,12 +158,24 @@ describe('dsh-tui-demo app', () => {
})
})
it('normalizes an empty resume id and routes apply through the same composition', () => {
it('creates a launcher-minted identity fresh rather than loading history', () => {
const { ctx, calls } = recordingContext({ id: SessionId('minted-session'), resume: false })
tuiAgent.composeTuiApp(ctx, {
provider: 'mock',
model: 'mock-model',
workspaceContext: false,
})
expect(calls[8]?.config).toEqual({ sessionId: 'minted-session' })
expect((calls[9]?.config as { agents: Array<Record<string, unknown>> }).agents[0])
.toMatchObject({ id: 'main', sessionId: 'minted-session' })
})
it('mints a fresh session with no launcher slot and routes apply through the same composition', () => {
const { ctx, calls } = recordingContext()
tuiAgent.apply(ctx, {
provider: 'mock',
model: 'mock-model',
resumeSessionId: '',
goals: false,
workspaceContext: false,
})

View File

@@ -20,6 +20,9 @@
{
"path": "../../core/session"
},
{
"path": "../../util/paths"
},
{
"path": "../../session-query/session-query"
},