diff --git a/packages/coding-agent/src/extensibility/extensions/wrapper.ts b/packages/coding-agent/src/extensibility/extensions/wrapper.ts index 8ce2305f2..7618e305f 100644 --- a/packages/coding-agent/src/extensibility/extensions/wrapper.ts +++ b/packages/coding-agent/src/extensibility/extensions/wrapper.ts @@ -239,6 +239,7 @@ export class ExtensionToolWrapper { + xdev = { ...xdev, tier: effectiveTier }; + }, + } + : undefined; + const result = await executable.execute(toolCallId, validated as never, signal, innerOnUpdate, executionContext); return { result, xdev: { ...xdev, inner: result.details } }; } catch (error) { if ( diff --git a/packages/coding-agent/test/extensions-runner.test.ts b/packages/coding-agent/test/extensions-runner.test.ts index a8cdd8324..a078575d9 100644 --- a/packages/coding-agent/test/extensions-runner.test.ts +++ b/packages/coding-agent/test/extensions-runner.test.ts @@ -2542,6 +2542,50 @@ describe("ExtensionRunner", () => { expect(fs.existsSync(recordPath)).toBe(false); // tool never executed }); + it("reports the effective tier after a tool_call handler revises xd:// input", async () => { + const recordPath = path.join(tempDir.path(), "xdev-effective-tier.jsonl"); + const extCode = ` + export default function(pi) { + pi.on("tool_call", async (event) => { + if (event.toolName !== "bash") return; + return { input: { command: "echo revised" } }; + }); + } + `; + fs.writeFileSync(path.join(extensionsDir, "tool-call-xdev-tier.ts"), extCode); + + const result = await loadTestExtensions(); + const runner = new ExtensionRunner( + result.extensions, + result.runtime, + tempDir.path(), + sessionManager, + modelRegistry, + ); + const tool = createRecordingTool(recordPath); + tool.approval = args => + args && + typeof args === "object" && + "command" in args && + typeof args.command === "string" && + args.command.includes("revised") + ? "write" + : "read"; + const wrapped = new ExtensionToolWrapper(tool, runner); + let effectiveTier: string | undefined; + const xdevContext = { + settings: { get: (key: string) => (key === "tools.approvalMode" ? "yolo" : {}) }, + xdevApproved: true, + xdevTierResolved: (tier: string) => { + effectiveTier = tier; + }, + } as never; + + await wrapped.execute("xdev-tier-id", { command: "echo original" }, undefined, undefined, xdevContext); + expect(JSON.parse(fs.readFileSync(recordPath, "utf8"))).toEqual({ command: "echo revised" }); + expect(effectiveTier).toBe("write"); + }); + it("emits tool_call before the approval prompt so approval sees the final input", async () => { const order: string[] = []; const extCode = ` diff --git a/packages/coding-agent/test/write-xdev-dispatch.test.ts b/packages/coding-agent/test/write-xdev-dispatch.test.ts index b20dd232f..efbc6fe5f 100644 --- a/packages/coding-agent/test/write-xdev-dispatch.test.ts +++ b/packages/coding-agent/test/write-xdev-dispatch.test.ts @@ -131,6 +131,45 @@ describe("read and write route xd:// device URLs", () => { expect(result.details?.xdev).toMatchObject({ tool: "peek", mode: "execute", tier: "read" }); }); + it("records the effective tier reported after an execution decorator rewrites device args", async () => { + let executedQuery: string | undefined; + const device: AgentTool = { + name: "peek", + label: "Peek", + description: "Argument-dependent device", + parameters: type({ q: "string" }), + approval: args => + args && typeof args === "object" && "q" in args && args.q === "mutate" ? "write" : "read", + async execute(_id, args) { + if (!args || typeof args !== "object" || !("q" in args) || typeof args.q !== "string") { + throw new Error("Expected a string query"); + } + executedQuery = args.q; + return { content: [{ type: "text", text: "done" }] }; + }, + }; + const xdev = createTestXdevState([device]); + xdev.decorateExecution = canonical => ({ + ...canonical, + async execute(id, _args, signal, onUpdate, context) { + const revised = { q: "mutate" }; + context?.xdevTierResolved?.("write"); + return canonical.execute(id, revised as never, signal, onUpdate, context); + }, + }); + const write = new WriteTool(xdevSession(process.cwd(), { xdev })); + + const result = await write.execute( + "write-xdev-revised", + { path: "xd://peek", content: JSON.stringify({ q: "inspect" }) }, + undefined, + undefined, + {} as never, + ); + expect(executedQuery).toBe("mutate"); + expect(result.details?.xdev?.tier).toBe("write"); + }); + it("rejects near-miss xd addresses before filesystem fallback", async () => { const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), "write-xdev-near-miss-")); try {