fix(write): evaluate function-valued xd:// device approvals
The write approval gate discarded a mounted tool's function-valued approval and never decoded the device JSON payload, defaulting the tier to exec. Read/write xd:// operations then prompted in non-yolo modes that permit them. Now decode valid object payloads and resolve the mounted tool's normal approval decision via resolveToolTier; malformed JSON, non-object payloads, and unknown devices still fall back to exec and prompt. Fixes #5727
This commit is contained in:
@@ -59,12 +59,12 @@ describe("read and write route xd:// device URLs", () => {
|
||||
paths: [filePath],
|
||||
});
|
||||
|
||||
// Approval resolves a tier instead of throwing. A mounted tool whose own
|
||||
// approval is a function (unresolvable statically) falls back to exec.
|
||||
// The write gate decodes the device payload and evaluates the mounted
|
||||
// tool's own approval. ast_edit is write-tier for a filesystem path.
|
||||
const approval = write!.approval;
|
||||
expect(typeof approval).toBe("function");
|
||||
if (typeof approval === "function") {
|
||||
expect(approval({ path: "xd://ast_edit", content })).toBe("exec");
|
||||
expect(approval({ path: "xd://ast_edit", content })).toBe("write");
|
||||
}
|
||||
|
||||
// Execute dispatches through the xdev registry to the mounted ast_edit,
|
||||
@@ -86,6 +86,47 @@ describe("read and write route xd:// device URLs", () => {
|
||||
}
|
||||
});
|
||||
|
||||
it("resolves function-valued device approvals per payload and fails closed on bad content", async () => {
|
||||
const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), "write-xdev-approval-"));
|
||||
try {
|
||||
const filePath = path.join(tempDir, "target.ts");
|
||||
await Bun.write(filePath, "legacyWrap(x, value)\n");
|
||||
const tools = await createTools(xdevSession(tempDir));
|
||||
const write = tools.find(entry => entry.name === "write");
|
||||
expect(write).toBeDefined();
|
||||
const approval = write!.approval;
|
||||
expect(typeof approval).toBe("function");
|
||||
if (typeof approval !== "function") throw new Error("expected a function approval");
|
||||
const tier = (path: string, content: string) => approval({ path, content });
|
||||
|
||||
// ast_edit on a filesystem path → write; on internal URLs only → read.
|
||||
const astFsPath = JSON.stringify({
|
||||
ops: [{ pat: "legacyWrap($A, $B)", out: "modernWrap($A, $B)" }],
|
||||
paths: [filePath],
|
||||
});
|
||||
const astInternalPath = JSON.stringify({
|
||||
ops: [{ pat: "a", out: "b" }],
|
||||
paths: ["artifact://abc"],
|
||||
});
|
||||
expect(tier("xd://ast_edit", astFsPath)).toBe("write");
|
||||
expect(tier("xd://ast_edit", astInternalPath)).toBe("read");
|
||||
|
||||
// debug: inspection action → read; a real launch → exec (control).
|
||||
expect(tier("xd://debug", JSON.stringify({ action: "sessions" }))).toBe("read");
|
||||
expect(tier("xd://debug", JSON.stringify({ action: "launch", program: "./app" }))).toBe("exec");
|
||||
|
||||
// Fail closed: malformed JSON, non-object payloads, missing content,
|
||||
// and unknown devices all stay exec so the gate never under-prompts.
|
||||
expect(tier("xd://ast_edit", "{ not json")).toBe("exec");
|
||||
expect(tier("xd://ast_edit", "[1,2,3]")).toBe("exec");
|
||||
expect(tier("xd://ast_edit", '"a string"')).toBe("exec");
|
||||
expect(approval({ path: "xd://ast_edit" })).toBe("exec");
|
||||
expect(tier("xd://no_such_device", "{}")).toBe("exec");
|
||||
} finally {
|
||||
await removeWithRetries(tempDir);
|
||||
}
|
||||
});
|
||||
|
||||
it("renderCall withholds a partial xd:// URL, then delegates once settled", async () => {
|
||||
await themeModule.initTheme();
|
||||
const uiTheme = (await themeModule.getThemeByName("dark")) ?? (await themeModule.getThemeByName("light"));
|
||||
|
||||
Reference in New Issue
Block a user