Dispatch Desk: device and path gates
At Dispatch Desk, Nia sees a call named write. She initially assumes it is a generic filesystem write. The address changes the question: the call targets xd://seed_slot, so write is carrying an invocation of another tool.
The operator must identify both the outer transport and the inner operation.
Read the envelope and its payload separately
This is the fictional envelope used for a reservation case. Inspect it; do not submit it to a running agent.
{
"path": "xd://seed_slot",
"content": "{\"action\":\"reserve\"}"
}
The content field is a JSON string containing the inner argument object. In the private verifier, the only mounted recording tool is seed_slot. Its declaration returns read for inspect, write for reserve, an explicit exec-tier deny for blocked, and exec for other action strings, including publish. The executor merely appends an in-memory record.
WriteTool.approval() parses the device payload and calls resolveToolTier() on the target. It returns the borrowed tier with policyKey: "seed_slot". It does not copy the target’s entire policy decision into the outer declaration.
During execution, dispatchXdevTool() resolves the enabled canonical tool and validates the decoded arguments before invoking the executable instance. The inner wrapper can still enforce a tool-owned deny or other applicable gate.
Predict an inspection and a reservation
dispatch-inspect-no-ui supplies action: inspect, always-ask mode, and no UI. dispatch-reserve-once supplies action: reserve in always-ask and one recorded Approve answer.
Recorded answers: the inspection produced no prompt and one inert execution. The reservation produced one prompt and one inert execution. Both produced two tool_call events: one for write, one for seed_slot, under the same outer call ID.
The reservation’s outer prompt was:
Allow tool: write
Path: xd://seed_slot
Content:
{"action":"reserve"}
The available responses were exactly Approve and Deny.
After the outer gate, WriteTool.execute() forwards xdevApproved: true when a context is available. For unchanged inner input, the wrapper can suppress the duplicate mode-tier prompt. Two wrapper layers therefore do not necessarily mean two questions, and two events do not mean two executions.
Policy identity can replace the generic write fallback
Compare these recorded cases:
| Case | Applicable policy data | Recorded outcome |
|---|---|---|
dispatch-fallback-deny | No device policy; write: deny; yolo | Outer throw; zero prompts, zero tool_call events, zero inert executions. |
dispatch-specific-allow | seed_slot: allow; write: deny; always-ask; no UI | Exec-tier fictional action returned; zero prompts, two tool_call events, one inert execution. |
dispatch-explicit-prompt-twice | seed_slot: prompt; yolo; two Approve answers | Two prompts, two tool_call events, one inert execution. |
dispatch-inner-tool-deny | Inner action blocked; yolo | Error result with tool-policy refusal; one outer tool_call, no inner execution. |
The device-specific allow is a valid replacement for the invoking-tool fallback. It is not an instruction to widen a real device policy. The explicit device prompt is consulted at both outer and inner layers in the recorded path, so the current implementation can ask twice for one action. Do not interpret that as two successful reservations.
The inner-deny case explains why borrowing a tier is not borrowing the whole decision. The outer gate permits exec under yolo, but the inner declaration still denies. The dispatcher converts that ordinary inner error into a result carrying isError: true. A returned result is not necessarily success.
Know the exact forwarded-prompt predicate
The relevant source expressions in ExtensionToolWrapper.execute() are:
const explicitPrompt = resolved.override || Object.hasOwn(userPolicies, resolved.policyKey ?? this.tool.name);
const xdevBypass = context?.xdevApproved === true && effectiveParams === params;
An ordinary resolved prompt is required when explicitPrompt is true or xdevBypass is false. Pending provider safety checks independently require approval.
Two limits follow directly from this source:
- Raw own-property presence is not the same as a normalized valid policy. An invalid entry can still affect this predicate for the key it checks.
- The unchanged-input test is object identity, not a deep content comparison or revision hash. The recorded replacement cases use new argument objects.
There is also a source-derived edge case: an unchanged forwarded inner decision that resolves to prompt but has neither a surviving override nor an own user-policy entry can be suppressed by this predicate. Do not generalize the tested explicit user prompt case into a claim that every possible tool-owned prompt produces an inner dialog. This edge case is not a separately executed Dispatch Desk scenario.
A revised input must be explained again
In dispatch-rewrite-prompts, the outer input is inspect. The recording handler supplies this replacement for the inner tool:
{
"action": "publish",
"note": "Revised fictional card"
}
Prediction: can the replacement ride the original read-tier admission?
Recorded answer: it cannot use the unchanged-input bypass. The inner wrapper reclassifies it as exec, prompts with the revised action, and records one inert execution after Approve. The returned dispatch tier is exec.
A useful diagnostic qualification appears in the same record: details.xdev.args still contains the dispatcher’s original validated inspect input, while the effective tier and inert execution ledger reflect the replacement. The tier callback updates the tier; it is not a general rewrite of every dispatch metadata field. Do not treat that nested metadata as a universal final-input audit.
dispatch-rewrite-denies replaces inspect with blocked. Recorded: two tool_call events, no prompt, no inert execution, and an error result naming tool policy. A previously admitted input does not authorize a newly denied one.
dispatch-malformed-json carries {not-json in yolo. Recorded: one outer event, no prompt, no inner execution, and an error result explaining that the device expects a JSON args object. The outer declaration falls back to exec for malformed JSON; yolo admitting that tier does not make the payload valid. In a prompting mode, the outer gate can be reached before this dispatch validation failure.
Ordinary paths have different rules
The device story does not replace path analysis.
WriteToolunwraps a hashline path header before classification. SSH-shaped targets escalate to exec before ordinary handler-backed write classification.resolveFileWriteApprovalTier()returns write for ordinary filesystem paths. For recognized internal-resource paths, a writable handler keeps write tier; absence of a handler write method can yield read tier.local://can subsequently resolve to session artifact storage and be written. Thus this classification is not proof of zero file effects.- A device-only
writetransport is not a general file-write grant. The existing dispatch tests verify refusal of filesystem targets, with the source’s specific active-plan local-sandbox exception. A fuller displayed description alone does not relax that execution guard. ReadToolandGrepToolusepathTargetsSsh()to escalate SSH-containing arguments. The substring scan can catch a remote entry before later path-list expansion. It does not authenticate to the host or grant access there.
The existing ssh-url-approval-gate.test.ts rejects remote-shaped read, grep, and write calls at the wrapper before an SSH connection, while exercising local counterparts. That proves the tested gate boundary, not remote execution.
Paper checkpoint: explain dispatch-reserve-once as one outer prompt, two tool-call events, and one inert execution. Then explain why dispatch-explicit-prompt-twice has two prompts without two executions. Worked answer: the first uses forwarded duplicate-tier suppression; the second retains an explicit device user policy at the inner gate.
Failure boundary: this route is not a model for every nested call. Same-tool native delegation through ExtensionRunner.invokeNativeTool() calls an unwrapped native implementation without another approval gate. Other bridges and direct calls have their own wiring. xd:// is a dispatch address, not a new source of authority.
Source anchors: packages/coding-agent/src/tools/write.ts — WriteTool.approval, WriteTool.execute; packages/coding-agent/src/tools/xdev.ts — parseDeviceArgs, resolveXdevTool, dispatchXdevTool; packages/coding-agent/src/extensibility/extensions/wrapper.ts — ExtensionToolWrapper.execute; packages/coding-agent/src/tools/path-utils.ts — resolveFileWriteApprovalTier, pathTargetsSsh; packages/coding-agent/test/write-xdev-dispatch.test.ts; packages/coding-agent/test/tools/ssh-url-approval-gate.test.ts.
Tool permissions and approvals · Source chapter: permissions/dispatch-desk-device-and-path-gates. Original evidence remains scoped to its recorded snapshot.