Every generated Remote method now resolves to `RemoteResult<T>`: the Client
face folds a carrier failure, a transport throw and a rejected result payload
into one error branch, so no consumer wraps a call to recover them. Only
assembly faults still reject — a wrong argument count, an unmounted method, a
missing Context binder, an absent Connection.
`RemoteFailure.code` stays an open string because the closed RPC code union
lives in the carrier package, which already depends on type-meta; naming it
here would invert that edge.
The goal surface drops its own try/catch plus the structural probe that
guessed whether a thrown cause was an RPC failure, and reads the result
instead.
Absence crosses the wire as a missing field: an omitted argument and a void
or undefined result both arrive as an absent JSON member, and the wide RPC
result slot accepts a success response without a value. Parameters declared
optional stay optional in the generated consumer declaration, so a business
signature is never widened to `T | undefined` to suit the wire. The weak SRC
descriptor reads parameter names from a JavaScript signature and cannot see
optionality, so a source-launched Host accepts an absent field and the strict
LIB pass owns rejecting a genuinely missing required parameter.
Adding COLLAPSE_SECTION_ORDER and CODE_ONLY_INSTRUCTION shifted every
later line in packages/core/tools/src/index.ts by 14, and three generated
artifacts cite those lines: the subsystems cordis-surface region, the
event producer/consumer matrix, and the Agent Note pair record left
inconsistent by the cherry-pick resolution.
`collapses()` read `defaultMode`, so the collapse only applied when the
DEPLOYMENT was `code`. An agent handed `code` by an agent preset under a
native default announced `[run_code]` on the wire and still executed a
model-direct native call -- the bypass this collapse exists to close,
reopened for exactly the composition `dsh-agent-tool-mode` produces.
`modeFor(scope)` is the same resolution `wireSchemas` and the SDK section
already use, so presentation and execution cannot disagree, and a mode
inherited from a standing preset scope collapses like a declared one.
The per-agent and preset tests asserted only the wire, which is why the
regression passed them. They now assert through the executor: the body
never runs, the call resolves UNKNOWN_TOOL, and the native sibling beside
it still executes.
The executor collapse landed without telling the model it exists. Every
tool contributes its own guidance section naming its tool, none of them
qualify how that tool is reached, and they all render before the SDK
(orders 100-199 against SDK_SECTION_ORDER 150), so the prompt said "Use
the read tool" eleven times and never said only run_code is callable.
A real session shows the consequence: the model emitted a native call,
read `unknown tool "read"` for a tool the same prompt declares, and
concluded the deployment was inconsistent rather than routing through
run_code.
The registry now contributes `tools:code-only` at order 99 -- ahead of
the guidance band -- stating the rule, registered wherever `tools:sdk`
is and rendering empty outside an effective `code`. `both` renders it
empty because its native calls do execute, which is also why
both-mode-turn no longer shares code-mode-turn's expected prompt. The
denial itself now names the route back, since a bare UNKNOWN_TOOL for a
declared tool is what misled the model.
wireSchemas() already advertised only run_code under mode: 'code', but the
executor resolved every call through get(), which returns the full visible
map plus the reserved transport. A model could name a native tool directly
and bypass run_code entirely. Route the execution-path lookups through a
new private resolveExecution() that applies the mode collapse at the
operation boundary: model-direct calls under 'code' may only name run_code
(UNKNOWN_TOOL otherwise), while SDK sub-dispatches (parent token set) keep
every visible tool. get()/schemas() public semantics are unchanged.
The denial happens at createExecution, before the extensible policy
pipeline — pre-execute listeners, approval ask, and guards never observe
a call that is deterministically denied. A collapsed call honors the
pre-dispatch cancellation contract, routes aborted results through the
visible tool's finalizeContent, and captures the finalizer before
argument materialization.
Regenerated docs, catalogs, graphs, scoped events, re-recorded
translation pairs, and updated test assertions.
Fixes#1815
LegGasai noted that filtering prompt sections by tool:* prefix is a poor
heuristic: it conflates section naming convention with presentation
semantics and would incorrectly drop tool:structured_output. The executor
collapse already enforces the boundary — a model-direct native call is
rejected as UNKNOWN_TOOL regardless of what the prompt says, so filtering
the prompt adds no security and only risks losing useful guidance.
The tool:read/tool:bash/etc sections describe capability usage patterns
that apply to both native and code presentations, and keeping them does
not reopen the native direct-call path because the executor blocks it.
wireSchemas() already advertised only run_code under mode: 'code', but the
executor resolved every call through get(), which returns the full visible
map plus the reserved transport. A model could name a native tool directly
and bypass run_code entirely. Route the execution-path lookups through a
new private resolveExecution() that applies the mode collapse at the
operation boundary: model-direct calls under 'code' may only name run_code
(UNKNOWN_TOOL otherwise), while SDK sub-dispatches (parent token set) keep
every visible tool. get()/schemas() public semantics are unchanged.
The denial happens at createExecution, before the extensible policy
pipeline — pre-execute listeners, approval ask, and guards never observe
a call that is deterministically denied. A collapsed call honors the
pre-dispatch cancellation contract, routes aborted results through the
visible tool's finalizeContent, and captures the finalizer before
argument materialization.
Under code mode, a system-prompt/assemble listener filters out tool:*
guidance sections that told the model to call native tools directly.
The tools:sdk section and SDK types remain so programs can still use
all tools through run_code.
Regenerated docs, catalogs, graphs, scoped events, and re-recorded
translation pairs.
Fixes#1815
wireSchemas() already advertised only run_code under mode: 'code', but the
executor resolved every call through get(), which returns the full visible
map plus the reserved transport. A model could name a native tool directly
and bypass run_code entirely. Route the execution-path lookups through a
new private resolveExecution() that applies the mode collapse at the
operation boundary: model-direct calls under 'code' may only name run_code
(UNKNOWN_TOOL otherwise), while SDK sub-dispatches (parent token set) keep
every visible tool. get()/schemas() public semantics are unchanged.
The denial happens at createExecution, before the extensible policy
pipeline — pre-execute listeners, approval ask, and guards never observe
a call that is deterministically denied. A collapsed call honors the
pre-dispatch cancellation contract, routes aborted results through the
visible tool's finalizeContent, and captures the finalizer before
argument materialization.
Under code mode, a system-prompt/assemble listener filters out tool:*
guidance sections that told the model to call native tools directly.
The tools:sdk section and SDK types remain so programs can still use
all tools through run_code.
Fixes#1815
CI captured `- text: Loading history…` where the golden holds the grandchild's
first message. The only wait before that capture is the offline banner, which
renders from the descriptor and says nothing about the transcript below it,
and `captureStableAria` calls two identical polls stable — two of which can
both be the loading placeholder on a loaded machine.
Waiting for the message the golden asserts makes the precondition match the
assertion. The race is not observable here (the history is already loaded when
the banner appears, so the wait returns in ~1ms), so this is reasoned from the
CI log and the helper's semantics rather than from a local reproduction; if
the history instead never arrives on CI, the lane now times out on the missing
message rather than committing a loading placeholder to the diff.
Empty `roots` stopped meaning "no roster" when the harness-home root became
a default, so the case that keeps the unjoined-agent diagnostic silent is the
one opting out of every root — which is what it already passes and what its
name now says.
Drop the filler and factually loose comments added with the tool-pwsh rows:
the platform expressions self-explain, the web-app overlay comment repeats
the file header, and the spec comments now state only the platform-scoped
evaluation fact.
Regenerates docs/config-catalog and re-records its pairing hashes: master's
side of that generated file merged textually, which leaves the recorded blob
hashes describing neither side's content.
The new default's whole claim is that a launcher configuring nothing still
finds a person's presets, and nothing asserted it through a real composition —
`user-root.spec.ts` plugs the service directly, and every other lane pins
`includeUserRoot: false` so the machine's home cannot decide a golden.
This lane boots the real base and web-app bundles with only the shipped root
patched in, points `$DSH_HOME` at a temp home before boot (the derived root is
resolved when the plugin is constructed), and asserts a preset placed there is
listed as `user`, reported healthy, authorable, and mountable into an agent
whose tool catalog it decides.
The service moved its "is a roster composed" reads to the derived root set;
the invariant companion still read `config.roots`. In the shape this change
exists for — an app configures nothing and the roster is the harness home
alone — that made the advisory warning fire while the fail-loud invariant
stayed silent, so an agent could address a model against an empty global
layer unchecked.
Both now read one source: `roots` exposes the resolved set, and the invariant
asks it. That decides the behavior deliberately rather than by omission — a
composition that mounts the roster now fails an unjoined agent whether its
roots were configured or derived, and `includeUserRoot: false` with no
configured roots is how a deployment keeps its agents on the host plane. Both
shapes are pinned; the derived-only case fails against the old predicate.
Three pieces of prose went stale with the first commit: the web-app bundle
comment still called the writable root an assembly fact patched in by
AppCLIEntry (removed in the profile-plugin-bundles refactor — `composeProfile`
owns it now, and only for the shipped root), and the shipped skill and its
Agent Note still called both roots "configuration". The README gains the
resolved-roster reader and the discoverable-but-undeletable preset a second
writable root produces.