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.
Systematic trace through Zed (crates/agent_ui thread_view.rs +
crates/acp_thread): kind:execute routes a tool call onto the
terminal-card layout, whose header (render_collapsible_command) has NO
disclosure toggle, whose body content renders only when is_open — a
flag only a real terminal entity can ever set — and which suppresses
the Raw Input view outright. Every prior attempt (rawInput, pending
content, completed content) targeted slots that layout structurally
never renders; the one slot it always shows is the TITLE, which said
"Run code". codex-acp confirms the idiom: execute cards are titled
with the command itself.
presentCall now titles the card with the program (rawInput kept as the
canonical input slot); presentResult omits the title — an update
replaces only provided fields, so the program header persists — and
carries the captured output as content. Goldens re-recorded; the unit
test pins title-carries-program on both frames.
The previous commit put the fenced program only on the pending card —
but an ACP tool_call_update's content REPLACES the card content (Zed
truncates to the new list, crates/acp_thread update_fields), so the
code vanished the moment the run completed and was effectively never
visible. presentResult now re-carries the fenced program before the
captured output via a shared fencedProgram helper; the completed card
body is program + output, rendered by Zed as syntax-highlighted
markdown behind the card disclosure. Goldens re-recorded (filtered
this time: DSH_SNAPSHOT=record vitest -u -t mode-turn); unit test pins
the two-block result content.
The generated program rode only rawInput — the detail/expanded slot
many ACP clients never open — so the code a run executed was invisible
in the UI stream. presentCall now also carries it as a fenced ts block
in the card's content, which the bridge already forwards as tool_call
content. The two code-mode snapshot goldens are re-recorded live and
replay green; the presentation unit test pins the fenced block.
New top-level packages/cordis/ group with the self-referential toolset:
cordis_inspect (services / plugin tree / tools / dynamic mounts / api / events,
the api section intersecting the generated catalog with the live service store),
cordis_mount (model-written code evaluated in a node:vm sandbox, mounted under
one cordis-dynamic group fiber as dyn-<n>), cordis_unmount (awaited disposal to
quiescence). Boundary mechanisms: dual-realm instanceof, JSON realm
normalization of dynamic tool results, marker-guarded registration, SchemaSpec
teaching errors, parse failures surfaced with the offending line + caret and a
line-scoped TypeScript hint, and the unmount-first recipe on tool-name
collisions. Config: vmTimeoutMs (schemastery, default 5000). Design record:
docs/rfc/implemented/feature/2026-07-08-self-referential-cordis-toolset.md.
The tool-catalog boot manifest, its regenerated output, and the pinned
tool-name list land here rather than with the other repo registration: the
completeness guard globs packages/*/tool-* and fails the generator (and the
core/tools spec) the moment the package directory exists.
Both demo:code-mode UIs now share one mechanism: the base example plus a
same-shaped code-mode.cordis.yml include overlay (insert the worker
runtime, flip tools.mode). Previously the REPL side was a hand-forked
example (examples/code-agent) that had also silently diverged — it
dropped compaction and the subagent stack — so the demo's UI argument
switched agents, not just surfaces. The fork is retired: coding-agent
gains the overlay, a Code Mode README section absorbing code-agent's,
and both of its tests (the keyless boot guard, retargeted at the
overlay; the with-key RFC proof, which hand-mounts its own harness and
moves untouched). The RFC's composed-surface and e2e-tier lines, the
examples index, the AGENTS.md smoke table, and the dsh-tools README
link now describe the overlay shape.
Verified live: overlay keyless smoke, with-key code-mode e2e from its
new home, demo:code-mode banner + EOF exit, and the acp handshake.
Code Mode is the point; the UI is just the surface it happens to wear.
demo:code and demo:acp-code collapse into one dispatcher
(scripts/demo-code-mode.mjs): `pnpm run demo:code-mode [repl|acp]` —
repl (default) boots the stdio REPL over examples/code-agent, acp
serves examples/acp-agent's code-mode overlay; each UI runs the exact
node invocation its standalone script ran, and an unknown argument
fails loud with usage. All nine references across READMEs, the RFC,
the overlay header, and the keyless-smoke comment renamed. Smoked all
three paths: usage exit 2, ACP initialize handshake, REPL boot + EOF.
[P1] review finding: the run-scoped abort + queue drain ran only after
runtime.run() FULFILLED, so a backend that starts a binding call and
then throws left the sub-dispatch running past run_code's settlement —
its tool/code-dispatch event could append after the parent call
returned, breaking the drain-before-return contract. The quiescence
pair now lives in a finally around runtime.run(); the folded queue tail
keeps the drain from masking the thrown error. Pinned by a test whose
fake runtime fails mid-flight: pre-fix it returns in milliseconds with
the slow tool still running.
Both bot criticals verified against the code and rejected as exploit
paths — pinned instead of patched:
The bindings loop already excludes run_code (the skip predates the
finding), and the runtime host resolves forged port calls as own
properties of the bindings record, so an absent binding is unreachable
from a program under any mode. A new both-mode test pins the invariant:
the record has no run_code key on any lookup path.
The drain await cannot mask a run failure: `queue` is the folded tail
(every link swallows its rejection), so `await queue` never rejects and
the runtime's own result.error always reaches the CodeRunFailedError
conversion — the existing abort test exercises exactly the
queued-abandonment-plus-run-failure scenario. Stated at the drain site so
the fold's purpose is explicit.
A tool declares its cooperative timeout budget on its own definition
rather than a deployment naming it in a central config map. The field
never reaches the model (schemas() whitelists name/description/parameters)
and defineTool rejects a non-positive-finite value at authorship.
Three findings from the PR-4 convergence round:
(A) A root-undefined binding argument passed normalization untouched, so
the sub-call DISPATCHED and only then failed the tool/code-dispatch append
(Session.append rejects undefined event data) — a sub-call executed with
no log record, violating the nothing-executes-unlogged contract. And the
tool received the SAME object later handed to the append, so a tool
mutating its args desynced the logged record from what was dispatched (or
re-poisoned the append). jsonNormalizeArgs now rejects undefined up front
with a model-correctable message and returns TWO independent parses of the
canonical JSON text: the tool gets one, the event logs the sibling —
identical by construction, mutation-proof.
(B) The bridge built its bindings record with plain-object assignment, so
a registered tool named __proto__ hit the prototype setter and silently
vanished (the runtime host resolves binding names as own properties). The
record is now null-prototype with defineProperty, mirroring the
worker-side namespace build.
(B) The header-pin sanity assertions ran only inside NON-pinning
scenarios, so a class consisting solely of its pinning scenario (the two
Code Mode classes) would accept a re-recorded pin carrying several headers
or a header-delta. A fixtures meta-test now asserts every pinning fixture
directly.
The dsh-tools half of the Code Mode RFC (its fourth, final change): the
registry gains its first config — mode: native | code | both — and OWNS how
its tools reach the model. 'code' contributes exactly one wire tool,
run_code, plus a lazy tools:sdk prompt section declaring every other tool
as a generated TypeScript API (jsonSchemaToTs: total over the defineTool
subset, unknown degradation, lexicographic byte-identical rendering);
'both' ships both representations; 'native' is byte-for-byte the old
behavior. Non-native modes fail every assembly loudly without a
typescript-language ctx.codeRuntime.
run_code's dispatch bridge: JSON-normalizes each binding argument before
dispatch (what dispatches is what the tool/code-dispatch event logs — the
append can never fail on payload shape; BigInt/circulars reject that one
call), serializes all program tool calls through a per-run queue (even
Promise.all — no concurrency-safety metadata yet), routes every sub-call
through tools/pre-execute → tools/post-execute (a deny rejects the
program-side promise), drops sub-call additionalContext (no safe outlet
mid-run; pinned), owns a run-scoped abort that follows the outer signal in
and fires on settlement (in-flight sub-dispatch aborted, queued abandoned,
queue drained before returning), and converts a failed run into
CodeRunFailedError → a structured isError carrying kind + captured logs.
tool/code-dispatch joins SessionEventMap by declaration merging (log-only;
deriveMessages ignores it).
The composed surface: the tools config forwards through agent-core and
both app packages; examples/code-agent + demo:code run the worker runtime
under mode code (keyless boot smoke + a with-key e2e proving the collapsed
[run_code] header, the dispatch events, and the file the program wrote);
two new snapshot scenarios (code-mode-turn, both-mode-turn) record the SDK
section, collapsed header, dispatch events, and result card — each its own
header-pinning class (the harness gains per-scenario config overlays and
per-class pins). Catalogs, graphs, cookbook, hooks-bridge notes, and the
RFC (moved to implemented/, restructured to decision-era headings) updated
in the same change.
Model-facing tool-call budgets were tangled into each capability's schema
(bash timeoutMs, web_fetch timeout_ms) with no shared home. Add a
tools/execute around-dispatch waterfall to dsh-tools whose base next() is
the dispatch-with-normalization thunk, and a new @deepseek-ai/dsh-timeout-policy
plugin (packages/timeout/) that arms a per-tool deadline on exec.signal and
returns a structured TOOL_TIMEOUT when it wins. Migrate web_fetch (drop the
model-facing timeout_ms) and web_search onto it; the fetch provider keeps its
timeout only as a resource backstop for direct callers. bash and hook command
execution keep BASH_TIMEOUT unchanged.
Named the plugin timeout-policy (not the RFC's tool-timeout) so it does not
trip the gen-tool-catalog packages/*/tool-* completeness guard, and replace
exec.signal by in-place mutation before next() since cordis waterfall next()
ignores passed arguments. RFC moved to implemented/ recording both deviations.
Three Codex findings on json-schema.ts, one discipline:
- required-declared and every value check now use Object.hasOwn — 'in' let
inherited names (toString) satisfy required, dodge additionalProperties:
false, and validate a declared property against the value's prototype
member instead of a carried one
- isObjectLike now means PLAIN JSON object (proto chain of at most one link,
realm-agnostic): a Date annotation or a Map-as-properties no longer passes
structurally and serializes lossily — they fail loud as subset violations
- startInProcessRun asserts BEFORE the defensive structuredClone, so a
hostile schema fails as OutputSchemaError, never a raw DataCloneError
Also the type-equiv catalog gap: tools.md gains the structured-output subset
vocabulary (4 blocks) with matching manifest entries. The driver index also
drops the runtime internals from its public re-export (runs acquire it
internally; no external consumer remains — see the following commit).
Carved out of #170 per review feedback — the foundation the workflow tool
builds on, now standing alone on master:
- dsh-tools: the structured-output JSON Schema subset (StructuredOutputSchema,
assertSupportedOutputSchema, validateStructuredValue) — rejects loud outside
the enforced subset, listing every violation
- dsh-subagent: SubagentStartRequest.outputSchema / SubagentResult.structured
become a real capability; the service rejects a schema'd request whose
provider lacks it
- dsh-subagent-inprocess: the shared structured runtime — one global
structured_output capture tool, a prepend final-assembly listener that
strips the placeholder for plain agents and swaps in the run's own schema
(plus the calling instruction as a trailing section) for structured
children, an agent/turn-continuation veto once captured, and the
capture/nudge loop in the run driver (structuredNudgeRetries, cancellation
honored mid-nudge); lifetime refcounted by backends and live runs
- subagent-spawn / subagent-fork flip outputSchema: true
One deliberate divergence from the #170 revision: the backends do NOT add
'tools' to their plugin inject. Doing so deferred their apply past the todo
plugin, and the delegation tool mirrors provider lifecycle — so the
model-visible tool order of every existing prompt changed, invalidating every
recorded snapshot fixture. The runtime now gates its capture-tool registration
on tools availability itself (sync when live, a scoped inject fiber when the
Loader starts the backend first), keeping this PR byte-invisible to existing
transcripts: all 35 snapshot scenarios pass against master's fixtures
unchanged.
A directory holding exactly one file adds a path level for nothing;
cordis-catalog/ keeps its folder because it holds two sibling pages
(events.md + services.md), matching config-catalog.md which was born flat.
docs/tool-catalog/tools.md -> docs/tool-catalog.md and
docs/persistence-catalog/log-events.md -> docs/persistence-catalog.md; their
generators' OUT paths and in-page relative links drop one directory level,
and every citation repo-wide (docs, RFCs, package READMEs, generator
headers) is updated. graph-atlas.md, config-catalog.md, and the doc graphs
regenerate with the new paths.
New doc-sync gate verify-export-jsdoc walks every module-level exported
name under packages/*/*/src and requires description prose everywhere,
plus @param per parameter and @returns on non-void annotated returns for
function-like exports, public class methods, properties, and accessors.
The parsing + check helpers move out of gen-cordis-catalog.ts into a
shared scripts/jsdoc.ts so 'documented' means one thing on both gated
surfaces.
Deliberate exemptions (documented in the RFC): heritage-declared class
members (the seam declaration is the doc's one home — the one checker
query in an otherwise pure-AST walk), cordis plugin-protocol slots
(name/inject/reusable/Config/apply, top-level and static), constructors,
overload implementations, declare-module augmentation bodies, and
re-export statements (checked at the defining module).
The 203 under-documented exports the gate found at adoption are filled
in this change, so the gate lands green; generated catalogs/graphs are
regenerated for the shifted line pointers.
RFC: docs/rfc/implemented/process/2026-07-06-export-surface-jsdoc-gate.md
Beyond the mechanical conflicts (provider capability lines vs master's new
inheritsParentContext field; generated catalogs regenerated rather than
hand-merged; knip/lockfile), three master-side reworks required semantic
adaptation of this branch:
- The persona rework removed AgentOptions.systemPrompt, which was the
structured-output instruction's channel. The instruction now rides the
SAME final-request enforcement listener that injects the schema'd tool:
appended per request to final.system (per-request wire state, not agent
prompt state). Tests assert the wire request (adapter.requests) instead
of child.options; the bare-direct-dispatch test pins the no-system arm.
- Tool guidance moved out of deployment prompts into per-tool prompt
sections; the examples' workflow paragraph became a tool:<toolName>
section contributed by dsh-tool-workflow (explicit-ask-only policy),
and both example personas resolve to master's minimal identity+behavior
form. tool-workflow gains inject: systemPrompt (+ peer dep, tsconfig
ref); the export-shape guard updated.
- The uniform-RFC-format gate: the dynamic-workflows RFC restructured to
the implemented/ skeleton (bare Status line; Proposal -> Decision;
What-was-rejected -> Alternatives considered; new Consequences), and
the overall-run-timeout deferral is now recorded in the RFC's Deferred
list. The doc-graphs atlas classification gains the workflows seam
(workflow-vm implementation, tool-workflow consumer).
Master's harness-identity section made "empty assembled prompt" states
unreachable through the loop, so the instruction-append is a plain
undefined-ternary and the structured tests assert append-not-replace.
All snapshot goldens (including workflow-run) replay unchanged. Full
local CI-equivalent gate sequence green on the merged tree.
A new capability family at packages/workflow/ in the bash seam shape,
modeled on Claude Code's dynamic workflows: the model writes a JavaScript
orchestration script (export const meta = {...} + plain-JS body), a runtime
executes it, and the script — not the conversation — holds the loop, the
branching, and the intermediate results.
- dsh-workflow (ctx.workflows): abstract WorkflowService + run vocabulary
(WorkflowRun whose result NEVER rejects) + observe-only workflow/* events
carrying data snapshots (id + meta, never the live run), per-listener
contained like subagent/*.
- dsh-workflow-vm: in-process node:vm engine. Meta extraction via a
string/comment-aware scanner (template interpolation rejected; literal
evaluated alone in an empty timed context; statement blanked line-
preservingly so stacks keep script line numbers). Hooks: agent(prompt,
{label, phase, schema, model}) over ctx.subagents, parallel(), pipeline()
(no cross-stage barrier), phase(), log(), args. Fatal-vs-null discipline:
hook misuse (unknown/deferred options, bad arguments, unsupported
schemas, tripped caps, seam start failures, cancellation) throws fatal
WorkflowErrors the combinators RE-THROW — never dissolved into the
per-item null reserved for child failures. Realm boundary: inbound values
materialized by descriptor walks that never invoke accessors (defineProperty
copies, __proto__-safe); outbound values rebuilt in-realm via the
context's own JSON.parse. Determinism bans (Date.now/Math.random/argless
new Date) kept so future resume support cannot break scripts. Caps and
timeouts are validated Config. Every hook promise carries a no-op
rejection consumer (app-boot exits on unhandled rejections).
- dsh-tool-workflow: the model-facing workflow tool, synchronous like
dsh-tool-subagent (start → await → try/finally dispose; abort bridged;
non-completed → isError). Generic render card titled by a textual
meta.name sniff. The tool description carries the authoring contract.
Wired into examples/{coding-agent,acp-agent} with explicit-ask-only
guidance. Coverage at every tier: unit (meta scanner, materializer incl.
counting-getter and __proto__ regressions, combinator semantics,
concurrency ceiling, caps, cancellation, no-unhandled-rejection abandon),
integration over the real spawn stack, with-key e2e (real two-phase run +
the tool through the registry pipeline), and a recorded ACP snapshot
scenario (workflow-run, 1 child session). RFC:
docs/rfc/implemented/feature/2026-07-05-dynamic-workflows.md (deferred
work explicitly listed). AGENTS.md budget 1575 → 1590 for the new group's
layout line.
The seam vocabulary (SubagentStartRequest.outputSchema, SubagentResult
.structured) existed but no in-process backend honored it — spawn/fork
advertised outputSchema: false. This lands the missing half:
- dsh-tools gains a structured-output JSON Schema subset (json-schema.ts):
StructuredOutputSchema, assertSupportedOutputSchema (rejects loud outside
the enforced subset, every violation listed), validateStructuredValue
(path-qualified issues, total). outputSchema's seam type becomes this raw
JSON-Schema subset instead of the author-facing SchemaSpec DSL — the schema
travels verbatim to the model as a forced tool's parameters.
- dsh-subagent-inprocess gains the shared structured runtime: one global
structured_output capture tool (placeholder parameters) + a prepend:true
agent/request listener doing FINAL-REQUEST enforcement (strip for plain
agents, per-run schema for structured children — survives downstream
request-replacing listeners) + an agent/turn-continuation veto that stops
a child's turn once captured (no wasted extra model step). Lifetime is
refcounted by backends (plugin lifetime) AND live runs (start→settle).
- startInProcessRun drives the capture: subset asserted before the child
exists, instruction appended to the child's system prompt, clean-finish
nudge loop (structuredNudgeRetries, backend Config, default 1), captured
value on result.structured; a clean finish without a capture settles
'error' (never a silent success with a missing field).
- spawn + fork flip outputSchema: true and inject 'tools'.