1
0
Эх сурвалжийг харах

fix(cli): restore shipped surface capabilities

Turtle 2 сар өмнө
parent
commit
857a4941be

+ 15 - 4
apps/cli/base.cordis.yml

@@ -8,8 +8,8 @@
 # A patch replaces the targeted row's whole `config` rather than merging into
 # A patch replaces the targeted row's whole `config` rather than merging into
 # it, so a row whose value differs per surface does NOT live here: it belongs to
 # it, so a row whose value differs per surface does NOT live here: it belongs to
 # each overlay, keeping any single row down to one overlay layer plus the user's.
 # each overlay, keeping any single row down to one overlay layer plus the user's.
-# That is why `agent-loop`, `system-prompt`, `tools`, `fs-local`, and
-# `llm-deepseek` thinking defaults are absent below.
+# Rows with surface-specific values appear below only with shared plugin identity
+# and neutral defaults; each overlay restates the complete surface configuration.
 #
 #
 # Row order carries no load semantics (activation is service-availability
 # Row order carries no load semantics (activation is service-availability
 # driven); the grouping is for readers.
 # driven); the grouping is for readers.
@@ -51,9 +51,20 @@
 - id: llm-retry
 - id: llm-retry
   name: '@deepseek-ai/dsh-llm-retry'
   name: '@deepseek-ai/dsh-llm-retry'
 
 
+- id: llm-pi-ai
+  name: '@deepseek-ai/dsh-llm-pi-ai'
+  config:
+    providers:
+      - provider: openai
+        apiKey: !!js process.env.OPENAI_API_KEY
+        baseURL: !!js process.env.OPENAI_BASE_URL
+      - provider: anthropic
+        apiKey: !!js process.env.ANTHROPIC_API_KEY
+        baseURL: !!js process.env.ANTHROPIC_BASE_URL
+
 # The session store root is the launcher's policy, not a plugin's: `dsh` shares
 # The session store root is the launcher's policy, not a plugin's: `dsh` shares
-# one store under the Harness home across every cwd, so `/resume` and
-# `/resume` spans workspaces. Without a launcher the project-local fallback keeps
+# one store under the Harness home across every cwd, so `/resume` spans
+# workspaces. Without a launcher the project-local fallback keeps
 # an embedder's sessions beside its project.
 # an embedder's sessions beside its project.
 - id: session-persistence-jsonl
 - id: session-persistence-jsonl
   name: '@deepseek-ai/dsh-session-persistence-jsonl'
   name: '@deepseek-ai/dsh-session-persistence-jsonl'

+ 9 - 3
apps/cli/composition.md

@@ -7,7 +7,7 @@ The TUI agent combines the real DeepSeek adapter, coding tools, compaction, suba
 
 
 ```mermaid
 ```mermaid
 flowchart LR
 flowchart LR
-  cfg["apps/cli (dsh)<br/>cordis.yml"]
+  cfg["apps/cli (dsh)<br/>base.cordis.yml + tui.cordis.yml"]
   plugin_tui_timer["timer<br/>@cordisjs/plugin-timer"]
   plugin_tui_timer["timer<br/>@cordisjs/plugin-timer"]
   cfg --> plugin_tui_timer
   cfg --> plugin_tui_timer
   plugin_tui_llm["llm<br/>@deepseek-ai/dsh-llm"]
   plugin_tui_llm["llm<br/>@deepseek-ai/dsh-llm"]
@@ -26,6 +26,8 @@ flowchart LR
   cfg --> plugin_tui_tasks
   cfg --> plugin_tui_tasks
   plugin_tui_llm_retry["llm-retry<br/>@deepseek-ai/dsh-llm-retry"]
   plugin_tui_llm_retry["llm-retry<br/>@deepseek-ai/dsh-llm-retry"]
   cfg --> plugin_tui_llm_retry
   cfg --> plugin_tui_llm_retry
+  plugin_tui_llm_pi_ai["llm-pi-ai<br/>@deepseek-ai/dsh-llm-pi-ai"]
+  cfg --> plugin_tui_llm_pi_ai
   plugin_tui_session_persistence_jsonl["session-persistence-jsonl<br/>@deepseek-ai/dsh-session-persistence-jsonl"]
   plugin_tui_session_persistence_jsonl["session-persistence-jsonl<br/>@deepseek-ai/dsh-session-persistence-jsonl"]
   cfg --> plugin_tui_session_persistence_jsonl
   cfg --> plugin_tui_session_persistence_jsonl
   plugin_tui_subprocess["subprocess<br/>@deepseek-ai/dsh-subprocess-local"]
   plugin_tui_subprocess["subprocess<br/>@deepseek-ai/dsh-subprocess-local"]
