fix(agent): skip prewalk switch on read-only xd:// device calls

xd:// devices dispatch through the write tool, so a read-only call such
as an lsp navigation matched PREWALK_ACTION_TOOLS and armed the one-way
model hand-off while still reasoning about code shape.

dispatchXdevTool now records the wrapped tool's approval tier on the
XdevDispatch, and the prewalk coordinator only treats a device write as
an implementation action at write/exec tier. Direct edit/write are
unchanged.

Fixes #7312
This commit is contained in:
roboomp
2026-08-01 19:39:49 +00:00
parent 09a7c86563
commit 0e923547aa
5 changed files with 199 additions and 3 deletions
@@ -96,6 +96,9 @@ describe("read and write route xd:// device URLs", () => {
expect(previewResult.isError).toBeUndefined();
expect(previewResult.details?.xdev?.tool).toBe("ast_edit");
expect(previewResult.details?.xdev?.mode).toBe("execute");
// The dispatch records the wrapped tool's approval tier so prewalk can
// tell a mutation from a read-only device call (issue #7312).
expect(previewResult.details?.xdev?.tier).toBe("write");
const previewText = previewResult.content.find(entry => entry.type === "text")?.text ?? "";
expect(previewText).toContain("modernWrap");
@@ -109,6 +112,25 @@ describe("read and write route xd:// device URLs", () => {
}
});
it("records a read tier on the dispatch of a read-only device", async () => {
const readDevice: AgentTool = {
name: "peek",
label: "Peek",
description: "Read-only device",
parameters: type({ q: "string" }),
approval: () => "read",
async execute() {
return { content: [{ type: "text", text: "peeked" }] };
},
};
const xdev = createTestXdevState([readDevice]);
const write = new WriteTool(xdevSession(process.cwd(), { xdev }));
const result = await write.execute("write-xdev-read", { path: "xd://peek", content: JSON.stringify({ q: "x" }) });
expect(result.isError).toBeUndefined();
expect(result.details?.xdev).toMatchObject({ tool: "peek", mode: "execute", tier: "read" });
});
it("rejects near-miss xd addresses before filesystem fallback", async () => {
const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), "write-xdev-near-miss-"));
try {