The stack rebases onto a moved master through its base branch. Beyond
mechanical unions (both branches' demo scripts, example rows, service
roles, tool lists, acp deps, doc budgets — each side fit alone, the
union needs the higher ceilings), three semantic reconciliations:
- The ACP bridge now carries BOTH per-session surfaces: the sandbox
stack's config options + approval answerer and this branch's session
modes; session/new and session/load advertise modes AND configOptions
side by side.
- The feature matrix supersedes the sandbox stance per the RFC's
second-lander rule: session/set_mode and current_mode_update flip to
shipped-by-dsh-mode, config-option rows stay as #169 wrote them, and
§6 records both landed features under the picker-to-modes /
knobs-to-config-options division.
- The snapshot pin grammar (#254: one header snapshot + declared deltas
+ a Markdown prompt golden) gains a symmetric declaration for what a
delta cannot express: expectedHeaderSnapshots — a plan-mode flip
resorts the canonical tool list, so its widening lands as a second
full snapshot, now its own Markdown section. The pin-less-class and
model-turn-only-pin amendments carry over; new fixtures cover the
extended writer paths, and the plan-acp-agent scenarios re-recorded
under the merged composition (the app now bundles the skill tool)
with the suite's refresh mode wired through.
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.
Every packages/*/* README now carries a canonical '## Known Limitations and
Deferred Work' section: condensed, evidence-backed bullets for consumer-visible
gaps (unimplemented features, platform caveats, MVP cuts) and consciously
postponed work (TODO/FIXME/XXX markers, RFC deferrals still open). The ten
pre-existing ad-hoc variants ('What is NOT here (TODO)', 'Deferred',
'Limitations (MVP)', 'Known limitations (tracked TODOs)', ...) are normalized
into the canonical heading.
A new doc-sync gate, scripts/verify-readme-limitations.ts, enforces the shape:
exactly one limitations-like heading per package README, byte-equal to the
canonical h2, with at least one bullet; near-miss headings fail so variants
cannot creep back. Packages with genuinely nothing to declare (dsh-brand,
dsh-timeout, dsh-subagent-mock, dsh-app-boot) are whitelisted in the script and
must NOT carry the section; whitelist entries are validated against the scanned
package set so a rename fails loud.
Wired into the doc-sync chain (package.json) and the run-gates doc-sync leaf
set; the standing rule lands in packages/AGENTS.md and the adding-a-package
cookbook; decision record in
docs/rfc/implemented/process/2026-07-10-readme-known-limitations-gate.md
(RFC index regenerated).
Also fixes two stale '(deferred)' markers claiming dsh-compact-basic is
unimplemented (the dsh-compact seam README's package table and the seam's
module doc comment).
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.
The three-entry cordis.yml (dsh-sandbox-local + dsh-bash-sandbox at a
read-only default + dsh-approval) served over ACP: the first live approval
composition. Recorded snapshot scenarios pin the wire end to end —
config-options advertisement, the mode-switching arc as the suite pinned
header (both switches, the prompt-section delta, one changed-by-the-user
notice per knob, a confined write landing under the switched mode), and
both escalation branches over scripted permissionAnswers (a grant runs
confined under workspace-write; a rejection executes nothing and pins the
fail-closed text). The with-key escalation e2e drives a real model +
real runner + the real bridge answerer, world-verified; ci.yml snapshot
lane and e2e.yml install bubblewrap so the confined replays actually
execute.
Both RFCs move to implemented/ (Decision/Consequences form, deferred
phases tracked in their own sections), with every cross-reference flipped.
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.
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.
The sandbox execute wrapper JSON round-tripped the return and blindly cast it
to ToolExecuteReturn. A JSON-valid but wrong-shape return — a bare string,
{ content: 'ok' }, blocks without a type tag — sailed through: the registry
spreads result.content, so { content: 'ok' } became ['o','k'], passed the
session log's isJsonValue gate, and the DeepSeek serializer then flattened it
to '(no output)' — silent corruption of the next model request and every
replay, instead of a contained tool error.
The round-tripped value is now shape-checked against the two ToolExecuteReturn
forms (array of content blocks, or { content: blocks, meta? }); block checks
are structural only (plain object + string type tag) because the ContentBlock
union is merge-extensible. A wrong shape — and the formerly cryptic
forgot-return/bare-string cases — fails that one call with a teaching error
echoing a truncated preview of what was returned and the two valid forms.
New specs pin the object-form pass-through (meta included), six rejection
shapes, and the preview truncation; per-file 100% coverage holds.
cordis provide() accepts any value and cross-mount composition advertises
ctx.provide('name', value), but the façade's readService unconditionally
proxied every declared service — new Proxy('42') throws "Cannot create proxy
with a non-object as target or handler", so a consumer of a primitive-valued
service crashed on first read with an error naming neither the service nor
the fix. A primitive or null value now passes through unwrapped (after the
denyContext check); only object- and function-valued services are proxied —
a primitive has no method that could hand back a Context, so nothing is lost.
New cross-mount spec pins both read paths (ctx.<name> and ctx.get) for a
number and a null provided value.
The sandbox docs overclaimed a containment contract the design never makes:
"capability access is routed through cordis services, never Node built-ins,
so everything a mounted plugin does stays inspectable and disposable". The
host-realm helpers on the sandbox global (harness, console, btoa) are
reachable functions, so mount code that goes looking can reach the host realm
through one of them — accepted under the trust stance, because the ctx a
mount ultimately receives is fully privileged anyway. Reword the sandbox
module doc, the README trust stance, and the RFC sandbox-semantics section to
say exactly that: the traps and small global surface STEER honest code onto
the cordis services; they are not a security boundary.
Two review findings (#220) on the sandbox context façade:
- Undeclared services were reachable: the façade resolved any live global via
ctx.get(name), so ctx.bash worked without inject: ['bash']. A cross-mount
consumer could then depend on a provider cordis never saw — unmounting the
provider would neither park the consumer nor unwind its registered tools,
leaving a model-visible tool that fails only at execution. The façade now
reads ctx.fiber.inject and refuses any service the mount did not declare
(with a teaching error naming the inject fix), so the dependency is always
visible to cordis and its activation/unload semantics bind.
- ctx.tools.get returned the live ToolDefinition, including execute — mount
code could call another tool directly and bypass ToolRegistry.execute and
its pre/post-execute hooks and accounting. get now returns the same
read-only name/description/parameters view as schemas(), never an invocable.
Adds inject-gate and schema-view regression cases to sandbox-context.spec.ts
(undeclared property/get denied, declared allowed, the cross-mount zombie-tool
scenario refused at call time, get exposes no execute). Package stays at
per-file 100% coverage. RFC, mount description, and tool-catalog updated.