@@ -94,6 +96,8 @@ flowchart LR
   cfg --> plugin_tui_fs_local
   cfg --> plugin_tui_fs_local
   plugin_tui_llm_deepseek["llm-deepseek<br/>@deepseek-ai/dsh-llm-deepseek"]
   plugin_tui_llm_deepseek["llm-deepseek<br/>@deepseek-ai/dsh-llm-deepseek"]
   cfg --> plugin_tui_llm_deepseek
   cfg --> plugin_tui_llm_deepseek
+  plugin_tui_tools["tools<br/>@deepseek-ai/dsh-tool-ask-user"]
+  cfg --> plugin_tui_tools
 ```
 ```
 
 
 | Plugin id | Package / module |
 | Plugin id | Package / module |
@@ -107,6 +111,7 @@ flowchart LR
 | `agent` | `@deepseek-ai/dsh-agent` |
 | `agent` | `@deepseek-ai/dsh-agent` |
 | `tasks` | `@deepseek-ai/dsh-tasks-local` |
 | `tasks` | `@deepseek-ai/dsh-tasks-local` |
 | `llm-retry` | `@deepseek-ai/dsh-llm-retry` |
 | `llm-retry` | `@deepseek-ai/dsh-llm-retry` |
+| `llm-pi-ai` | `@deepseek-ai/dsh-llm-pi-ai` |
 | `session-persistence-jsonl` | `@deepseek-ai/dsh-session-persistence-jsonl` |
 | `session-persistence-jsonl` | `@deepseek-ai/dsh-session-persistence-jsonl` |
 | `subprocess` | `@deepseek-ai/dsh-subprocess-local` |
 | `subprocess` | `@deepseek-ai/dsh-subprocess-local` |
 | `bash-local` | `@deepseek-ai/dsh-bash-local` |
 | `bash-local` | `@deepseek-ai/dsh-bash-local` |
@@ -141,7 +146,8 @@ flowchart LR
 | `agent-loop` | `@deepseek-ai/dsh-agent-loop` |
 | `agent-loop` | `@deepseek-ai/dsh-agent-loop` |
 | `fs-local` | `@deepseek-ai/dsh-fs-local` |
 | `fs-local` | `@deepseek-ai/dsh-fs-local` |
 | `llm-deepseek` | `@deepseek-ai/dsh-llm-deepseek` |
 | `llm-deepseek` | `@deepseek-ai/dsh-llm-deepseek` |
+| `tools` | `@deepseek-ai/dsh-tool-ask-user` |
 
 
-Source config: [`apps/cli/base.cordis.yml`](base.cordis.yml).
+Source configs: [`apps/cli/base.cordis.yml`](base.cordis.yml), [`apps/cli/tui.cordis.yml`](tui.cordis.yml).
 
 
-Maintenance mode: hybrid: the leaf plugin list is parsed from its `cordis.yml`; app package expansion is curated from package source.
+Maintenance mode: hybrid: the leaf plugin list is parsed from its shipped config files; app package expansion is curated from package source.

+ 8 - 1
apps/cli/package.json

@@ -22,6 +22,7 @@
     "@deepseek-ai/dsh-agent-loop": "workspace:^",
     "@deepseek-ai/dsh-agent-loop": "workspace:^",
     "@deepseek-ai/dsh-app-boot": "workspace:^",
     "@deepseek-ai/dsh-app-boot": "workspace:^",
     "@deepseek-ai/dsh-bash-local": "workspace:^",
     "@deepseek-ai/dsh-bash-local": "workspace:^",
+    "@deepseek-ai/dsh-bash-sandbox": "workspace:^",
     "@deepseek-ai/dsh-client-connection": "workspace:^",
     "@deepseek-ai/dsh-client-connection": "workspace:^",
     "@deepseek-ai/dsh-client-hmr": "workspace:^",
     "@deepseek-ai/dsh-client-hmr": "workspace:^",
     "@deepseek-ai/dsh-client-locale": "workspace:^",
     "@deepseek-ai/dsh-client-locale": "workspace:^",
@@ -33,6 +34,7 @@
     "@deepseek-ai/dsh-client-ui-layout": "workspace:^",
     "@deepseek-ai/dsh-client-ui-layout": "workspace:^",
     "@deepseek-ai/dsh-client-ui-model": "workspace:^",
     "@deepseek-ai/dsh-client-ui-model": "workspace:^",
     "@deepseek-ai/dsh-client-ui-models": "workspace:^",
     "@deepseek-ai/dsh-client-ui-models": "workspace:^",
+    "@deepseek-ai/dsh-client-ui-permission": "workspace:^",
     "@deepseek-ai/dsh-client-ui-plan": "workspace:^",
     "@deepseek-ai/dsh-client-ui-plan": "workspace:^",
     "@deepseek-ai/dsh-client-ui-question": "workspace:^",
     "@deepseek-ai/dsh-client-ui-question": "workspace:^",
     "@deepseek-ai/dsh-client-ui-settings": "workspace:^",
     "@deepseek-ai/dsh-client-ui-settings": "workspace:^",
