Skip to content

Commit 2bc44f1

Browse files
committed
Align inherit tests with middleware deny and grant retry
Workers deny without asking when parent-gated MCP tools are wrapped again. A later call id after a parent grant must still pass gateToolCall instead of reusing the prior deny.
1 parent 88c881d commit 2bc44f1

2 files changed

Lines changed: 45 additions & 4 deletions

File tree

src/permission/reactor-authorize.test.ts

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,7 @@
11
import { expect, test } from "bun:test";
2+
import { mkdtempSync } from "node:fs";
3+
import { tmpdir } from "node:os";
4+
import { join } from "node:path";
25
import { createPermissionGate } from "./gate.js";
36
import {
47
createReactorAuthorize,
@@ -33,6 +36,43 @@ const gate = (opts?: { interactive?: boolean; auto?: boolean }) =>
3336
},
3437
});
3538

39+
test("worker grant after a denied write allows a later call id through gateToolCall", async () => {
40+
const cwd = mkdtempSync(join(tmpdir(), "worker-grant-"));
41+
const path = join(cwd, "probe.txt");
42+
const policy = createPermissionGate({
43+
cwd,
44+
approvals: [],
45+
interactive: false,
46+
auto: false,
47+
skipPermissions: false,
48+
reactorGated: true,
49+
requestApproval: async () => {
50+
throw new Error("worker must never ask");
51+
},
52+
});
53+
const workerGate = workerPermissionGate(policy);
54+
const first: ToolCall = {
55+
id: "call_auto_0",
56+
name: "write_file",
57+
arguments: { path, content: "unauthorized" },
58+
};
59+
const second: ToolCall = {
60+
id: "call_auto_1",
61+
name: "write_file",
62+
arguments: { path, content: "unauthorized" },
63+
};
64+
expect((await workerGate.authorizeCall(first)).effect).toBe("deny");
65+
policy.setSeededApprovals([{ tool: "write_file", pattern: path }]);
66+
expect((await workerGate.authorizeCall(second)).effect).toBe("allow");
67+
let called = false;
68+
const result = await gateToolCall(workerGate, second, new AbortController().signal, async () => {
69+
called = true;
70+
return { callId: second.id, content: "executed", isError: false };
71+
});
72+
expect(result.isError).toBe(false);
73+
expect(called).toBe(true);
74+
});
75+
3676
test("worker maps unresolved ask to deny while main reactor suspends", async () => {
3777
const policy = gate();
3878
expect((await createReactorAuthorize(policy)("tool:write_file", "invoke", call)).effect).toBe(

tests/integration/subagent-permission.test.ts

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -485,7 +485,7 @@ test.serial(
485485
20000,
486486
);
487487

488-
test("storing parent-gated MCP tools then wrapping again still calls requestApproval", async () => {
488+
test("storing parent-gated MCP tools then wrapping again denies without requestApproval", async () => {
489489
const cwd = await mkdtemp(join(tmpdir(), "worker-permission-"));
490490
let asks = 0;
491491
let calls = 0;
@@ -522,12 +522,13 @@ test("storing parent-gated MCP tools then wrapping again still calls requestAppr
522522
const doubleWrapped = gateAgentTools(parentGated, workerPermissionGate(parent));
523523
const tool = doubleWrapped[0];
524524
if (tool?.kind !== "full") throw new Error("expected full inherited MCP tool");
525-
await tool.handler(
525+
const result = await tool.handler(
526526
{ id: "c1", name: "mcp__probe__mutate", arguments: {} },
527527
new AbortController().signal,
528528
);
529-
expect(asks).toBe(1);
530-
expect(calls).toBe(1);
529+
expect(asks).toBe(0);
530+
expect(calls).toBe(0);
531+
expect(result.isError).toBe(true);
531532
} finally {
532533
await rm(cwd, { recursive: true, force: true });
533534
}

0 commit comments

Comments
 (0)