diff --git a/apps/cli/tests/profiles/headless/credentials-snapshot.patch.yml b/apps/cli/tests/profiles/headless/credentials-snapshot.patch.yml
index 8293d9919f..0a737948e8 100644
--- a/apps/cli/tests/profiles/headless/credentials-snapshot.patch.yml
+++ b/apps/cli/tests/profiles/headless/credentials-snapshot.patch.yml
@@ -30,3 +30,9 @@
name: '@deepseek-ai/dsh-llm-deepseek-api-key'
config:
baseURL: 'http://127.0.0.1:9'
+
+- id: sandbox-policy
+ inject: []
+ config:
+ mode: !!js process.env.DSH_PERMISSION_MODE ?? 'workspace-write'
+ workspaceRoot: !!js process.cwd()
diff --git a/apps/cli/tests/profiles/headless/goal-snapshot.patch.yml b/apps/cli/tests/profiles/headless/goal-snapshot.patch.yml
index 3778da03de..5abe21e491 100644
--- a/apps/cli/tests/profiles/headless/goal-snapshot.patch.yml
+++ b/apps/cli/tests/profiles/headless/goal-snapshot.patch.yml
@@ -31,3 +31,9 @@
- id: session-log-deepseek
config:
enabled: false
+
+- id: sandbox-policy
+ inject: []
+ config:
+ mode: !!js process.env.DSH_PERMISSION_MODE ?? 'workspace-write'
+ workspaceRoot: !!js process.cwd()
diff --git a/apps/cli/tests/profiles/headless/real-model.patch.yml b/apps/cli/tests/profiles/headless/real-model.patch.yml
index b0c161b3c6..1a264aac93 100644
--- a/apps/cli/tests/profiles/headless/real-model.patch.yml
+++ b/apps/cli/tests/profiles/headless/real-model.patch.yml
@@ -30,3 +30,9 @@
config:
root: './.sessions'
compression: !!js "process.env.DSH_SNAPSHOT === undefined ? 'zstd' : 'none'"
+
+- id: sandbox-policy
+ inject: []
+ config:
+ mode: !!js process.env.DSH_PERMISSION_MODE ?? 'workspace-write'
+ workspaceRoot: !!js process.cwd()
diff --git a/apps/cli/tests/profiles/headless/retry-snapshot.patch.yml b/apps/cli/tests/profiles/headless/retry-snapshot.patch.yml
index ff13fa3303..9870ef3678 100644
--- a/apps/cli/tests/profiles/headless/retry-snapshot.patch.yml
+++ b/apps/cli/tests/profiles/headless/retry-snapshot.patch.yml
@@ -24,3 +24,9 @@
- insert:
- id: retry-snapshot-backend
name: './tests/fixtures/retry-snapshot-backend.mjs'
+
+- id: sandbox-policy
+ inject: []
+ config:
+ mode: !!js process.env.DSH_PERMISSION_MODE ?? 'workspace-write'
+ workspaceRoot: !!js process.cwd()
diff --git a/apps/cli/tests/profiles/headless/semantic-checkpoint-snapshot.patch.yml b/apps/cli/tests/profiles/headless/semantic-checkpoint-snapshot.patch.yml
index 0c818b1641..5007125e1c 100644
--- a/apps/cli/tests/profiles/headless/semantic-checkpoint-snapshot.patch.yml
+++ b/apps/cli/tests/profiles/headless/semantic-checkpoint-snapshot.patch.yml
@@ -27,3 +27,9 @@
- id: session-log-deepseek
config:
enabled: false
+
+- id: sandbox-policy
+ inject: []
+ config:
+ mode: !!js process.env.DSH_PERMISSION_MODE ?? 'workspace-write'
+ workspaceRoot: !!js process.cwd()
diff --git a/apps/cli/tests/profiles/headless/subagent-diagnostic-snapshot.patch.yml b/apps/cli/tests/profiles/headless/subagent-diagnostic-snapshot.patch.yml
index 49b42c91e2..0cdb4c5aff 100644
--- a/apps/cli/tests/profiles/headless/subagent-diagnostic-snapshot.patch.yml
+++ b/apps/cli/tests/profiles/headless/subagent-diagnostic-snapshot.patch.yml
@@ -30,3 +30,9 @@
- id: session-log-deepseek
config:
enabled: false
+
+- id: sandbox-policy
+ inject: []
+ config:
+ mode: !!js process.env.DSH_PERMISSION_MODE ?? 'workspace-write'
+ workspaceRoot: !!js process.cwd()
diff --git a/apps/cli/tests/profiles/headless/subagent-inheritance-snapshot.patch.yml b/apps/cli/tests/profiles/headless/subagent-inheritance-snapshot.patch.yml
index d347a9cfe4..7511e92167 100644
--- a/apps/cli/tests/profiles/headless/subagent-inheritance-snapshot.patch.yml
+++ b/apps/cli/tests/profiles/headless/subagent-inheritance-snapshot.patch.yml
@@ -10,6 +10,7 @@
disabled: true
- id: sandbox-policy
+ inject: []
config:
mode: workspace-write
workspaceRoot: !!js process.cwd()
diff --git a/apps/cli/tests/profiles/headless/subagent-settlement-snapshot.patch.yml b/apps/cli/tests/profiles/headless/subagent-settlement-snapshot.patch.yml
index 2b4b636ecb..2c56a67f6f 100644
--- a/apps/cli/tests/profiles/headless/subagent-settlement-snapshot.patch.yml
+++ b/apps/cli/tests/profiles/headless/subagent-settlement-snapshot.patch.yml
@@ -35,3 +35,9 @@
- id: session-log-deepseek
config:
enabled: false
+
+- id: sandbox-policy
+ inject: []
+ config:
+ mode: !!js process.env.DSH_PERMISSION_MODE ?? 'workspace-write'
+ workspaceRoot: !!js process.cwd()
diff --git a/apps/cli/tests/profiles/headless/team-snapshot.patch.yml b/apps/cli/tests/profiles/headless/team-snapshot.patch.yml
index caa28fa54d..dec80a9fe9 100644
--- a/apps/cli/tests/profiles/headless/team-snapshot.patch.yml
+++ b/apps/cli/tests/profiles/headless/team-snapshot.patch.yml
@@ -50,3 +50,9 @@
- id: team-fixture-llm
name: './tests/fixtures/team-llm.mjs'
+
+- id: sandbox-policy
+ inject: []
+ config:
+ mode: !!js process.env.DSH_PERMISSION_MODE ?? 'workspace-write'
+ workspaceRoot: !!js process.cwd()
diff --git a/apps/cli/tests/profiles/headless/tests/fixtures/cli.patch.yml b/apps/cli/tests/profiles/headless/tests/fixtures/cli.patch.yml
index 76a138452d..d4c74ac347 100644
--- a/apps/cli/tests/profiles/headless/tests/fixtures/cli.patch.yml
+++ b/apps/cli/tests/profiles/headless/tests/fixtures/cli.patch.yml
@@ -35,3 +35,9 @@
- insert:
- id: cli-mock-llm
name: '../../../../../../../packages/test-support/loader-smoke/tests/fixtures/cli-mock-llm.ts'
+
+- id: sandbox-policy
+ inject: []
+ config:
+ mode: !!js process.env.DSH_PERMISSION_MODE ?? 'workspace-write'
+ workspaceRoot: !!js process.cwd()
diff --git a/apps/cli/tests/profiles/headless/tests/fixtures/deepseek-defaults.patch.yml b/apps/cli/tests/profiles/headless/tests/fixtures/deepseek-defaults.patch.yml
index 6809f21a93..28150a1a47 100644
--- a/apps/cli/tests/profiles/headless/tests/fixtures/deepseek-defaults.patch.yml
+++ b/apps/cli/tests/profiles/headless/tests/fixtures/deepseek-defaults.patch.yml
@@ -27,3 +27,9 @@
config:
root: './.sessions'
compression: none
+
+- id: sandbox-policy
+ inject: []
+ config:
+ mode: !!js process.env.DSH_PERMISSION_MODE ?? 'workspace-write'
+ workspaceRoot: !!js process.cwd()
diff --git a/apps/cli/tests/profiles/headless/tests/fixtures/pi-ai-conversation-updates.patch.yml b/apps/cli/tests/profiles/headless/tests/fixtures/pi-ai-conversation-updates.patch.yml
index 6d2a8dde06..4029cf5fd2 100644
--- a/apps/cli/tests/profiles/headless/tests/fixtures/pi-ai-conversation-updates.patch.yml
+++ b/apps/cli/tests/profiles/headless/tests/fixtures/pi-ai-conversation-updates.patch.yml
@@ -34,3 +34,9 @@
- insert:
- id: dynamic-tool-prompt-updates
name: '../../../../../../../packages/test-support/session-snapshot/tests/fixtures/dynamic-tool-prompt-updates.ts'
+
+- id: sandbox-policy
+ inject: []
+ config:
+ mode: !!js process.env.DSH_PERMISSION_MODE ?? 'workspace-write'
+ workspaceRoot: !!js process.cwd()
diff --git a/apps/cli/tests/profiles/headless/tests/fixtures/pi-ai-defaults.patch.yml b/apps/cli/tests/profiles/headless/tests/fixtures/pi-ai-defaults.patch.yml
index a14bbb4d37..a3e983cee7 100644
--- a/apps/cli/tests/profiles/headless/tests/fixtures/pi-ai-defaults.patch.yml
+++ b/apps/cli/tests/profiles/headless/tests/fixtures/pi-ai-defaults.patch.yml
@@ -35,3 +35,9 @@
config:
root: './.sessions'
compression: none
+
+- id: sandbox-policy
+ inject: []
+ config:
+ mode: !!js process.env.DSH_PERMISSION_MODE ?? 'workspace-write'
+ workspaceRoot: !!js process.cwd()
diff --git a/apps/cli/tests/profiles/headless/tests/headless.expected.e2e.ts b/apps/cli/tests/profiles/headless/tests/headless.expected.e2e.ts
index 061227ca92..24793d7284 100644
--- a/apps/cli/tests/profiles/headless/tests/headless.expected.e2e.ts
+++ b/apps/cli/tests/profiles/headless/tests/headless.expected.e2e.ts
@@ -1,4 +1,4 @@
-import { mkdtemp, readFile, readdir, rm, writeFile } from 'node:fs/promises'
+import { mkdtemp, readFile, readdir, realpath, rm, writeFile } from 'node:fs/promises'
import { createServer } from 'node:http'
import type { IncomingMessage, ServerResponse } from 'node:http'
import { tmpdir } from 'node:os'
@@ -345,7 +345,9 @@ describe('headless stream-json snapshots', () => {
configPath: headlessOverlayPath,
binArgs: [
'--profile', 'headless', '--patch', headlessOverlayPath,
- '--permission-mode', 'workspace-write', '--add-dir', tmpdir(), '--json', task,
+ '--permission-mode', 'workspace-write',
+ ...process.platform === 'win32' ? [] : ['--add-dir', await realpath(tmpdir())],
+ '--json', task,
],
tsconfigPath,
env: {
@@ -358,7 +360,7 @@ describe('headless stream-json snapshots', () => {
const content = logs[0]?.content ?? ''
const modes = parseJsonl(content).filter(event => event.type === 'sandbox/mode')
expect(modes.at(-1)?.data).toEqual({ mode: 'workspace-write' })
- expect(content).toContain('Additional writable directories:')
+ if (process.platform !== 'win32') expect(content).toContain('Additional writable directories:')
},
})
@@ -370,6 +372,7 @@ describe('headless stream-json snapshots', () => {
expect(events.map(event => event.type)).toContain('thinking')
expect(events.map(event => event.type)).toContain('tool_call')
expect(events.map(event => event.type)).toContain('tool_result')
+ expect(events.filter(event => event.type === 'tool_result').every(event => event.status === 'completed')).toBe(true)
expect(events.map(event => event.type)).not.toContain('error')
expect(result.stderr).toBe('')
}, LOADER_SMOKE_TEST_TIMEOUT_MS)
diff --git a/apps/cli/tests/profiles/headless/tests/profile-resolution.ts b/apps/cli/tests/profiles/headless/tests/profile-resolution.ts
index 9978557402..9c4b58df8d 100644
--- a/apps/cli/tests/profiles/headless/tests/profile-resolution.ts
+++ b/apps/cli/tests/profiles/headless/tests/profile-resolution.ts
@@ -225,6 +225,7 @@ export function testProfileResolution(mode: ExampleMode): void {
].join('\n'),
'cordis.patch.yml': JSON.stringify([
{ id: 'headless-startup', disabled: true },
+ { id: 'sandbox-policy', inject: [], config: { mode: process.env.DSH_PERMISSION_MODE ?? 'workspace-write' } },
{ id: 'headless-runner', disabled: true },
{ id: 'llm-deepseek', disabled: true },
{ insert: [
diff --git a/apps/cli/tests/profiles/headless/tests/source-tool.built.e2e.ts b/apps/cli/tests/profiles/headless/tests/source-tool.built.e2e.ts
index 9eae3e8f4a..4988ef80fd 100644
--- a/apps/cli/tests/profiles/headless/tests/source-tool.built.e2e.ts
+++ b/apps/cli/tests/profiles/headless/tests/source-tool.built.e2e.ts
@@ -32,6 +32,7 @@ describe.skipIf(!existsSync(join(repoRoot, 'apps/cli/lib/bin.js')))('dsh SOURCE
const patch = join(root, 'source-tool.patch.yml')
await writeFile(patch, JSON.stringify([
{ id: 'headless-startup', disabled: true },
+ { id: 'sandbox-policy', inject: [], config: { mode: process.env.DSH_PERMISSION_MODE ?? 'workspace-write', workspaceRoot: cwd } },
{ id: 'headless-runner', disabled: true },
{ id: 'llm-deepseek', disabled: true },
{ id: 'agent-default-model', config: { provider: 'cli-mock', model: 'cli-mock' } },
diff --git a/apps/cli/tests/profiles/headless/workspace-context-resume-snapshot.patch.yml b/apps/cli/tests/profiles/headless/workspace-context-resume-snapshot.patch.yml
index 42a63474af..b7a2dbd8ca 100644
--- a/apps/cli/tests/profiles/headless/workspace-context-resume-snapshot.patch.yml
+++ b/apps/cli/tests/profiles/headless/workspace-context-resume-snapshot.patch.yml
@@ -31,3 +31,9 @@
- id: session-log-deepseek
config:
enabled: false
+
+- id: sandbox-policy
+ inject: []
+ config:
+ mode: !!js process.env.DSH_PERMISSION_MODE ?? 'workspace-write'
+ workspaceRoot: !!js process.cwd()
diff --git a/docs/capability-seams.i18n.yaml b/docs/capability-seams.i18n.yaml
index c2056c9963..573a76f8b7 100644
--- a/docs/capability-seams.i18n.yaml
+++ b/docs/capability-seams.i18n.yaml
@@ -6,5 +6,5 @@
en: 41d772192075e133
zh: 5ef3da69b4de1d8a
/capability-seams-and-core-services:
- en: c0c69a4be20d2157
- zh: 9218ae36d2fe4a04
+ en: f209321ab0da4f90
+ zh: 4af5c135eec51ce1
diff --git a/docs/capability-seams.md b/docs/capability-seams.md
index 1b73ed96df..865162e7df 100644
--- a/docs/capability-seams.md
+++ b/docs/capability-seams.md
@@ -7,6 +7,8 @@ A service can be a core spine service, a swappable capability seam, a bundle/com
```mermaid
flowchart LR
+ pkg_headless["headless"]
+ svc_headlessStartup["ctx.headlessStartup
Headless invocation options"]
pkg_experimental_client_ui_cot_translation["experimental-client-ui-cot-translation"]
svc_cotTranslation["ctx.cotTranslation
Reasoning translation Remote"]
pkg_experimental_translator["experimental-translator"]
@@ -188,7 +190,6 @@ flowchart LR
svc_agents["ctx.agents
Agent service"]
pkg_acp["acp"]
svc_agentDefaultModel["ctx.agentDefaultModel
Default Agent model selection"]
- pkg_headless["headless"]
svc_agentLoop["ctx.agentLoop
Concrete loop driver"]
pkg_base["base"]
pkg_sdk_minimal["sdk-minimal"]
@@ -357,6 +358,7 @@ flowchart LR
pkg_fs_sandbox --> svc_fs
pkg_fs_ssh --> svc_fs
pkg_goal --> svc_goals
+ pkg_headless --> svc_headlessStartup
pkg_hmr --> svc_hmr
pkg_host_directory_picker --> svc_directoryPicker
pkg_host_directory_picker_browse --> svc_directoryPicker
@@ -490,6 +492,7 @@ flowchart LR
svc_fileReferences --> pkg_api_session_controller
svc_fileUploads --> pkg_api_session_controller
svc_fs --> pkg_tool_fs
+ svc_headlessStartup --> pkg_headless
svc_hmr --> pkg_app_boot
svc_jobs --> pkg_api_job_controller
svc_jobs --> pkg_tool_bash
@@ -611,6 +614,7 @@ flowchart LR
| ctx key | Role | Owner | Implementations | Direct consumers | Companion plugins | Note |
| --- | --- | --- | --- | --- | --- | --- |
+| `ctx.headlessStartup` | `core` | [`headless`](../packages/bundle/headless) | - | [`headless`](../packages/bundle/headless) | - | Supplies parsed task and run options to the headless runner and its sandbox-policy configuration. |
| `ctx.cotTranslation` | `service` | [`experimental-client-ui-cot-translation`](../packages/experimental/client-ui-cot-translation) | - | - | - | Serves reasoning translations, reuses saved results, and joins ordinary Session activation for uncached inactive Sessions. |
| `ctx.translator` | `service` | [`experimental-translator`](../packages/experimental/translator) | - | [`experimental-client-ui-cot-translation`](../packages/experimental/client-ui-cot-translation) | - | Resolves Google or Bing requests and retains shared experimental records through the active Session writer. |
| `ctx.hmr` | `core` | [`hmr`](../packages/boot/hmr) | - | [`app-boot`](../packages/boot/app-boot) | - | Owns module and exact configuration watchers; application mutations share its queue and automatic reloads await the application file lock. |
diff --git a/docs/capability-seams.zh.md b/docs/capability-seams.zh.md
index 7e66fc677a..395986ab96 100644
--- a/docs/capability-seams.zh.md
+++ b/docs/capability-seams.zh.md
@@ -9,6 +9,8 @@
```mermaid
flowchart LR
+ pkg_headless["headless"]
+ svc_headlessStartup["ctx.headlessStartup
Headless invocation options"]
pkg_experimental_client_ui_cot_translation["experimental-client-ui-cot-translation"]
svc_cotTranslation["ctx.cotTranslation
Reasoning translation Remote"]
pkg_experimental_translator["experimental-translator"]
@@ -190,7 +192,6 @@ flowchart LR
svc_agents["ctx.agents
Agent service"]
pkg_acp["acp"]
svc_agentDefaultModel["ctx.agentDefaultModel
Default Agent model selection"]
- pkg_headless["headless"]
svc_agentLoop["ctx.agentLoop
Concrete loop driver"]
pkg_base["base"]
pkg_sdk_minimal["sdk-minimal"]
@@ -359,6 +360,7 @@ flowchart LR
pkg_fs_sandbox --> svc_fs
pkg_fs_ssh --> svc_fs
pkg_goal --> svc_goals
+ pkg_headless --> svc_headlessStartup
pkg_hmr --> svc_hmr
pkg_host_directory_picker --> svc_directoryPicker
pkg_host_directory_picker_browse --> svc_directoryPicker
@@ -492,6 +494,7 @@ flowchart LR
svc_fileReferences --> pkg_api_session_controller
svc_fileUploads --> pkg_api_session_controller
svc_fs --> pkg_tool_fs
+ svc_headlessStartup --> pkg_headless
svc_hmr --> pkg_app_boot
svc_jobs --> pkg_api_job_controller
svc_jobs --> pkg_tool_bash
@@ -613,6 +616,7 @@ flowchart LR
| ctx 键 | 角色 | 所属包 | 实现 | 直接消费方 | 配套插件 | 说明 |
| --- | --- | --- | --- | --- | --- | --- |
+| `ctx.headlessStartup` | `core` | [`headless`](../packages/bundle/headless) | - | [`headless`](../packages/bundle/headless) | - | 将解析后的任务和运行选项提供给无界面运行器及其 sandbox-policy 配置。 |
| `ctx.cotTranslation` | `service` | [`experimental-client-ui-cot-translation`](../packages/experimental/client-ui-cot-translation) | - | - | - | 为展开的思考内容提供有界的 Session 翻译请求,不改变模型可见历史。 |
| `ctx.translator` | `service` | [`experimental-translator`](../packages/experimental/translator) | - | [`experimental-client-ui-cot-translation`](../packages/experimental/client-ui-cot-translation) | - | 解析 Google 或 Bing 请求,并可选保留统一的实验性 Session 记录。 |
| `ctx.hmr` | `core` | [`hmr`](../packages/boot/hmr) | - | [`app-boot`](../packages/boot/app-boot) | - | 负责模块和精确配置监听;应用修改共用其队列,自动重载等待应用文件锁。 |
diff --git a/docs/cli-help.md b/docs/cli-help.md
index af70440f9b..a6b7147de2 100644
--- a/docs/cli-help.md
+++ b/docs/cli-help.md
@@ -118,8 +118,8 @@ Options:
unknown id is an error
--permission-mode select a configured permission preset for this
task, including on resume
- --add-dir allow an additional absolute directory under
- workspace-write (repeatable)
+ --add-dir allow an additional physical absolute directory
+ under workspace-write (repeatable)
-h, --help show this help
Examples:
@@ -127,7 +127,8 @@ Examples:
echo "run the tests" | dsh --profile headless read the task from stdin
dsh --profile headless --json "run the tests" emit machine-readable run events
dsh --profile headless --session-id session-… "continue" resume an existing Session
- dsh headless --permission-mode workspace-write --add-dir /repo/.git "commit the fix"
+ dsh --profile headless --permission-mode workspace-write --add-dir /repo/.git "commit the fix"
+ grant shared Git metadata for this task
```
diff --git a/docs/cli-help.zh.md b/docs/cli-help.zh.md
index be2bc29468..40f321da1b 100644
--- a/docs/cli-help.zh.md
+++ b/docs/cli-help.zh.md
@@ -120,8 +120,8 @@ Options:
unknown id is an error
--permission-mode select a configured permission preset for this
task, including on resume
- --add-dir allow an additional absolute directory under
- workspace-write (repeatable)
+ --add-dir allow an additional physical absolute directory
+ under workspace-write (repeatable)
-h, --help show this help
Examples:
@@ -129,7 +129,8 @@ Examples:
echo "run the tests" | dsh --profile headless read the task from stdin
dsh --profile headless --json "run the tests" emit machine-readable run events
dsh --profile headless --session-id session-… "continue" resume an existing Session
- dsh headless --permission-mode workspace-write --add-dir /repo/.git "commit the fix"
+ dsh --profile headless --permission-mode workspace-write --add-dir /repo/.git "commit the fix"
+ grant shared Git metadata for this task
```
diff --git a/docs/config-catalog.md b/docs/config-catalog.md
index b335f051fd..ed550684c8 100644
--- a/docs/config-catalog.md
+++ b/docs/config-catalog.md
@@ -1483,7 +1483,7 @@ export interface Config {
## `@deepseek-ai/dsh-headless`
- `inject`: `agentDefaultModel` · `agents` · `sessions`
-- `source`: [`packages/bundle/headless/src/index.ts:45`](../packages/bundle/headless/src/index.ts)
+- `source`: [`packages/bundle/headless/src/index.ts:46`](../packages/bundle/headless/src/index.ts)
```ts config-catalog
/** Plugin config: the task and run options resolved from this app's injected provider service. */
@@ -1496,8 +1496,6 @@ export interface Config {
json?: boolean
/** Permission preset applied before the task; omission keeps the session's current policy. */
permissionMode?: string
- /** Absolute directories requested by the CLI; the composed sandbox policy must include each one. */
- additionalWritableRoots?: string[]
}
```
diff --git a/docs/config-catalog.zh.md b/docs/config-catalog.zh.md
index c6dbe0117e..eb18d3ac06 100644
--- a/docs/config-catalog.zh.md
+++ b/docs/config-catalog.zh.md
@@ -1485,7 +1485,7 @@ export interface Config {
## `@deepseek-ai/dsh-headless`
- `inject`: `agentDefaultModel` · `agents` · `sessions`
-- `source`: [`packages/bundle/headless/src/index.ts:45`](../packages/bundle/headless/src/index.ts)
+- `source`: [`packages/bundle/headless/src/index.ts:46`](../packages/bundle/headless/src/index.ts)
```ts config-catalog
/** Plugin config: the task and run options resolved from this app's injected provider service. */
@@ -1498,8 +1498,6 @@ export interface Config {
json?: boolean
/** Permission preset applied before the task; omission keeps the session's current policy. */
permissionMode?: string
- /** Absolute directories requested by the CLI; the composed sandbox policy must include each one. */
- additionalWritableRoots?: string[]
}
```
diff --git a/docs/subsystems/boot.md b/docs/subsystems/boot.md
index ba75525a03..606ad59bcb 100644
--- a/docs/subsystems/boot.md
+++ b/docs/subsystems/boot.md
@@ -92,6 +92,14 @@ async edit( entry: Entry, change: (current: Record, inherited:
Source: [`packages/boot/config-editor/src/index.ts`](../../packages/boot/config-editor/src/index.ts)
+
+
+### `ctx.headlessStartup` — `HeadlessStartupValues`
+
+What the runner row reads from HEADLESS_STARTUP_SERVICE.
+
+Source: [`packages/bundle/headless/src/startup.ts`](../../packages/bundle/headless/src/startup.ts)
+
### `ctx.hmr` — `Hmr`
diff --git a/docs/subsystems/boot.zh.md b/docs/subsystems/boot.zh.md
index f6260e8583..c7f830fbb9 100644
--- a/docs/subsystems/boot.zh.md
+++ b/docs/subsystems/boot.zh.md
@@ -92,6 +92,14 @@ async edit( entry: Entry, change: (current: Record, inherited:
Source: [`packages/boot/config-editor/src/index.ts`](../../packages/boot/config-editor/src/index.ts)
+
+
+### `ctx.headlessStartup` — `HeadlessStartupValues`
+
+What the runner row reads from HEADLESS_STARTUP_SERVICE.
+
+Source: [`packages/bundle/headless/src/startup.ts`](../../packages/bundle/headless/src/startup.ts)
+
### `ctx.hmr` — `Hmr`
diff --git a/docs/upgrade-guide/v0.2.1-rc.2/headless-policy-composition/guide.i18n.yaml b/docs/upgrade-guide/v0.2.1-rc.2/headless-policy-composition/guide.i18n.yaml
new file mode 100644
index 0000000000..d22be57baa
--- /dev/null
+++ b/docs/upgrade-guide/v0.2.1-rc.2/headless-policy-composition/guide.i18n.yaml
@@ -0,0 +1,16 @@
+# Bilingual-pair consistency record for guide.md (docs/i18n/README.md): per heading
+# section, a hash of its English and Chinese blocks outside code blocks and generated regions.
+# After editing either side, bring the other along and re-record with:
+# pnpm run verify-translation-pairing --write docs/upgrade-guide/v0.2.1-rc.2/headless-policy-composition/guide.md
+/:
+ en: 610cf36c44391560
+ zh: 7fdfc8873482071b
+/configure-policy-when-replacing-headless-startup:
+ en: 2fb959361985dd4e
+ zh: ebd14ca868ed7ddf
+/configure-policy-when-replacing-headless-startup/change:
+ en: 85378b0c14641e84
+ zh: e7e859c5e59e737a
+/configure-policy-when-replacing-headless-startup/migration:
+ en: 8d315115e046638b
+ zh: 7736ddaa79f77206
diff --git a/docs/upgrade-guide/v0.2.1-rc.2/headless-policy-composition/guide.md b/docs/upgrade-guide/v0.2.1-rc.2/headless-policy-composition/guide.md
new file mode 100644
index 0000000000..56725a4227
--- /dev/null
+++ b/docs/upgrade-guide/v0.2.1-rc.2/headless-policy-composition/guide.md
@@ -0,0 +1,27 @@
+---
+kind: upgrade-guide
+description: "Headless compositions that disable the command-line provider must supply their sandbox policy configuration directly."
+---
+
+# Configure policy when replacing headless startup
+
+English | [中文](guide.zh.md)
+
+## Change
+
+The headless bundle reads `--add-dir` through `headlessStartup` before activating its sandbox policy. An overlay that disables `headless-startup` must also remove that dependency from `sandbox-policy`. Otherwise the policy and its dependent tools remain pending. Ordinary `dsh headless` invocations need no configuration change.
+
+## Migration
+
+1. In the overlay that disables `headless-startup`, supply the policy directly. Retain any existing policy settings in this replacement config:
+
+ ```yaml
+ - id: sandbox-policy
+ inject: []
+ config:
+ mode: !!js process.env.DSH_PERMISSION_MODE ?? 'workspace-write'
+ workspaceRoot: !!js process.cwd()
+ ```
+
+2. If the overlay keeps command-line startup but replaces `headless-runner` or `sandbox-policy`, retain their `headlessStartup` bindings from the [headless bundle](../../../../packages/bundle/headless/cordis.patch.yml).
+3. Launch a small task through the custom profile and verify that its tools activate. If startup remains enabled, pass `--add-dir` and check that a write to the selected directory succeeds.
diff --git a/docs/upgrade-guide/v0.2.1-rc.2/headless-policy-composition/guide.zh.md b/docs/upgrade-guide/v0.2.1-rc.2/headless-policy-composition/guide.zh.md
new file mode 100644
index 0000000000..eba6f2890a
--- /dev/null
+++ b/docs/upgrade-guide/v0.2.1-rc.2/headless-policy-composition/guide.zh.md
@@ -0,0 +1,27 @@
+---
+kind: upgrade-guide
+description: "禁用命令行提供方的 Headless 组合必须直接提供沙箱策略配置。"
+---
+
+# 替换 headless startup 时配置策略
+
+[English](guide.md) | 中文
+
+## 变更
+
+headless 组合包在激活沙箱策略前,通过 `headlessStartup` 读取 `--add-dir`。禁用 `headless-startup` 的 overlay 也必须从 `sandbox-policy` 移除该依赖,否则策略及其依赖工具会保持 pending 状态。普通 `dsh headless` 调用无需修改配置。
+
+## 迁移
+
+1. 在禁用 `headless-startup` 的 overlay 中直接提供策略。在此替换配置中保留已有策略设置:
+
+ ```yaml
+ - id: sandbox-policy
+ inject: []
+ config:
+ mode: !!js process.env.DSH_PERMISSION_MODE ?? 'workspace-write'
+ workspaceRoot: !!js process.cwd()
+ ```
+
+2. 如果 overlay 保留命令行 startup,但替换了 `headless-runner` 或 `sandbox-policy`,请保留 [headless 组合包](../../../../packages/bundle/headless/cordis.patch.yml)中的 `headlessStartup` 绑定。
+3. 通过自定义配置档运行一个小任务并确认工具已激活。如果保留 startup,请传入 `--add-dir`,并检查对所选目录的写入是否成功。
diff --git a/packages/bundle/headless/README.i18n.yaml b/packages/bundle/headless/README.i18n.yaml
index c8cedf2eea..5861ae9f97 100644
--- a/packages/bundle/headless/README.i18n.yaml
+++ b/packages/bundle/headless/README.i18n.yaml
@@ -18,8 +18,8 @@
en: 4447012f4c84100d
zh: faf579ae9f04c096
/deepseek-ai-dsh-headless/use-this-package/running-a-one-shot-task:
- en: 8e6712735126b2d4
- zh: a9730d6a55e8aee0
+ en: b3d0ddfb56a1ac7f
+ zh: c1762158e49a4851
/deepseek-ai-dsh-headless/use-this-package/credentials:
en: ca8217837ee9c698
zh: fa20daf27d4e09ef
@@ -27,11 +27,11 @@
en: dbaa2fac63910a4c
zh: 4fd5c17815ca444b
/deepseek-ai-dsh-headless/use-this-package/supervising-implementation-tasks:
- en: ade12ec55659bac7
- zh: 1b90ec982ee761e9
+ en: 71d556ea725df5ce
+ zh: e397938f6c39bc99
/deepseek-ai-dsh-headless/use-this-package/machine-readable-output:
- en: 5fe924c792e15394
- zh: 1cd80cb0ff021c25
+ en: df1ffa7f21b89136
+ zh: 52bf016b3c2bc0f9
/deepseek-ai-dsh-headless/use-this-package/when-to-use-it:
en: 5262b93dcef0d489
zh: 1638965b2aabcd38
@@ -42,17 +42,17 @@
en: be1b73a6d0ee3902
zh: bdba099685d68b27
/deepseek-ai-dsh-headless/understand-the-implementation/run-flow:
- en: 3729fbdc598637da
- zh: 254c6961b65b9e2b
+ en: c210869730df90e2
+ zh: 1bf5f8fd9a887bac
/deepseek-ai-dsh-headless/understand-the-implementation/patch-surface-over-base:
- en: a22752c052878579
- zh: 1e42c946d8673eeb
+ en: ae3e3906d091f010
+ zh: d22c6b053e8d4f23
/deepseek-ai-dsh-headless/understand-the-implementation/exit-mapping:
en: c2466d3d8a26177a
zh: b75743bfa2e7723a
/deepseek-ai-dsh-headless/understand-the-implementation/source-map:
- en: d82278600e0efdbd
- zh: 0f546bdba164ef25
+ en: fbda2df5fd109682
+ zh: 3a92aec4d9dfd9c8
/deepseek-ai-dsh-headless/further-exploration:
en: 65aebf0f8ab3047c
zh: 85616d3859073c4b
@@ -63,8 +63,8 @@
en: 0b0f74b412c7c6d8
zh: 6e8f918d0c4bf342
/deepseek-ai-dsh-headless/known-limitations-and-deferred-work:
- en: 3a6dc9666d852219
- zh: b764f203815e179e
+ en: ff000db625d2d0db
+ zh: 7d08a158aade75f9
/deepseek-ai-dsh-headless/known-limitations-and-deferred-work/dev-note:
en: e67f633dc6fe7773
zh: 3bb0c20c93f16f98
diff --git a/packages/bundle/headless/README.md b/packages/bundle/headless/README.md
index 9e4c01540d..c849d613e7 100644
--- a/packages/bundle/headless/README.md
+++ b/packages/bundle/headless/README.md
@@ -47,7 +47,6 @@ The task and run options use these settings:
| `sessionId` | `session-` | Exact Session identity to adopt; an unknown id fails |
| `json` | `false` | Project the run as newline-delimited events on stdout |
| `permissionMode` | session policy | Apply a configured permission preset before submitting the task, including on resume |
-| `additionalWritableRoots` | none | Explicit absolute directories requested by `--add-dir`; the sandbox composition must include them |
The generated [configuration catalog](../../../docs/config-catalog.md#deepseek-aidsh-headless) is the exhaustive source for every accepted field and its JSDoc.
@@ -84,7 +83,7 @@ git_dir=$(git rev-parse --path-format=absolute --git-common-dir)
dsh headless --permission-mode workspace-write --add-dir "$git_dir" --json - < task.txt > run.jsonl
```
-`--add-dir` accepts repeatable absolute execution-world paths. The shared Git directory permits staging, committing, and repository hooks without granting the parent checkout's source files. It grants all metadata in that directory, including shared refs and configuration. Additional grants apply to this invocation and must be supplied again when resuming. The shipped profile passes them to `sandbox-policy`; an overlay that replaces that row must include `ctx.headlessStartup.additionalWritableRoots`, or the runner rejects the request before model work. The [local sandbox provider](../../sandbox/sandbox-local/README.md) defines platform support.
+`--add-dir` accepts repeatable physical absolute directory paths in the execution environment. The shared Git directory permits staging, committing, and repository hooks without granting the parent checkout's source files. It grants all metadata in that directory, including shared refs, hooks, and configuration. Later host Git commands can execute modified hooks or configuration outside the sandbox; use this grant for trusted tasks and review metadata changes before host Git operations. Additional grants apply to this invocation and must be supplied again when resuming. The shipped profile passes them to `sandbox-policy`; the runner checks the original parsed CLI grants directly. An overlay that replaces the policy row must include `ctx.headlessStartup.additionalWritableRoots`, or the runner rejects the request before model work. The [local sandbox provider](../../sandbox/sandbox-local/README.md) defines platform support.
Read the opening `session.sessionId`, review the diff, and send revision instructions with `--session-id` from the same worktree. Repeat the directory grants. `--permission-mode` selects an existing permission preset through the permission service and overrides the session's saved selection before the task runs; omission preserves normal defaults and saved policy. Neither option edits saved profiles or user settings.
@@ -94,7 +93,7 @@ A supervisor that interrupts a process must await its exit before resuming its S
### Machine-readable output
-`--json` replaces the final-text stdout line with a newline-delimited JSON event stream, while stderr keeps only the `dsh:` diagnostics. The stream opens with `session` (carrying the identity the run used) and closes with `final`, and carries `status`, `text`, `thinking`, `tool_call`, and `tool_result` events in between. `text` and `thinking` are projected from committed assistant messages, so a retried or discarded attempt never reaches the stream; they arrive when the step commits, not per token, and default-mode stderr reasoning remains the only live text channel. The terminal `final` event carries the exact `sessionId`, the durable turn `reason` (or `null` when the final turn has no terminal event), and the same lossless answer as the default mode; it is not capped; every other string and object key is capped at 8 KiB and flagged with `truncated`, and one event line, its newline included, is capped at 32 KiB — an over-long event keeps its scalar fields, drops structured ones, and at the extreme reduces to `type` and `truncated`, while a payload nested 64 levels or deeper is cut at that depth. An empty tool-argument string projects as `{}`, matching what the executor runs, while arguments that JSON cannot round-trip — an overflowing number such as `1e400` — keep their raw text rather than the `null` that `JSON.stringify` would report. A failure the runner raises outside a turn writes an `error` event and ends the stream without `final`, in addition to the `dsh:` stderr line; a profile whose own plugins fail to load exits before the runner mounts, so that case keeps only the loader's stderr diagnostics. A turn that fails in-turn still ends with a `final` event (often empty) and no `error` event, so a well-formed stream can still describe a failed run: treat exit code 1 and the `turn_end` reason as the failure signal.
+`--json` replaces the final-text stdout line with a newline-delimited JSON event stream, while stderr keeps only the `dsh:` diagnostics. The stream opens with `session` (carrying the identity the run used) and closes with `final`, and carries `status`, `text`, `thinking`, `tool_call`, and `tool_result` events in between. `text` and `thinking` are projected from committed assistant messages, so a retried or discarded attempt never reaches the stream; they arrive when the step commits, not per token, and default-mode stderr reasoning remains the only live text channel. The terminal `final` event carries the exact `sessionId`, the durable turn `reason` (or `null` when the final turn has no terminal event), and the same lossless answer as the default mode; it is not capped; every other string and object key is capped at 8 KiB and flagged with `truncated`, and one event line, its newline included, is capped at 32 KiB — an over-long event keeps its scalar fields, drops structured ones, and at the extreme reduces to `type` and `truncated`, while a payload nested 64 levels or deeper is cut at that depth. An empty tool-argument string projects as `{}`, matching what the executor runs, while arguments that JSON cannot round-trip — an overflowing number such as `1e400` — keep their raw text rather than the `null` that `JSON.stringify` would report. A failure the runner raises outside a turn writes an `error` event and ends the stream without `final`, in addition to the `dsh:` stderr line; a profile whose own plugins fail to load exits before the runner mounts, so that case keeps only the loader's stderr diagnostics. A turn that fails in-turn still ends with a `final` event (often empty) and no `error` event, so a well-formed stream can still describe a failed run: check the process exit status and `final.reason` before assessing the task report.
### When to use it
@@ -116,11 +115,11 @@ The runner is a direct driver over the core API carrier: it resolves the Agent i
### Run flow
-The runner awaits the complete application (`ctx.get('loader')?.await()`) so the composed tools and adapters are not half-mounted, reads the shared [`agentDefaultModel`](../../core/agent-default-model/README.md) selection, resolves the task from config or stdin, then resolves the Agent identity: a fresh `session-` by default, or the persisted Session `--session-id` names, which it adopts through [`sessionQuery`](../../session-query/session-query/README.md) and refuses when no log exists. It submits the task as an ordinary user message. Without `--json` it streams that Agent's non-empty reasoning deltas to stderr; with `--json` it projects the run instead. It waits for child work and rechecks that the parent remains idle with an unchanged Session sequence before finishing, so settlement notices can drive a final parent answer. Idle children with parked inbox messages do not delay completion; process shutdown discards their unclaimed messages. It then flushes the Session and folds the owned interval (`firstSeq` onward) into the last non-empty `assistant/message` text and final `turn/end` reason. It writes the final text to stdout (or the `final` event) and requests exit.
+The runner awaits the complete application (`ctx.get('loader')?.await()`) so the composed tools and adapters are not half-mounted, validates the requested permission preset and CLI directory grants before reading stdin, resolves the task and the shared [`agentDefaultModel`](../../core/agent-default-model/README.md) selection, then resolves the Agent identity: a fresh `session-` by default, or the persisted Session `--session-id` names, which it adopts through [`sessionQuery`](../../session-query/session-query/README.md) and refuses when no log exists. It submits the task as an ordinary user message. Without `--json` it streams that Agent's non-empty reasoning deltas to stderr; with `--json` it projects the run instead. It waits for child work and rechecks that the parent remains idle with an unchanged Session sequence before finishing, so settlement notices can drive a final parent answer. Idle children with parked inbox messages do not delay completion; process shutdown discards their unclaimed messages. It then flushes the Session and folds the owned interval (`firstSeq` onward) into the last non-empty `assistant/message` text and the last turn's terminal reason. An unclosed last turn has no terminal reason and cannot inherit an earlier completed outcome. It writes the final text to stdout (or the `final` event) and requests exit.
### Patch surface over base
-The patch rides over `dsh-base`: it inherits the projection cache and shared PTC runtime, sets the coding persona prefix and separate cwd suffix on the base `system-prompt` row, keeps the same temporary process-wide PTC mode opt-in (`DSH_TOOLS_MODE`) as the Web surface, disables the shared HMR row, and mounts the startup provider and the runner. The cache checkpoints each persisted one-shot session for later consumers; its durability barrier flushes each covered log prefix before publishing the cache row and may split otherwise coalesced JSONL runs. The startup provider ([`src/startup.ts`](src/startup.ts)) injects `ctx.cmdlineArgs` ([`dsh-cmdline`](../../boot/cmdline/README.md)), reads the positional argument and the `--session-id`/`--json` options, prints the app's `--help`, and provides `headlessStartup`; the runner injects that service and reads its task and run options from lazy config.
+The patch rides over `dsh-base`: it inherits the projection cache and shared PTC runtime, sets the coding persona prefix and separate cwd suffix on the base `system-prompt` row, keeps the same temporary process-wide PTC mode opt-in (`DSH_TOOLS_MODE`) as the Web surface, disables the shared HMR row, and mounts the startup provider and the runner. The cache checkpoints each persisted one-shot session for later consumers; its durability barrier flushes each covered log prefix before publishing the cache row and may split otherwise coalesced JSONL runs. The startup provider ([`src/startup.ts`](src/startup.ts)) injects `ctx.cmdlineArgs` ([`dsh-cmdline`](../../boot/cmdline/README.md)), reads the task and invocation options, prints the app's `--help`, and provides `headlessStartup`; the runner reads its task and run options from lazy config. The sandbox-policy row also waits for this service, retaining the base mode and workspace defaults while receiving the explicit directory grants.
### Exit mapping
@@ -131,7 +130,7 @@ A completed final `turn/end` exits 0; any other outcome — aborted, error, or n
| File | Role |
|---|---|
| [`src/index.ts`](src/index.ts) | The `headless-runner` plugin: run flow, session resolution, output contract, exit mapping |
-| [`src/startup.ts`](src/startup.ts) | The `headless-startup` provider: task positional, `--session-id`, `--json`, and `--help` |
+| [`src/startup.ts`](src/startup.ts) | The `headless-startup` provider: task input, Session identity, permission options, and output selection |
| [`src/json-stream.ts`](src/json-stream.ts) | The `--json` projection: event vocabulary, commit-point emission, string bounding |
| [`cordis.patch.yml`](cordis.patch.yml) | The one-shot patch over `dsh-base` |
| [`tests/headless.spec.ts`](tests/headless.spec.ts) | Run flow, aggregation, flush, session adoption, and exit mapping |
@@ -173,6 +172,8 @@ These limits tell you when headless does not fit and what it needs from the `dsh
- **One task per run** — after the task is answered the process exits; there is no interactive follow-up, so split multi-step work into separate runs.
- **Final background-job settlement may be absent from persisted history** — process teardown can end Session routing or close its writer before an owned job settles, including normal completion and signal shutdown; the [job lifecycle contract](../../../docs/subsystems/jobs.md#session-lifecycle-records) defines how consumers retain incomplete intervals.
+- **Custom startup** — compositions that disable `headless-startup` must [configure the policy directly](../../../docs/upgrade-guide/v0.2.1-rc.2/headless-policy-composition/guide.md).
+- **Windows extra roots** — the ACL backend rejects subprocess calls with extra roots when a tool executes; it does not stop the model request. Filesystem tools enforce the grants independently. See [platform limits](../../sandbox/sandbox-local/README.md).
- **Runs through the `dsh` launcher** — starting the headless profile another way fails at startup, because only the launcher can request the process exit.
- **No pre-token heartbeat** — in default mode stderr stays silent until the provider emits a non-empty reasoning delta; a delayed first token exposes no earlier progress signal.
- **Reasoning enters stderr logs** — in default mode, redirection and supervisors may retain substantially more and potentially sensitive model output; route stderr to a controlled sink when needed.
diff --git a/packages/bundle/headless/README.zh.md b/packages/bundle/headless/README.zh.md
index 4eb20e8a27..74ab0fb12b 100644
--- a/packages/bundle/headless/README.zh.md
+++ b/packages/bundle/headless/README.zh.md
@@ -47,7 +47,6 @@ agent 会完成该任务,把提供方的每个非空推理(reasoning)增
| `sessionId` | `session-` | 要沿用的精确 Session 标识;未知 id 会失败 |
| `json` | `false` | 把本次运行投影为 stdout 上的按行 JSON 事件 |
| `permissionMode` | 会话策略 | 提交任务前应用已配置的权限预设,恢复会话时同样生效 |
-| `additionalWritableRoots` | 无 | `--add-dir` 请求的显式绝对目录;沙箱组合必须包含这些目录 |
生成的[配置目录](../../../docs/config-catalog.zh.md#deepseek-aidsh-headless)是所有受支持字段及其 JSDoc 的完整真源。
@@ -84,7 +83,7 @@ git_dir=$(git rev-parse --path-format=absolute --git-common-dir)
dsh headless --permission-mode workspace-write --add-dir "$git_dir" --json - < task.txt > run.jsonl
```
-`--add-dir` 接受可重复的执行环境绝对路径。共享 Git 目录允许暂存、提交和执行仓库钩子,而不授予父检出目录源文件的写权限。它授权该目录中的所有元数据,包括共享引用和配置。额外授权仅适用于本次调用,恢复会话时必须再次提供。随附配置档将其传给 `sandbox-policy`;替换该行的 overlay 必须包含 `ctx.headlessStartup.additionalWritableRoots`,否则 runner 会在模型工作前拒绝请求。[本地沙箱提供方](../../sandbox/sandbox-local/README.zh.md)定义各平台的支持情况。
+`--add-dir` 接受可重复的执行环境物理绝对目录路径。共享 Git 目录允许暂存、提交和执行仓库钩子,而不授予父检出目录源文件的写权限。它授权该目录中的所有元数据,包括共享引用、钩子和配置。后续主机 Git 命令可能在沙箱之外执行修改后的钩子或配置;仅对可信任务使用此授权,并在运行主机 Git 操作前审查元数据变更。额外授权仅适用于本次调用,恢复会话时必须再次提供。随附配置档将其传给 `sandbox-policy`;runner 直接检查原始解析出的 CLI 授权。替换策略行的 overlay 必须包含 `ctx.headlessStartup.additionalWritableRoots`,否则 runner 会在模型工作前拒绝请求。[本地沙箱提供方](../../sandbox/sandbox-local/README.zh.md)定义各平台的支持情况。
读取开头的 `session.sessionId`,审查 diff,然后在同一 worktree 中用 `--session-id` 发送修改指令,并再次提供目录授权。`--permission-mode` 通过权限服务选择已有权限预设,并在任务运行前覆盖会话保存的选择;省略时保留正常默认值和已保存策略。两个选项都不会修改已保存的配置档或用户设置。
@@ -94,7 +93,7 @@ dsh headless --permission-mode workspace-write --add-dir "$git_dir" --json - < t
### 机器可读输出
-`--json` 用按行 JSON 事件流取代 stdout 的最终文本行,stderr 仅保留 `dsh:` 诊断信息。事件流以 `session`(携带本次运行使用的标识)开头、以 `final` 结尾,其间为 `status`、`text`、`thinking`、`tool_call` 与 `tool_result` 事件。`text` 与 `thinking` 只从已提交的 assistant 消息投影,因此被重试或丢弃的尝试不会进入事件流;它们在步骤提交时到达,而不是逐 token 到达,默认模式的 stderr 推理仍是唯一的实时文本通道。终止 `final` 事件携带精确的 `sessionId`、持久化轮次的 `reason`(最后一轮没有终止事件时为 `null`),以及与默认模式相同的无损答案;该事件不做限长;其他每个字符串与对象键上限为 8 KiB,超出时标记 `truncated`,单条事件行(含换行)上限为 32 KiB——超长事件保留标量字段、丢弃结构化字段,极端情况下只剩 `type` 与 `truncated`,嵌套达到 64 层及以上的负载会在该深度被截断。空工具参数字符串会投影为 `{}`,与执行器实际运行的值一致;而 JSON 无法往返的参数——例如溢出为 `Infinity` 的数字 `1e400`——会保留原始文本,而不是 `JSON.stringify` 会报告的 `null`。runner 在轮次之外抛出的失败会写出 `error` 事件并在没有 `final` 的情况下结束事件流,同时向 stderr 写入 `dsh:` 行;若 profile 自身的插件加载失败,进程会在 runner 挂载前退出,该情形只保留 loader 的 stderr 诊断。轮次内失败的运行仍会以 `final` 事件(通常为空)结束且没有 `error` 事件,因此格式良好的事件流也可能描述一次失败的运行:请把退出码 1 与 `turn_end` 原因作为失败信号。
+`--json` 用按行 JSON 事件流取代 stdout 的最终文本行,stderr 仅保留 `dsh:` 诊断信息。事件流以 `session`(携带本次运行使用的标识)开头、以 `final` 结尾,其间为 `status`、`text`、`thinking`、`tool_call` 与 `tool_result` 事件。`text` 与 `thinking` 只从已提交的 assistant 消息投影,因此被重试或丢弃的尝试不会进入事件流;它们在步骤提交时到达,而不是逐 token 到达,默认模式的 stderr 推理仍是唯一的实时文本通道。终止 `final` 事件携带精确的 `sessionId`、持久化轮次的 `reason`(最后一轮没有终止事件时为 `null`),以及与默认模式相同的无损答案;该事件不做限长;其他每个字符串与对象键上限为 8 KiB,超出时标记 `truncated`,单条事件行(含换行)上限为 32 KiB——超长事件保留标量字段、丢弃结构化字段,极端情况下只剩 `type` 与 `truncated`,嵌套达到 64 层及以上的负载会在该深度被截断。空工具参数字符串会投影为 `{}`,与执行器实际运行的值一致;而 JSON 无法往返的参数——例如溢出为 `Infinity` 的数字 `1e400`——会保留原始文本,而不是 `JSON.stringify` 会报告的 `null`。runner 在轮次之外抛出的失败会写出 `error` 事件并在没有 `final` 的情况下结束事件流,同时向 stderr 写入 `dsh:` 行;若 profile 自身的插件加载失败,进程会在 runner 挂载前退出,该情形只保留 loader 的 stderr 诊断。轮次内失败的运行仍会以 `final` 事件(通常为空)结束且没有 `error` 事件,因此格式良好的事件流也可能描述一次失败的运行:请在判断任务报告前检查进程退出状态和 `final.reason`。
### 何时使用
@@ -116,11 +115,11 @@ runner 是核心 API 载体之上的直接驱动器:它确定 Agent 标识—
### 运行流程
-runner 等待整个应用结算(`ctx.get('loader')?.await()`),确保已组合的工具与适配器不会半挂载,读取共享的 [`agentDefaultModel`](../../core/agent-default-model/README.zh.md) 选择,从配置或 stdin 解析任务,然后确定 Agent 标识:默认是全新的 `session-`,或是 `--session-id` 指名的持久化 Session——通过 [`sessionQuery`](../../session-query/session-query/README.zh.md) 沿用,日志不存在时拒绝。它把任务作为普通用户消息提交。不带 `--json` 时,它把该 Agent 的非空推理增量流式写入 stderr;带 `--json` 时改为投影本次运行。它等待子级工作,并在结束前复核父级仍空闲且 Session 序号未变,让结算通知可以驱动父级生成最终答案。收件箱消息处于停放状态的空闲子级不会延迟完成;进程关闭时丢弃这些未领取消息。随后它对会话执行 flush,并把所属区间(从 `firstSeq` 起)折叠为最后一条非空 `assistant/message` 文本与最终 `turn/end` 原因。最后,它把最终文本写入 stdout(或 `final` 事件)并请求退出。
+runner 等待整个应用结算(`ctx.get('loader')?.await()`),确保已组合的工具与适配器不会半挂载,在读取 stdin 前验证请求的权限预设与 CLI 目录授权,解析任务和共享的 [`agentDefaultModel`](../../core/agent-default-model/README.zh.md) 选择,然后确定 Agent 标识:默认是全新的 `session-`,或是 `--session-id` 指名的持久化 Session——通过 [`sessionQuery`](../../session-query/session-query/README.zh.md) 沿用,日志不存在时拒绝。它把任务作为普通用户消息提交。不带 `--json` 时,它把该 Agent 的非空推理增量流式写入 stderr;带 `--json` 时改为投影本次运行。它等待子级工作,并在结束前复核父级仍空闲且 Session 序号未变,让结算通知可以驱动父级生成最终答案。收件箱消息处于停放状态的空闲子级不会延迟完成;进程关闭时丢弃这些未领取消息。随后它对会话执行 flush,并把所属区间(从 `firstSeq` 起)折叠为最后一条非空 `assistant/message` 文本与最后一轮的终止原因。未闭合的最后一轮没有终止原因,不能继承更早的完成结果。最后,它把最终文本写入 stdout(或 `final` 事件)并请求退出。
### 基于 base 的 patch 内容
-patch 叠加在 `dsh-base` 之上:继承投影缓存与共享 PTC 运行时,在基础 `system-prompt` 行上设置编码 persona 前缀与独立的 cwd 后缀,保留与 Web 表层相同的临时进程级 PTC mode 开关(`DSH_TOOLS_MODE`),禁用共享的 HMR(热模块替换)行,并挂载启动提供方与 runner。缓存为每个已持久化的一次性会话写入检查点,供后续消费方使用;其持久性屏障会在发布缓存行前 flush 所覆盖的日志前缀,因此可能拆分原本会合并的 JSONL 连续段。启动提供方([`src/startup.ts`](src/startup.ts))注入 `ctx.cmdlineArgs`([`dsh-cmdline`](../../boot/cmdline/README.zh.md)),读取位置参数与 `--session-id`/`--json` 选项、打印应用自己的 `--help`,并提供 `headlessStartup`;runner 注入该服务,再从惰性配置中读取任务与运行选项。
+patch 叠加在 `dsh-base` 之上:继承投影缓存与共享 PTC 运行时,在基础 `system-prompt` 行上设置编码 persona 前缀与独立的 cwd 后缀,保留与 Web 表层相同的临时进程级 PTC mode 开关(`DSH_TOOLS_MODE`),禁用共享的 HMR(热模块替换)行,并挂载启动提供方与 runner。缓存为每个已持久化的一次性会话写入检查点,供后续消费方使用;其持久性屏障会在发布缓存行前 flush 所覆盖的日志前缀,因此可能拆分原本会合并的 JSONL 连续段。启动提供方([`src/startup.ts`](src/startup.ts))注入 `ctx.cmdlineArgs`([`dsh-cmdline`](../../boot/cmdline/README.zh.md)),读取任务和调用选项、打印应用自己的 `--help`,并提供 `headlessStartup`;runner 从延迟配置读取任务与运行选项。sandbox-policy 行也等待此服务,在保留基础模式和工作区默认值的同时接收显式目录授权。
### 退出映射
@@ -131,7 +130,7 @@ patch 叠加在 `dsh-base` 之上:继承投影缓存与共享 PTC 运行时,
| 文件 | 职责 |
|---|---|
| [`src/index.ts`](src/index.ts) | `headless-runner` 插件:运行流程、Session 解析、输出约定、退出映射 |
-| [`src/startup.ts`](src/startup.ts) | `headless-startup` 提供方:任务位置参数、`--session-id`、`--json` 与 `--help` |
+| [`src/startup.ts`](src/startup.ts) | `headless-startup` 提供方:任务输入、Session 标识、权限选项与输出选择 |
| [`src/json-stream.ts`](src/json-stream.ts) | `--json` 投影:事件词汇、提交点发射、字符串限长 |
| [`cordis.patch.yml`](cordis.patch.yml) | 叠加在 `dsh-base` 之上的一次性 patch |
| [`tests/headless.spec.ts`](tests/headless.spec.ts) | 运行流程、汇总、flush、Session 沿用与退出映射 |
@@ -173,6 +172,8 @@ runner 不向请求前缀添加任何内容;它只是驱动组合出的配置
- **每次运行一个任务**——任务得到回答后进程即退出;没有交互式后续,因此多步工作请拆成多次运行。
- **持久化历史可能缺少后台 job 的最后一条结算**——进程销毁可能在有主 job 结算前停止 Session 路由或关闭写句柄,正常结束和信号关停也可能如此;[job 生命周期约定](../../../docs/subsystems/jobs.zh.md#session-lifecycle-records)说明消费方如何保留不完整区间。
+- **自定义 startup**——禁用 `headless-startup` 的组合必须[直接配置策略](../../../docs/upgrade-guide/v0.2.1-rc.2/headless-policy-composition/guide.zh.md)。
+- **Windows 额外根目录**——ACL 后端在工具执行时拒绝带额外根目录的子进程调用,不会阻止模型请求。文件系统工具独立执行这些授权。参见[平台限制](../../sandbox/sandbox-local/README.zh.md)。
- **通过 `dsh` 启动器运行**——以其他方式启动 headless profile 会在启动时失败,因为只有启动器能请求进程退出。
- **首个 token 前没有心跳**——默认模式下,提供方发出第一个非空推理增量前,stderr 保持静默;延迟首个 token 的提供方不会更早给出进度信号。
- **推理进入 stderr 日志**——默认模式下,重定向与监督进程可能保留显著更多且可能敏感的模型输出;需要时应把 stderr 路由到受控位置。
diff --git a/packages/bundle/headless/cordis.patch.yml b/packages/bundle/headless/cordis.patch.yml
index c3856e5bc4..2005181565 100644
--- a/packages/bundle/headless/cordis.patch.yml
+++ b/packages/bundle/headless/cordis.patch.yml
@@ -1,8 +1,8 @@
# The dsh-headless bundle patch: one-shot task mode directly over dsh-base.
# It mounts no Host, HTTP server, Web runtime, or browser plugin. An ordinary
# provider plugin injects `cmdlineArgs`, parses the task positional
-# (`dsh --profile headless ""`), the `--session-id` and `--json` options,
-# and this app's --help, then the direct driver creates a fresh Agent, or adopts
+# (`dsh --profile headless ""`), Session identity, permission and output
+# options, and this app's --help, then the direct driver creates a fresh Agent, or adopts
# the exact Session the flag names, through the core registry and prints its
# durable result.
@@ -29,8 +29,8 @@
sessionId: !!js ctx.headlessStartup.sessionId
json: !!js ctx.headlessStartup.json
permissionMode: !!js ctx.headlessStartup.permissionMode
- additionalWritableRoots: !!js ctx.headlessStartup.additionalWritableRoots
+# Replacing config retains the base mode/root defaults while adding the CLI grants.
- id: sandbox-policy
inject: [headlessStartup]
config:
diff --git a/packages/bundle/headless/src/index.ts b/packages/bundle/headless/src/index.ts
index 8cb5081692..09c6136d7c 100644
--- a/packages/bundle/headless/src/index.ts
+++ b/packages/bundle/headless/src/index.ts
@@ -32,6 +32,7 @@ import type {} from '@deepseek-ai/dsh-session-query'
import type {} from '@deepseek-ai/dsh-subagent'
import type {} from '@deepseek-ai/dsh-permission-presets'
import type {} from '@deepseek-ai/dsh-sandbox-policy'
+import type {} from './startup.ts'
import { internals } from './runner-internals.ts'
import { projectJsonRun, boundJsonLine } from './json-stream.ts'
@@ -51,8 +52,6 @@ export interface Config {
json?: boolean
/** Permission preset applied before the task; omission keeps the session's current policy. */
permissionMode?: string
- /** Absolute directories requested by the CLI; the composed sandbox policy must include each one. */
- additionalWritableRoots?: string[]
}
export const Config: z = z.object({
@@ -60,7 +59,6 @@ export const Config: z = z.object({
sessionId: z.string(),
json: z.boolean(),
permissionMode: z.string(),
- additionalWritableRoots: z.array(z.string()).default([]),
})
/** Outcome of one owned run interval. */
@@ -77,7 +75,7 @@ interface HeadlessIo {
exit(code: number): void
}
-/** Aggregate the last assistant text and turn outcome in one owned interval. */
+/** Aggregate the interval's last assistant text; only its final turn can establish completion. */
function summarize(session: Session, firstSeq: SessionLogOffset): RunOutcome {
let started = false
let text = ''
@@ -314,7 +312,7 @@ function fail(io: HeadlessIo, error: unknown, json: boolean): void {
* Run one task through one Agent and request process exit.
* Live Agent failures require a later durable terminal before a recovered run can succeed.
* @param ctx - plugin context carrying the Agent, default model, Session, and launcher IO services.
- * @param config - task, optional exact Session identity, and output mode.
+ * @param config - task, exact Session identity, output mode, and permission preset.
* @param io - process-facing effects.
*/
async function run(ctx: Context, config: Config, io: HeadlessIo): Promise {
@@ -333,6 +331,19 @@ async function run(ctx: Context, config: Config, io: HeadlessIo): Promise
throw new Error('headless-runner: sessionId must not be blank')
}
+ const permissions = ctx.get('permissionPresets')
+ if (config.permissionMode !== undefined) {
+ if (permissions === undefined) throw new Error('headless --permission-mode requires the permissionPresets service')
+ permissions.resolve(config.permissionMode)
+ }
+ const requestedRoots = ctx.get('headlessStartup')?.additionalWritableRoots
+ if (requestedRoots !== undefined && requestedRoots.length > 0) {
+ const policy = ctx.get('sandboxPolicy')
+ if (policy === undefined || requestedRoots.some(root => !policy.additionalWritableRoots?.includes(root))) {
+ throw new Error('headless --add-dir requires the sandbox-policy row to include headlessStartup.additionalWritableRoots; check the profile overlays')
+ }
+ }
+
const task = config.task === undefined || config.task === '-'
? await internals.readStdin()
: config.task
@@ -341,17 +352,6 @@ async function run(ctx: Context, config: Config, io: HeadlessIo): Promise
}
const selection = defaultModel.currentSelection()
- const permissions = ctx.get('permissionPresets')
- if (config.permissionMode !== undefined) {
- if (permissions === undefined) throw new Error('headless --permission-mode requires the permissionPresets service')
- permissions.resolve(config.permissionMode)
- }
- if (config.additionalWritableRoots !== undefined && config.additionalWritableRoots.length > 0) {
- const policy = ctx.get('sandboxPolicy')
- if (policy === undefined || config.additionalWritableRoots.some(root => !policy.additionalWritableRoots?.includes(root))) {
- throw new Error('headless --add-dir requires the sandbox-policy row to include headlessStartup.additionalWritableRoots; check the profile overlays')
- }
- }
const agentOptions = { provider: selection.provider, model: selection.model }
// This bundle composes no preset roster, so the model-facing rows sit in the
// host plane and the agent reads them from the global layer. A deployment
diff --git a/packages/bundle/headless/src/json-stream.ts b/packages/bundle/headless/src/json-stream.ts
index db4e25a00f..d014265f15 100644
--- a/packages/bundle/headless/src/json-stream.ts
+++ b/packages/bundle/headless/src/json-stream.ts
@@ -232,8 +232,8 @@ function addUsage(state: StepUsageState, next: StepUsage | undefined): StepUsage
* The opening `session` event is written before the subscription starts, so a
* caller must invoke this before submitting the task. Text and reasoning are
* emitted only when the step's `assistant/message` commits them, and the
- * terminal `final` event carries the same lossless answer the default mode
- * prints (it is deliberately not bounded).
+ * terminal `final` event carries the exact Session identity, the durable turn
+ * reason (null without a terminal turn), and the lossless answer. It is not bounded.
* @param ctx - plugin context carrying the live Session feed.
* @param agent - the exact Agent whose events belong to this invocation.
* @param sink - stdout sink receiving one JSON object per line.
diff --git a/packages/bundle/headless/src/startup.ts b/packages/bundle/headless/src/startup.ts
index 455834744d..9458b91040 100644
--- a/packages/bundle/headless/src/startup.ts
+++ b/packages/bundle/headless/src/startup.ts
@@ -36,6 +36,13 @@ export interface HeadlessStartupValues {
additionalWritableRoots?: string[]
}
+declare module '@deepseek-ai/cordis' {
+ interface Context {
+ /** Parsed arguments of this headless invocation. */
+ headlessStartup: HeadlessStartupValues
+ }
+}
+
/**
* This app's command: the task positional, its options, and its help text.
* @returns a fresh program, so one process can parse more than once (tests).
@@ -48,7 +55,8 @@ function headlessCommand(): Command {
.option('--json', 'write newline-delimited run events to stdout instead of the final message')
.option('--session-id ', 'adopt the persisted Session with this id; an unknown id is an error')
.option('--permission-mode ', 'select a configured permission preset for this task, including on resume')
- .option('--add-dir ', 'allow an additional absolute directory under workspace-write (repeatable)', (value: string, previous?: string[]) => [...previous ?? [], value])
+ .option('--add-dir ', 'allow an additional physical absolute directory under workspace-write (repeatable)',
+ (value: string, previous?: string[]) => [...previous ?? [], value])
.argument('[task...]', 'the task text; multiple words are joined by spaces, and `-` reads stdin')
.addHelpText('after', `
Examples:
@@ -56,7 +64,8 @@ Examples:
echo "run the tests" | dsh --profile headless read the task from stdin
dsh --profile headless --json "run the tests" emit machine-readable run events
dsh --profile headless --session-id session-… "continue" resume an existing Session
- dsh headless --permission-mode workspace-write --add-dir /repo/.git "commit the fix"
+ dsh --profile headless --permission-mode workspace-write --add-dir /repo/.git "commit the fix"
+ grant shared Git metadata for this task
`)
}
@@ -129,7 +138,7 @@ export function apply(ctx: Context): void {
task,
sessionId,
json: options.json === true,
- ...options.permissionMode === undefined ? {} : { permissionMode: options.permissionMode },
+ ...options.permissionMode === undefined ? {} : { permissionMode: options.permissionMode.trim() },
...options.addDir === undefined ? {} : { additionalWritableRoots: [...new Set(options.addDir)] },
} satisfies HeadlessStartupValues)
})
diff --git a/packages/bundle/headless/tests/headless.spec.ts b/packages/bundle/headless/tests/headless.spec.ts
index fbcc9d7951..108571927b 100644
--- a/packages/bundle/headless/tests/headless.spec.ts
+++ b/packages/bundle/headless/tests/headless.spec.ts
@@ -131,6 +131,12 @@ async function bench(script: Script, options: BenchOptions = {}): Promise<{
run(): Promise<{ code: number; out: string; err: string; order: string[] }>
}> {
const ctx = new Context()
+ if (options.additionalWritableRoots !== undefined) {
+ ctx.provide('headlessStartup', {
+ task: options.task, sessionId: options.sessionId, json: options.json ?? false,
+ additionalWritableRoots: options.additionalWritableRoots,
+ })
+ }
provideWorkingDirectoryFixture(ctx)
if (options.filesystemCwd !== undefined) {
const cwd = options.filesystemCwd
@@ -229,7 +235,6 @@ async function bench(script: Script, options: BenchOptions = {}): Promise<{
...options.sessionId === undefined ? {} : { sessionId: options.sessionId },
...options.json === undefined ? {} : { json: options.json },
...options.permissionMode === undefined ? {} : { permissionMode: options.permissionMode },
- ...options.additionalWritableRoots === undefined ? {} : { additionalWritableRoots: options.additionalWritableRoots },
})
return { code: await exited, out, err, order }
},
@@ -259,7 +264,9 @@ describe('headless runner', () => {
})
it('rejects an unknown preset before creating an Agent', async () => {
- const test = await bench({ afterPrompt() { throw new Error('must not run') } }, { permissionMode: 'missing' })
+ const test = await bench({ afterPrompt() { throw new Error('must not run') } }, {
+ permissionMode: 'missing', useStdin: true, readStdin() { throw new Error('must not read stdin') },
+ })
test.ctx.provide('permissionPresets', { resolve() { throw new Error('unknown preset') } } as never)
try {
const result = await test.run()
@@ -1230,8 +1237,8 @@ describe('headless runner', () => {
})
it('validates config: the task and run options are optional', () => {
- expect(new Config({})).toEqual({ additionalWritableRoots: [] })
+ expect(new Config({})).toEqual({})
expect(new Config({ task: 'x', sessionId: 'session-x', json: true }))
- .toEqual({ task: 'x', sessionId: 'session-x', json: true, additionalWritableRoots: [] })
+ .toEqual({ task: 'x', sessionId: 'session-x', json: true })
})
})
diff --git a/packages/bundle/headless/tests/startup.spec.ts b/packages/bundle/headless/tests/startup.spec.ts
index 614ae696d3..02340a0489 100644
--- a/packages/bundle/headless/tests/startup.spec.ts
+++ b/packages/bundle/headless/tests/startup.spec.ts
@@ -75,6 +75,7 @@ export const apply = ctx => globalThis.__headlessStartupApply(ctx)
' task: !!js ctx.headlessStartup.task',
' sessionId: !!js ctx.headlessStartup.sessionId',
' json: !!js ctx.headlessStartup.json',
+ ' permissionMode: !!js ctx.headlessStartup.permissionMode',
'- id: headless-startup',
` name: ${pathToFileURL(join(dir, 'startup.mjs')).href}`,
'',
@@ -155,8 +156,9 @@ describe('headless command-line provider', () => {
it('accepts an explicit permission preset and deduplicates absolute directory grants', async () => {
const directory = join(tmpdir(), 'headless-grant')
- const { task } = await bootStartup(['--permission-mode', 'workspace-write', '--add-dir', directory, '--add-dir', directory, 'do it'])
+ const { task, observed } = await bootStartup(['--permission-mode', 'workspace-write', '--add-dir', directory, '--add-dir', directory, 'do it'])
expect(task).toMatchObject({ permissionMode: 'workspace-write', additionalWritableRoots: [directory], task: 'do it' })
+ expect(observed.runnerConfig).toMatchObject({ permissionMode: 'workspace-write' })
})
it.each([
diff --git a/packages/bundle/headless/tsconfig.json b/packages/bundle/headless/tsconfig.json
index 0b3fad2d8b..0a4bb2cc6a 100644
--- a/packages/bundle/headless/tsconfig.json
+++ b/packages/bundle/headless/tsconfig.json
@@ -8,12 +8,6 @@
"src"
],
"references": [
- {
- "path": "../../interaction/permission-presets"
- },
- {
- "path": "../../sandbox/sandbox-policy"
- },
{
"path": "../../../vendor/cordis"
},
@@ -46,6 +40,12 @@
},
{
"path": "../../subagent/subagent"
+ },
+ {
+ "path": "../../interaction/permission-presets"
+ },
+ {
+ "path": "../../sandbox/sandbox-policy"
}
]
}
diff --git a/packages/client/product-analytics/tests/fixtures/environment.patch.yml b/packages/client/product-analytics/tests/fixtures/environment.patch.yml
index 8d150bedc1..4e96322d0d 100644
--- a/packages/client/product-analytics/tests/fixtures/environment.patch.yml
+++ b/packages/client/product-analytics/tests/fixtures/environment.patch.yml
@@ -17,3 +17,9 @@
config:
enabled: true
appVersion: synthetic-release
+
+- id: sandbox-policy
+ inject: []
+ config:
+ mode: !!js process.env.DSH_PERMISSION_MODE ?? 'workspace-write'
+ workspaceRoot: !!js process.cwd()
diff --git a/packages/context/time-context/tests/fixtures/time-context.patch.yml b/packages/context/time-context/tests/fixtures/time-context.patch.yml
index 399d5d78ac..9da6cbee60 100644
--- a/packages/context/time-context/tests/fixtures/time-context.patch.yml
+++ b/packages/context/time-context/tests/fixtures/time-context.patch.yml
@@ -25,3 +25,9 @@
- id: time-context
name: '@deepseek-ai/dsh-time-context'
+
+- id: sandbox-policy
+ inject: []
+ config:
+ mode: !!js process.env.DSH_PERMISSION_MODE ?? 'workspace-write'
+ workspaceRoot: !!js process.cwd()
diff --git a/packages/extensions/tool-cordis/src/api-catalog.ts b/packages/extensions/tool-cordis/src/api-catalog.ts
index a496012359..59fbbb41ab 100644
--- a/packages/extensions/tool-cordis/src/api-catalog.ts
+++ b/packages/extensions/tool-cordis/src/api-catalog.ts
@@ -1273,6 +1273,38 @@ export const SERVICE_API: readonly ServiceApiEntry[] = [
},
],
},
+ {
+ key: 'headlessStartup',
+ summary: 'What the runner row reads from HEADLESS_STARTUP_SERVICE.',
+ description: 'What the runner row reads from HEADLESS_STARTUP_SERVICE.',
+ methods: [
+ {
+ signature: 'task: string | undefined',
+ description: 'The task text this invocation asked for; absent when the runner reads stdin.',
+ parameters: [],
+ },
+ {
+ signature: 'sessionId: string | undefined',
+ description: 'Exact Session identity to adopt; absent for a fresh random identity.',
+ parameters: [],
+ },
+ {
+ signature: 'json: boolean',
+ description: 'Whether stdout carries the machine-readable event stream instead of final text.',
+ parameters: [],
+ },
+ {
+ signature: 'permissionMode?: string',
+ description: 'Permission preset to apply before the task; absent preserves session policy.',
+ parameters: [],
+ },
+ {
+ signature: 'additionalWritableRoots?: string[]',
+ description: 'Explicit absolute write grants for this invocation; absent adds no directories.',
+ parameters: [],
+ },
+ ],
+ },
{
key: 'hmr',
summary: 'Hot reload service with Cordis-compatible module configuration and events.',
diff --git a/packages/goal/goal/tests/fixtures/domain/goal.patch.yml b/packages/goal/goal/tests/fixtures/domain/goal.patch.yml
index bc7af0b9e7..4562e37b5e 100644
--- a/packages/goal/goal/tests/fixtures/domain/goal.patch.yml
+++ b/packages/goal/goal/tests/fixtures/domain/goal.patch.yml
@@ -29,3 +29,9 @@
- id: seed-goal
name: './seed-goal.ts'
+
+- id: sandbox-policy
+ inject: []
+ config:
+ mode: !!js process.env.DSH_PERMISSION_MODE ?? 'workspace-write'
+ workspaceRoot: !!js process.cwd()
diff --git a/packages/host/product-telemetry-otel/tests/fixtures/shutdown.patch.yml b/packages/host/product-telemetry-otel/tests/fixtures/shutdown.patch.yml
index 60ea02a782..a5ea354489 100644
--- a/packages/host/product-telemetry-otel/tests/fixtures/shutdown.patch.yml
+++ b/packages/host/product-telemetry-otel/tests/fixtures/shutdown.patch.yml
@@ -16,3 +16,9 @@
timeoutMillis: 15000
exportTimeoutMillis: 20000
shutdownTimeoutMillis: 2000
+
+- id: sandbox-policy
+ inject: []
+ config:
+ mode: !!js process.env.DSH_PERMISSION_MODE ?? 'workspace-write'
+ workspaceRoot: !!js process.cwd()
diff --git a/packages/host/product-telemetry-otel/tests/fixtures/telemetry.patch.yml b/packages/host/product-telemetry-otel/tests/fixtures/telemetry.patch.yml
index d9104a0bfb..33b98e539f 100644
--- a/packages/host/product-telemetry-otel/tests/fixtures/telemetry.patch.yml
+++ b/packages/host/product-telemetry-otel/tests/fixtures/telemetry.patch.yml
@@ -13,3 +13,9 @@
serviceVersion: !!js process.env.DSH_APP_VERSION
compression: gzip
scheduledDelayMillis: 60000
+
+- id: sandbox-policy
+ inject: []
+ config:
+ mode: !!js process.env.DSH_PERMISSION_MODE ?? 'workspace-write'
+ workspaceRoot: !!js process.cwd()
diff --git a/packages/session/session-telemetry-otel/tests/fixtures/telemetry.patch.yml b/packages/session/session-telemetry-otel/tests/fixtures/telemetry.patch.yml
index c5542517a8..9ab74bb2c6 100644
--- a/packages/session/session-telemetry-otel/tests/fixtures/telemetry.patch.yml
+++ b/packages/session/session-telemetry-otel/tests/fixtures/telemetry.patch.yml
@@ -38,3 +38,9 @@
# shipped telemetry plugin deliberately owns none.
- id: telemetry-redact-rule
name: './telemetry-redact-rule.ts'
+
+- id: sandbox-policy
+ inject: []
+ config:
+ mode: !!js process.env.DSH_PERMISSION_MODE ?? 'workspace-write'
+ workspaceRoot: !!js process.cwd()
diff --git a/packages/subagent/subagent-acp/tests/fixtures/loader/acp.patch.yml b/packages/subagent/subagent-acp/tests/fixtures/loader/acp.patch.yml
index 1ec2245d7e..2ba98a9cf2 100644
--- a/packages/subagent/subagent-acp/tests/fixtures/loader/acp.patch.yml
+++ b/packages/subagent/subagent-acp/tests/fixtures/loader/acp.patch.yml
@@ -38,3 +38,9 @@
- !!js process.env.DSH_TEST_MOCK_ACP_SERVER
permission: reject
env: !!js "process.env.DSH_TEST_ACP_MODE === 'diagnostic' ? { MOCK_TEXT: 'partial loader answer', MOCK_STOP: 'max_turn_requests' } : { MOCK_ECHO_CWD: '1' }"
+
+- id: sandbox-policy
+ inject: []
+ config:
+ mode: !!js process.env.DSH_PERMISSION_MODE ?? 'workspace-write'
+ workspaceRoot: !!js process.cwd()
diff --git a/packages/subagent/subagent-claude-code/tests/fixtures/loader/claude-code.patch.yml b/packages/subagent/subagent-claude-code/tests/fixtures/loader/claude-code.patch.yml
index 46ee07bd32..20168e019b 100644
--- a/packages/subagent/subagent-claude-code/tests/fixtures/loader/claude-code.patch.yml
+++ b/packages/subagent/subagent-claude-code/tests/fixtures/loader/claude-code.patch.yml
@@ -55,3 +55,9 @@
provider: claude-secondary
toolName: subagent_claude_secondary
maxDepth: 'provider-managed'
+
+- id: sandbox-policy
+ inject: []
+ config:
+ mode: !!js process.env.DSH_PERMISSION_MODE ?? 'workspace-write'
+ workspaceRoot: !!js process.cwd()
diff --git a/packages/subagent/subagent-codex/tests/fixtures/loader/codex.patch.yml b/packages/subagent/subagent-codex/tests/fixtures/loader/codex.patch.yml
index 81e2d1b000..1b917b4cfe 100644
--- a/packages/subagent/subagent-codex/tests/fixtures/loader/codex.patch.yml
+++ b/packages/subagent/subagent-codex/tests/fixtures/loader/codex.patch.yml
@@ -45,3 +45,9 @@
provider: codex-secondary
toolName: subagent_codex_secondary
maxDepth: 'provider-managed'
+
+- id: sandbox-policy
+ inject: []
+ config:
+ mode: !!js process.env.DSH_PERMISSION_MODE ?? 'workspace-write'
+ workspaceRoot: !!js process.cwd()
diff --git a/packages/subagent/subagent-dsh-sdk/tests/fixtures/loader/dsh-sdk.patch.yml b/packages/subagent/subagent-dsh-sdk/tests/fixtures/loader/dsh-sdk.patch.yml
index 5bd7fa4df3..d0e0718a02 100644
--- a/packages/subagent/subagent-dsh-sdk/tests/fixtures/loader/dsh-sdk.patch.yml
+++ b/packages/subagent/subagent-dsh-sdk/tests/fixtures/loader/dsh-sdk.patch.yml
@@ -55,3 +55,9 @@
env:
DSH_TELEMETRY_DISABLED: '1'
DSH_TEST_CHILD_FAILURE: !!js String(process.env.DSH_TEST_CHILD_FAILURE ?? '')
+
+- id: sandbox-policy
+ inject: []
+ config:
+ mode: !!js process.env.DSH_PERMISSION_MODE ?? 'workspace-write'
+ workspaceRoot: !!js process.cwd()
diff --git a/scripts/gen-cordis-catalog.ts b/scripts/gen-cordis-catalog.ts
index ebd7527142..de705e4bb2 100644
--- a/scripts/gen-cordis-catalog.ts
+++ b/scripts/gen-cordis-catalog.ts
@@ -50,6 +50,7 @@ export { REGION_BEGIN, REGION_END }
* errors, so the partition can never silently drift from the service API.
*/
export const SERVICE_PAGE: Record = {
+ headlessStartup: 'boot.md',
translator: 'translation.md',
cotTranslation: 'translation.md',
speechToText: 'voice-input.md',
diff --git a/scripts/gen-doc-graphs.ts b/scripts/gen-doc-graphs.ts
index 69eedf37b2..7b4b9df647 100644
--- a/scripts/gen-doc-graphs.ts
+++ b/scripts/gen-doc-graphs.ts
@@ -107,6 +107,14 @@ const GROUP_ORDER = [
]
const SERVICE_ROLES: ServiceRole[] = [
+ {
+ key: 'headlessStartup',
+ pkg: 'headless',
+ title: 'Headless invocation options',
+ mode: 'core',
+ consumers: ['headless'],
+ note: 'Supplies parsed task and run options to the headless runner and its sandbox-policy configuration.',
+ },
{
key: 'cotTranslation',
pkg: 'experimental-client-ui-cot-translation',
diff --git a/snapshots/session/session-sandbox-root/cordis.snapshot.yml b/snapshots/session/session-sandbox-root/cordis.snapshot.yml
index 1e8aac0571..2f1112860a 100644
--- a/snapshots/session/session-sandbox-root/cordis.snapshot.yml
+++ b/snapshots/session/session-sandbox-root/cordis.snapshot.yml
@@ -51,6 +51,7 @@
inject: [headlessStartup]
config:
task: !!js ctx.headlessStartup.task
+ sessionId: !!js ctx.headlessStartup.sessionId
json: !!js ctx.headlessStartup.json
permissionMode: workspace-write
diff --git a/snapshots/session/session-sandbox-root/cordis.yml b/snapshots/session/session-sandbox-root/cordis.yml
index 3dc7c64f9e..4c1988708e 100644
--- a/snapshots/session/session-sandbox-root/cordis.yml
+++ b/snapshots/session/session-sandbox-root/cordis.yml
@@ -13,5 +13,6 @@
inject: [headlessStartup]
config:
task: !!js ctx.headlessStartup.task
+ sessionId: !!js ctx.headlessStartup.sessionId
json: !!js ctx.headlessStartup.json
permissionMode: workspace-write