Rewrite the agent-scope RFC with executable examples and an explicit security non-goal. Harden subagent scalar and depth validation, and pin live tool-filter semantics across code, tests, and generated docs.
Make Cordis construction and teardown ownership reentrancy-safe, then carry caller and provider ownership through reservation, setup, publication, quiescence, and sentinel retirement.
Stabilize registry carriers and factory/workflow boundaries, add adversarial lifecycle regressions, and align the rewritten RFC plus generated contracts with the enforced behavior.
Drain idle injection flushes before agent teardown, snapshot approval and subagent provider inputs, and gate subagent lifecycle events on real child readiness. Align the RFCs and generated contracts with the hardened behavior.
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.
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.
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.
Codex follow-up findings on the carrier commit, both verified:
B: the invariant guard only protected the bind path — an overlay key
colliding with a non-configurable, non-writable OWN prop of a (pathological)
base would have the get trap report the overlay value, which the engine
rejects as a proxy invariant violation (TypeError at read time). The pin
check now runs FIRST and covers both invariant-pinned shapes (non-writable
own data prop reported as-is; getterless non-configurable accessor reported
undefined via the delegated read) before overlay and bind alike. Such a base
forgoes scope filtering by construction — correctness of the read beats
filtering for a base no production code ships.
C: `constructor` is looked up for identity, never invoked as a subject
method — binding it broke `carrier.constructor === Subject` for no benefit;
it now returns raw (the same special-case withProps had). Both pinned in
scope.spec: the frozen-own-filter collision yields the base's value without
throwing, and class identity survives the carrier.
ds-review-bot delta-round finding, verified: cordis hands the carrier to
listeners as `this`, and the event declarations type it Scoped<Agent> — so
driving the subject through it (this.send(...) in an agent/* listener) is a
SUPPORTED shape. The withProps-based carrier delegated gets with the PROXY
as receiver, so ReactLoopAgent's send/steer/cancel — which read the
native-private #carrier through a getter — threw TypeError when called that
way (private members do not exist on proxy receivers).
scopeTarget now builds its own proxy: overlay props (the composed filter and
the carrier mark) answer from a null-shadowed literal via hasOwn (`in` would
let Object.prototype's toString/constructor shadow the subject's), every
other get delegates with the BASE as receiver (getters see the real object)
and returns functions bound to the base (method calls execute on the real
receiver), sets land on the base. A proxy-invariant guard reports frozen own
function props unchanged (binding them would violate the get invariant).
This kills the class at the seam — any subject with native privates works,
today's agents and whatever carries them next — instead of patching the one
#carrier field.
Pinned both ways: a scope.spec matrix (native-#private method/getter through
the carrier mutates the real object; set delegation; frozen-own-prop
invariant; overlay non-shadowing) and the bot's exact end-to-end scenario
(an agent/session-start listener calling this.send drives a real turn) —
both fail with TypeError against the withProps carrier.
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.
The exact-disposer fix (5fbac8be B1) repaired agents.register but left the
same wrapper (return () => void dispose()) at seven sibling sites:
tools.register, tools.restrict, systemPrompt.section/tools/variable,
agents.setFactory, and subagents.registerProvider. A wrapper makes correct
composite usage unrepresentable — the exact disposer cannot be recovered, so
a generator effect yielding it leaves the inner effect disposing as a
CONCURRENT SIBLING on owner unload, silently reproducing B1's ordering
corruption. The exact disposer serves both usages (composite-nestable AND
fire-and-forget callable); all seven now return it, typed
() => Promise<void> | void, with the convention pinned by a discriminating
test: an async-link composite probe that passes with the exact disposer and
observes the sibling unregistration firing mid-drain with a wrapper.
Re-auditing also surfaced that B1 itself SHIPPED a full-lint failure: it
changed register()'s return type without updating cross-file consumers
(agent.spec.ts dispose() statements, tool-bash's disposer list), which the
staged-scoped pre-commit lint never saw — pnpm run lint was red at HEAD.
Those three sites and this change's own fallout are fixed together: tests
now await disposers (stronger — they observe the full unwind), sync
paths void them, and the two annotation sites carry the honest union type.
agents.register's README line had drifted the same way (B1 updated the
JSDoc, not the README) — all seven README signatures now match; services
catalog regenerated.
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.