Skip to content

Commit 44ab280

Browse files
committed
test(permissions): pin auto-allow for agentId-targeted fleet calls
1 parent b167db3 commit 44ab280

1 file changed

Lines changed: 45 additions & 0 deletions

File tree

‎src/permission/permission.test.ts‎

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ import {
2525
classifyTool,
2626
buildRequests,
2727
isAutoAllowedShellCall,
28+
callTargetsRestricted,
2829
} from "./classify.js";
2930
import { createPermissionGate } from "./gate.js";
3031
import { APPROVAL_TIMEOUT_RESULT_TEXT } from "./decline-markers.js";
@@ -1596,6 +1597,50 @@ describe("createPermissionGate", () => {
15961597
expect(asked).toBe(tools.length);
15971598
});
15981599

1600+
// CL-9362: agentId-targeted fleet calls address workers by opaque session
1601+
// id (`target`), never by path — there is nothing path-shaped for
1602+
// callTargetsRestricted to judge, so the gate's auto-allow `!restricted`
1603+
// guard is intentionally vacuous for them. Path restriction is enforced
1604+
// where paths are actually touched: inside the target worker, whose own
1605+
// gate binds restriction judgments to its process cwd. This pins that
1606+
// decision: a fleet call aimed at a restricted-worktree worker still
1607+
// auto-allows in auto mode, exactly like spawn_agent/wait_agents.
1608+
test("auto mode auto-allows agentId-targeted fleet calls regardless of target worktree", async () => {
1609+
let asked = 0;
1610+
const gate = createPermissionGate({
1611+
approvals: [],
1612+
requestApproval: async () => {
1613+
asked++;
1614+
return { allow: false };
1615+
},
1616+
interactive: true,
1617+
skipPermissions: false,
1618+
reactorGated: false,
1619+
auto: true,
1620+
});
1621+
const calls: ToolCall[] = [
1622+
{ id: "c", name: "close_agent", arguments: { target: "worker-1" } },
1623+
{ id: "c", name: "interrupt_agent", arguments: { target: "worker-1" } },
1624+
{
1625+
id: "c",
1626+
name: "send_input",
1627+
arguments: { target: "worker-1", message: "continue" },
1628+
},
1629+
];
1630+
for (const call of calls) {
1631+
const verdict = await gate.evaluate(call);
1632+
expect(verdict.allowed).toBe(true);
1633+
}
1634+
expect(asked).toBe(0);
1635+
// The carve-out is intentional, not an oversight: even an isRestricted
1636+
// that reports everything restricted (the target worktree is restricted)
1637+
// does not flag these calls — they carry agent ids, not paths.
1638+
const alwaysRestricted = () => true;
1639+
for (const call of calls) {
1640+
expect(callTargetsRestricted(call, alwaysRestricted)).toBe(false);
1641+
}
1642+
});
1643+
15991644
// manage_tasks's handler has no side effect — the task list is mutated
16001645
// earlier by the director, before this tool ever executes — so denying it
16011646
// cannot undo anything. It auto-allows unconditionally, not just in auto

0 commit comments

Comments
 (0)