2026-06-20 16:24:56 +08:00
# Bash Executor
2026-08-13 00:36:22 +08:00
English | [中文 ](shell.zh.md )
2026-07-15 23:11:25 -07:00
2026-08-13 11:55:38 +08:00
The bash execution seam is split across a Service Definition ([dsh-shell ](../../packages/shell/shell ), `ctx.shell` ), Service Providers ([dsh-bash-local ](../../packages/shell/bash-local ) and [dsh-bash-sandbox ](../../packages/shell/bash-sandbox )), and Consumer ([dsh-tool-bash ](../../packages/shell/tool-bash ), the `bash` schema). Generic background-job ids, ownership, and controls live in [jobs.md ](jobs.md ); this seam returns a task-free process handle. Raw process-group mechanics live behind the [subprocess seam ](subprocess.md ).
2026-06-20 16:24:56 +08:00
2026-08-13 00:36:22 +08:00
Source: [`packages/shell/shell/src/types.ts` ](../../packages/shell/shell/src/types.ts )
2026-06-20 16:24:56 +08:00
2026-07-16 16:45:52 +08:00
## Managed shell environment namespace
fix(bash): close managed environment review gaps
The managed DSH_* runtime path was correct, but its public extension and documentation contracts were incomplete. A contributor following the README could access ctx.bashEnv without declaring an injection, the new environment types had no drift-checked catalog entries, and the capability graph omitted three packages that now query sessionPersistence.
Declare the README injection, catalog DshEnvironmentKey and DshEnvironment, and add tool-bash plus both hook bridges to the generated persistence consumer graph. Keep BashEnvRegistry.list() contributor-only for now because no production caller treats it as exhaustive, but record the built-in enumeration gap before diagnostics, prompt, or UI code depends on it.
Validated on the exact resulting tree with typecheck, lint, coverage, snapshot, documentation, module-graph, build, hygiene, demo-smoke, and built-artifact checks.
2026-07-15 00:04:00 +08:00
2026-08-13 00:36:22 +08:00
`DSH_*` variables are Harness-owned child-process facts. The model-facing bash tool collects them through `ctx.shellEnv` and passes them through `ShellExecRequest.dshEnv` ; the subprocess service removes inherited `DSH_*` names before merging the current snapshot. The `DshEnvironmentKey` /`DshEnvironment` vocabulary is owned by the [subprocess seam ](subprocess.md ) and re-exported by `dsh-shell` .
fix(bash): close managed environment review gaps
The managed DSH_* runtime path was correct, but its public extension and documentation contracts were incomplete. A contributor following the README could access ctx.bashEnv without declaring an injection, the new environment types had no drift-checked catalog entries, and the capability graph omitted three packages that now query sessionPersistence.
Declare the README injection, catalog DshEnvironmentKey and DshEnvironment, and add tool-bash plus both hook bridges to the generated persistence consumer graph. Keep BashEnvRegistry.list() contributor-only for now because no production caller treats it as exhaustive, but record the built-in enumeration gap before diagnostics, prompt, or UI code depends on it.
Validated on the exact resulting tree with typecheck, lint, coverage, snapshot, documentation, module-graph, build, hygiene, demo-smoke, and built-artifact checks.
2026-07-15 00:04:00 +08:00
2026-06-20 16:24:56 +08:00
## Request vs. spec: the `resolve()` split
2026-08-13 00:36:22 +08:00
The seam separates the **model-/plugin-facing request** (optional `workdir` /`timeoutMs` /`stdoutMaxBytes` , filled from config or request policy) from the **fully-resolved spec** the executor acts on (those fields required). The tool layer calls `ctx.shell.resolve(request)` between them (the repo's "explicit > implicit at package boundaries" rule); a `ShellExecSpec` carries resolved values.
2026-06-20 16:24:56 +08:00
```ts type-equiv
2026-07-19 12:25:40 +08:00
/**
* A caller's execution REQUEST: `workdir` and `timeoutMs` are optional and
2026-08-13 00:36:22 +08:00
* filled by {@link ShellExecutor.resolve} from the implementation's config.
2026-07-19 12:25:40 +08:00
* This is the model-/plugin-facing shape; pass it to `resolve()` to obtain a
2026-08-13 00:36:22 +08:00
* fully-resolved {@link ShellExecSpec}.
2026-07-19 12:25:40 +08:00
*/
2026-08-13 00:36:22 +08:00
interface ShellExecRequest {
2026-06-20 16:24:56 +08:00
command: string
/** Working directory override (default: implementation-configured). */
workdir?: string | undefined
/** Timeout override in milliseconds (implementations cap it). */
timeoutMs?: number | undefined
2026-07-10 11:53:02 +08:00
/**
* Foreground stdout capture budget in bytes. Absent uses the executor's
* default output cap. Trusted in-process consumers use this when they must
* parse complete stdout up to their own bounded limit; the model-facing bash
* tool does not expose it as a parameter.
*/
stdoutMaxBytes?: number | undefined
2026-06-20 16:24:56 +08:00
/** Abort signal — implementations kill the command when it fires. */
signal?: AbortSignal | undefined
feat(bash): add stdin + extra env to the executor seam as a trusted-plugin surface
The hooks subsystem runs external hook commands the Claude Code / Codex way:
JSON payload on stdin, context in CLAUDE_PROJECT_DIR / CLAUDE_PLUGIN_ROOT env.
Reusing the ctx.bash seam for that needs two new inputs — but stdin and arbitrary
env are exactly what dsh-bash-local's credential scrub exists to keep away from
model-driven commands. So this adds them as a TRUSTED-PLUGIN surface:
- BashExecRequest + BashExecSpec gain optional `stdin` and `env`. They are plain
optionals on the resolved spec (not required-but-nullable like `owner`): a
missing one means "none", the safe default, not a security footgun.
- dsh-bash-local threads them through resolve/run/start. `env` merges AFTER the
credential scrub, so a trusted caller's explicit entry wins even on a
credential-shaped name — the scrub guards the harness's OWN ambient creds from
model-driven commands, not a trusted plugin. stdin is always a pipe, closed
immediately (with bytes when supplied, empty otherwise — EOF as before); an
EPIPE from a child that exits without reading is swallowed.
- The model-facing dsh-tool-bash NEVER forwards model input into stdin/env (its
request is command/workdir/timeoutMs/signal/owner only). A regression guard
drives the real tool with adversarial args and asserts the request carries
neither field — proven to go red if the consumer ever forwards them.
Configurable scrub (in an earlier sketch) is dropped as speculative: the explicit
`env` field already gives a trusted caller full control, and no caller needs to
broaden the ambient scrub. Documented in a new architecture RFC, the bash.md
type-equiv blocks, and the three bash READMEs.
2026-06-30 13:52:25 +08:00
/**
* Bytes to write to the command's stdin, then close it. Absent leaves stdin
docs(bash): reframe stdin/env — the scrub is the security control, not a trust boundary
Address review: the "trusted-plugin surface" framing overstated the security
story. A model driving the `bash` tool already has equivalent power to set env
vars and feed stdin through ordinary shell syntax (`FOO=bar cmd`, heredocs), so
the `env`/`stdin` seam fields grant it no new capability — and they cannot
exfiltrate the harness's ambient credentials, because the credential SCRUB in
dsh-bash-local (which strips *KEY*/*SECRET*/*TOKEN* from process.env before the
child sees it) is the actual control, and it works regardless of these fields
(tool-call args are static JSON, never shell-evaluated).
So drop the "dangerous / trusted-plugin boundary" language across the RFC, the
three bash-package READMEs, the bash/src/types.ts JSDoc, and docs/bash.md (both
the type-equiv blocks — kept 1:1 with source — and the prose). The reality that
remains: the `bash` tool doesn't EXPOSE env/stdin as parameters because they'd
be redundant with shell syntax; the fields exist for in-process plugins (the
hooks bridges) to pass a JSON payload + CLAUDE_* vars cleanly. The guard test is
kept but reframed: it catches a future `...args` spread that would silently
forward model input into the post-scrub env merge, NOT a trust wall. No code or
behavior change.
2026-07-02 04:08:24 +08:00
* closed/empty (the default for model-driven tool calls). Set by in-process
* plugins (e.g. the hooks bridges, which write a hook command's JSON payload
* to its stdin); the model-facing bash tool does not expose it as a parameter
* (a model that needs stdin uses shell syntax like a heredoc or a pipe).
feat(bash): add stdin + extra env to the executor seam as a trusted-plugin surface
The hooks subsystem runs external hook commands the Claude Code / Codex way:
JSON payload on stdin, context in CLAUDE_PROJECT_DIR / CLAUDE_PLUGIN_ROOT env.
Reusing the ctx.bash seam for that needs two new inputs — but stdin and arbitrary
env are exactly what dsh-bash-local's credential scrub exists to keep away from
model-driven commands. So this adds them as a TRUSTED-PLUGIN surface:
- BashExecRequest + BashExecSpec gain optional `stdin` and `env`. They are plain
optionals on the resolved spec (not required-but-nullable like `owner`): a
missing one means "none", the safe default, not a security footgun.
- dsh-bash-local threads them through resolve/run/start. `env` merges AFTER the
credential scrub, so a trusted caller's explicit entry wins even on a
credential-shaped name — the scrub guards the harness's OWN ambient creds from
model-driven commands, not a trusted plugin. stdin is always a pipe, closed
immediately (with bytes when supplied, empty otherwise — EOF as before); an
EPIPE from a child that exits without reading is swallowed.
- The model-facing dsh-tool-bash NEVER forwards model input into stdin/env (its
request is command/workdir/timeoutMs/signal/owner only). A regression guard
drives the real tool with adversarial args and asserts the request carries
neither field — proven to go red if the consumer ever forwards them.
Configurable scrub (in an earlier sketch) is dropped as speculative: the explicit
`env` field already gives a trusted caller full control, and no caller needs to
broaden the ambient scrub. Documented in a new architecture RFC, the bash.md
type-equiv blocks, and the three bash READMEs.
2026-06-30 13:52:25 +08:00
*/
stdin?: string | undefined
/**
2026-07-16 16:45:52 +08:00
* Ordinary environment entries for the command, merged after the credential
2026-07-27 04:14:51 +08:00
* scrub. Managed facts belong in {@link dshEnv}, which merges after this
* map, so an entry here can never displace one. Set by in-process plugins
* (the hooks bridges set `CLAUDE_PROJECT_DIR` , `CLAUDE_PLUGIN_ROOT` , …); the
* model-facing bash tool does not expose it as a parameter.
feat(bash): add stdin + extra env to the executor seam as a trusted-plugin surface
The hooks subsystem runs external hook commands the Claude Code / Codex way:
JSON payload on stdin, context in CLAUDE_PROJECT_DIR / CLAUDE_PLUGIN_ROOT env.
Reusing the ctx.bash seam for that needs two new inputs — but stdin and arbitrary
env are exactly what dsh-bash-local's credential scrub exists to keep away from
model-driven commands. So this adds them as a TRUSTED-PLUGIN surface:
- BashExecRequest + BashExecSpec gain optional `stdin` and `env`. They are plain
optionals on the resolved spec (not required-but-nullable like `owner`): a
missing one means "none", the safe default, not a security footgun.
- dsh-bash-local threads them through resolve/run/start. `env` merges AFTER the
credential scrub, so a trusted caller's explicit entry wins even on a
credential-shaped name — the scrub guards the harness's OWN ambient creds from
model-driven commands, not a trusted plugin. stdin is always a pipe, closed
immediately (with bytes when supplied, empty otherwise — EOF as before); an
EPIPE from a child that exits without reading is swallowed.
- The model-facing dsh-tool-bash NEVER forwards model input into stdin/env (its
request is command/workdir/timeoutMs/signal/owner only). A regression guard
drives the real tool with adversarial args and asserts the request carries
neither field — proven to go red if the consumer ever forwards them.
Configurable scrub (in an earlier sketch) is dropped as speculative: the explicit
`env` field already gives a trusted caller full control, and no caller needs to
broaden the ambient scrub. Documented in a new architecture RFC, the bash.md
type-equiv blocks, and the three bash READMEs.
2026-06-30 13:52:25 +08:00
*/
env?: Record< string , string > | undefined
2026-06-20 16:24:56 +08:00
/**
2026-07-27 04:14:51 +08:00
* Harness-owned `DSH_*` variables for this execution (typed to managed
* keys). Executors discard ambient `DSH_*` entries before merging this
* snapshot last, so an unavailable current fact cannot inherit a stale
* value from the harness process and a caller {@link env} entry cannot
* displace a managed one.
2026-06-20 16:24:56 +08:00
*/
2026-07-12 15:41:42 +08:00
dshEnv?: DshEnvironment | undefined
2026-07-21 00:44:28 +08:00
/** Fully resolved per-call sandbox policy; sandboxing executors default it. */
sandboxPolicy?: SandboxExecutionPolicy | undefined
2026-06-20 16:24:56 +08:00
}
```
```ts type-equiv
2026-07-19 12:25:40 +08:00
/**
2026-08-13 00:36:22 +08:00
* A resolved execution spec. {@link ShellExecutor.resolve} fills and caps the
* required fields; {@link ShellExecutor.start} ignores `timeoutMs` because
2026-07-19 12:25:40 +08:00
* background processes have no executor timeout.
*/
2026-08-13 00:36:22 +08:00
interface ShellExecSpec {
2026-06-20 16:24:56 +08:00
command: string
workdir: string
timeoutMs: number
feat(bash): add stdin + extra env to the executor seam as a trusted-plugin surface
The hooks subsystem runs external hook commands the Claude Code / Codex way:
JSON payload on stdin, context in CLAUDE_PROJECT_DIR / CLAUDE_PLUGIN_ROOT env.
Reusing the ctx.bash seam for that needs two new inputs — but stdin and arbitrary
env are exactly what dsh-bash-local's credential scrub exists to keep away from
model-driven commands. So this adds them as a TRUSTED-PLUGIN surface:
- BashExecRequest + BashExecSpec gain optional `stdin` and `env`. They are plain
optionals on the resolved spec (not required-but-nullable like `owner`): a
missing one means "none", the safe default, not a security footgun.
- dsh-bash-local threads them through resolve/run/start. `env` merges AFTER the
credential scrub, so a trusted caller's explicit entry wins even on a
credential-shaped name — the scrub guards the harness's OWN ambient creds from
model-driven commands, not a trusted plugin. stdin is always a pipe, closed
immediately (with bytes when supplied, empty otherwise — EOF as before); an
EPIPE from a child that exits without reading is swallowed.
- The model-facing dsh-tool-bash NEVER forwards model input into stdin/env (its
request is command/workdir/timeoutMs/signal/owner only). A regression guard
drives the real tool with adversarial args and asserts the request carries
neither field — proven to go red if the consumer ever forwards them.
Configurable scrub (in an earlier sketch) is dropped as speculative: the explicit
`env` field already gives a trusted caller full control, and no caller needs to
broaden the ambient scrub. Documented in a new architecture RFC, the bash.md
type-equiv blocks, and the three bash READMEs.
2026-06-30 13:52:25 +08:00
/**
2026-07-10 11:53:02 +08:00
* Resolved foreground stdout capture budget in bytes. `run()` uses it for
2026-08-13 00:36:22 +08:00
* stdout; background jobs and stderr keep the executor's own output cap.
feat(bash): add stdin + extra env to the executor seam as a trusted-plugin surface
The hooks subsystem runs external hook commands the Claude Code / Codex way:
JSON payload on stdin, context in CLAUDE_PROJECT_DIR / CLAUDE_PLUGIN_ROOT env.
Reusing the ctx.bash seam for that needs two new inputs — but stdin and arbitrary
env are exactly what dsh-bash-local's credential scrub exists to keep away from
model-driven commands. So this adds them as a TRUSTED-PLUGIN surface:
- BashExecRequest + BashExecSpec gain optional `stdin` and `env`. They are plain
optionals on the resolved spec (not required-but-nullable like `owner`): a
missing one means "none", the safe default, not a security footgun.
- dsh-bash-local threads them through resolve/run/start. `env` merges AFTER the
credential scrub, so a trusted caller's explicit entry wins even on a
credential-shaped name — the scrub guards the harness's OWN ambient creds from
model-driven commands, not a trusted plugin. stdin is always a pipe, closed
immediately (with bytes when supplied, empty otherwise — EOF as before); an
EPIPE from a child that exits without reading is swallowed.
- The model-facing dsh-tool-bash NEVER forwards model input into stdin/env (its
request is command/workdir/timeoutMs/signal/owner only). A regression guard
drives the real tool with adversarial args and asserts the request carries
neither field — proven to go red if the consumer ever forwards them.
Configurable scrub (in an earlier sketch) is dropped as speculative: the explicit
`env` field already gives a trusted caller full control, and no caller needs to
broaden the ambient scrub. Documented in a new architecture RFC, the bash.md
type-equiv blocks, and the three bash READMEs.
2026-06-30 13:52:25 +08:00
*/
2026-07-10 11:53:02 +08:00
stdoutMaxBytes: number
2026-06-20 16:24:56 +08:00
/** Abort signal — implementations kill the command when it fires. */
signal?: AbortSignal | undefined
2026-07-19 12:25:40 +08:00
/** Bytes to write to stdin before closing it; absent means no stdin. */
feat(bash): add stdin + extra env to the executor seam as a trusted-plugin surface
The hooks subsystem runs external hook commands the Claude Code / Codex way:
JSON payload on stdin, context in CLAUDE_PROJECT_DIR / CLAUDE_PLUGIN_ROOT env.
Reusing the ctx.bash seam for that needs two new inputs — but stdin and arbitrary
env are exactly what dsh-bash-local's credential scrub exists to keep away from
model-driven commands. So this adds them as a TRUSTED-PLUGIN surface:
- BashExecRequest + BashExecSpec gain optional `stdin` and `env`. They are plain
optionals on the resolved spec (not required-but-nullable like `owner`): a
missing one means "none", the safe default, not a security footgun.
- dsh-bash-local threads them through resolve/run/start. `env` merges AFTER the
credential scrub, so a trusted caller's explicit entry wins even on a
credential-shaped name — the scrub guards the harness's OWN ambient creds from
model-driven commands, not a trusted plugin. stdin is always a pipe, closed
immediately (with bytes when supplied, empty otherwise — EOF as before); an
EPIPE from a child that exits without reading is swallowed.
- The model-facing dsh-tool-bash NEVER forwards model input into stdin/env (its
request is command/workdir/timeoutMs/signal/owner only). A regression guard
drives the real tool with adversarial args and asserts the request carries
neither field — proven to go red if the consumer ever forwards them.
Configurable scrub (in an earlier sketch) is dropped as speculative: the explicit
`env` field already gives a trusted caller full control, and no caller needs to
broaden the ambient scrub. Documented in a new architecture RFC, the bash.md
type-equiv blocks, and the three bash READMEs.
2026-06-30 13:52:25 +08:00
stdin?: string | undefined
/**
2026-07-19 12:25:40 +08:00
* Ordinary environment entries carried through from
2026-08-13 00:36:22 +08:00
* {@link ShellExecRequest.env}; {@link dshEnv} still merges after them.
2026-07-19 12:25:40 +08:00
* OPTIONAL on the spec for the same reason as `stdin` : absent means no
* ordinary extra environment.
feat(bash): add stdin + extra env to the executor seam as a trusted-plugin surface
The hooks subsystem runs external hook commands the Claude Code / Codex way:
JSON payload on stdin, context in CLAUDE_PROJECT_DIR / CLAUDE_PLUGIN_ROOT env.
Reusing the ctx.bash seam for that needs two new inputs — but stdin and arbitrary
env are exactly what dsh-bash-local's credential scrub exists to keep away from
model-driven commands. So this adds them as a TRUSTED-PLUGIN surface:
- BashExecRequest + BashExecSpec gain optional `stdin` and `env`. They are plain
optionals on the resolved spec (not required-but-nullable like `owner`): a
missing one means "none", the safe default, not a security footgun.
- dsh-bash-local threads them through resolve/run/start. `env` merges AFTER the
credential scrub, so a trusted caller's explicit entry wins even on a
credential-shaped name — the scrub guards the harness's OWN ambient creds from
model-driven commands, not a trusted plugin. stdin is always a pipe, closed
immediately (with bytes when supplied, empty otherwise — EOF as before); an
EPIPE from a child that exits without reading is swallowed.
- The model-facing dsh-tool-bash NEVER forwards model input into stdin/env (its
request is command/workdir/timeoutMs/signal/owner only). A regression guard
drives the real tool with adversarial args and asserts the request carries
neither field — proven to go red if the consumer ever forwards them.
Configurable scrub (in an earlier sketch) is dropped as speculative: the explicit
`env` field already gives a trusted caller full control, and no caller needs to
broaden the ambient scrub. Documented in a new architecture RFC, the bash.md
type-equiv blocks, and the three bash READMEs.
2026-06-30 13:52:25 +08:00
*/
env?: Record< string , string > | undefined
2026-07-27 04:14:51 +08:00
/** Managed `DSH_*` snapshot (typed to managed keys); merges after {@link env}. */
2026-07-12 15:41:42 +08:00
dshEnv?: DshEnvironment | undefined
2026-07-21 00:44:28 +08:00
/** Resolved sandbox policy; ignored by executors that do not confine. */
sandboxPolicy: SandboxExecutionPolicy | undefined
2026-06-20 16:24:56 +08:00
}
```
2026-07-24 19:54:25 +08:00
`stdin` and `env` are trusted in-process plugin inputs and are not exposed by `dsh-tool-bash` . The local executor scrubs ambient credentials before merging explicit caller-supplied env. See [the bash-stdin-env Agent Note ](../../.agents/notes/implemented/architecture/2026-06-30-bash-stdin-env-trusted-plugin-api.md ).
feat(bash): add stdin + extra env to the executor seam as a trusted-plugin surface
The hooks subsystem runs external hook commands the Claude Code / Codex way:
JSON payload on stdin, context in CLAUDE_PROJECT_DIR / CLAUDE_PLUGIN_ROOT env.
Reusing the ctx.bash seam for that needs two new inputs — but stdin and arbitrary
env are exactly what dsh-bash-local's credential scrub exists to keep away from
model-driven commands. So this adds them as a TRUSTED-PLUGIN surface:
- BashExecRequest + BashExecSpec gain optional `stdin` and `env`. They are plain
optionals on the resolved spec (not required-but-nullable like `owner`): a
missing one means "none", the safe default, not a security footgun.
- dsh-bash-local threads them through resolve/run/start. `env` merges AFTER the
credential scrub, so a trusted caller's explicit entry wins even on a
credential-shaped name — the scrub guards the harness's OWN ambient creds from
model-driven commands, not a trusted plugin. stdin is always a pipe, closed
immediately (with bytes when supplied, empty otherwise — EOF as before); an
EPIPE from a child that exits without reading is swallowed.
- The model-facing dsh-tool-bash NEVER forwards model input into stdin/env (its
request is command/workdir/timeoutMs/signal/owner only). A regression guard
drives the real tool with adversarial args and asserts the request carries
neither field — proven to go red if the consumer ever forwards them.
Configurable scrub (in an earlier sketch) is dropped as speculative: the explicit
`env` field already gives a trusted caller full control, and no caller needs to
broaden the ambient scrub. Documented in a new architecture RFC, the bash.md
type-equiv blocks, and the three bash READMEs.
2026-06-30 13:52:25 +08:00
2026-08-13 00:36:22 +08:00
`stdoutMaxBytes` is also trusted-plugin-only. It lets a foreground consumer request complete stdout up to a bounded parser budget without changing stderr, background jobs, or the model-facing bash tool's ordinary output cap.
feat(types): brand bash ids + stop brand erosion; extract Branded to dsh-brand
Type-only change (brands are zero-cost casts; no runtime/wire impact). Closes
the two gaps in the "brand ids that cross package boundaries" policy and fixes
the dependency direction so a capability package never pulls in an unrelated one.
- Extract the `Branded<B>` primitive into a new standalone type-only package
`@deepseek-ai/dsh-brand` (packages/util/brand) with no harness-package deps.
dsh-llm keeps its owned CallId but imports Branded from dsh-brand; dsh-session,
dsh-agent, and dsh-bash all import Branded from there. dsh-bash depends on
dsh-brand ALONE — never on dsh-llm or dsh-session (the architectural fix: a
generic execution backend must not couple to the LLM or session vocabulary).
- Mint BashTaskId + OwnerToken in dsh-bash and thread them through BashTask.id,
the get/ownerOf/list/readOutput/kill seam, the bash-local generation site, and
the dsh-tool-bash validate/access surface. OwnerToken is a DISTINCT brand from
SessionId so the seam stays decoupled; dsh-tool-bash is the single boundary
that casts SessionId -> OwnerToken.
- Brand at the SOURCE, not via mid-pipeline casts: agent-loop's Config types
agents[].id as AgentId and resumeSessionId as SessionId, so the brand enters
at the config boundary and the inner create()/resume casts disappear (only the
genuinely-new per-run session-id string is cast).
- Stop brand erosion: propagate CallId/SessionId/AgentId to the registry/store
Map keys and public params/exports (SessionStore, AgentRegistry + factory
options, the ACP session-id surface + ToolPresenter CallId map, the
persistence coordinator, invariants pendingCalls, the pi-ai tool-call maps).
- Docs: document BashTaskId/OwnerToken in bash.md (type-equiv re-pasted), point
the Branded type-equiv at dsh-brand, fix stale param types in the session/
agent/bash READMEs, regenerate the cordis catalog + module graph.
Implements docs/rfc/proposed/architecture/2026-06-20-branded-ids.md
2026-06-21 07:17:25 +08:00
2026-08-13 00:36:22 +08:00
## Foreground runs: `ShellRunResult`
2026-06-20 16:24:56 +08:00
The outcome of one completed (or killed) foreground run. Orthogonal outcomes are reported **independently** — a process can both time out AND exit 0 because it trapped the signal — so `timedOut` , `aborted` , `signal` , and `exitCode` are each their own field; a caller never reads a cut-short run as a clean success.
```ts type-equiv
2026-07-19 12:25:40 +08:00
/** The outcome of one completed (or killed) foreground run. */
2026-08-13 00:36:22 +08:00
interface ShellRunResult {
2026-06-20 16:24:56 +08:00
/** Exit code; null when the process died from a signal. */
exitCode: number | null
/** Terminating signal (e.g. 'SIGTERM'); null on normal exit. */
signal: NodeJS.Signals | null
2026-07-19 12:25:40 +08:00
/**
* True when the executor's own timeout was the FIRST cause to cut the command
* short. Mutually exclusive with {@link aborted}: one fused deadline drives
* both the timeout and the caller's cancellation, so a timeout and an abort
* racing before process close report the single first-abort cause, not both
2026-07-19 22:50:49 +08:00
* (see the [timeout-library Agent Note ](../../../../.agents/notes/implemented/architecture/2026-07-06-timeout-deadline-library.md )).
2026-07-19 12:25:40 +08:00
*/
2026-06-20 16:24:56 +08:00
timedOut: boolean
2026-07-19 12:25:40 +08:00
/**
* True when the caller's `AbortSignal` was the FIRST cause to kill the command
* (and it was not the executor's own timeout). Mutually exclusive with
* {@link timedOut} — see there for the first-cause classification.
*/
2026-06-20 16:24:56 +08:00
aborted: boolean
/** The effective timeout applied to this run (after defaulting/capping). */
timeoutMs: number
stdout: CollectedOutput
stderr: CollectedOutput
2026-07-19 12:25:40 +08:00
/** Sandbox execution facts, absent for an unsandboxed executor. */
2026-08-13 00:36:22 +08:00
sandbox?: ShellSandboxInfo
2026-06-20 16:24:56 +08:00
}
```
2026-08-13 00:36:22 +08:00
Each stream is a `CollectedOutput` — the (possibly truncated) text plus recovery info; when truncated, `text` is the **tail** and the complete stream spills to a private file. The fields are owned by the [subprocess seam ](subprocess.md ) and re-exported by `dsh-shell` .
2026-06-20 16:24:56 +08:00
2026-08-13 00:36:22 +08:00
## File sandbox: `ShellSandboxInfo`
2026-07-09 16:05:44 +08:00
2026-08-13 00:36:22 +08:00
A sandbox-consuming executor exposes its configured mode fallback through `ShellExecutor.sandboxMode` . The tool layer asks [`@deepseek-ai/dsh-sandbox-policy` ](../../packages/sandbox/sandbox-policy/README.md ) to resolve each calling session's durable `sandbox/mode` override and immutable cwd into `ShellExecRequest.sandboxPolicy` ; a user-approved strictly wider call replaces only the mode. The mode/root/enforcement vocabulary is owned by the [`@deepseek-ai/dsh-sandbox` seam ](sandbox.md ); modes govern file effects only.
2026-07-09 16:05:44 +08:00
2026-07-11 23:04:27 +08:00
A sandboxed run reports its mode, conservative denial classification, and enforcement completeness. `runnerFailed` marks a sandbox runner failure before the command ran; foreground execution throws `SANDBOX_UNAVAILABLE` , while a settled background process has only its facts channel.
2026-07-09 16:05:44 +08:00
```ts type-equiv
2026-07-19 12:25:40 +08:00
/**
* Sandbox facts for one run, present iff a sandboxing executor handled it.
* Facts are reported independently of process exit status so callers can
* distinguish command failures from policy denials and runner failures.
*/
2026-08-13 00:36:22 +08:00
interface ShellSandboxInfo {
2026-07-09 16:05:44 +08:00
/** The mode the command actually ran under. */
mode: SandboxMode
2026-07-19 12:25:40 +08:00
/** Whether the sandbox denied a file operation. */
2026-07-09 16:05:44 +08:00
denied: boolean
2026-07-19 12:25:40 +08:00
/** How completely the selected runner enforced the requested mode. */
2026-07-09 16:05:44 +08:00
enforcement?: SandboxEnforcement
2026-07-19 12:25:40 +08:00
/** Whether the sandbox runner failed before the command could run. */
2026-07-09 16:05:44 +08:00
runnerFailed?: boolean
}
```
2026-08-13 00:36:22 +08:00
The `SANDBOX_UNAVAILABLE` error code (owned by the [sandbox seam ](sandbox.md )) is what the `ctx.sandbox` provider throws — and the executor propagates — when a confined mode has no usable backend. A selected runner refusing its profile reaches the same fail-closed foreground error; a settled background job records `runnerFailed` . The model receives denial/runner facts in results, learns the effective mode only when a denial marker names it, and can request a one-shot strictly wider retry through `sandbox_permissions` plus `justification` ; `ctx.approval` must grant that exact call before anything executes. The complete policy and switching design is the [sandbox Agent Note ](../../.agents/notes/implemented/feature/2026-07-06-sandbox.md ).
2026-07-09 16:05:44 +08:00
2026-08-13 00:36:22 +08:00
## Background processes: `ShellProcess`
2026-06-20 16:24:56 +08:00
2026-08-13 00:36:22 +08:00
`start()` returns a handle with no id or owner. `dsh-tool-bash` adapts it into `ctx.jobs.start()` hooks; the generic runtime then owns job identity and lifecycle. `done` resolves when the process closes and never rejects, reads remain valid after settlement, and sandbox facts are stamped before `done` resolves.
2026-06-20 16:24:56 +08:00
```ts type-equiv
2026-07-19 12:25:40 +08:00
/**
2026-08-13 00:36:22 +08:00
* A background process handle returned by {@link ShellExecutor.start}. It is the
2026-07-26 07:54:42 +08:00
* only access path; buffered output remains readable after exit. Composition
refactor(subprocess): rename the process seam to subprocess and address review
Review feedback (tianyicui): 'process' is a poor service name. The family is
now packages/subprocess/ — @deepseek-ai/dsh-subprocess (ctx.subprocess,
abstract SubprocessService, Subprocess* vocabulary) and
@deepseek-ai/dsh-subprocess-local (LocalSubprocessService) — renamed
throughout code, compositions, docs (en+zh, pairs re-recorded), catalogs,
and gates. 'subprocess' is the precise term for managed OS children (the
Python-stdlib sense), avoids colliding with Node's global process object,
and reads as one system beside dsh-subagent-subprocess.
ds-review-bot findings addressed:
- kill() on a settled handle is now a no-op (no signal to a possibly-reused
pgid, no referenced grace timer delaying exit); pinned by a spy test.
- The moved DshEnvironmentKey/DshEnvironment/CollectedOutput types get
drift-checked type-equiv blocks on the new subprocess.md page, restoring
their manifest registration.
- subprocess.md is registered in the core.md sub-page index (en+zh).
2026-07-26 12:43:14 +08:00
* teardown (the subprocess service's disposal) kills running processes and
2026-07-26 07:54:42 +08:00
* awaits {@link done}; an executor-only reload leaves them running.
2026-07-19 12:25:40 +08:00
*/
2026-08-13 00:36:22 +08:00
interface ShellProcess {
feat(tasks): background task runtime, generic task_* control tools, bash/subagent producers
One shared ctx.tasks registry (branded <kind>-N ids, owner-fenced
read/kill/wait/list, attachSurface misconfiguration fence, reported-flag
notice dedup, atomic register) + dsh-tool-tasks (task_output/task_list/
task_kill, completion-notice injection, background prompt habit).
Producers opt in via their own enableRunInBackground config: bash
(stream kind; seam slimmed to resolve/run/start returning a BashProcess
handle, bash_output/bash_kill deleted) and subagent (final-output kind;
done settles after run.dispose()). Owner disposal drains tasks through
the new awaited ctx.agents.onCleanup seam in the loop's disposal chain.
Both RFCs moved to implemented/; docs, catalogs, snapshots re-pinned.
2026-07-09 21:22:54 +08:00
/** Process lifecycle state (settled exactly once). */
2026-08-13 00:36:22 +08:00
status: ShellProcessStatus
2026-06-20 16:24:56 +08:00
/** Exit code once finished (null = killed by signal / still running). */
exitCode: number | null
/** Terminating signal name, when signal-killed. */
signal: NodeJS.Signals | null
feat(tasks): background task runtime, generic task_* control tools, bash/subagent producers
One shared ctx.tasks registry (branded <kind>-N ids, owner-fenced
read/kill/wait/list, attachSurface misconfiguration fence, reported-flag
notice dedup, atomic register) + dsh-tool-tasks (task_output/task_list/
task_kill, completion-notice injection, background prompt habit).
Producers opt in via their own enableRunInBackground config: bash
(stream kind; seam slimmed to resolve/run/start returning a BashProcess
handle, bash_output/bash_kill deleted) and subagent (final-output kind;
done settles after run.dispose()). Owner disposal drains tasks through
the new awaited ctx.agents.onCleanup seam in the loop's disposal chain.
Both RFCs moved to implemented/; docs, catalogs, snapshots re-pinned.
2026-07-09 21:22:54 +08:00
/** Resolves when the underlying process closes (never rejects — a spawn failure settles as `killed` with the error on stderr). */
2026-06-20 16:24:56 +08:00
readonly done: Promise< void >
2026-07-11 23:04:27 +08:00
/** Sandbox facts, stamped once a confined process settles. */
2026-08-13 00:36:22 +08:00
sandbox?: ShellSandboxInfo
2026-07-09 16:05:44 +08:00
/**
feat(tasks): background task runtime, generic task_* control tools, bash/subagent producers
One shared ctx.tasks registry (branded <kind>-N ids, owner-fenced
read/kill/wait/list, attachSurface misconfiguration fence, reported-flag
notice dedup, atomic register) + dsh-tool-tasks (task_output/task_list/
task_kill, completion-notice injection, background prompt habit).
Producers opt in via their own enableRunInBackground config: bash
(stream kind; seam slimmed to resolve/run/start returning a BashProcess
handle, bash_output/bash_kill deleted) and subagent (final-output kind;
done settles after run.dispose()). Owner disposal drains tasks through
the new awaited ctx.agents.onCleanup seam in the loop's disposal chain.
Both RFCs moved to implemented/; docs, catalogs, snapshots re-pinned.
2026-07-09 21:22:54 +08:00
* Read output produced since the previous read (consuming — consecutive
* reads never re-deliver). Reads that lost data flag `lossy` and point at
* full-stream spill files when available.
2026-07-09 16:05:44 +08:00
*/
2026-08-13 00:36:22 +08:00
readOutput(): ShellProcessRead
feat(tasks): background task runtime, generic task_* control tools, bash/subagent producers
One shared ctx.tasks registry (branded <kind>-N ids, owner-fenced
read/kill/wait/list, attachSurface misconfiguration fence, reported-flag
notice dedup, atomic register) + dsh-tool-tasks (task_output/task_list/
task_kill, completion-notice injection, background prompt habit).
Producers opt in via their own enableRunInBackground config: bash
(stream kind; seam slimmed to resolve/run/start returning a BashProcess
handle, bash_output/bash_kill deleted) and subagent (final-output kind;
done settles after run.dispose()). Owner disposal drains tasks through
the new awaited ctx.agents.onCleanup seam in the loop's disposal chain.
Both RFCs moved to implemented/; docs, catalogs, snapshots re-pinned.
2026-07-09 21:22:54 +08:00
/**
* Kill the process group. Returns false when it had already finished
* (no-op); idempotent.
*/
kill(): boolean
2026-06-20 16:24:56 +08:00
}
```
2026-07-11 23:04:27 +08:00
`readOutput()` returns the incremental delta and spill recovery facts:
2026-06-20 16:24:56 +08:00
```ts type-equiv
2026-08-13 00:36:22 +08:00
/** One incremental {@link ShellProcess.readOutput} read. */
interface ShellProcessRead {
2026-06-20 16:24:56 +08:00
/** Output produced since the previous read (stderr in a marked section). */
delta: string
/** True when truncation dropped unread bytes the delta cannot include. */
lossy: boolean
/** Full stdout spill file, when stdout truncation occurred and a safe path is available. */
stdoutSpillPath?: string
/** Full stderr spill file, when stderr truncation occurred and a safe path is available. */
stderrSpillPath?: string
}
```
## The service
2026-08-13 00:36:22 +08:00
`ShellExecutor` owns `resolve` , foreground `run` , background-process `start` , and the `sandboxMode` capability fact. `dsh-bash-local` owns command defaulting, timeout/abort classification, the terminal environment, and the background read merge; process groups, bounded collectors, spill files, credential scrubbing, and disposal quiescence are the [subprocess service ](subprocess.md )'s. `dsh-tool-bash` owns model-facing rendering and adapts background handles into the [generic job runtime ](jobs.md ). `dsh-shell` owns the shell tools' shared exit-status contract: the exported `parseExitStatus` /`ParsedExitStatus` inverts the `[exit code: N]` / `[killed by signal: X]` markers `dsh-tool-bash` 's `renderResult` and `dsh-tool-pwsh` 's `renderPwshResult` append, and both tools' `presentResult` use it to split the rendered text into the terminal card's output body and its exit-status pill.
2026-07-30 21:40:58 +08:00
<!-- BEGIN GENERATED cordis - surface (gen - cordis - catalog.ts) — do not edit between markers -->
< a id = "cordis-surface" > < / a >
2026-07-24 19:54:25 +08:00
## Cordis API
2026-07-30 21:40:58 +08:00
2026-08-18 21:02:50 +08:00
Generated from source by `scripts/gen-cordis-catalog.ts` (verified fresh by `pnpm run verify-cordis-catalog` in doc-sync; regenerate with `pnpm run gen-cordis-catalog` ) — the language sides differ only in locale-specific paired document paths. Signature blocks use a `ts cordis-catalog` fence and keep the original source JSDoc; dispatch modes are defined in the [primer ](../cordis-primer.md#dispatch-modes ), and the framework-inherited `ctx` API lives in [cordis-api/inherited.md ](../cordis-api/inherited.md ).
2026-07-30 21:40:58 +08:00
2026-08-13 00:36:22 +08:00
< a id = "ctxshell--shellexecutor-abstract-seam" > < / a >
2026-07-30 21:40:58 +08:00
2026-08-13 00:36:22 +08:00
### `ctx.shell` — `ShellExecutor` (abstract seam)
2026-07-30 21:40:58 +08:00
2026-08-13 00:36:22 +08:00
Abstract bash execution service. Subclass, implement the abstract methods, and load the subclass as a plugin — it registers as `ctx.shell` (one implementation per context; loading a second throws, which is cordis' standard duplicate-service behavior).
2026-07-30 21:40:58 +08:00
Implementations must honor these semantics:
2026-08-13 00:36:22 +08:00
- run rejects only for infrastructure failures. Nonzero exits, timeout kills, and abort kills resolve with a ShellRunResult.
2026-07-30 21:40:58 +08:00
- start returns immediately; no timeout applies to background processes. `done` settles at process close and never rejects; spawn failures settle as `killed` with the error on stderr.
2026-08-13 00:36:22 +08:00
- ShellProcess.readOutput is incremental: consecutive reads never repeat output. Lossy reads report truncation and available spill files.
2026-07-30 21:40:58 +08:00
- A still-running background process is stopped and awaited when its owning composition tears down. With the subprocess seam that boundary is `ctx.subprocess` disposal, so a background process survives an executor-only reload.
```ts cordis-catalog
/**
* Apply implementation-owned defaults and caps to a request before execution.
* @param request - the caller's request; omitted fields get this
* implementation's defaults, capped fields are clamped.
* @returns the fully-specified spec to hand to {@link run}/{@link start}.
*/
2026-08-13 00:36:22 +08:00
abstract resolve(request: ShellExecRequest): ShellExecSpec
2026-07-30 21:40:58 +08:00
/**
* Run a command in the foreground; resolves when it finishes.
* @param spec - a resolved spec from {@link resolve}, never a raw request.
* @returns the outcome; nonzero exits, timeout kills, and abort kills
* resolve with a descriptive result rather than reject.
*/
2026-08-13 00:36:22 +08:00
abstract run(spec: ShellExecSpec): Promise< ShellRunResult >
2026-07-30 21:40:58 +08:00
/**
* Start a background process and return its handle immediately.
* @param spec - a resolved spec from {@link resolve}, never a raw request.
* @returns the live process handle (reads, kill, quiescence promise).
*/
2026-08-13 00:36:22 +08:00
abstract start(spec: ShellExecSpec): ShellProcess
2026-07-30 21:40:58 +08:00
```
2026-08-20 19:48:43 +08:00
Source: [`packages/shell/shell/src/index.ts` ](../../packages/shell/shell/src/index.ts )
2026-07-30 21:40:58 +08:00
2026-08-13 00:36:22 +08:00
< a id = "ctxshellenv--shellenvregistry" > < / a >
2026-07-30 21:40:58 +08:00
2026-08-13 00:36:22 +08:00
### `ctx.shellEnv` — `ShellEnvRegistry`
2026-07-30 21:40:58 +08:00
2026-08-13 00:36:22 +08:00
Registry (`ctx.shellEnv` ) for trusted, per-execution `DSH_*` variables. The namespace is rebuilt for every model shell call: ambient `DSH_*` values are discarded by the executor, then the registry's current snapshot is injected. Built-in shell facts remain owned by the registry itself while plugins can register additional, enumerable facts with effect-scoped disposal.
2026-07-30 21:40:58 +08:00
```ts cordis-catalog
/**
* Register one environment contributor. Names and keys are unique; built-in
* keys are reserved. Registration is disposed with the calling plugin fiber.
* @param contributor - declared key ownership and per-execution resolver.
* @returns the disposer that unregisters the contribution.
*/
register(contributor: BashEnvContributor): () => void
/**
* Build the trusted `DSH_*` snapshot for one shell tool execution.
* @param execution - the current tool execution.
* @returns an immutable environment overlay containing built-ins and current contributions.
*/
collect(execution: ToolExecution): DshEnvironment
/**
* Enumerate plugin-contributed variables without executing their resolvers.
* @returns declarations sorted by environment variable name.
*/
list(): BashEnvVariableInfo[]
```
Types: [DshEnvironment ](subprocess.md ) · [ToolExecution ](tools.md )
2026-08-20 19:48:43 +08:00
Source: [`packages/shell/shell-env/src/index.ts` ](../../packages/shell/shell-env/src/index.ts )
2026-07-30 21:40:58 +08:00
<!-- END GENERATED cordis - surface -->