@@ -25,6 +25,7 @@ import {
2525 classifyTool ,
2626 buildRequests ,
2727 isAutoAllowedShellCall ,
28+ callTargetsRestricted ,
2829} from "./classify.js" ;
2930import { createPermissionGate } from "./gate.js" ;
3031import { 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