ctx.approval (dsh-approval): request() dispatches the approval/request
waterfall and always resolves a closed outcome — allowed-once / rejected /
cancelled / unavailable — never rejects; zero listeners fall through to
fail-closed unavailable; abort settles cancelled and discards late answers;
throwing or rogue answerers are contained as unavailable; every ask lands
the log-only approval/asked / approval/decided audit pair. dsh-tools routes
a pre-execute ask through the seam opportunistically (ctx.get) with three
distinct deny reasons, keeping the historical ask→deny degrade when the
seam is absent.
The per-session policy tier, the ACP bridge answerer, and the sandbox
escalation asker are staged follow-ups of the approval-seam RFC.
Plan mode's stage 2 (RFC 2026-07-07-plan-mode). The exit tool: one
required plan argument (the durable log artifact), execute re-checks the
folded mode, then conducts the review over the user-interaction seam —
one single-select question (Approve / Keep planning) with free text open
— so an approval appends mode/set back to default in-turn and every
other outcome (keep-planning feedback verbatim, aborted, no provider)
returns the corrective isError with the mode unchanged. presentCall is a
generic card titled by the plan's first heading carrying the plan
markdown; over ACP the review rides the ask_user elicitation flow, in
the terminal the stdio prompt queue — no approval-seam dependency.
The ACP bridge maps the picker 1:1 onto ctx.modes (opportunistic, a
type-only peer edge): session/new + session/load advertise
availableModes/currentModeId, session/set_mode validates through set()
and echoes an optimistic current_mode_update (the pending mode IS the
selection; the logged mode/set lands at the boundary and, matching, is
not re-sent), and a session/event listener re-notifies on each logged
flip that differs from the last sent — the tool-driven exit updates the
picker. The feature matrix rows move from 'not modeled' to the
picker-to-modes / knobs-to-config-options division, with the ACP v2
removal direction recorded as a mechanical-migration risk.
The snapshot harness gains the setMode/setModeExpectError ops and a
scripted elicitationAnswers FIFO (cancel on exhaustion; a stray choice
string reaches the agent verbatim as a non-consenting custom answer, so
a scenario bug fails safe). The suite factory's header-pin requirement
now applies only to model-turn scenarios — a protocol-only suite has no
header content to anchor. examples/plan-acp-agent is the live
composition; its keyless modes-advertise scenario pins the wire surface
(advertisement, both set_mode round-trips, unknown-id rejection). The
recorded plan-mode approve/reject arc awaits a with-key recording
session; its texts are pinned at the unit tier meanwhile.
examples/AGENTS.md ceiling 653 → 680: the new example's required smoke
row does not fit the old budget.
Plan mode's stage 1 (RFC 2026-07-07-plan-mode): a new packages/mode/ group
with one product package owning the mode/set SessionEventMap vocabulary
(log-only, non-surface, whole-value replace), the pure foldMode, and the
ctx.modes service (list/get/set). User flips are pending intents flushed
at turn/start / step/end — turn enclosure makes an idle append illegal —
with one coalesced context/message notice when the flushed mode differs
from what the last logged request header told the model; a folded mode
the config no longer defines reads as default plus one boundary notice.
Enforcement is two covering layers: a system-prompt/assemble wrapper
filters the RETURNED assembly's tools to the mode's allowlist (and shows
exit_plan_mode IFF the folded mode is plan) beside the mode:policy
section at order 50, and a tools/pre-execute gate denies deny-by-default
against the same allowlist, judging by the logged mode only. The default
mode is the absence of policy — assemblies stay byte-identical to a
no-dsh-mode deployment.
AgentOptions.mode (declaration-merged) seeds a child's initial mode
through the same flush on agent/created; the stdio app gains /mode
(print/switch, never sent to the model) over an opportunistic
ctx.get('modes'). Config is an explicit resolve step: the built-in plan
definition (read-only allowlist; bash/subagent excluded until the
sandbox family lands) merges unless overridden, 'default' as a key
throws at load, unknown names throw at set() time.
Adopts #211 (Code Mode tools: run_code + the code/both-mode snapshot
scenarios). Tool-catalog expectations take the union (run_code +
task_*); the two new pinsHeader fixtures (code-mode-turn,
both-mode-turn) were recorded on master without the task runtime, so
they are re-pinned KEYLESSLY by replaying their recorded chunks against
the merged tree (same procedure as text-turn) — the fixture diff is
exactly the header delta: task tool schemas, the tool:tasks prompt
section, and the bash background wording.
Master advanced 11 commits mid-merge (the Code Mode registry integration:
mode config, run_code, the tools:sdk section, the ACP run_code cards and
unified demos). The fusion makes Code Mode scope-aware end to end:
- wireSchemas(scope): the mode-aware wire contribution is computed from the
CALLING SCOPE's visible set (scoped tools join, shadowing and restrictions
apply) and feeds the {schemas, knownNames} provider protocol.
- knownNames under the mode collapse: a per-scope RESTRICTION is runtime
state, so the universe stays pre-restriction (a restricted-away tool in
toolOrder is a normal absence) — but the MODE collapse is deployment
config, so under mode 'code' the universe is [run_code] and a toolOrder
naming a native tool fails every assembly loud (master's tested decision,
kept).
- The tools:sdk section renders per assembly CONTEXT: the SDK declares
exactly the calling agent's callable set, using the section-text provider
signature this branch already had.
- run_code bindings enumerate schemas(exec.agent) — a program can bind
exactly what its prompt promised; sub-dispatches already threaded
exec.agent through registry.execute, so scoped resolution and carriers
flow unchanged.
- dsh-tools declares both sides' new deps (dsh-scope + dsh-session);
lockfile and all generated catalogs/graphs/api-catalog regenerated.
Gates green on the merged tree: typecheck, lint, per-file 100% coverage
(2710 tests), snapshots (41), doc-sync, module graph, build, hygiene, demo
smoke.
Master brought 50 commits (the tool-cordis group, dsh-code-runtime + worker,
the tools/execute around-dispatch seam + timeout-policy, repeat-tool-guard,
agent/session-prefix, the ui reorganization). Beyond the ten textual
conflicts, the merge reconciles master's new seams with this branch's
scoped-registration world:
- tools/execute (new waterfall around core dispatch): dispatched with the
SAME exec.agent carrier as the pre/post waterfalls — an agent.ctx wrapper
times/retries only its own agent's calls — and its base thunk resolves the
tool through the caller's visible view (get(exec.name, exec.agent)), so a
scoped/shadowed tool dispatches and a restricted-away global stays
UNKNOWN_TOOL. Declared this: Scoped<ToolRegistry> with the scope-filtered
doc sentence; invariants table + verify-scoped-dispatch pin it (21 events).
- agent/session-prefix (new waterfall, once per loop instance): composed via
the fused agentEvents dispatcher (scope-filtered like every agent-subject
event), declared this: Scoped<Agent>, table-pinned. agent/pre-step keeps
master's new sessionPrefix parameter with this branch's Scoped this.
- timeout-policy reads the budget through the caller's visible view
(get(exec.name, exec.agent)): a scoped tool's own timeoutMs governs its
calls; a global name-twin's budget is never misapplied to a shadowing
per-agent variant.
- tool-cordis: cordis_inspect's tools section lists the CALLING agent's view
(its description promises "what you can call"); the sandbox tool façade's
reads resolve through the mount's own scope, mirroring where its register
lands writes; sandboxRegisterTool's return type carries the exact-disposer
union honestly. dsh-scope declared as peer+dev with the project reference.
- doc-sync chain unions master's verify-cordis-api with this branch's
verify-scoped-dispatch; the generated catalogs, event matrix (the
zero-dispatcher guard passes over master's new events), module graph, and
the cordis api-catalog are regenerated on the merged surface.
Full gate sequence green on the merged tree: typecheck, lint, per-file 100%
coverage (2668 tests), snapshots (38), doc-sync, module graph, build,
hygiene, demo smoke.
Adopts #220 (tool-cordis + the gen-cordis-api gate: the runtime API
catalog regenerated with ctx.tasks/onCleanup on it) and #225 (shared
transcript renderer). Tool-catalog expectations take the union
(cordis_* + task_*); packages/README budget adopts master's 660
ceiling, which absorbs both new group rows.
Adopts #185 (dsh-timeout: clampTimeout/deadline/timeoutOf drive bash
run() timeout classification; runBash loses its own timer) and #108
(ask_user_question) across the task-runtime rework: bash-local keeps
the BashProcess handle shape with master's deadline mechanics, tool
catalogs/expectations carry both the task_* and ask-user tools, and
generated docs are regenerated on the union.
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.
Implements docs/rfc/implemented/feature/2026-07-09-bash-backed-grep-glob-
discovery.md: model-facing glob/grep in a new @deepseek-ai/dsh-tool-fs-search
package, executing fixed ripgrep templates through ctx.bash.resolve/run —
not ctx.fs provider methods — so filesystem backends stay free of a search
contract and sandboxed/remote executors substitute cleanly. The tools never
call ctx.bash.start(); the tool layer owns quoting (one singleQuote safety
boundary), rg --json parsing, ItemRetainer/TextRetainer retention, and the
first tool-owned ctx.spillFiles.saveText() handoff (item-level retention the
generic post-execute spill policy cannot recover).
RFC amendments on the way to implemented/: a shared src/search-core.ts (the
SEARCH_* vocabulary + bash-run/raw-spill/spill plumbing was byte-identical
across both tools — the missed-extraction smell), and a snapshot-gap note:
wiring the acp-agent tree changes the assembled prompt, so goldens need a
keyed re-record; the spill notice text is pinned by unit tests instead and
only the coding-agent example ships the tools for now.
P1 review finding: extractMeta timed only the literal's vm evaluation;
materializing the RESULT then read properties ordinarily on the HOST
stack, so a meta literal smuggling a getter (get name() { while(true){} })
could wedge the host outside any timeout — defeating the exact spin
isolation the worker thread exists for.
Rather than harden the evaluator (descriptor walks, AST validation),
delete the mechanism: the workflow's identity now reaches the seam as a
plain JSON field (WorkflowStartRequest.meta), carried by the tool as a
schema-validated `meta` object parameter the model fills directly. The
engine only shape-validates data (validateMeta, every violation named)
and pre-parses the body; the scanner, the vm evaluation, and the
host-side materialization are gone, and with them the hole. A body
still opening with a Claude Code-style `export const meta` statement
gets a pointed SCRIPT_PARSE message (the likeliest authoring slip; a
CC script's body stays drop-in, only its meta header moves into the
parameter). syncTimeoutMs now governs exactly one thing: the initial
synchronous slice inside the worker.
The RFC's decision section is rewritten in place (implemented-RFC
rule); the embedded-meta format moves to alternatives-considered with
the hole as the reason. Tool description, presentation (title now reads
meta.name directly — the textual sniff is gone), seam vocabulary docs,
and catalogs follow.
Pure mechanical rename now that the package's internals are the
worker-thread engine: directory, package name, spec/e2e filenames,
module tags and logger prefixes, tsconfig/knip/run-gates/AGENTS.md
references, example cordis.yml plugin ids, doc links; catalogs
regenerated and the lockfile refreshed.
The outer ring catches up with the engine swap (the package's own
README/JSDoc rode the port commit):
- Seam module doc and README name the worker-thread engine as THE
implementation, with isolated-vm/separate-process sandboxing as the
deferred hardening; the seam service doc states the holder-owned-runs
contract (engine-fiber disposal deliberately leaves live runs to
their holders).
- Seam contract precision: agentsStarted documents the termination-path
degradation to the host-observed count; the events section scopes the
agent-start/agent-end pair to calls that STARTED a child run;
WorkflowRun wording drops the vm-era abandonment language.
- The dynamic-workflows RFC is rewritten in place to the shipped
mechanism (implemented-RFC rule): why worker threads, the thread's
concrete buys, the in-process node:vm first cut recorded under
alternatives considered; the tool section describes the usage policy
as the tool's own prompt section.
- gen-doc-graphs: six workflow/* DYNAMIC_EVENT_DISPATCHERS entries (the
catalog no longer claims nothing dispatches them) and the seam-note
wording; core-data-structures gains its workflow.md index row;
packages/README + AGENTS.md layout line + example cordis.yml comments
say worker-thread; catalogs regenerated.
Review finding (#220): the guarded proxy only special-cased ctx.tools, so
mount code could reach an UNGUARDED context through ctx.root, ctx.extend(), or
a service instance's .ctx, then ctx.root.tools.register({…}) to bypass the
marker check and host-realm normalization — a raw vm-realm result would later
error a real agent turn at the session-log plainness check.
The sandbox ctx is now a whitelist façade, not a pass-through proxy: it exposes
only what a mount needs — tools.register (marker-guarded), on/once, provide, the
timer helpers, and injected services resolved through a guarded get — and denies
every framework-plumbing member (root, parent, fiber, reflect, registry, extend,
isolate, intercept, plugin, set, mixin, …) with a teaching error. Injected
services are wrapped so a method returning a Context is rejected on the way back
(the .ctx escape), closing the one indirect leak. There is no context-valued
member left to reach; cross-mount provide/inject is untouched (the plugin's own
inject and the fiber's pending/active gating are unchanged). ctx.plugin (child
plugins) and ctx.set are denied by design; ctx.effect is deferred (FIXME).
Adds tests/sandbox-context.spec.ts covering the escape class (root/extend/fiber/
plugin/set/… denied, the classic root.tools.register bypass, the .ctx escape,
read-only writes) plus the async-service and symbol/in-operator paths for 100%
coverage. RFC/README/tool-catalog/config-catalog updated; api-catalog.ts
regenerated (also picks up the codeRuntime service that entered on the master
merge and was left stale).
Everything outside the package and example that a new top-level group and a
new demo touch: GROUP_ORDER in gen-module-graph and gen-doc-graphs (plus the
tools-service consumers list, the APP_EXAMPLES entry, and the graph-atlas
label/mode rows), the knip e2e entries, the packages/README group row, the
AGENTS.md layout and demo lines, and the regenerated module-graph /
config-catalog / graph-atlas / capability-seams / composition artifacts.
AGENTS.md and examples/AGENTS.md word-budget ceilings rise to current+5%
(1802 / 653): the new group and demo rows are genuine additions to both docs,
not condensable restatements.
The extracted library's name sat one edit away from @deepseek-ai/dsh-subagent-inprocess
(process/inprocess), inviting a typo'd import to silently resolve to the wrong
package. subagent-subprocess also reads as the deliberate counterpart to
subagent-inprocess (in-process vs. subprocess), matching how the two shared
drivers actually differ.
Package directory, npm name, module doc, JSDoc module tags, test-file name and
its temp-dir prefixes, the subagent-acp import and its Config/tsconfig/package.json
references, root tsconfig.json/tsconfig.build.json/knip.json entries, and the
packages/subagent group README all renamed together; regenerated
docs/module-graph.md and docs/config-catalog.md. Pure rename — no behavior,
export, or Config shape changed.
createScope(ctx, key) mints a tagged context over a synchronously-usable
no-op-plugin fiber (one fact drives visibility AND lifetime); scopeOf reads
the tag through the prototype chain; scopeTarget(base, key) builds the
scope-filtered dispatch carrier over cordis Context.filter, composing the
base's own filter, branded Scoped<T> and runtime-marked for the dev
invariants. Scope.rawDispose exposes the exact cordis disposer so a
composite effect can nest the scope's teardown at its yield position.
New group packages/code-runtime/ with the interface package
@deepseek-ai/dsh-code-runtime, per the Code Mode RFC: abstract CodeRuntime
service (run() resolves program failures as an error field, rejects only
for seam misuse), the CodeRunRequest/CodeBindingNamespace/CodeRunResult/
CodeLogEntry/CodeRunFailure vocabulary, and readonly language/isolation
backend descriptors. Registered in the tsconfig maps, packages/README,
architecture service map, and the doc-graph service-role classification;
catalogs regenerated.
The RFC's one forward path token to the worker package becomes an npm-name
mention until PR3 creates that directory (verify-package-paths is
drift-scoped: the now-existing group made the token checkable).
docs/architecture.md ceiling 1630 -> 1640: the doc gained a genuinely new
capability-service row; the row itself is already minimal.
The credential env scrub (SENSITIVE_ENV_PATTERN/buildChildEnv), the
spawn-failure capture (spawnFailure), the child-exit waits
(waitForExit/exitsWithin), and the stdin-EOF -> SIGTERM -> SIGKILL dispose
ladder move out of subagent-acp into a new pure library package (the
subagent-inprocess shape), with the ladder taking its two grace periods as
parameters — defaults stay in the plugin Config. New isolated-config-dir
helpers (mkdtemp create, best-effort remove; a pinned dir is never removed)
land alongside for the CLAUDE_CONFIG_DIR / CODEX_HOME redirection the RFC
names.
The ACP backend migrates onto the library with no semantic change: its suite
passes with import-path edits only. bash-local keeps its sibling copy, per
the RFC's blast-radius call.
RFC: docs/rfc/proposed/feature/2026-07-07-claude-code-and-codex-subagent-backends.md
Brings in the refreshed base (master merged through the stack after #203
and #205 landed), including the acp-snapshot extraction (#204), and
re-ports this PR's snapshot-suite extensions onto the extracted package:
- dsh-acp-snapshot's Scenario gains headerClass and configPath; the suite
factory pins the request header PER CLASS (construction rejects a
missing or duplicated class pin), forwards a scenario's configPath to
the harness (RunOptions.configPath overrides AgentUnderTest.configPath),
and a new fixtures meta-test asserts every pinning fixture carries
exactly one request/header and no deltas.
- The acp-agent example's thin scenario table re-registers code-mode-turn
and both-mode-turn with their overlay configs and per-class pins; the
committed fixtures replay unchanged.
- The package's synthetic suites cover the new surface (explicit
headerClass on one suite, the default on the other, a configPath
override through the fake bin, and the two construction throws).