@@ -52,10 +54,11 @@
     "@deepseek-ai/dsh-frontend": "workspace:^",
     "@deepseek-ai/dsh-frontend": "workspace:^",
     "@deepseek-ai/dsh-fs-local": "workspace:^",
     "@deepseek-ai/dsh-fs-local": "workspace:^",
     "@deepseek-ai/dsh-fs-policy": "workspace:^",
     "@deepseek-ai/dsh-fs-policy": "workspace:^",
+    "@deepseek-ai/dsh-fs-sandbox": "workspace:^",
     "@deepseek-ai/dsh-goal": "workspace:^",
     "@deepseek-ai/dsh-goal": "workspace:^",
     "@deepseek-ai/dsh-goal-session": "workspace:^",
     "@deepseek-ai/dsh-goal-session": "workspace:^",
-    "@deepseek-ai/dsh-helper": "workspace:^",
     "@deepseek-ai/dsh-host-apiproxy": "workspace:^",
     "@deepseek-ai/dsh-host-apiproxy": "workspace:^",
+    "@deepseek-ai/dsh-host-directory-picker-browse": "workspace:^",
     "@deepseek-ai/dsh-host-webserver": "workspace:^",
     "@deepseek-ai/dsh-host-webserver": "workspace:^",
     "@deepseek-ai/dsh-invariants": "workspace:^",
     "@deepseek-ai/dsh-invariants": "workspace:^",
     "@deepseek-ai/dsh-llm": "workspace:^",
     "@deepseek-ai/dsh-llm": "workspace:^",
@@ -63,7 +66,10 @@
     "@deepseek-ai/dsh-llm-pi-ai": "workspace:^",
     "@deepseek-ai/dsh-llm-pi-ai": "workspace:^",
     "@deepseek-ai/dsh-llm-retry": "workspace:^",
     "@deepseek-ai/dsh-llm-retry": "workspace:^",
     "@deepseek-ai/dsh-paths": "workspace:^",
     "@deepseek-ai/dsh-paths": "workspace:^",
+    "@deepseek-ai/dsh-permission": "workspace:^",
     "@deepseek-ai/dsh-plan-mode": "workspace:^",
     "@deepseek-ai/dsh-plan-mode": "workspace:^",
+    "@deepseek-ai/dsh-sandbox-local": "workspace:^",
+    "@deepseek-ai/dsh-sandbox-policy": "workspace:^",
     "@deepseek-ai/dsh-scope": "workspace:^",
     "@deepseek-ai/dsh-scope": "workspace:^",
     "@deepseek-ai/dsh-session": "workspace:^",
     "@deepseek-ai/dsh-session": "workspace:^",
     "@deepseek-ai/dsh-session-checkpoint-policy": "workspace:^",
     "@deepseek-ai/dsh-session-checkpoint-policy": "workspace:^",
@@ -102,6 +108,7 @@
     "@deepseek-ai/dsh-tool-workflow": "workspace:^",
     "@deepseek-ai/dsh-tool-workflow": "workspace:^",
     "@deepseek-ai/dsh-tools": "workspace:^",
     "@deepseek-ai/dsh-tools": "workspace:^",
     "@deepseek-ai/dsh-tui": "workspace:^",
     "@deepseek-ai/dsh-tui": "workspace:^",
+    "@deepseek-ai/dsh-user-approval": "workspace:^",
     "@deepseek-ai/dsh-user-interaction": "workspace:^",
     "@deepseek-ai/dsh-user-interaction": "workspace:^",
     "@deepseek-ai/dsh-workflow-workerthread": "workspace:^",
     "@deepseek-ai/dsh-workflow-workerthread": "workspace:^",
     "@deepseek-ai/dsh-workspace": "workspace:^",
     "@deepseek-ai/dsh-workspace": "workspace:^",

+ 2 - 17
apps/cli/src/args.ts

@@ -213,27 +213,12 @@ Examples:
       resolved = resolveWeb(options)
       resolved = resolveWeb(options)
     })
     })
 
 
-  // `--resume` is NOT redeclared here: an option a subcommand shares with its
-  // parent parses into `program.opts()` and leaves the subcommand's own options
-  // empty, so redeclaring it would silently drop the id. Commander therefore
-  // omits it from this subcommand's option list, hence the trailing help text.
   program
   program
     .command('meta')
     .command('meta')
     .description('work on the dsh source that runs this command, from any directory')
     .description('work on the dsh source that runs this command, from any directory')
