Add bash execution: dsh-bash seam, dsh-bash-local impl, dsh-tool-bash tools
Three packages following the new capability-seam pattern (interface /
implementation / consumer, now documented in docs/architecture.md):
- dsh-bash: abstract BashExecutor service (ctx.bash) + vocabulary types.
- dsh-bash-local: local subprocesses — bash -c per call in a detached
process group, SIGTERM→SIGKILL group kills, tail-keep truncation with
full-stream spill files, model-friendly env, background task registry.
- dsh-tool-bash: the bash / bash_output / bash_kill tool schemas with
runtime arg validation and background completion notices via
agent.inject(). Non-zero exits are reported, not errored.
Design surveyed against the bash tools of Claude Code, OpenCode, Codex,
and pi (notes in the package READMEs). Permissions/sandbox stay TODO on
the tools/execute waterfall seam; stateful-shell alternatives recorded
in run.ts.
2026-06-12 23:28:44 +08:00
/ * *
docs(tasks): condense background task prose
The background-task change repeated its lifecycle design across implemented RFCs, package READMEs, JSDoc, test commentary, and model-visible schemas. That repetition obscured the contracts that maintainers must preserve and added avoidable prompt tokens.
Rewrite the implemented RFCs around the current design, keep authorization, exact-owner cleanup, wait/abort ordering, producer quiescence, and teardown-failure guarantees at their owning surfaces, and remove peer surveys, review history, control-flow narration, and emphatic restatement.
Shorten the task and subagent schema wording, synchronize the bilingual tool cookbook, and regenerate the config, service, RFC, tool, and replay snapshot derivatives. Runtime behavior is unchanged; test edits update prose-only assertions and descriptions.
2026-07-15 21:08:58 +08:00
* Model - facing ` bash ` tool over the ` ctx.bash ` executor seam . Background calls
* register process handles with ` ctx.tasks ` ; their work uses task cancellation
* rather than the tool - call signal after an id is returned .
Add bash execution: dsh-bash seam, dsh-bash-local impl, dsh-tool-bash tools
Three packages following the new capability-seam pattern (interface /
implementation / consumer, now documented in docs/architecture.md):
- dsh-bash: abstract BashExecutor service (ctx.bash) + vocabulary types.
- dsh-bash-local: local subprocesses — bash -c per call in a detached
process group, SIGTERM→SIGKILL group kills, tail-keep truncation with
full-stream spill files, model-friendly env, background task registry.
- dsh-tool-bash: the bash / bash_output / bash_kill tool schemas with
runtime arg validation and background completion notices via
agent.inject(). Non-zero exits are reported, not errored.
Design surveyed against the bash tools of Claude Code, OpenCode, Codex,
and pi (notes in the package READMEs). Permissions/sandbox stay TODO on
the tools/execute waterfall seam; stateful-shell alternatives recorded
in run.ts.
2026-06-12 23:28:44 +08:00
*
docs(tasks): condense background task prose
The background-task change repeated its lifecycle design across implemented RFCs, package READMEs, JSDoc, test commentary, and model-visible schemas. That repetition obscured the contracts that maintainers must preserve and added avoidable prompt tokens.
Rewrite the implemented RFCs around the current design, keep authorization, exact-owner cleanup, wait/abort ordering, producer quiescence, and teardown-failure guarantees at their owning surfaces, and remove peer surveys, review history, control-flow narration, and emphatic restatement.
Shorten the task and subagent schema wording, synchronize the bilingual tool cookbook, and regenerate the config, service, RFC, tool, and replay snapshot derivatives. Runtime behavior is unchanged; test edits update prose-only assertions and descriptions.
2026-07-15 21:08:58 +08:00
* TODO ( permissions ) : deployment policy belongs in ` tools/pre-execute ` and
* sandboxing executors ; see docs / architecture . md § Extending The Harness .
Add bash execution: dsh-bash seam, dsh-bash-local impl, dsh-tool-bash tools
Three packages following the new capability-seam pattern (interface /
implementation / consumer, now documented in docs/architecture.md):
- dsh-bash: abstract BashExecutor service (ctx.bash) + vocabulary types.
- dsh-bash-local: local subprocesses — bash -c per call in a detached
process group, SIGTERM→SIGKILL group kills, tail-keep truncation with
full-stream spill files, model-friendly env, background task registry.
- dsh-tool-bash: the bash / bash_output / bash_kill tool schemas with
runtime arg validation and background completion notices via
agent.inject(). Non-zero exits are reported, not errored.
Design surveyed against the bash tools of Claude Code, OpenCode, Codex,
and pi (notes in the package READMEs). Permissions/sandbox stay TODO on
the tools/execute waterfall seam; stateful-shell alternatives recorded
in run.ts.
2026-06-12 23:28:44 +08:00
* @module @deepseek - ai / dsh - tool - bash
* /
2026-07-12 15:41:42 +08:00
import { Service , type Context } from 'cordis'
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
import z from 'schemastery'
2026-06-17 10:01:18 +08:00
import { isAbsolute , resolve as resolvePath } from 'node:path'
2026-07-21 23:39:03 +08:00
import { defineTool , TOOL_ABORTED } from '@deepseek-ai/dsh-tools'
2026-07-09 16:37:10 +08:00
import type { GenericCallView , TerminalCallView , ToolExecution , ToolResult , ToolResultView } from '@deepseek-ai/dsh-tools'
2026-07-21 23:39:03 +08:00
import { HarnessError } from '@deepseek-ai/dsh-llm'
Add bash execution: dsh-bash seam, dsh-bash-local impl, dsh-tool-bash tools
Three packages following the new capability-seam pattern (interface /
implementation / consumer, now documented in docs/architecture.md):
- dsh-bash: abstract BashExecutor service (ctx.bash) + vocabulary types.
- dsh-bash-local: local subprocesses — bash -c per call in a detached
process group, SIGTERM→SIGKILL group kills, tail-keep truncation with
full-stream spill files, model-friendly env, background task registry.
- dsh-tool-bash: the bash / bash_output / bash_kill tool schemas with
runtime arg validation and background completion notices via
agent.inject(). Non-zero exits are reported, not errored.
Design surveyed against the bash tools of Claude Code, OpenCode, Codex,
and pi (notes in the package READMEs). Permissions/sandbox stay TODO on
the tools/execute waterfall seam; stateful-shell alternatives recorded
in run.ts.
2026-06-12 23:28:44 +08:00
import type { Agent } from '@deepseek-ai/dsh-agent'
2026-07-10 20:52:27 +08:00
import type { } from '@deepseek-ai/dsh-session-persistence'
2026-07-05 01:54:46 +08:00
import type { } from '@deepseek-ai/dsh-system-prompt'
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
import type { } from '@deepseek-ai/dsh-tasks'
2026-07-11 21:37:38 +08:00
import type { } from '@deepseek-ai/dsh-user-approval'
2026-07-21 00:44:28 +08:00
import type { SandboxExecutionPolicy , SandboxMode } from '@deepseek-ai/dsh-sandbox'
2026-07-21 19:53:02 +08:00
import { ESCALATION_TARGETS , approveEscalation , canonicalPath , validateEscalationArgs } from '@deepseek-ai/dsh-sandbox'
2026-07-21 00:44:28 +08:00
import type { SandboxPolicyService } from '@deepseek-ai/dsh-sandbox-policy'
2026-07-20 11:40:29 +08:00
import { DSH_ENV_PREFIX } from '@deepseek-ai/dsh-bash'
2026-07-21 03:08:35 +08:00
import type { BashRunResult , DshEnvironment , DshEnvironmentKey } from '@deepseek-ai/dsh-bash'
2026-07-21 13:52:00 +08:00
import { DSH_HOME_ENV , resolveDshHome } from '@deepseek-ai/dsh-paths'
2026-07-15 13:38:17 +08:00
import { processOutcome } from './background.ts'
import { parseExitStatus , renderProcessRead , renderResult } from './render.ts'
Add bash execution: dsh-bash seam, dsh-bash-local impl, dsh-tool-bash tools
Three packages following the new capability-seam pattern (interface /
implementation / consumer, now documented in docs/architecture.md):
- dsh-bash: abstract BashExecutor service (ctx.bash) + vocabulary types.
- dsh-bash-local: local subprocesses — bash -c per call in a detached
process group, SIGTERM→SIGKILL group kills, tail-keep truncation with
full-stream spill files, model-friendly env, background task registry.
- dsh-tool-bash: the bash / bash_output / bash_kill tool schemas with
runtime arg validation and background completion notices via
agent.inject(). Non-zero exits are reported, not errored.
Design surveyed against the bash tools of Claude Code, OpenCode, Codex,
and pi (notes in the package READMEs). Permissions/sandbox stay TODO on
the tools/execute waterfall seam; stateful-shell alternatives recorded
in run.ts.
2026-06-12 23:28:44 +08:00
2026-07-12 15:41:42 +08:00
declare module 'cordis' {
interface Context {
bashEnv : BashEnvRegistry
}
}
Add bash execution: dsh-bash seam, dsh-bash-local impl, dsh-tool-bash tools
Three packages following the new capability-seam pattern (interface /
implementation / consumer, now documented in docs/architecture.md):
- dsh-bash: abstract BashExecutor service (ctx.bash) + vocabulary types.
- dsh-bash-local: local subprocesses — bash -c per call in a detached
process group, SIGTERM→SIGKILL group kills, tail-keep truncation with
full-stream spill files, model-friendly env, background task registry.
- dsh-tool-bash: the bash / bash_output / bash_kill tool schemas with
runtime arg validation and background completion notices via
agent.inject(). Non-zero exits are reported, not errored.
Design surveyed against the bash tools of Claude Code, OpenCode, Codex,
and pi (notes in the package READMEs). Permissions/sandbox stay TODO on
the tools/execute waterfall seam; stateful-shell alternatives recorded
in run.ts.
2026-06-12 23:28:44 +08:00
export const name = 'tool-bash'
2026-07-05 01:54:46 +08:00
export const inject = [ 'tools' , 'bash' , 'systemPrompt' ]
Add bash execution: dsh-bash seam, dsh-bash-local impl, dsh-tool-bash tools
Three packages following the new capability-seam pattern (interface /
implementation / consumer, now documented in docs/architecture.md):
- dsh-bash: abstract BashExecutor service (ctx.bash) + vocabulary types.
- dsh-bash-local: local subprocesses — bash -c per call in a detached
process group, SIGTERM→SIGKILL group kills, tail-keep truncation with
full-stream spill files, model-friendly env, background task registry.
- dsh-tool-bash: the bash / bash_output / bash_kill tool schemas with
runtime arg validation and background completion notices via
agent.inject(). Non-zero exits are reported, not errored.
Design surveyed against the bash tools of Claude Code, OpenCode, Codex,
and pi (notes in the package READMEs). Permissions/sandbox stay TODO on
the tools/execute waterfall seam; stateful-shell alternatives recorded
in run.ts.
2026-06-12 23:28:44 +08:00
2026-07-12 15:41:42 +08:00
/** Configuration for the bash tool and its managed child environment. */
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
export interface Config {
docs(tasks): condense background task prose
The background-task change repeated its lifecycle design across implemented RFCs, package READMEs, JSDoc, test commentary, and model-visible schemas. That repetition obscured the contracts that maintainers must preserve and added avoidable prompt tokens.
Rewrite the implemented RFCs around the current design, keep authorization, exact-owner cleanup, wait/abort ordering, producer quiescence, and teardown-failure guarantees at their owning surfaces, and remove peer surveys, review history, control-flow narration, and emphatic restatement.
Shorten the task and subagent schema wording, synchronize the bilingual tool cookbook, and regenerate the config, service, RFC, tool, and replay snapshot derivatives. Runtime behavior is unchanged; test edits update prose-only assertions and descriptions.
2026-07-15 21:08:58 +08:00
/** Expose `run_in_background` (default true); disabled calls are also rejected. */
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
enableRunInBackground? : boolean
2026-07-12 15:41:42 +08:00
/** DeepSeek Harness home directory exposed as `DSH_HOME`; defaults to `$DSH_HOME` or `~/.dsh`. */
dshHome? : string
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
}
2026-07-12 15:41:42 +08:00
/** Runtime configuration schema for the bash tool plugin. */
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
export const Config : z < Config > = z . object ( {
enableRunInBackground : z.boolean ( ) . default ( true ) ,
2026-07-12 15:41:42 +08:00
dshHome : z.string ( ) ,
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
} )
2026-07-12 15:41:42 +08:00
/** Model-visible metadata for one managed `DSH_*` environment variable. */
export interface BashEnvVariable {
/** Concise description of the environment fact represented by the variable. */
description : string
}
/ * *
* A plugin contribution to the managed environment of each model bash call .
* Declared keys make ownership conflicts detectable before the first command ;
* ` resolve ` computes only the values available for the current execution .
* /
export interface BashEnvContributor {
/** Stable contributor name used in diagnostics and duplicate detection. */
name : string
/** Complete set of `DSH_*` keys this contributor may return. */
2026-07-12 16:14:13 +08:00
variables : Readonly < Record < DshEnvironmentKey , BashEnvVariable > >
2026-07-12 15:41:42 +08:00
/ * *
* Resolve this contributor ' s available values for one tool execution .
* @param execution - the bash tool execution and its optional calling agent .
* @returns a partial map containing only keys declared in { @link variables } .
* /
2026-07-12 16:14:13 +08:00
resolve ( execution : ToolExecution ) : Readonly < Partial < Record < DshEnvironmentKey , string > > >
2026-07-12 15:41:42 +08:00
}
/** An enumerable declaration returned by {@link BashEnvRegistry.list}. */
export interface BashEnvVariableInfo extends BashEnvVariable {
/** Contributor that owns the variable. */
contributor : string
/** Declared `DSH_*` environment variable name. */
2026-07-12 16:14:13 +08:00
key : DshEnvironmentKey
2026-07-12 15:41:42 +08:00
}
2026-07-12 16:14:13 +08:00
const DSH_SHELL_KEY = ` ${ DSH_ENV_PREFIX } SHELL ` as const
const DSH_SESSION_ID_KEY = ` ${ DSH_ENV_PREFIX } SESSION_ID ` as const
const DSH_SESSION_JSONL_KEY = ` ${ DSH_ENV_PREFIX } SESSION_JSONL ` as const
const RESERVED_BASH_ENV_KEYS = new Set < DshEnvironmentKey > ( [
2026-07-12 16:30:01 +08:00
DSH_HOME_ENV ,
2026-07-12 16:14:13 +08:00
DSH_SHELL_KEY ,
DSH_SESSION_ID_KEY ,
2026-07-12 15:41:42 +08:00
] )
2026-07-12 16:14:13 +08:00
const BASH_ENV_KEY_SUFFIX = /^[A-Z][A-Z0-9_]*$/
2026-07-12 15:41:42 +08:00
/ * *
* Registry ( ` ctx.bashEnv ` ) for trusted , per - execution ` DSH_* ` variables .
* The namespace is rebuilt for every model bash 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 .
* /
export class BashEnvRegistry extends Service {
private readonly contributors = new Map < string , BashEnvContributor > ( )
2026-07-12 16:14:13 +08:00
private readonly keyOwners = new Map < DshEnvironmentKey , string > ( )
2026-07-12 15:41:42 +08:00
private readonly dshHome : string
/ * *
* Create and install the ` ctx.bashEnv ` service .
* @param ctx - Cordis context that owns the service and registrations .
* @param config - home - directory configuration for the built - in variables .
* /
constructor ( ctx : Context , config : Config = { } ) {
super ( ctx , 'bashEnv' )
2026-07-12 16:30:01 +08:00
this . dshHome = resolveDshHome ( config . dshHome )
2026-07-12 15:41:42 +08:00
}
/ * *
* 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 {
const dispose = this . ctx . effect ( function * ( this : BashEnvRegistry ) {
if ( contributor . name . trim ( ) . length === 0 ) {
throw new Error ( 'bash env contributor name must be non-empty' )
}
if ( this . contributors . has ( contributor . name ) ) {
throw new Error ( ` bash env contributor " ${ contributor . name } " is already registered ` )
}
2026-07-12 16:14:13 +08:00
const variables = Object . entries ( contributor . variables ) as [ DshEnvironmentKey , BashEnvVariable ] [ ]
2026-07-12 15:41:42 +08:00
for ( const [ key , variable ] of variables ) {
2026-07-12 16:14:13 +08:00
if ( ! key . startsWith ( DSH_ENV_PREFIX )
|| ! BASH_ENV_KEY_SUFFIX . test ( key . slice ( DSH_ENV_PREFIX . length ) ) ) {
2026-07-12 15:41:42 +08:00
throw new Error ( ` bash env contributor " ${ contributor . name } " declared invalid key " ${ key } " ` )
}
if ( RESERVED_BASH_ENV_KEYS . has ( key ) ) {
throw new Error ( ` bash env contributor " ${ contributor . name } " cannot own reserved key " ${ key } " ` )
}
if ( variable . description . trim ( ) . length === 0 ) {
throw new Error ( ` bash env contributor " ${ contributor . name } " must describe " ${ key } " ` )
}
const owner = this . keyOwners . get ( key )
if ( owner !== undefined ) {
throw new Error ( ` bash env key " ${ key } " is already owned by contributor " ${ owner } "; contributor " ${ contributor . name } " cannot also own it ` )
}
}
this . contributors . set ( contributor . name , contributor )
for ( const [ key ] of variables ) this . keyOwners . set ( key , contributor . name )
yield ( ) = > {
this . contributors . delete ( contributor . name )
for ( const [ key ] of variables ) this . keyOwners . delete ( key )
}
} . bind ( this ) , 'bashEnv.register()' )
return ( ) = > void dispose ( )
}
/ * *
* Build the trusted ` DSH_* ` snapshot for one bash tool execution .
* @param execution - the current tool execution .
* @returns an immutable environment overlay containing built - ins and current contributions .
* /
collect ( execution : ToolExecution ) : DshEnvironment {
2026-07-12 16:14:13 +08:00
const values : Record < DshEnvironmentKey , string > = {
2026-07-12 16:30:01 +08:00
[ DSH_HOME_ENV ] : this . dshHome ,
2026-07-12 16:14:13 +08:00
[ DSH_SHELL_KEY ] : '1' ,
2026-07-12 15:41:42 +08:00
}
if ( execution . agent !== undefined ) {
2026-07-12 16:14:13 +08:00
values [ DSH_SESSION_ID_KEY ] = execution . agent . session . header . id
2026-07-12 15:41:42 +08:00
}
for ( const contributor of [ . . . this . contributors . values ( ) ] . sort ( ( left , right ) = > left . name . localeCompare ( right . name ) ) ) {
const resolved = contributor . resolve ( execution )
for ( const [ rawKey , value ] of Object . entries ( resolved ) ) {
2026-07-12 16:14:13 +08:00
const key = rawKey as DshEnvironmentKey
2026-07-12 15:41:42 +08:00
if ( ! Object . hasOwn ( contributor . variables , key ) ) {
throw new Error ( ` bash env contributor " ${ contributor . name } " returned undeclared key " ${ key } " ` )
}
if ( typeof value !== 'string' ) {
throw new Error ( ` bash env contributor " ${ contributor . name } " returned a non-string value for " ${ key } " ` )
}
values [ key ] = value
}
}
return Object . freeze ( Object . fromEntries ( Object . entries ( values ) . sort ( ( [ left ] , [ right ] ) = > left . localeCompare ( right ) ) ) )
}
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
// TODO(bash-env-list-builtins): Include registry-owned built-ins before diagnostics,
// prompt, or UI code treats list() as an exhaustive environment catalog.
2026-07-12 15:41:42 +08:00
/ * *
* Enumerate plugin - contributed variables without executing their resolvers .
* @returns declarations sorted by environment variable name .
* /
list ( ) : BashEnvVariableInfo [ ] {
return [ . . . this . contributors . values ( ) ]
. flatMap ( contributor = > Object . entries ( contributor . variables ) . map ( ( [ key , variable ] ) = > ( {
contributor : contributor.name ,
description : variable.description ,
2026-07-12 16:14:13 +08:00
key : key as DshEnvironmentKey ,
2026-07-12 15:41:42 +08:00
} ) ) )
. sort ( ( left , right ) = > left . key . localeCompare ( right . key ) )
}
}
2026-07-21 01:11:55 +08:00
/** Parsed tool args; execute validates value constraints absent from ParameterSchemaSpec. */
2026-07-11 23:04:27 +08:00
interface BashToolArgs {
Add bash execution: dsh-bash seam, dsh-bash-local impl, dsh-tool-bash tools
Three packages following the new capability-seam pattern (interface /
implementation / consumer, now documented in docs/architecture.md):
- dsh-bash: abstract BashExecutor service (ctx.bash) + vocabulary types.
- dsh-bash-local: local subprocesses — bash -c per call in a detached
process group, SIGTERM→SIGKILL group kills, tail-keep truncation with
full-stream spill files, model-friendly env, background task registry.
- dsh-tool-bash: the bash / bash_output / bash_kill tool schemas with
runtime arg validation and background completion notices via
agent.inject(). Non-zero exits are reported, not errored.
Design surveyed against the bash tools of Claude Code, OpenCode, Codex,
and pi (notes in the package READMEs). Permissions/sandbox stay TODO on
the tools/execute waterfall seam; stateful-shell alternatives recorded
in run.ts.
2026-06-12 23:28:44 +08:00
command : string
description : string
timeoutMs? : number
workdir? : string
run_in_background? : boolean
2026-07-11 23:04:27 +08:00
sandbox_permissions? : string
justification? : string
}
2026-07-09 16:37:10 +08:00
function validateBashArgs ( args : BashToolArgs ) : void {
2026-06-13 23:00:42 +08:00
if ( args . command . trim ( ) . length === 0 ) {
Add bash execution: dsh-bash seam, dsh-bash-local impl, dsh-tool-bash tools
Three packages following the new capability-seam pattern (interface /
implementation / consumer, now documented in docs/architecture.md):
- dsh-bash: abstract BashExecutor service (ctx.bash) + vocabulary types.
- dsh-bash-local: local subprocesses — bash -c per call in a detached
process group, SIGTERM→SIGKILL group kills, tail-keep truncation with
full-stream spill files, model-friendly env, background task registry.
- dsh-tool-bash: the bash / bash_output / bash_kill tool schemas with
runtime arg validation and background completion notices via
agent.inject(). Non-zero exits are reported, not errored.
Design surveyed against the bash tools of Claude Code, OpenCode, Codex,
and pi (notes in the package READMEs). Permissions/sandbox stay TODO on
the tools/execute waterfall seam; stateful-shell alternatives recorded
in run.ts.
2026-06-12 23:28:44 +08:00
throw new Error ( 'invalid command: expected a non-empty string' )
}
2026-06-13 23:00:42 +08:00
if ( args . description . trim ( ) . length === 0 ) {
Add bash execution: dsh-bash seam, dsh-bash-local impl, dsh-tool-bash tools
Three packages following the new capability-seam pattern (interface /
implementation / consumer, now documented in docs/architecture.md):
- dsh-bash: abstract BashExecutor service (ctx.bash) + vocabulary types.
- dsh-bash-local: local subprocesses — bash -c per call in a detached
process group, SIGTERM→SIGKILL group kills, tail-keep truncation with
full-stream spill files, model-friendly env, background task registry.
- dsh-tool-bash: the bash / bash_output / bash_kill tool schemas with
runtime arg validation and background completion notices via
agent.inject(). Non-zero exits are reported, not errored.
Design surveyed against the bash tools of Claude Code, OpenCode, Codex,
and pi (notes in the package READMEs). Permissions/sandbox stay TODO on
the tools/execute waterfall seam; stateful-shell alternatives recorded
in run.ts.
2026-06-12 23:28:44 +08:00
throw new Error ( 'invalid description: expected a non-empty string' )
}
2026-06-13 23:00:42 +08:00
if ( args . timeoutMs !== undefined && ( ! Number . isFinite ( args . timeoutMs ) || args . timeoutMs <= 0 ) ) {
Add bash execution: dsh-bash seam, dsh-bash-local impl, dsh-tool-bash tools
Three packages following the new capability-seam pattern (interface /
implementation / consumer, now documented in docs/architecture.md):
- dsh-bash: abstract BashExecutor service (ctx.bash) + vocabulary types.
- dsh-bash-local: local subprocesses — bash -c per call in a detached
process group, SIGTERM→SIGKILL group kills, tail-keep truncation with
full-stream spill files, model-friendly env, background task registry.
- dsh-tool-bash: the bash / bash_output / bash_kill tool schemas with
runtime arg validation and background completion notices via
agent.inject(). Non-zero exits are reported, not errored.
Design surveyed against the bash tools of Claude Code, OpenCode, Codex,
and pi (notes in the package READMEs). Permissions/sandbox stay TODO on
the tools/execute waterfall seam; stateful-shell alternatives recorded
in run.ts.
2026-06-12 23:28:44 +08:00
throw new Error ( ` invalid timeoutMs: expected a positive number, got ${ JSON . stringify ( args . timeoutMs ) } ` )
}
feat(sandbox): cross-family file sandbox — one policy home, sandboxed fs provider, fs escalation parity
Extend SandboxMode enforcement from bash to the filesystem tools, the sandbox
RFC's deferred cross-family phase.
- dsh-sandbox-policy (new, ctx.sandboxPolicy): the single home for the
deployment default mode + workspaceRoot and the per-session override event,
renamed bash/sandbox-mode -> sandbox/mode and moved here with its fold/setter.
Decouples the bash seam from dsh-session.
- dsh-fs-sandbox (new): SandboxedFileSystem extends LocalFileSystem and fences
write/edit by the per-call mode (read-only denies, workspace-write contains to
the workspace + temp roots via the shared writableRoots, danger passes
through); reads pass through. Structured FS_SANDBOX_DENIED; in-lock parent
re-canonicalization. A policy fence in trusted code, not a kernel boundary.
- dsh-sandbox: the shared escalation kit (writableRoots, the strictly-wider
ladder, denial/hint markers, approveEscalation) both tool families use;
approveEscalation takes a structural approver so dsh-sandbox gains no
approval/agent dependency, and both tools stay duplication-free.
- tool-fs: write/edit advertise sandbox_permissions/justification under a
confining ctx.fs, map FS_SANDBOX_DENIED to the shared [sandbox: ...] marker,
and resolve the same one-approved-wider retry.
- examples/acp-agent: composes sandbox-policy + fs-sandbox, drops the gating
that disabled the fs stack under confined modes.
RFC docs/rfc/implemented/feature/2026-07-14-cross-family-fs-sandbox.md; the old
sandbox RFC's In-process/deferred/FAQ sections updated to shipped fact.
2026-07-14 20:05:57 +08:00
// The escalation pairing (sandbox_permissions ⇔ justification, non-empty) is
// the shared rule both enforcing families validate identically.
validateEscalationArgs ( args . sandbox_permissions , args . justification )
Add bash execution: dsh-bash seam, dsh-bash-local impl, dsh-tool-bash tools
Three packages following the new capability-seam pattern (interface /
implementation / consumer, now documented in docs/architecture.md):
- dsh-bash: abstract BashExecutor service (ctx.bash) + vocabulary types.
- dsh-bash-local: local subprocesses — bash -c per call in a detached
process group, SIGTERM→SIGKILL group kills, tail-keep truncation with
full-stream spill files, model-friendly env, background task registry.
- dsh-tool-bash: the bash / bash_output / bash_kill tool schemas with
runtime arg validation and background completion notices via
agent.inject(). Non-zero exits are reported, not errored.
Design surveyed against the bash tools of Claude Code, OpenCode, Codex,
and pi (notes in the package READMEs). Permissions/sandbox stay TODO on
the tools/execute waterfall seam; stateful-shell alternatives recorded
in run.ts.
2026-06-12 23:28:44 +08:00
}
2026-07-11 23:04:27 +08:00
function bashDescription ( backgroundEnabled : boolean , escalationModes : readonly SandboxMode [ ] ) : string {
const background = backgroundEnabled
? 'Set `run_in_background: true` for long-running commands: the call returns a task id immediately; read its output with `task_output` and stop it with `task_kill`.'
: 'Background execution is not available; long-running commands must finish within the timeout.'
2026-07-09 16:37:10 +08:00
const base = 'Execute a bash command (`bash -c`) and return its stdout/stderr. '
+ 'Each call runs in a fresh shell: no state (cwd, variables, functions) persists between calls — '
+ 'pass `workdir` instead of using `cd`. Non-zero exits are reported as `[exit code: N]`. '
2026-07-12 16:14:13 +08:00
+ ` Current harness environment facts are exposed through managed \` $ ${ DSH_ENV_PREFIX } * \` variables; inspect them when needed. `
2026-07-11 23:04:27 +08:00
+ 'Commands may run under a file sandbox; a blocked file operation is reported as `[sandbox: file access denied under <mode> mode]` — a policy denial, not a bug in the command; do not retry another way. '
2026-07-09 16:37:10 +08:00
+ 'Long output is truncated to its tail; the full output is saved to a file whose path is reported when available. '
2026-07-11 23:04:27 +08:00
+ background
2026-07-09 16:37:10 +08:00
if ( escalationModes . length === 0 ) return base
return base + ' Attempting a command the sandbox may deny is safe and expected: run it and read the '
2026-07-14 14:37:16 +08:00
+ 'marker rather than assuming the denial. When a command is denied and a wider mode would let it '
+ 'succeed, escalate immediately in the same turn — the one sanctioned exception to a denial: retry '
2026-07-09 16:37:10 +08:00
+ 'the exact same command once with `sandbox_permissions` (the narrowest wider mode that suffices) '
+ 'plus a one-sentence `justification`. Do not detour through chat to ask permission first — the '
2026-07-14 14:37:16 +08:00
+ 'approval prompt raised by that retry is how the user consents. If the session states approval '
2026-07-09 16:37:10 +08:00
+ 'prompts are disabled, there is no exception: a denial is final — do not set `sandbox_permissions`. '
2026-07-14 14:37:16 +08:00
+ 'Never escalate speculatively: ground the request in a real denial — normally the one this command '
2026-07-09 16:37:10 +08:00
+ 'just hit; escalating up front is fine only when this session already denied the same access. '
2026-07-14 14:37:16 +08:00
+ 'A rejected escalation is final for that command — stop and explain, never work around '
2026-07-09 16:37:10 +08:00
+ 'it — but it does not forbid attempting or escalating other commands later.'
}
feat(acp): tool-owned tool-call UI presentation (title/command/output)
In Zed the tool-call card showed only "bash" — the bare tool name — instead
of what the command does. Fix it by letting each TOOL own how its calls render,
rather than the bridge special-casing names.
dsh-tools: add an optional two-state presentation seam to ToolDefinition /
defineTool — `presentCall(args)` (pending: title, kind, rawInput) and
`presentResult(args, result)` (completed: title?, content?). Provider-neutral
`ToolCallKind`/`ToolCallPresentation`/`ToolResultPresentation` vocabulary so
tools never depend on ACP. defineTool soft-validates args (display runs on log
replay, so a malformed/old shape returns undefined instead of throwing).
dsh-tool-bash: bash declares presentCall (model `description` → title, exact
`command` → rawInput, kind execute) and presentResult (wrap output in a fenced
```console block — a UI-only affordance kept out of the model-facing result);
bash_output/bash_kill present task-scoped titles.
dsh-acp: inject `tools`; a per-session `ToolPresenter` looks the tool up by name
and maps its neutral presentation to the ACP tool_call/tool_call_update wire
shape, with a generic fallback (title = name) for tools that declare nothing.
Because the `tool/result` event carries only {callId, content, isError}, the
presenter keeps a small bridge-local map of ONLY in-flight calls' (name, args),
keyed by callId and removed as each result is presented — no event-schema or
core change. Replay uses a throwaway presenter so loaded sessions render
identically to live ones.
Tests: dsh-tools defineTool presenters (typed args, soft-validate), tool-bash
bash/bash_output/bash_kill presenters, acp ToolPresenter (tool-owned mapping,
unknown-callId fallback, in-flight-only map), and an end-to-end turn through the
bridge. The key-gated e2e now asserts a real bash call's title is the model
description (not "bash") and rawInput is the command — verified against the real
DeepSeek model. The test harness derives its inject from the bridge's exported
`inject` so it can't drift again.
2026-06-18 09:01:36 +08:00
/ * *
docs(tasks): condense background task prose
The background-task change repeated its lifecycle design across implemented RFCs, package READMEs, JSDoc, test commentary, and model-visible schemas. That repetition obscured the contracts that maintainers must preserve and added avoidable prompt tokens.
Rewrite the implemented RFCs around the current design, keep authorization, exact-owner cleanup, wait/abort ordering, producer quiescence, and teardown-failure guarantees at their owning surfaces, and remove peer surveys, review history, control-flow narration, and emphatic restatement.
Shorten the task and subagent schema wording, synchronize the bilingual tool cookbook, and regenerate the config, service, RFC, tool, and replay snapshot derivatives. Runtime behavior is unchanged; test edits update prose-only assertions and descriptions.
2026-07-15 21:08:58 +08:00
* Present foreground calls as terminals and background starts as generic cards .
* The command remains the title on both paths ; foreground cwd is passed through
* for the bridge to resolve , while background descriptions remain card content .
feat(acp): tool-owned tool-call UI presentation (title/command/output)
In Zed the tool-call card showed only "bash" — the bare tool name — instead
of what the command does. Fix it by letting each TOOL own how its calls render,
rather than the bridge special-casing names.
dsh-tools: add an optional two-state presentation seam to ToolDefinition /
defineTool — `presentCall(args)` (pending: title, kind, rawInput) and
`presentResult(args, result)` (completed: title?, content?). Provider-neutral
`ToolCallKind`/`ToolCallPresentation`/`ToolResultPresentation` vocabulary so
tools never depend on ACP. defineTool soft-validates args (display runs on log
replay, so a malformed/old shape returns undefined instead of throwing).
dsh-tool-bash: bash declares presentCall (model `description` → title, exact
`command` → rawInput, kind execute) and presentResult (wrap output in a fenced
```console block — a UI-only affordance kept out of the model-facing result);
bash_output/bash_kill present task-scoped titles.
dsh-acp: inject `tools`; a per-session `ToolPresenter` looks the tool up by name
and maps its neutral presentation to the ACP tool_call/tool_call_update wire
shape, with a generic fallback (title = name) for tools that declare nothing.
Because the `tool/result` event carries only {callId, content, isError}, the
presenter keeps a small bridge-local map of ONLY in-flight calls' (name, args),
keyed by callId and removed as each result is presented — no event-schema or
core change. Replay uses a throwaway presenter so loaded sessions render
identically to live ones.
Tests: dsh-tools defineTool presenters (typed args, soft-validate), tool-bash
bash/bash_output/bash_kill presenters, acp ToolPresenter (tool-owned mapping,
unknown-callId fallback, in-flight-only map), and an end-to-end turn through the
bridge. The key-gated e2e now asserts a real bash call's title is the model
description (not "bash") and rawInput is the command — verified against the real
DeepSeek model. The test harness derives its inject from the bridge's exported
`inject` so it can't drift again.
2026-06-18 09:01:36 +08:00
* /
2026-06-18 19:35:15 +08:00
type BashCallArgs = { command : string ; description : string ; workdir? : string ; run_in_background? : boolean }
2026-07-03 02:04:03 +08:00
function presentBashCall ( args : BashCallArgs ) : GenericCallView | TerminalCallView {
if ( args . run_in_background === true ) {
return {
card : 'generic' ,
title : args.command ,
kind : 'execute' ,
rawInput : args.command ,
content : [ { type : 'text' , text : args.description } ] ,
}
}
return {
card : 'terminal' ,
2026-06-18 18:54:32 +08:00
title : args.command ,
2026-07-03 02:04:03 +08:00
description : args.description ,
. . . args . workdir !== undefined ? { cwd : args.workdir } : { } ,
2026-06-18 17:25:09 +08:00
}
feat(acp): tool-owned tool-call UI presentation (title/command/output)
In Zed the tool-call card showed only "bash" — the bare tool name — instead
of what the command does. Fix it by letting each TOOL own how its calls render,
rather than the bridge special-casing names.
dsh-tools: add an optional two-state presentation seam to ToolDefinition /
defineTool — `presentCall(args)` (pending: title, kind, rawInput) and
`presentResult(args, result)` (completed: title?, content?). Provider-neutral
`ToolCallKind`/`ToolCallPresentation`/`ToolResultPresentation` vocabulary so
tools never depend on ACP. defineTool soft-validates args (display runs on log
replay, so a malformed/old shape returns undefined instead of throwing).
dsh-tool-bash: bash declares presentCall (model `description` → title, exact
`command` → rawInput, kind execute) and presentResult (wrap output in a fenced
```console block — a UI-only affordance kept out of the model-facing result);
bash_output/bash_kill present task-scoped titles.
dsh-acp: inject `tools`; a per-session `ToolPresenter` looks the tool up by name
and maps its neutral presentation to the ACP tool_call/tool_call_update wire
shape, with a generic fallback (title = name) for tools that declare nothing.
Because the `tool/result` event carries only {callId, content, isError}, the
presenter keeps a small bridge-local map of ONLY in-flight calls' (name, args),
keyed by callId and removed as each result is presented — no event-schema or
core change. Replay uses a throwaway presenter so loaded sessions render
identically to live ones.
Tests: dsh-tools defineTool presenters (typed args, soft-validate), tool-bash
bash/bash_output/bash_kill presenters, acp ToolPresenter (tool-owned mapping,
unknown-callId fallback, in-flight-only map), and an end-to-end turn through the
bridge. The key-gated e2e now asserts a real bash call's title is the model
description (not "bash") and rawInput is the command — verified against the real
DeepSeek model. The test harness derives its inject from the bridge's exported
`inject` so it can't drift again.
2026-06-18 09:01:36 +08:00
}
/ * *
2026-07-13 23:27:00 +08:00
* Present completed foreground output as a terminal ; background acknowledgements
* and execution errors use generic fenced output without an exit - status pill .
feat(acp): tool-owned tool-call UI presentation (title/command/output)
In Zed the tool-call card showed only "bash" — the bare tool name — instead
of what the command does. Fix it by letting each TOOL own how its calls render,
rather than the bridge special-casing names.
dsh-tools: add an optional two-state presentation seam to ToolDefinition /
defineTool — `presentCall(args)` (pending: title, kind, rawInput) and
`presentResult(args, result)` (completed: title?, content?). Provider-neutral
`ToolCallKind`/`ToolCallPresentation`/`ToolResultPresentation` vocabulary so
tools never depend on ACP. defineTool soft-validates args (display runs on log
replay, so a malformed/old shape returns undefined instead of throwing).
dsh-tool-bash: bash declares presentCall (model `description` → title, exact
`command` → rawInput, kind execute) and presentResult (wrap output in a fenced
```console block — a UI-only affordance kept out of the model-facing result);
bash_output/bash_kill present task-scoped titles.
dsh-acp: inject `tools`; a per-session `ToolPresenter` looks the tool up by name
and maps its neutral presentation to the ACP tool_call/tool_call_update wire
shape, with a generic fallback (title = name) for tools that declare nothing.
Because the `tool/result` event carries only {callId, content, isError}, the
presenter keeps a small bridge-local map of ONLY in-flight calls' (name, args),
keyed by callId and removed as each result is presented — no event-schema or
core change. Replay uses a throwaway presenter so loaded sessions render
identically to live ones.
Tests: dsh-tools defineTool presenters (typed args, soft-validate), tool-bash
bash/bash_output/bash_kill presenters, acp ToolPresenter (tool-owned mapping,
unknown-callId fallback, in-flight-only map), and an end-to-end turn through the
bridge. The key-gated e2e now asserts a real bash call's title is the model
description (not "bash") and rawInput is the command — verified against the real
DeepSeek model. The test harness derives its inject from the bridge's exported
`inject` so it can't drift again.
2026-06-18 09:01:36 +08:00
* /
2026-07-03 02:04:03 +08:00
function presentBashResult ( args : unknown , result : ToolResult ) : ToolResultView | undefined {
feat(acp): tool-owned tool-call UI presentation (title/command/output)
In Zed the tool-call card showed only "bash" — the bare tool name — instead
of what the command does. Fix it by letting each TOOL own how its calls render,
rather than the bridge special-casing names.
dsh-tools: add an optional two-state presentation seam to ToolDefinition /
defineTool — `presentCall(args)` (pending: title, kind, rawInput) and
`presentResult(args, result)` (completed: title?, content?). Provider-neutral
`ToolCallKind`/`ToolCallPresentation`/`ToolResultPresentation` vocabulary so
tools never depend on ACP. defineTool soft-validates args (display runs on log
replay, so a malformed/old shape returns undefined instead of throwing).
dsh-tool-bash: bash declares presentCall (model `description` → title, exact
`command` → rawInput, kind execute) and presentResult (wrap output in a fenced
```console block — a UI-only affordance kept out of the model-facing result);
bash_output/bash_kill present task-scoped titles.
dsh-acp: inject `tools`; a per-session `ToolPresenter` looks the tool up by name
and maps its neutral presentation to the ACP tool_call/tool_call_update wire
shape, with a generic fallback (title = name) for tools that declare nothing.
Because the `tool/result` event carries only {callId, content, isError}, the
presenter keeps a small bridge-local map of ONLY in-flight calls' (name, args),
keyed by callId and removed as each result is presented — no event-schema or
core change. Replay uses a throwaway presenter so loaded sessions render
identically to live ones.
Tests: dsh-tools defineTool presenters (typed args, soft-validate), tool-bash
bash/bash_output/bash_kill presenters, acp ToolPresenter (tool-owned mapping,
unknown-callId fallback, in-flight-only map), and an end-to-end turn through the
bridge. The key-gated e2e now asserts a real bash call's title is the model
description (not "bash") and rawInput is the command — verified against the real
DeepSeek model. The test harness derives its inject from the bridge's exported
`inject` so it can't drift again.
2026-06-18 09:01:36 +08:00
const block = result . content . length === 1 ? result . content [ 0 ] : undefined
if ( block === undefined || block . type !== 'text' ) return undefined
2026-06-18 18:54:32 +08:00
const raw = block . text
2026-06-18 19:35:15 +08:00
const isBackground = typeof args === 'object' && args !== null && ( args as { run_in_background? : unknown } ) . run_in_background === true
docs(tasks): condense background task prose
The background-task change repeated its lifecycle design across implemented RFCs, package READMEs, JSDoc, test commentary, and model-visible schemas. That repetition obscured the contracts that maintainers must preserve and added avoidable prompt tokens.
Rewrite the implemented RFCs around the current design, keep authorization, exact-owner cleanup, wait/abort ordering, producer quiescence, and teardown-failure guarantees at their owning surfaces, and remove peer surveys, review history, control-flow narration, and emphatic restatement.
Shorten the task and subagent schema wording, synchronize the bilingual tool cookbook, and regenerate the config, service, RFC, tool, and replay snapshot derivatives. Runtime behavior is unchanged; test edits update prose-only assertions and descriptions.
2026-07-15 21:08:58 +08:00
// Background acknowledgements and errors have no terminal exit status.
2026-07-03 02:04:03 +08:00
if ( isBackground || result . isError ) {
return { card : 'generic' , content : [ { type : 'text' , text : ` \` \` \` console \ n ${ raw . replace ( /\n+$/ , '' ) } \ n \` \` \` ` } ] }
}
return { card : 'terminal' , output : raw , . . . parseExitStatus ( raw ) }
feat(acp): tool-owned tool-call UI presentation (title/command/output)
In Zed the tool-call card showed only "bash" — the bare tool name — instead
of what the command does. Fix it by letting each TOOL own how its calls render,
rather than the bridge special-casing names.
dsh-tools: add an optional two-state presentation seam to ToolDefinition /
defineTool — `presentCall(args)` (pending: title, kind, rawInput) and
`presentResult(args, result)` (completed: title?, content?). Provider-neutral
`ToolCallKind`/`ToolCallPresentation`/`ToolResultPresentation` vocabulary so
tools never depend on ACP. defineTool soft-validates args (display runs on log
replay, so a malformed/old shape returns undefined instead of throwing).
dsh-tool-bash: bash declares presentCall (model `description` → title, exact
`command` → rawInput, kind execute) and presentResult (wrap output in a fenced
```console block — a UI-only affordance kept out of the model-facing result);
bash_output/bash_kill present task-scoped titles.
dsh-acp: inject `tools`; a per-session `ToolPresenter` looks the tool up by name
and maps its neutral presentation to the ACP tool_call/tool_call_update wire
shape, with a generic fallback (title = name) for tools that declare nothing.
Because the `tool/result` event carries only {callId, content, isError}, the
presenter keeps a small bridge-local map of ONLY in-flight calls' (name, args),
keyed by callId and removed as each result is presented — no event-schema or
core change. Replay uses a throwaway presenter so loaded sessions render
identically to live ones.
Tests: dsh-tools defineTool presenters (typed args, soft-validate), tool-bash
bash/bash_output/bash_kill presenters, acp ToolPresenter (tool-owned mapping,
unknown-callId fallback, in-flight-only map), and an end-to-end turn through the
bridge. The key-gated e2e now asserts a real bash call's title is the model
description (not "bash") and rawInput is the command — verified against the real
DeepSeek model. The test harness derives its inject from the bridge's exported
`inject` so it can't drift again.
2026-06-18 09:01:36 +08:00
}
2026-06-17 10:01:18 +08:00
/ * *
2026-07-21 19:53:02 +08:00
* Resolve an explicit workdir first , making a relative one session - workspace - relative ;
* otherwise use the filesystem identity of the session cwd and leave executor
* defaulting as the fallback . A resolved sandbox - policy root wins so workdir
* and confinement use the exact same per - call identity .
2026-06-17 10:01:18 +08:00
* /
2026-07-21 19:53:02 +08:00
function resolveWorkdir (
modelWorkdir : string | undefined ,
exec : { agent? : Agent } ,
policyWorkspaceRoot? : string ,
) : string | undefined {
const headerCwd = exec . agent ? . session . header . cwd
const sessionCwd = policyWorkspaceRoot ? ? ( headerCwd === undefined ? undefined : canonicalPath ( headerCwd ) )
2026-06-17 10:01:18 +08:00
if ( modelWorkdir === undefined ) return sessionCwd
if ( sessionCwd !== undefined && ! isAbsolute ( modelWorkdir ) ) {
return resolvePath ( sessionCwd , modelWorkdir )
}
return modelWorkdir
}
2026-07-21 03:08:35 +08:00
/** Detach the executor DTO from readonly seam interfaces into plain JSON data. */
function canonicalBashResult ( result : BashRunResult ) {
const output = ( stream : BashRunResult [ 'stdout' ] ) = > ( {
text : stream.text ,
truncated : stream.truncated ,
. . . stream . spillPath !== undefined ? { spillPath : stream.spillPath } : { } ,
} )
return {
exitCode : result.exitCode ,
signal : result.signal ,
timedOut : result.timedOut ,
aborted : result.aborted ,
timeoutMs : result.timeoutMs ,
stdout : output ( result . stdout ) ,
stderr : output ( result . stderr ) ,
. . . result . sandbox !== undefined ? {
sandbox : {
mode : result.sandbox.mode ,
denied : result.sandbox.denied ,
. . . result . sandbox . enforcement !== undefined ? { enforcement : result.sandbox.enforcement } : { } ,
. . . result . sandbox . runnerFailed !== undefined ? { runnerFailed : result.sandbox.runnerFailed } : { } ,
} ,
} : { } ,
}
}
/** Canonical background-handle properties shared by the bash output union. */
const BACKGROUND_OUTPUT_PROPERTIES = {
kind : { type : 'string' , required : true , const : 'background' } ,
taskId : { type : 'string' , required : true } ,
} as const
2026-07-12 15:41:42 +08:00
export function apply ( ctx : Context , config : Config = { } ) : void {
const bashEnv = new BashEnvRegistry ( ctx , config )
bashEnv . register ( {
name : 'session-persistence' ,
variables : {
2026-07-12 16:14:13 +08:00
[ DSH_SESSION_JSONL_KEY ] : {
2026-07-12 15:41:42 +08:00
description : 'Absolute target path of the current session JSONL when the active persistence backend provides one.' ,
} ,
} ,
resolve ( execution ) {
const agent = execution . agent
if ( agent === undefined ) return { }
const location = ctx . get ( 'sessionPersistence' ) ? . locate ( agent . session . header )
2026-07-12 16:14:13 +08:00
return location ? . kind === 'jsonl' ? { [ DSH_SESSION_JSONL_KEY ] : location . path } : { }
2026-07-12 15:41:42 +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
const backgroundEnabled = config . enableRunInBackground ? ? true
2026-07-09 16:37:10 +08:00
const defaultMode = ctx . bash . sandboxMode
const escalationModes : readonly SandboxMode [ ] = defaultMode === undefined ? [ ] : ESCALATION_TARGETS
2026-07-21 00:44:28 +08:00
const sandboxPolicy : SandboxPolicyService | undefined = defaultMode === undefined ? undefined : ctx . get ( 'sandboxPolicy' )
if ( defaultMode !== undefined && sandboxPolicy === undefined ) {
throw new Error ( 'tool-bash: the mounted bash executor confines but ctx.sandboxPolicy is missing' )
}
2026-07-09 16:37:10 +08:00
2026-07-21 00:44:28 +08:00
/** Resolve the complete standing policy for this call when a confining executor is mounted. */
const resolveSandboxPolicy = ( exec : ToolExecution ) : SandboxExecutionPolicy | undefined = >
sandboxPolicy ? . resolve ( exec . agent === undefined ? { } : { session : exec.agent.session } )
feat(modes): per-session sandbox/approval switching — the session log as the store, ACP config options
effective(session) = findLast(the session own knob events)?.value ?? the
composition-config default. One log-only event per knob, owned by its
domain (bash/sandbox-mode in dsh-bash, approval/policy in dsh-approval),
each exporting the same three-piece kit: the event declaration, a pure
fold, and THE write path — a switch IS its event; no owner service, no
facts map. Restart immunity and multi-session isolation fall out of the
log replay by construction.
Execution follows the fold on both sides: the bash tool stamps
escalation grant > session override > executor default, and the approval
seam prepends the never-gate that auto-rejects before any interactive
answerer. Visibility is two layers per knob: a per-agent prompt section
states the effective value on every request (logged through
request/header*, so what-the-model-was-told replays from the log), and an
agent/pre-step narrator injects at most one coalesced delta notice with
positional attribution (user switch vs operator/config drift). The ACP
bridge advertises one capability-gated select per composable knob with
currentValue folded per session, validates set_config_option against the
closed vocabularies, and anchors idle switches at the next turn
prompt-submit under the turn-enclosure contract.
2026-07-09 16:41:03 +08:00
2026-07-09 16:37:10 +08:00
/ * *
* Resolve a sandbox - escalation request through ` ctx.approval ` BEFORE
feat(sandbox): cross-family file sandbox — one policy home, sandboxed fs provider, fs escalation parity
Extend SandboxMode enforcement from bash to the filesystem tools, the sandbox
RFC's deferred cross-family phase.
- dsh-sandbox-policy (new, ctx.sandboxPolicy): the single home for the
deployment default mode + workspaceRoot and the per-session override event,
renamed bash/sandbox-mode -> sandbox/mode and moved here with its fold/setter.
Decouples the bash seam from dsh-session.
- dsh-fs-sandbox (new): SandboxedFileSystem extends LocalFileSystem and fences
write/edit by the per-call mode (read-only denies, workspace-write contains to
the workspace + temp roots via the shared writableRoots, danger passes
through); reads pass through. Structured FS_SANDBOX_DENIED; in-lock parent
re-canonicalization. A policy fence in trusted code, not a kernel boundary.
- dsh-sandbox: the shared escalation kit (writableRoots, the strictly-wider
ladder, denial/hint markers, approveEscalation) both tool families use;
approveEscalation takes a structural approver so dsh-sandbox gains no
approval/agent dependency, and both tools stay duplication-free.
- tool-fs: write/edit advertise sandbox_permissions/justification under a
confining ctx.fs, map FS_SANDBOX_DENIED to the shared [sandbox: ...] marker,
and resolve the same one-approved-wider retry.
- examples/acp-agent: composes sandbox-policy + fs-sandbox, drops the gating
that disabled the fs stack under confined modes.
RFC docs/rfc/implemented/feature/2026-07-14-cross-family-fs-sandbox.md; the old
sandbox RFC's In-process/deferred/FAQ sections updated to shipped fact.
2026-07-14 20:05:57 +08:00
* anything executes , delegating the shared fail - closed sequence ( strict
* widening , channel resolution , outcome mapping ) to
* { @link approveEscalation } . This tool contributes only the composition
* guard ( the fields are unadvertised without a sandboxing executor , yet
* schema validation checks advertised keys only , so an unadvertised
2026-07-16 23:31:51 +08:00
* ` sandbox_permissions ` still reaches execute ) and the approval ingredients
2026-07-21 00:44:28 +08:00
* The shared policy resolver is required whenever the executor advertises
* confinement , so a split composition fails at tool - plugin load .
2026-07-09 16:37:10 +08:00
* /
2026-07-21 00:44:28 +08:00
const approveBashEscalation = (
mode : string ,
justification : string ,
exec : ToolExecution ,
standingPolicy : SandboxExecutionPolicy | undefined ,
) : Promise < SandboxMode > = > {
2026-07-09 16:37:10 +08:00
if ( escalationModes . length === 0 ) {
throw new Error ( 'sandbox_permissions is not available in this composition (no sandboxing executor to escalate)' )
}
2026-07-21 00:44:28 +08:00
const effectiveMode = ( standingPolicy as SandboxExecutionPolicy ) . mode
feat(sandbox): cross-family file sandbox — one policy home, sandboxed fs provider, fs escalation parity
Extend SandboxMode enforcement from bash to the filesystem tools, the sandbox
RFC's deferred cross-family phase.
- dsh-sandbox-policy (new, ctx.sandboxPolicy): the single home for the
deployment default mode + workspaceRoot and the per-session override event,
renamed bash/sandbox-mode -> sandbox/mode and moved here with its fold/setter.
Decouples the bash seam from dsh-session.
- dsh-fs-sandbox (new): SandboxedFileSystem extends LocalFileSystem and fences
write/edit by the per-call mode (read-only denies, workspace-write contains to
the workspace + temp roots via the shared writableRoots, danger passes
through); reads pass through. Structured FS_SANDBOX_DENIED; in-lock parent
re-canonicalization. A policy fence in trusted code, not a kernel boundary.
- dsh-sandbox: the shared escalation kit (writableRoots, the strictly-wider
ladder, denial/hint markers, approveEscalation) both tool families use;
approveEscalation takes a structural approver so dsh-sandbox gains no
approval/agent dependency, and both tools stay duplication-free.
- tool-fs: write/edit advertise sandbox_permissions/justification under a
confining ctx.fs, map FS_SANDBOX_DENIED to the shared [sandbox: ...] marker,
and resolve the same one-approved-wider retry.
- examples/acp-agent: composes sandbox-policy + fs-sandbox, drops the gating
that disabled the fs stack under confined modes.
RFC docs/rfc/implemented/feature/2026-07-14-cross-family-fs-sandbox.md; the old
sandbox RFC's In-process/deferred/FAQ sections updated to shipped fact.
2026-07-14 20:05:57 +08:00
return approveEscalation (
{ requestedMode : mode , justification , effectiveMode , subject : 'command' } ,
{
approver : ctx.get ( 'approval' ) ,
agent : exec.agent ,
callId : exec.callId ,
toolName : 'bash' ,
2026-07-20 23:00:21 +08:00
signal : exec.signal ,
feat(sandbox): cross-family file sandbox — one policy home, sandboxed fs provider, fs escalation parity
Extend SandboxMode enforcement from bash to the filesystem tools, the sandbox
RFC's deferred cross-family phase.
- dsh-sandbox-policy (new, ctx.sandboxPolicy): the single home for the
deployment default mode + workspaceRoot and the per-session override event,
renamed bash/sandbox-mode -> sandbox/mode and moved here with its fold/setter.
Decouples the bash seam from dsh-session.
- dsh-fs-sandbox (new): SandboxedFileSystem extends LocalFileSystem and fences
write/edit by the per-call mode (read-only denies, workspace-write contains to
the workspace + temp roots via the shared writableRoots, danger passes
through); reads pass through. Structured FS_SANDBOX_DENIED; in-lock parent
re-canonicalization. A policy fence in trusted code, not a kernel boundary.
- dsh-sandbox: the shared escalation kit (writableRoots, the strictly-wider
ladder, denial/hint markers, approveEscalation) both tool families use;
approveEscalation takes a structural approver so dsh-sandbox gains no
approval/agent dependency, and both tools stay duplication-free.
- tool-fs: write/edit advertise sandbox_permissions/justification under a
confining ctx.fs, map FS_SANDBOX_DENIED to the shared [sandbox: ...] marker,
and resolve the same one-approved-wider retry.
- examples/acp-agent: composes sandbox-policy + fs-sandbox, drops the gating
that disabled the fs stack under confined modes.
RFC docs/rfc/implemented/feature/2026-07-14-cross-family-fs-sandbox.md; the old
sandbox RFC's In-process/deferred/FAQ sections updated to shipped fact.
2026-07-14 20:05:57 +08:00
} ,
)
2026-07-09 16:37:10 +08:00
}
docs(tasks): condense background task prose
The background-task change repeated its lifecycle design across implemented RFCs, package READMEs, JSDoc, test commentary, and model-visible schemas. That repetition obscured the contracts that maintainers must preserve and added avoidable prompt tokens.
Rewrite the implemented RFCs around the current design, keep authorization, exact-owner cleanup, wait/abort ordering, producer quiescence, and teardown-failure guarantees at their owning surfaces, and remove peer surveys, review history, control-flow narration, and emphatic restatement.
Shorten the task and subagent schema wording, synchronize the bilingual tool cookbook, and regenerate the config, service, RFC, tool, and replay snapshot derivatives. Runtime behavior is unchanged; test edits update prose-only assertions and descriptions.
2026-07-15 21:08:58 +08:00
// Cross-call guidance belongs in the prompt rather than one-call schema prose.
2026-07-05 01:54:46 +08:00
ctx . systemPrompt . section ( {
name : 'tool:bash' ,
order : 105 ,
text : 'Check the [exit code: N] marker on every bash result; investigate failures before moving on.' ,
} )
Add bash execution: dsh-bash seam, dsh-bash-local impl, dsh-tool-bash tools
Three packages following the new capability-seam pattern (interface /
implementation / consumer, now documented in docs/architecture.md):
- dsh-bash: abstract BashExecutor service (ctx.bash) + vocabulary types.
- dsh-bash-local: local subprocesses — bash -c per call in a detached
process group, SIGTERM→SIGKILL group kills, tail-keep truncation with
full-stream spill files, model-friendly env, background task registry.
- dsh-tool-bash: the bash / bash_output / bash_kill tool schemas with
runtime arg validation and background completion notices via
agent.inject(). Non-zero exits are reported, not errored.
Design surveyed against the bash tools of Claude Code, OpenCode, Codex,
and pi (notes in the package READMEs). Permissions/sandbox stay TODO on
the tools/execute waterfall seam; stateful-shell alternatives recorded
in run.ts.
2026-06-12 23:28:44 +08:00
ctx . tools . register ( defineTool ( {
name : 'bash' ,
2026-07-11 23:04:27 +08:00
description : bashDescription ( backgroundEnabled , escalationModes ) ,
Add bash execution: dsh-bash seam, dsh-bash-local impl, dsh-tool-bash tools
Three packages following the new capability-seam pattern (interface /
implementation / consumer, now documented in docs/architecture.md):
- dsh-bash: abstract BashExecutor service (ctx.bash) + vocabulary types.
- dsh-bash-local: local subprocesses — bash -c per call in a detached
process group, SIGTERM→SIGKILL group kills, tail-keep truncation with
full-stream spill files, model-friendly env, background task registry.
- dsh-tool-bash: the bash / bash_output / bash_kill tool schemas with
runtime arg validation and background completion notices via
agent.inject(). Non-zero exits are reported, not errored.
Design surveyed against the bash tools of Claude Code, OpenCode, Codex,
and pi (notes in the package READMEs). Permissions/sandbox stay TODO on
the tools/execute waterfall seam; stateful-shell alternatives recorded
in run.ts.
2026-06-12 23:28:44 +08:00
parameters : {
command : { type : 'string' , required : true , description : 'The bash command to execute.' } ,
description : {
type : 'string' ,
required : true ,
description : 'Clear, concise description of what this command does in active voice, '
+ '5-10 words (shown in the UI). Examples: "ls" → "List files in current directory"; '
+ '"git status" → "Show working tree status"; "npm install" → "Install package dependencies".' ,
} ,
2026-06-17 21:26:44 +08:00
timeoutMs : { type : 'number' , description : 'Timeout in milliseconds. The executor applies its configured default and cap, and kills the command on expiry.' } ,
2026-06-17 10:01:18 +08:00
workdir : { type : 'string' , description : 'Working directory for this command. Defaults to the session workspace; a relative path is resolved against it.' } ,
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
. . . backgroundEnabled ? {
run_in_background : { type : 'boolean' as const , description : 'Run in the background and return a task id immediately (collect with task_output, stop with task_kill). No timeout applies.' } ,
} : { } ,
2026-07-09 16:37:10 +08:00
. . . escalationModes . length > 0 ? {
sandbox_permissions : {
type : 'string' as const ,
enum : [ . . . escalationModes ] ,
2026-07-11 23:04:27 +08:00
description : 'The wider sandbox mode this command needs. Only valid as a one-shot retry of a command the sandbox just denied; requires justification and user approval.' ,
2026-07-09 16:37:10 +08:00
} ,
justification : {
type : 'string' as const ,
2026-07-11 23:04:27 +08:00
description : 'Required with sandbox_permissions: one sentence for the user explaining why this exact command needs the wider access.' ,
2026-07-09 16:37:10 +08:00
} ,
} : { } ,
Add bash execution: dsh-bash seam, dsh-bash-local impl, dsh-tool-bash tools
Three packages following the new capability-seam pattern (interface /
implementation / consumer, now documented in docs/architecture.md):
- dsh-bash: abstract BashExecutor service (ctx.bash) + vocabulary types.
- dsh-bash-local: local subprocesses — bash -c per call in a detached
process group, SIGTERM→SIGKILL group kills, tail-keep truncation with
full-stream spill files, model-friendly env, background task registry.
- dsh-tool-bash: the bash / bash_output / bash_kill tool schemas with
runtime arg validation and background completion notices via
agent.inject(). Non-zero exits are reported, not errored.
Design surveyed against the bash tools of Claude Code, OpenCode, Codex,
and pi (notes in the package READMEs). Permissions/sandbox stay TODO on
the tools/execute waterfall seam; stateful-shell alternatives recorded
in run.ts.
2026-06-12 23:28:44 +08:00
} ,
2026-07-21 03:08:35 +08:00
output : {
schema : {
oneOf : [
{
type : 'object' ,
additionalProperties : false ,
properties : BACKGROUND_OUTPUT_PROPERTIES ,
} ,
{
type : 'object' ,
additionalProperties : false ,
properties : {
kind : { type : 'string' , required : true , const : 'foreground' } ,
exitCode : { required : true , oneOf : [ { type : 'integer' } , { type : 'null' } ] } ,
signal : { required : true , oneOf : [ { type : 'string' } , { type : 'null' } ] } ,
timedOut : { type : 'boolean' , required : true } ,
aborted : { type : 'boolean' , required : true } ,
timeoutMs : { type : 'number' , required : true } ,
stdout : {
type : 'object' ,
additionalProperties : false ,
required : true ,
properties : {
text : { type : 'string' , required : true } ,
truncated : { type : 'boolean' , required : true } ,
spillPath : { type : 'string' } ,
} ,
} ,
stderr : {
type : 'object' ,
additionalProperties : false ,
required : true ,
properties : {
text : { type : 'string' , required : true } ,
truncated : { type : 'boolean' , required : true } ,
spillPath : { type : 'string' } ,
} ,
} ,
sandbox : {
type : 'object' ,
additionalProperties : false ,
properties : {
mode : { type : 'string' , required : true } ,
denied : { type : 'boolean' , required : true } ,
enforcement : { type : 'string' } ,
runnerFailed : { type : 'boolean' } ,
} ,
} ,
} ,
} ,
] ,
} ,
render : ( _args , value ) = > [ {
type : 'text' ,
text : value.kind === 'background'
? ` started background task ${ value . taskId } `
: renderResult ( value as { kind : 'foreground' } & BashRunResult , escalationModes ) ,
} ] ,
} ,
2026-07-09 16:37:10 +08:00
async execute ( args : BashToolArgs , exec ) {
Add bash execution: dsh-bash seam, dsh-bash-local impl, dsh-tool-bash tools
Three packages following the new capability-seam pattern (interface /
implementation / consumer, now documented in docs/architecture.md):
- dsh-bash: abstract BashExecutor service (ctx.bash) + vocabulary types.
- dsh-bash-local: local subprocesses — bash -c per call in a detached
process group, SIGTERM→SIGKILL group kills, tail-keep truncation with
full-stream spill files, model-friendly env, background task registry.
- dsh-tool-bash: the bash / bash_output / bash_kill tool schemas with
runtime arg validation and background completion notices via
agent.inject(). Non-zero exits are reported, not errored.
Design surveyed against the bash tools of Claude Code, OpenCode, Codex,
and pi (notes in the package READMEs). Permissions/sandbox stay TODO on
the tools/execute waterfall seam; stateful-shell alternatives recorded
in run.ts.
2026-06-12 23:28:44 +08:00
validateBashArgs ( args )
docs(tasks): condense background task prose
The background-task change repeated its lifecycle design across implemented RFCs, package READMEs, JSDoc, test commentary, and model-visible schemas. That repetition obscured the contracts that maintainers must preserve and added avoidable prompt tokens.
Rewrite the implemented RFCs around the current design, keep authorization, exact-owner cleanup, wait/abort ordering, producer quiescence, and teardown-failure guarantees at their owning surfaces, and remove peer surveys, review history, control-flow narration, and emphatic restatement.
Shorten the task and subagent schema wording, synchronize the bilingual tool cookbook, and regenerate the config, service, RFC, tool, and replay snapshot derivatives. Runtime behavior is unchanged; test edits update prose-only assertions and descriptions.
2026-07-15 21:08:58 +08:00
// Description is display metadata; workdir defaults to the caller's session.
2026-07-21 00:44:28 +08:00
const standingPolicy = resolveSandboxPolicy ( exec )
const approvedMode = args . sandbox_permissions !== undefined && args . justification !== undefined
? await approveBashEscalation ( args . sandbox_permissions , args . justification , exec , standingPolicy )
: undefined
const policy = approvedMode === undefined
? standingPolicy
: { . . . ( standingPolicy as SandboxExecutionPolicy ) , mode : approvedMode }
2026-07-21 19:53:02 +08:00
const workdir = resolveWorkdir ( args . workdir , exec , standingPolicy ? . workspaceRoot )
2026-07-12 15:41:42 +08:00
const dshEnv = bashEnv . collect ( exec )
Add bash execution: dsh-bash seam, dsh-bash-local impl, dsh-tool-bash tools
Three packages following the new capability-seam pattern (interface /
implementation / consumer, now documented in docs/architecture.md):
- dsh-bash: abstract BashExecutor service (ctx.bash) + vocabulary types.
- dsh-bash-local: local subprocesses — bash -c per call in a detached
process group, SIGTERM→SIGKILL group kills, tail-keep truncation with
full-stream spill files, model-friendly env, background task registry.
- dsh-tool-bash: the bash / bash_output / bash_kill tool schemas with
runtime arg validation and background completion notices via
agent.inject(). Non-zero exits are reported, not errored.
Design surveyed against the bash tools of Claude Code, OpenCode, Codex,
and pi (notes in the package READMEs). Permissions/sandbox stay TODO on
the tools/execute waterfall seam; stateful-shell alternatives recorded
in run.ts.
2026-06-12 23:28:44 +08:00
const request = {
command : args.command ,
2026-06-17 10:01:18 +08:00
. . . workdir !== undefined ? { workdir } : { } ,
Add bash execution: dsh-bash seam, dsh-bash-local impl, dsh-tool-bash tools
Three packages following the new capability-seam pattern (interface /
implementation / consumer, now documented in docs/architecture.md):
- dsh-bash: abstract BashExecutor service (ctx.bash) + vocabulary types.
- dsh-bash-local: local subprocesses — bash -c per call in a detached
process group, SIGTERM→SIGKILL group kills, tail-keep truncation with
full-stream spill files, model-friendly env, background task registry.
- dsh-tool-bash: the bash / bash_output / bash_kill tool schemas with
runtime arg validation and background completion notices via
agent.inject(). Non-zero exits are reported, not errored.
Design surveyed against the bash tools of Claude Code, OpenCode, Codex,
and pi (notes in the package READMEs). Permissions/sandbox stay TODO on
the tools/execute waterfall seam; stateful-shell alternatives recorded
in run.ts.
2026-06-12 23:28:44 +08:00
. . . args . timeoutMs !== undefined ? { timeoutMs : args.timeoutMs } : { } ,
2026-07-12 15:41:42 +08:00
dshEnv ,
2026-07-21 00:44:28 +08:00
. . . policy !== undefined ? { sandboxPolicy : policy } : { } ,
Add bash execution: dsh-bash seam, dsh-bash-local impl, dsh-tool-bash tools
Three packages following the new capability-seam pattern (interface /
implementation / consumer, now documented in docs/architecture.md):
- dsh-bash: abstract BashExecutor service (ctx.bash) + vocabulary types.
- dsh-bash-local: local subprocesses — bash -c per call in a detached
process group, SIGTERM→SIGKILL group kills, tail-keep truncation with
full-stream spill files, model-friendly env, background task registry.
- dsh-tool-bash: the bash / bash_output / bash_kill tool schemas with
runtime arg validation and background completion notices via
agent.inject(). Non-zero exits are reported, not errored.
Design surveyed against the bash tools of Claude Code, OpenCode, Codex,
and pi (notes in the package READMEs). Permissions/sandbox stay TODO on
the tools/execute waterfall seam; stateful-shell alternatives recorded
in run.ts.
2026-06-12 23:28:44 +08:00
}
if ( args . run_in_background === true ) {
docs(tasks): condense background task prose
The background-task change repeated its lifecycle design across implemented RFCs, package READMEs, JSDoc, test commentary, and model-visible schemas. That repetition obscured the contracts that maintainers must preserve and added avoidable prompt tokens.
Rewrite the implemented RFCs around the current design, keep authorization, exact-owner cleanup, wait/abort ordering, producer quiescence, and teardown-failure guarantees at their owning surfaces, and remove peer surveys, review history, control-flow narration, and emphatic restatement.
Shorten the task and subagent schema wording, synchronize the bilingual tool cookbook, and regenerate the config, service, RFC, tool, and replay snapshot derivatives. Runtime behavior is unchanged; test edits update prose-only assertions and descriptions.
2026-07-15 21:08:58 +08:00
// Undeclared keys are allowed, so schema omission also needs enforcement.
2026-07-09 23:37:51 +08:00
if ( ! backgroundEnabled ) {
throw new Error ( 'run_in_background is disabled for this deployment (enableRunInBackground: false)' )
}
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
const tasks = ctx . get ( 'tasks' )
if ( tasks === undefined ) {
2026-07-26 12:17:45 +08:00
throw new Error ( 'background tasks unavailable: load @deepseek-ai/dsh-tasks and @deepseek-ai/dsh-tool-tasks' )
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
}
2026-07-26 05:13:39 +08:00
// The caller owns cancellation until ctx.tasks commits detached ownership.
2026-07-21 23:39:03 +08:00
if ( exec . signal . aborted ) {
const error = new HarnessError ( 'tool call aborted' , TOOL_ABORTED )
error . name = 'AbortError'
throw error
}
docs(tasks): condense background task prose
The background-task change repeated its lifecycle design across implemented RFCs, package READMEs, JSDoc, test commentary, and model-visible schemas. That repetition obscured the contracts that maintainers must preserve and added avoidable prompt tokens.
Rewrite the implemented RFCs around the current design, keep authorization, exact-owner cleanup, wait/abort ordering, producer quiescence, and teardown-failure guarantees at their owning surfaces, and remove peer surveys, review history, control-flow narration, and emphatic restatement.
Shorten the task and subagent schema wording, synchronize the bilingual tool cookbook, and regenerate the config, service, RFC, tool, and replay snapshot derivatives. Runtime behavior is unchanged; test edits update prose-only assertions and descriptions.
2026-07-15 21:08:58 +08:00
// Task preflight finishes before the starter can spawn a process.
refactor(tasks): declare-then-execute — ctx.tasks.start() replaces register()
start({ kind, label, owner, run }) preflights everything that can fail
(the attachSurface fence, validation, the owner-cleanup attach) BEFORE
invoking the producer's run() starter, then commits atomically —
'work started but never got a collectable id' is now structurally
impossible instead of a producer try/catch rollback obligation (the
P1 review fix, rebuilt on #185's declare/execute split). Producers
lose their catch-wraps; the leak tests now pin the stronger property
that a failed preflight never spawns anything. TaskRegistration splits
into TaskStart (identity + run) and TaskHooks (cancel/done/readOutput);
docs, type-equiv manifest, catalogs, and both RFCs move with it.
2026-07-09 21:53:48 +08:00
const id = tasks . start ( {
kind : 'bash' ,
label : args.command ,
. . . exec . agent ? { owner : exec.agent } : { } ,
run : ( ) = > {
const proc = ctx . bash . start ( ctx . bash . resolve ( request ) )
return {
cancel : ( ) = > void proc . kill ( ) ,
done : proc.done.then ( ( ) = > processOutcome ( proc ) ) ,
2026-07-11 23:04:27 +08:00
readOutput : ( ) = > renderProcessRead ( proc . readOutput ( ) , proc . sandbox , escalationModes ) ,
refactor(tasks): declare-then-execute — ctx.tasks.start() replaces register()
start({ kind, label, owner, run }) preflights everything that can fail
(the attachSurface fence, validation, the owner-cleanup attach) BEFORE
invoking the producer's run() starter, then commits atomically —
'work started but never got a collectable id' is now structurally
impossible instead of a producer try/catch rollback obligation (the
P1 review fix, rebuilt on #185's declare/execute split). Producers
lose their catch-wraps; the leak tests now pin the stronger property
that a failed preflight never spawns anything. TaskRegistration splits
into TaskStart (identity + run) and TaskHooks (cancel/done/readOutput);
docs, type-equiv manifest, catalogs, and both RFCs move with it.
2026-07-09 21:53:48 +08:00
}
} ,
} )
2026-07-21 03:08:35 +08:00
return { kind : 'background' as const , taskId : id }
Add bash execution: dsh-bash seam, dsh-bash-local impl, dsh-tool-bash tools
Three packages following the new capability-seam pattern (interface /
implementation / consumer, now documented in docs/architecture.md):
- dsh-bash: abstract BashExecutor service (ctx.bash) + vocabulary types.
- dsh-bash-local: local subprocesses — bash -c per call in a detached
process group, SIGTERM→SIGKILL group kills, tail-keep truncation with
full-stream spill files, model-friendly env, background task registry.
- dsh-tool-bash: the bash / bash_output / bash_kill tool schemas with
runtime arg validation and background completion notices via
agent.inject(). Non-zero exits are reported, not errored.
Design surveyed against the bash tools of Claude Code, OpenCode, Codex,
and pi (notes in the package READMEs). Permissions/sandbox stay TODO on
the tools/execute waterfall seam; stateful-shell alternatives recorded
in run.ts.
2026-06-12 23:28: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
const result = await ctx . bash . run ( ctx . bash . resolve ( {
. . . request ,
2026-07-19 23:38:54 +08:00
signal : exec.signal ,
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
} ) )
Add bash execution: dsh-bash seam, dsh-bash-local impl, dsh-tool-bash tools
Three packages following the new capability-seam pattern (interface /
implementation / consumer, now documented in docs/architecture.md):
- dsh-bash: abstract BashExecutor service (ctx.bash) + vocabulary types.
- dsh-bash-local: local subprocesses — bash -c per call in a detached
process group, SIGTERM→SIGKILL group kills, tail-keep truncation with
full-stream spill files, model-friendly env, background task registry.
- dsh-tool-bash: the bash / bash_output / bash_kill tool schemas with
runtime arg validation and background completion notices via
agent.inject(). Non-zero exits are reported, not errored.
Design surveyed against the bash tools of Claude Code, OpenCode, Codex,
and pi (notes in the package READMEs). Permissions/sandbox stay TODO on
the tools/execute waterfall seam; stateful-shell alternatives recorded
in run.ts.
2026-06-12 23:28:44 +08:00
if ( result . aborted ) throw new Error ( 'command aborted' )
2026-07-21 03:08:35 +08:00
return { kind : 'foreground' as const , . . . canonicalBashResult ( result ) }
Add bash execution: dsh-bash seam, dsh-bash-local impl, dsh-tool-bash tools
Three packages following the new capability-seam pattern (interface /
implementation / consumer, now documented in docs/architecture.md):
- dsh-bash: abstract BashExecutor service (ctx.bash) + vocabulary types.
- dsh-bash-local: local subprocesses — bash -c per call in a detached
process group, SIGTERM→SIGKILL group kills, tail-keep truncation with
full-stream spill files, model-friendly env, background task registry.
- dsh-tool-bash: the bash / bash_output / bash_kill tool schemas with
runtime arg validation and background completion notices via
agent.inject(). Non-zero exits are reported, not errored.
Design surveyed against the bash tools of Claude Code, OpenCode, Codex,
and pi (notes in the package READMEs). Permissions/sandbox stay TODO on
the tools/execute waterfall seam; stateful-shell alternatives recorded
in run.ts.
2026-06-12 23:28:44 +08:00
} ,
feat(acp): tool-owned tool-call UI presentation (title/command/output)
In Zed the tool-call card showed only "bash" — the bare tool name — instead
of what the command does. Fix it by letting each TOOL own how its calls render,
rather than the bridge special-casing names.
dsh-tools: add an optional two-state presentation seam to ToolDefinition /
defineTool — `presentCall(args)` (pending: title, kind, rawInput) and
`presentResult(args, result)` (completed: title?, content?). Provider-neutral
`ToolCallKind`/`ToolCallPresentation`/`ToolResultPresentation` vocabulary so
tools never depend on ACP. defineTool soft-validates args (display runs on log
replay, so a malformed/old shape returns undefined instead of throwing).
dsh-tool-bash: bash declares presentCall (model `description` → title, exact
`command` → rawInput, kind execute) and presentResult (wrap output in a fenced
```console block — a UI-only affordance kept out of the model-facing result);
bash_output/bash_kill present task-scoped titles.
dsh-acp: inject `tools`; a per-session `ToolPresenter` looks the tool up by name
and maps its neutral presentation to the ACP tool_call/tool_call_update wire
shape, with a generic fallback (title = name) for tools that declare nothing.
Because the `tool/result` event carries only {callId, content, isError}, the
presenter keeps a small bridge-local map of ONLY in-flight calls' (name, args),
keyed by callId and removed as each result is presented — no event-schema or
core change. Replay uses a throwaway presenter so loaded sessions render
identically to live ones.
Tests: dsh-tools defineTool presenters (typed args, soft-validate), tool-bash
bash/bash_output/bash_kill presenters, acp ToolPresenter (tool-owned mapping,
unknown-callId fallback, in-flight-only map), and an end-to-end turn through the
bridge. The key-gated e2e now asserts a real bash call's title is the model
description (not "bash") and rawInput is the command — verified against the real
DeepSeek model. The test harness derives its inject from the bridge's exported
`inject` so it can't drift again.
2026-06-18 09:01:36 +08:00
presentCall : presentBashCall ,
presentResult : presentBashResult ,
Add bash execution: dsh-bash seam, dsh-bash-local impl, dsh-tool-bash tools
Three packages following the new capability-seam pattern (interface /
implementation / consumer, now documented in docs/architecture.md):
- dsh-bash: abstract BashExecutor service (ctx.bash) + vocabulary types.
- dsh-bash-local: local subprocesses — bash -c per call in a detached
process group, SIGTERM→SIGKILL group kills, tail-keep truncation with
full-stream spill files, model-friendly env, background task registry.
- dsh-tool-bash: the bash / bash_output / bash_kill tool schemas with
runtime arg validation and background completion notices via
agent.inject(). Non-zero exits are reported, not errored.
Design surveyed against the bash tools of Claude Code, OpenCode, Codex,
and pi (notes in the package READMEs). Permissions/sandbox stay TODO on
the tools/execute waterfall seam; stateful-shell alternatives recorded
in run.ts.
2026-06-12 23:28:44 +08:00
} ) )
}