11import { describe , expect , test } from "bun:test" ;
2+ import { join } from "node:path" ;
23import { type } from "arktype" ;
34
5+ import { createPermissionGate } from "../src/permission/gate.js" ;
6+
47import { createSubAgentSessionStore } from "../src/subagent/index.js" ;
58import { withMockedModuleDuring } from "../testkit/mock-module.js" ;
69import {
710 fromHost ,
11+ waitAgentsGrantRequestId ,
812 waitAgentsResults ,
913 WORKER_HOST ,
1014 WORKER_PROVIDER ,
@@ -13,17 +17,43 @@ import {
1317 closeE2ESession ,
1418 e2ePermissionGate ,
1519 openE2ESession ,
20+ replyOnceAtRequest ,
1621 runUntilDone ,
1722} from "./harness.js" ;
1823import { DIRECTOR_RESOLVABLE_ESCALATION } from "../testkit/escalation.js" ;
1924
2025const RequestURL = type ( { url : "string" } ) ;
26+ type AuthorizationCheckpoint =
27+ | "denied-write"
28+ | "asked-for-approval"
29+ | "approval-issued"
30+ | "authorized-retry" ;
31+
32+ test ( "wait_agents grant correlation accepts pretty serialized tool output" , ( ) => {
33+ const result = JSON . stringify (
34+ {
35+ results : [
36+ {
37+ agent_id : "lane-1" ,
38+ grant_request_id : "grant-1" ,
39+ status : "awaiting_director" ,
40+ } ,
41+ ] ,
42+ timed_out : false ,
43+ } ,
44+ null ,
45+ 2 ,
46+ ) ;
47+ const request = JSON . stringify ( { messages : [ { content : result } ] } ) ;
48+ expect ( waitAgentsGrantRequestId ( request ) ) . toBe ( "grant-1" ) ;
49+ } ) ;
2150
2251async function openFleetSession (
2352 sessions : ReturnType < typeof createSubAgentSessionStore > ,
53+ permissionGate = e2ePermissionGate ( ) ,
2454) {
2555 return openE2ESession ( {
26- permissionGate : e2ePermissionGate ( ) ,
56+ permissionGate,
2757 subAgent : {
2858 provider : WORKER_PROVIDER ,
2959 sessions,
@@ -140,6 +170,194 @@ describe("e2e — fleet orchestration", () => {
140170 60000 ,
141171 ) ;
142172
173+ test . serial (
174+ "a real denied worker write is approved once with live correlation IDs and retried once" ,
175+ async ( ) => {
176+ const fleetSessions = createSubAgentSessionStore ( ) ;
177+ const session = await openFleetSession (
178+ fleetSessions ,
179+ createPermissionGate ( {
180+ approvals : [
181+ { tool : "spawn_agent" , pattern : "*" } ,
182+ { tool : "wait_agents" , pattern : "*" } ,
183+ { tool : "approve_worker_call" , pattern : "*" } ,
184+ ] ,
185+ interactive : true ,
186+ skipPermissions : false ,
187+ reactorGated : true ,
188+ requestApproval : async ( ) => ( { allow : true } ) ,
189+ } ) ,
190+ ) ;
191+ try {
192+ const parent = fromHost ( "api.anthropic.com" ) ;
193+ const worker = fromHost ( WORKER_HOST ) ;
194+ const output = "approved.txt" ;
195+ const checkpoints : AuthorizationCheckpoint [ ] = [ ] ;
196+ let approvedRequestId : string | undefined ;
197+ session . harness . scenario . replyOnce ( "anthropic" , {
198+ predicate : parent ,
199+ toolCalls : [
200+ {
201+ name : "spawn_agent" ,
202+ args : {
203+ description : "authorized write lane" ,
204+ prompt :
205+ "Write approved.txt with approved, ask for authorization if denied, then report." ,
206+ intent : "implement" ,
207+ success_criteria : [ "approved.txt contains approved" ] ,
208+ } ,
209+ } ,
210+ ] ,
211+ } ) ;
212+ session . harness . scenario . replyOnce ( "openai" , {
213+ predicate : worker ,
214+ toolCalls : [
215+ { name : "write_file" , args : { path : output , content : "approved" } } ,
216+ ] ,
217+ } ) ;
218+ replyOnceAtRequest ( session , "openai" , worker , ( { body } ) => {
219+ const grantRequestId = / g r a n t r e q u e s t ( [ \w - ] + ) / . exec ( body ) ?. [ 1 ] ;
220+ if ( grantRequestId === undefined ) {
221+ throw new Error (
222+ `worker did not receive the denied-call grant marker: ${ body } ` ,
223+ ) ;
224+ }
225+ checkpoints . push ( "denied-write" ) ;
226+ return {
227+ toolCalls : [
228+ {
229+ name : "ask_director" ,
230+ args : {
231+ question : "Please approve the denied write." ,
232+ grant_request_id : grantRequestId ,
233+ escalation : DIRECTOR_RESOLVABLE_ESCALATION ,
234+ } ,
235+ } ,
236+ ] ,
237+ } ;
238+ } ) ;
239+ replyOnceAtRequest ( session , "openai" , worker , ( { body } ) =>
240+ body . includes ( "Parent approved the exact denied call" )
241+ ? ( checkpoints . push ( "authorized-retry" ) ,
242+ {
243+ toolCalls : [
244+ {
245+ name : "write_file" ,
246+ args : { path : output , content : "approved" } ,
247+ } ,
248+ ] ,
249+ } )
250+ : undefined ,
251+ ) ;
252+ replyOnceAtRequest ( session , "openai" , worker , ( { body } ) =>
253+ body . includes ( "wrote 8 bytes to" )
254+ ? {
255+ text : "## Summary\nWrite completed.\n## Findings\nAuthorized retry succeeded.\n## Blockers\nNone.\n## Paths\napproved.txt" ,
256+ }
257+ : undefined ,
258+ ) ;
259+ session . harness . scenario . replyOnce ( "anthropic" , {
260+ predicate : parent ,
261+ toolCalls : [ { name : "wait_agents" , args : { timeout_ms : 30_000 } } ] ,
262+ } ) ;
263+ session . harness . scenario . replyOnce ( "anthropic" , {
264+ predicate : parent ,
265+ toolCalls : [ { name : "wait_agents" , args : { timeout_ms : 30_000 } } ] ,
266+ } ) ;
267+ replyOnceAtRequest ( session , "anthropic" , parent , ( { body } ) => {
268+ const lane = fleetSessions . list ( ) [ 0 ] ;
269+ const ask =
270+ lane === undefined ? undefined : fleetSessions . peekAsk ( lane . id ) ;
271+ const requestId = ask ?. deniedCall ?. requestId ;
272+ const publicRequestId = waitAgentsGrantRequestId ( body ) ;
273+ if (
274+ lane === undefined ||
275+ ask ?. questionId === undefined ||
276+ requestId === undefined ||
277+ publicRequestId === undefined
278+ ) {
279+ return undefined ;
280+ }
281+ if ( publicRequestId !== requestId ) {
282+ throw new Error (
283+ `wait_agents grant_request_id ${ publicRequestId } did not match pending ask ${ requestId } ` ,
284+ ) ;
285+ }
286+ checkpoints . push ( "asked-for-approval" ) ;
287+ approvedRequestId = publicRequestId ;
288+ const args = {
289+ target : lane . id ,
290+ question_id : ask . questionId ,
291+ request_id : publicRequestId ,
292+ } ;
293+ return { toolCalls : [ { name : "approve_worker_call" , args } ] } ;
294+ } ) ;
295+ replyOnceAtRequest ( session , "anthropic" , parent , ( { body } ) =>
296+ / \\ ? " s t a t u s \\ ? " : \s * \\ ? " a u t h o r i z e d / . test ( body )
297+ ? ( checkpoints . push ( "approval-issued" ) ,
298+ {
299+ toolCalls : [
300+ { name : "wait_agents" , args : { timeout_ms : 30_000 } } ,
301+ ] ,
302+ } )
303+ : undefined ,
304+ ) ;
305+ replyOnceAtRequest ( session , "anthropic" , parent , ( { body } ) =>
306+ body . includes ( "Write completed" )
307+ ? { text : "The approved worker write completed." }
308+ : undefined ,
309+ ) ;
310+
311+ const result = await withMockedModuleDuring (
312+ import . meta. resolve ( "../src/session/assemble-runtime.js" ) ,
313+ ( real : typeof import ( "../src/session/assemble-runtime.js" ) ) => ( {
314+ ...real ,
315+ assembleInferenceBase : async ( ) => session . harness . deps ,
316+ } ) ,
317+ ( ) => runUntilDone ( session , "Run the authorized write" ) ,
318+ ) ;
319+
320+ const waits = waitAgentsResults ( result . events ) ;
321+ const parked = waits [ 0 ] ;
322+ const settled = waits . at ( - 1 ) ;
323+ const lane = parked ?. results [ 0 ] ;
324+ expect ( checkpoints . slice ( 0 , 2 ) ) . toEqual ( [
325+ "denied-write" ,
326+ "asked-for-approval" ,
327+ ] ) ;
328+ expect ( checkpoints . slice ( 2 ) . sort ( ) ) . toEqual ( [
329+ "approval-issued" ,
330+ "authorized-retry" ,
331+ ] ) ;
332+ expect ( lane ?. status ) . toBe ( "awaiting_director" ) ;
333+ expect ( lane ?. question_id ) . toBeString ( ) ;
334+ expect ( lane ?. grant_request_id ) . toBeString ( ) ;
335+ expect ( lane ?. grant_request_id ) . toBe ( approvedRequestId ) ;
336+ expect ( settled ?. results [ 0 ] ?. status ) . toBe ( "done" ) ;
337+ expect ( await Bun . file ( join ( session . cwd , output ) ) . text ( ) ) . toBe (
338+ "approved" ,
339+ ) ;
340+ const approvals = result . events . filter (
341+ ( event ) =>
342+ event . type === "tool.done" &&
343+ typeof event . data . result . content === "string" &&
344+ / " s t a t u s " : \s * " a u t h o r i z e d " / . test ( event . data . result . content ) ,
345+ ) ;
346+ expect ( approvals ) . toHaveLength ( 1 ) ;
347+ const workerRequests = session . harness . scenario
348+ . matchedRequests ( )
349+ . filter ( ( request ) =>
350+ RequestURL . assert ( request ) . url . includes ( WORKER_HOST ) ,
351+ ) ;
352+ expect ( workerRequests ) . toHaveLength ( 4 ) ;
353+ expect ( result . reply ) . toContain ( "completed" ) ;
354+ } finally {
355+ await closeE2ESession ( session ) ;
356+ }
357+ } ,
358+ 60000 ,
359+ ) ;
360+
143361 test . serial (
144362 "a retryable first send triggers the outer retry and the worker recovers" ,
145363 async ( ) => {
0 commit comments