-    .addHelpText('after', '\nAccepts --resume <id> to resume a persisted session from this checkout.\n')
     .action(() => {
     .action(() => {
-      // Commander parses the parent (default-surface) options on either side of
-      // the subcommand into `program.opts()`. `meta` accepts only `--resume`, so
-      // a leaked `--config`/`-p` is a mistyped invocation that must fail loud
-      // rather than silently be dropped.
-      const parent = program.opts<{ config?: string; configReplace?: string; prompt?: string; resume?: string }>()
-      if (parent.config !== undefined || parent.configReplace !== undefined || parent.prompt !== undefined) {
-        program.error('error: meta takes none of --config, --config-replace, or -p/--prompt')
-      }
-      // Same reason as the default surface: an empty id would start a fresh
-      // session downstream instead of failing the mistyped resume.
-      if (parent.resume === '') program.error('error: --resume needs a session id')
-      resolved = { mode: 'meta', ...parent.resume !== undefined && { resume: parent.resume } }
+      rejectParentOptions('meta')
+      resolved = { mode: 'meta' }
     })
     })
 
 
   try {
   try {

+ 7 - 5
apps/cli/src/tui.ts

@@ -18,7 +18,7 @@
  */
  */
 
 
 import { randomUUID } from 'node:crypto'
 import { randomUUID } from 'node:crypto'
-import { join } from 'node:path'
+import { join, resolve } from 'node:path'
 import { fileURLToPath } from 'node:url'
 import { fileURLToPath } from 'node:url'
 import {
 import {
   addHarnessSourceSection,
   addHarnessSourceSection,
@@ -155,6 +155,8 @@ export async function runTui(
   // selected session may belong to another workspace, so the handoff also enters
   // selected session may belong to another workspace, so the handoff also enters
   // that directory. The host is offered only when Node exposes `process.execve`
   // that directory. The host is offered only when Node exposes `process.execve`
   // and knows its own entry.
   // and knows its own entry.
+  const resolvedConfig = config === undefined ? undefined : resolve(config)
+  const resolvedConfigReplace = configReplace === undefined ? undefined : resolve(configReplace)
   const entry = process.argv[1]
   const entry = process.argv[1]
   const execve = process.execve?.bind(process)
   const execve = process.execve?.bind(process)
   const app: { current?: Context } = {}
   const app: { current?: Context } = {}
@@ -172,8 +174,8 @@ export async function runTui(
         `--resume=${sessionId}`,
         `--resume=${sessionId}`,
         // Both config flags must survive the handoff: resuming into a different
         // Both config flags must survive the handoff: resuming into a different
         // tree than the session was created in would silently change the agent.
         // tree than the session was created in would silently change the agent.
-        ...config !== undefined ? ['--config', config] : [],
-        ...configReplace !== undefined ? ['--config-replace', configReplace] : [],
+        ...resolvedConfig !== undefined ? ['--config', resolvedConfig] : [],
+        ...resolvedConfigReplace !== undefined ? ['--config-replace', resolvedConfigReplace] : [],
       ]
       ]
   // Mint the fresh id here rather than in the app bundle: the exit line names
   // Mint the fresh id here rather than in the app bundle: the exit line names
   // the session to resume, so the launcher must know it before the tree boots.
   // the session to resume, so the launcher must know it before the tree boots.
@@ -223,11 +225,11 @@ export async function runTui(
     ...loadOverlayPatches(NAME, TUI_OVERLAY),
     ...loadOverlayPatches(NAME, TUI_OVERLAY),
     ...config === undefined
     ...config === undefined
       ? loadPersonalPatches(NAME) ?? []
       ? loadPersonalPatches(NAME) ?? []
-      : loadOverlayPatches(NAME, resolveConfigPath(config, undefined)),
+      : loadOverlayPatches(NAME, resolveConfigPath(resolve(config), undefined)),
   ]
   ]
   const ctx = await boot(
   const ctx = await boot(
     NAME,
     NAME,
-    replaceTree ? resolveConfigPath(configReplace, undefined) : BASE_CONFIG,
+    replaceTree ? resolveConfigPath(resolve(configReplace), undefined) : BASE_CONFIG,
     patches,
     patches,
     (hostCtx) => {
     (hostCtx) => {
       // The launcher owns session identity and the exit line: a config-mounted
       // The launcher owns session identity and the exit line: a config-mounted

+ 3 - 8
apps/cli/tests/args.spec.ts

@@ -30,12 +30,7 @@ describe('parseDshArgs', () => {
     expect(parse(['--config-replace', 'tree.yml'])).toEqual({ mode: 'tui', configReplace: 'tree.yml' })
     expect(parse(['--config-replace', 'tree.yml'])).toEqual({ mode: 'tui', configReplace: 'tree.yml' })
     expect(parse(['--resume', 'sess', '--config', 'app.yml'])).toEqual({ mode: 'tui', config: 'app.yml', resume: 'sess' })
     expect(parse(['--resume', 'sess', '--config', 'app.yml'])).toEqual({ mode: 'tui', config: 'app.yml', resume: 'sess' })
     expect(parse(['-p', 'do the thing'])).toEqual({ mode: 'headless', prompt: 'do the thing' })
     expect(parse(['-p', 'do the thing'])).toEqual({ mode: 'headless', prompt: 'do the thing' })
-    // `meta` accepts `--resume` but does not redeclare it: a shared option parses
-    // into program.opts() on either side of the subcommand, and redeclaring it
-    // would leave the subcommand's own options empty and drop the id.
     expect(parse(['meta'])).toEqual({ mode: 'meta' })
     expect(parse(['meta'])).toEqual({ mode: 'meta' })
-    expect(parse(['meta', '--resume', 'sess'])).toEqual({ mode: 'meta', resume: 'sess' })
-    expect(parse(['--resume', 'sess', 'meta'])).toEqual({ mode: 'meta', resume: 'sess' })
     // Credential setup is option-free: it writes the Harness-home .env, so
     // Credential setup is option-free: it writes the Harness-home .env, so
     // there is nothing for a flag to select.
     // there is nothing for a flag to select.
     // Bare `web` carries no host/port: the shipped cordis.yml owns the default.
     // Bare `web` carries no host/port: the shipped cordis.yml owns the default.
@@ -72,9 +67,9 @@ describe('parseDshArgs', () => {
     expect(exitCode(['--config-replace', 'tree.yml', 'web'])).toBe(1)
     expect(exitCode(['--config-replace', 'tree.yml', 'web'])).toBe(1)
     // Same rule for credential setup: it shares no option with the default
     // Same rule for credential setup: it shares no option with the default
     // surface, so a leaked flag is a typo, not something to ignore.
     // surface, so a leaked flag is a typo, not something to ignore.
-    // `meta` fixes its own config tree and is interactive, so --config/-p are
-    // rejected; an empty id is swallowed downstream exactly as above.
-    expect(exitCode(['meta', '--resume='])).toBe(1)
+    // `meta` fixes its own config tree and always starts fresh, so every
+    // default-surface option is rejected.
+    expect(exitCode(['meta', '--resume', 's'])).toBe(1)
     expect(exitCode(['meta', '--config', 'c.yml'])).toBe(1)
     expect(exitCode(['meta', '--config', 'c.yml'])).toBe(1)
     expect(exitCode(['meta', '--config-replace', 'tree.yml'])).toBe(1)
     expect(exitCode(['meta', '--config-replace', 'tree.yml'])).toBe(1)
     expect(exitCode(['meta', '-p', 'task'])).toBe(1)
     expect(exitCode(['meta', '-p', 'task'])).toBe(1)

+ 2 - 3
apps/cli/tui.cordis.yml

@@ -94,9 +94,8 @@
     - id: session-reference
     - id: session-reference
       name: '@deepseek-ai/dsh-session-reference'
       name: '@deepseek-ai/dsh-session-reference'
 
 
-    # Refuses write/edit inside the dsh checkout this launcher runs from, on that
-    # checkout's own branch, until the session loads dsh-customize. Inert
-    # everywhere else, so an ordinary project sees no change.
+    # Compacts oversized tool results before the broader conversation compactor
+    # runs, preserving the model-visible result within the configured budget.
     - id: tool-result-prune
     - id: tool-result-prune
       name: '@deepseek-ai/dsh-compact-tool-result-prune'
       name: '@deepseek-ai/dsh-compact-tool-result-prune'
 
 

+ 48 - 0
apps/cli/web.cordis.yml

@@ -31,7 +31,49 @@
     apiKey: !!js process.env.DEEPSEEK_API_KEY
     apiKey: !!js process.env.DEEPSEEK_API_KEY
     baseURL: !!js process.env.DEEPSEEK_BASE_URL
     baseURL: !!js process.env.DEEPSEEK_BASE_URL
 
 
+# The web surface replaces the unrestricted local executors with the shared
+# sandbox policy. Its default preserves the previous unrestricted behavior;
+# DSH_PERMISSION_MODE and the browser permission picker can confine a session.
+- insert:
+    - id: sandbox
+      name: '@deepseek-ai/dsh-sandbox-local'
+
+    - id: sandbox-policy
+      name: '@deepseek-ai/dsh-sandbox-policy'
+      config:
+        mode: !!js process.env.DSH_PERMISSION_MODE ?? 'danger-full-access'
+        workspaceRoot: !!js process.cwd()
+
+    - id: bash-sandbox
+      name: '@deepseek-ai/dsh-bash-sandbox'
+
+    - id: approval
+      name: '@deepseek-ai/dsh-user-approval'
+      config:
+        policy: !!js "(process.env.DSH_PERMISSION_MODE ?? 'danger-full-access') === 'danger-full-access' ? 'never' : 'ask'"
+
+    - id: permission
+      name: '@deepseek-ai/dsh-permission'
+      config:
+        presets:
+          read-only:
+            sandbox: read-only
+            approval: ask
+          workspace-write:
+            sandbox: workspace-write
+            approval: ask
+          danger-full-access:
+            sandbox: danger-full-access
+            approval: never
+
+    - id: fs-sandbox
+      name: '@deepseek-ai/dsh-fs-sandbox'
+
+- id: bash-local
+  remove: true
+
 - id: fs-local
 - id: fs-local
+  remove: true
 
 
 # ── web-only host rows, the transport layer, and the browser roster ─────────
 # ── web-only host rows, the transport layer, and the browser roster ─────────
 
 
@@ -72,6 +114,9 @@
     # The API gateway: the transport-agnostic dispatch face every client shape
     # The API gateway: the transport-agnostic dispatch face every client shape
     # shares. provider/model are the host default routing — the profile json's
     # shares. provider/model are the host default routing — the profile json's
     # mapping target (user config overrides these engineering defaults).
     # mapping target (user config overrides these engineering defaults).
+    - id: directory-picker
+      name: '@deepseek-ai/dsh-host-directory-picker-browse'
+
     - id: api-gateway
     - id: api-gateway
       name: '@deepseek-ai/dsh-host-apiproxy'
       name: '@deepseek-ai/dsh-host-apiproxy'
       config:
       config:
@@ -156,6 +201,9 @@
     - id: ui-model
     - id: ui-model
       name: '@deepseek-ai/dsh-client-ui-model'
       name: '@deepseek-ai/dsh-client-ui-model'
 
 
+    - id: ui-permission
+      name: '@deepseek-ai/dsh-client-ui-permission'
+
     # Plan control: the composer plan seat over the plan projection + /plan channel.
     # Plan control: the composer plan seat over the plan projection + /plan channel.
     - id: ui-plan
     - id: ui-plan
       name: '@deepseek-ai/dsh-client-ui-plan'
       name: '@deepseek-ai/dsh-client-ui-plan'

+ 1 - 1
examples/acp-agent/composition.md

@@ -94,4 +94,4 @@ flowchart LR
 
 
 Source config: [`examples/acp-agent/cordis.yml`](cordis.yml).
 Source config: [`examples/acp-agent/cordis.yml`](cordis.yml).
 
 
-Maintenance mode: hybrid: the leaf plugin list is parsed from its `cordis.yml`; app package expansion is curated from package source.
+Maintenance mode: hybrid: the leaf plugin list is parsed from its shipped config files; app package expansion is curated from package source.

+ 1 - 1
examples/headless-agent/composition.md

@@ -76,4 +76,4 @@ flowchart LR
 
 
 Source config: [`examples/headless-agent/cordis.yml`](cordis.yml).
 Source config: [`examples/headless-agent/cordis.yml`](cordis.yml).
 
 
-Maintenance mode: hybrid: the leaf plugin list is parsed from its `cordis.yml`; app package expansion is curated from package source.
+Maintenance mode: hybrid: the leaf plugin list is parsed from its shipped config files; app package expansion is curated from package source.

+ 24 - 3
pnpm-lock.yaml

@@ -134,6 +134,9 @@ importers:
       '@deepseek-ai/dsh-bash-local':
       '@deepseek-ai/dsh-bash-local':
         specifier: workspace:^
         specifier: workspace:^
         version: link:../../packages/bash/bash-local
         version: link:../../packages/bash/bash-local
+      '@deepseek-ai/dsh-bash-sandbox':
+        specifier: workspace:^
+        version: link:../../packages/bash/bash-sandbox
       '@deepseek-ai/dsh-client-connection':
       '@deepseek-ai/dsh-client-connection':
         specifier: workspace:^
         specifier: workspace:^
         version: link:../../packages/client/connection
         version: link:../../packages/client/connection
@@ -167,6 +170,9 @@ importers:
       '@deepseek-ai/dsh-client-ui-models':
       '@deepseek-ai/dsh-client-ui-models':
         specifier: workspace:^
         specifier: workspace:^
         version: link:../../packages/client/ui-models
         version: link:../../packages/client/ui-models
+      '@deepseek-ai/dsh-client-ui-permission':
+        specifier: workspace:^
+        version: link:../../packages/client/ui-permission
       '@deepseek-ai/dsh-client-ui-plan':
       '@deepseek-ai/dsh-client-ui-plan':
         specifier: workspace:^
         specifier: workspace:^
         version: link:../../packages/client/ui-plan
         version: link:../../packages/client/ui-plan
@@ -224,18 +230,21 @@ importers:
       '@deepseek-ai/dsh-fs-policy':
       '@deepseek-ai/dsh-fs-policy':
         specifier: workspace:^
         specifier: workspace:^
         version: link:../../packages/fs/fs-policy
         version: link:../../packages/fs/fs-policy
+      '@deepseek-ai/dsh-fs-sandbox':
+        specifier: workspace:^
+        version: link:../../packages/fs/fs-sandbox
       '@deepseek-ai/dsh-goal':
       '@deepseek-ai/dsh-goal':
         specifier: workspace:^
         specifier: workspace:^
         version: link:../../packages/goal/goal
         version: link:../../packages/goal/goal
       '@deepseek-ai/dsh-goal-session':
       '@deepseek-ai/dsh-goal-session':
         specifier: workspace:^
         specifier: workspace:^
         version: link:../../packages/goal/goal-session
         version: link:../../packages/goal/goal-session
-      '@deepseek-ai/dsh-helper':
-        specifier: workspace:^
-        version: link:../../packages/sdk/helper
       '@deepseek-ai/dsh-host-apiproxy':
       '@deepseek-ai/dsh-host-apiproxy':
         specifier: workspace:^
         specifier: workspace:^
         version: link:../../packages/host/apiproxy
         version: link:../../packages/host/apiproxy
+      '@deepseek-ai/dsh-host-directory-picker-browse':
+        specifier: workspace:^
+        version: link:../../packages/host/directory-picker-browse
       '@deepseek-ai/dsh-host-webserver':
       '@deepseek-ai/dsh-host-webserver':
         specifier: workspace:^
         specifier: workspace:^
         version: link:../../packages/host/webserver
         version: link:../../packages/host/webserver
@@ -257,9 +266,18 @@ importers:
       '@deepseek-ai/dsh-paths':
       '@deepseek-ai/dsh-paths':
         specifier: workspace:^
         specifier: workspace:^
         version: link:../../packages/util/paths
         version: link:../../packages/util/paths
+      '@deepseek-ai/dsh-permission':
+        specifier: workspace:^
+        version: link:../../packages/ui/permission
       '@deepseek-ai/dsh-plan-mode':
       '@deepseek-ai/dsh-plan-mode':
         specifier: workspace:^
         specifier: workspace:^
         version: link:../../packages/plan/plan-mode
         version: link:../../packages/plan/plan-mode
+      '@deepseek-ai/dsh-sandbox-local':
+        specifier: workspace:^
+        version: link:../../packages/sandbox/sandbox-local
+      '@deepseek-ai/dsh-sandbox-policy':
+        specifier: workspace:^
+        version: link:../../packages/sandbox/sandbox-policy
       '@deepseek-ai/dsh-scope':
       '@deepseek-ai/dsh-scope':
         specifier: workspace:^
         specifier: workspace:^
         version: link:../../packages/core/scope
         version: link:../../packages/core/scope
@@ -374,6 +392,9 @@ importers:
       '@deepseek-ai/dsh-tui':
       '@deepseek-ai/dsh-tui':
         specifier: workspace:^
         specifier: workspace:^
         version: link:../../packages/ui/tui
         version: link:../../packages/ui/tui
+      '@deepseek-ai/dsh-user-approval':
+        specifier: workspace:^
+        version: link:../../packages/ui/user-approval
       '@deepseek-ai/dsh-user-interaction':
       '@deepseek-ai/dsh-user-interaction':
         specifier: workspace:^
         specifier: workspace:^
         version: link:../../packages/ui/user-interaction
         version: link:../../packages/ui/user-interaction

+ 7 - 7
scripts/gen-doc-graphs.ts

@@ -597,7 +597,7 @@ const APP_EXAMPLES = [
     rel: 'apps/cli/composition.md',
     rel: 'apps/cli/composition.md',
     title: 'dsh TUI Composition',
     title: 'dsh TUI Composition',
     label: 'apps/cli (dsh)',
     label: 'apps/cli (dsh)',
-    config: 'apps/cli/base.cordis.yml',
+    configs: ['apps/cli/base.cordis.yml', 'apps/cli/tui.cordis.yml'],
     summary: 'The TUI agent combines the real DeepSeek adapter, coding tools, compaction, subagents, and workflows with the full-screen terminal app package.',
     summary: 'The TUI agent combines the real DeepSeek adapter, coding tools, compaction, subagents, and workflows with the full-screen terminal app package.',
   },
   },
   {
   {
@@ -605,7 +605,7 @@ const APP_EXAMPLES = [
     rel: 'examples/headless-agent/composition.md',
     rel: 'examples/headless-agent/composition.md',
     title: 'Headless Agent App Composition',
     title: 'Headless Agent App Composition',
     label: 'examples/headless-agent',
     label: 'examples/headless-agent',
-    config: 'examples/headless-agent/cordis.yml',
+    configs: ['examples/headless-agent/cordis.yml'],
     summary: 'The headless demo combines the real DeepSeek adapter and coding capabilities with the one-shot app package, format-pure stdout, and one fresh persisted top-level session.',
     summary: 'The headless demo combines the real DeepSeek adapter and coding capabilities with the one-shot app package, format-pure stdout, and one fresh persisted top-level session.',
   },
   },
   {
   {
@@ -613,7 +613,7 @@ const APP_EXAMPLES = [
     rel: 'examples/acp-agent/composition.md',
     rel: 'examples/acp-agent/composition.md',
     title: 'ACP Automation App Composition',
     title: 'ACP Automation App Composition',
     label: 'examples/acp-agent',
     label: 'examples/acp-agent',
-    config: 'examples/acp-agent/cordis.yml',
+    configs: ['examples/acp-agent/cordis.yml'],
     summary: 'The ACP demo exposes fresh baseline-prompt agent sessions to programmatic clients over JSON-RPC stdio, with no stdout logger, human UI, or pre-created agent.',
     summary: 'The ACP demo exposes fresh baseline-prompt agent sessions to programmatic clients over JSON-RPC stdio, with no stdout logger, human UI, or pre-created agent.',
   },
   },
 ]
 ]
@@ -639,15 +639,15 @@ function renderAppExpansion(lines: string[], appNode: string, pluginName: string
 }
 }
 
 
 function renderAppComposition(example: AppExample): string {
 function renderAppComposition(example: AppExample): string {
-  const plugins = parseExampleCordis(example.config)
-  const maintenance = 'hybrid: the leaf plugin list is parsed from its `cordis.yml`; app package expansion is curated from package source'
+  const plugins = example.configs.flatMap(parseExampleCordis)
+  const maintenance = 'hybrid: the leaf plugin list is parsed from its shipped config files; app package expansion is curated from package source'
   const lines = generatedHeader(example.title)
   const lines = generatedHeader(example.title)
   lines.push(
   lines.push(
     example.summary,
     example.summary,
     '',
     '',
     '```mermaid',
     '```mermaid',
     'flowchart LR',
     'flowchart LR',
-    `  cfg["${escLabel(example.label)}<br/>cordis.yml"]`,
+    `  cfg["${escLabel(example.label)}<br/>${escLabel(example.configs.map(config => config.split('/').at(-1)).join(' + '))}"]`,
   )
   )
   for (const plugin of plugins) {
   for (const plugin of plugins) {
     const pluginNode = nodeId(`plugin_${example.id}`, plugin.id)
     const pluginNode = nodeId(`plugin_${example.id}`, plugin.id)
@@ -664,7 +664,7 @@ function renderAppComposition(example: AppExample): string {
     '| --- | --- |',
     '| --- | --- |',
     ...plugins.map(plugin => `| \`${plugin.id}\` | \`${plugin.name}\` |`),
     ...plugins.map(plugin => `| \`${plugin.id}\` | \`${plugin.name}\` |`),
     '',
     '',
-    `Source config: [\`${example.config}\`](${linkFromDoc(example.rel, example.config)}).`,
+    `Source config${example.configs.length === 1 ? '' : 's'}: ${example.configs.map(config => `[\`${config}\`](${linkFromDoc(example.rel, config)})`).join(', ')}.`,
   )
   )
   lines.push('', ...maintenanceFooter(maintenance))
   lines.push('', ...maintenanceFooter(maintenance))
   return lines.join('\n')
   return lines.join('\n')

+ 6 - 1
scripts/verify-cordis-config.ts

@@ -78,6 +78,11 @@ function validateEntry(value: unknown, file: string, path: string): void {
       validateEntry(value.config[index], file, `${path}.config[${index}]`)
       validateEntry(value.config[index], file, `${path}.config[${index}]`)
     }
     }
   }
   }
+  if (isUnknownArray(value.insert)) {
+    for (let index = 0; index < value.insert.length; index++) {
+      validateEntry(value.insert[index], file, `${path}.insert[${index}]`)
+    }
+  }
   if (value.name !== '@cordisjs/plugin-include') return
   if (value.name !== '@cordisjs/plugin-include') return
   const config = value.config
   const config = value.config
   if (!isRecord(config) || !isUnknownArray(config.patches)) return
   if (!isRecord(config) || !isUnknownArray(config.patches)) return
@@ -124,7 +129,7 @@ function validateExampleResolution(): string[] {
 
 
 function validateAppResolution(): string[] {
 function validateAppResolution(): string[] {
   const dependencies = readManifest('apps/cli/package.json').dependencies ?? {}
   const dependencies = readManifest('apps/cli/package.json').dependencies ?? {}
-  const references = pluginReferences.filter(reference => reference.file === 'apps/cli/base.cordis.yml')
+  const references = pluginReferences.filter(reference => reference.file.startsWith('apps/cli/'))
   return missingPluginDependencies(references, dependencies, 'apps/cli/package.json')
   return missingPluginDependencies(references, dependencies, 'apps/cli/package.json')
 }
 }