@@ -14,8 +14,11 @@ import {
1414import { runSubAgent , type RunSubAgentParams } from "../../src/subagent/run.js" ;
1515import { withMockedModuleDuring } from "../helpers/mock-module.js" ;
1616import { mcpClientToAgentTools } from "../../src/mcp/plugin.js" ;
17+ import type { MCPClient } from "../../src/mcp/client.js" ;
1718import { getSubAgentIdentity } from "../../src/subagent/identity-context.js" ;
1819import { createSubAgentSessionStore } from "../../src/subagent/session-store.js" ;
20+ import { workerPermissionGate } from "../../src/permission/reactor-authorize.js" ;
21+ import { gateAgentTools } from "../../src/plugins/permission-plugin.js" ;
1922
2023const report =
2124 "## Summary\nFinished.\n## Findings\nAttempted write.\n## Blockers\nNone.\n## Paths\nprobe.txt" ;
@@ -352,6 +355,184 @@ test.serial(
352355 20000 ,
353356) ;
354357
358+ async function bindCreateAgentToolsetInherit ( args : {
359+ cwd : string ;
360+ permissionGate : PermissionGate ;
361+ client : MCPClient ;
362+ } ) : Promise < {
363+ inheritMcpTools : NonNullable < RunSubAgentParams [ "inheritMcpTools" ] > ;
364+ dispose : ( ) => Promise < void > ;
365+ } > {
366+ let inheritMcpTools : RunSubAgentParams [ "inheritMcpTools" ] ;
367+ let dispose : ( ) => Promise < void > = async ( ) => undefined ;
368+ await withMockedModuleDuring (
369+ import . meta. resolve ( "../../src/subagent/agent-fleet.js" ) ,
370+ ( real : typeof import ( "../../src/subagent/agent-fleet.js" ) ) => ( {
371+ ...real ,
372+ createSpawnAgentTool : ( deps : Parameters < typeof real . createSpawnAgentTool > [ 0 ] ) => {
373+ inheritMcpTools = deps . inheritMcpTools ;
374+ return real . createSpawnAgentTool ( deps ) ;
375+ } ,
376+ } ) ,
377+ async ( ) =>
378+ withMockedModuleDuring (
379+ import . meta. resolve ( "../../src/mcp/client.js" ) ,
380+ ( real : typeof import ( "../../src/mcp/client.js" ) ) => ( {
381+ ...real ,
382+ connectMCPServer : async ( ) => ( { ok : true as const , client : args . client } ) ,
383+ } ) ,
384+ async ( ) => {
385+ const { createAgentToolset } = await import ( "../../src/agent/tools.js" ) ;
386+ const toolset = await createAgentToolset ( {
387+ cwd : args . cwd ,
388+ permissionGate : args . permissionGate ,
389+ onOperatorGate : async ( ) => ( { kind : "cancel" } ) ,
390+ mcpServers : [ ] ,
391+ subAgent : {
392+ provider : {
393+ providerName : "openai" ,
394+ baseURL : "https://api.openai.com/v1" ,
395+ model : "test-model" ,
396+ } ,
397+ getWorkdirBase : ( ) => join ( args . cwd , "state" ) ,
398+ sessions : createSubAgentSessionStore ( ) ,
399+ } ,
400+ } ) ;
401+ dispose = ( ) => toolset . dispose ( ) ;
402+ await toolset . connectMCPServer (
403+ { name : "probe" , type : "http" , url : "https://mcp.probe.test/mcp" } ,
404+ {
405+ interactiveAuth : false ,
406+ onStatus : ( ) => undefined ,
407+ onToolsChanged : ( ) => undefined ,
408+ } ,
409+ ) ;
410+ } ,
411+ ) ,
412+ ) ;
413+ if ( inheritMcpTools === undefined ) {
414+ await dispose ( ) ;
415+ throw new Error ( "createAgentToolset did not wire inheritMcpTools" ) ;
416+ }
417+ return { inheritMcpTools, dispose } ;
418+ }
419+
420+ test . serial (
421+ "createAgentToolset inherit wraps ungated MCP tools with the passed worker gate" ,
422+ async ( ) => {
423+ let asks = 0 ;
424+ await withWorker (
425+ async ( { cwd, harness, params, audit } ) => {
426+ let calls = 0 ;
427+ const client = {
428+ serverName : "probe" ,
429+ tools : [
430+ {
431+ name : "mutate" ,
432+ description : "mutates" ,
433+ inputSchema : { type : "object" , properties : { } } ,
434+ } ,
435+ ] ,
436+ call : async ( ) => {
437+ calls ++ ;
438+ return "changed" ;
439+ } ,
440+ close : async ( ) => undefined ,
441+ } ;
442+ const bound = await bindCreateAgentToolsetInherit ( {
443+ cwd,
444+ permissionGate : params . permissionGate ,
445+ client,
446+ } ) ;
447+ try {
448+ params . permissionGate . setSeededApprovals ( [
449+ { tool : "mcp__probe__mutate" , pattern : "mcp__probe__mutate" } ,
450+ ] ) ;
451+ const authorize = params . permissionGate . authorizeCall ;
452+ params . permissionGate . authorizeCall = async ( call ) => {
453+ const result = await authorize ( call ) ;
454+ params . permissionGate . setSeededApprovals ( [ ] ) ;
455+ return result ;
456+ } ;
457+ params . inheritMcpTools = bound . inheritMcpTools ;
458+ harness . scenario . replyOnce ( "openai" , {
459+ toolCalls : [ { name : "mcp__probe__mutate" , args : { } } ] ,
460+ } ) ;
461+ harness . scenario . replyOnce ( "openai" , { text : report } ) ;
462+ await Promise . all ( [ runSubAgent ( params ) , harness . run ( { wallClockBudgetMs : 15000 } ) ] ) ;
463+ expect ( calls ) . toBe ( 1 ) ;
464+ expect ( asks ) . toBe ( 0 ) ;
465+ expect ( ( await audit ( ) ) [ 0 ] ?. authz ?. effect ) . toBe ( "allow" ) ;
466+ } finally {
467+ await bound . dispose ( ) ;
468+ }
469+ } ,
470+ ( cwd ) =>
471+ createPermissionGate ( {
472+ cwd,
473+ approvals : [ ] ,
474+ interactive : true ,
475+ auto : false ,
476+ skipPermissions : false ,
477+ reactorGated : false ,
478+ requestApproval : async ( ) => {
479+ asks ++ ;
480+ return { allow : true } ;
481+ } ,
482+ } ) ,
483+ ) ;
484+ } ,
485+ 20000 ,
486+ ) ;
487+
488+ test ( "storing parent-gated MCP tools then wrapping again still calls requestApproval" , async ( ) => {
489+ const cwd = await mkdtemp ( join ( tmpdir ( ) , "worker-permission-" ) ) ;
490+ let asks = 0 ;
491+ let calls = 0 ;
492+ try {
493+ const parent = createPermissionGate ( {
494+ cwd,
495+ approvals : [ ] ,
496+ interactive : true ,
497+ auto : false ,
498+ skipPermissions : false ,
499+ reactorGated : false ,
500+ requestApproval : async ( ) => {
501+ asks ++ ;
502+ return { allow : true } ;
503+ } ,
504+ } ) ;
505+ const client = {
506+ serverName : "probe" ,
507+ tools : [
508+ {
509+ name : "mutate" ,
510+ description : "mutates" ,
511+ inputSchema : { type : "object" , properties : { } } ,
512+ } ,
513+ ] ,
514+ call : async ( ) => {
515+ calls ++ ;
516+ return "changed" ;
517+ } ,
518+ close : async ( ) => undefined ,
519+ } ;
520+ parent . registerMcpClient ( client ) ;
521+ const parentGated = mcpClientToAgentTools ( client , parent ) ;
522+ const doubleWrapped = gateAgentTools ( parentGated , workerPermissionGate ( parent ) ) ;
523+ const tool = doubleWrapped [ 0 ] ;
524+ if ( tool ?. kind !== "full" ) throw new Error ( "expected full inherited MCP tool" ) ;
525+ await tool . handler (
526+ { id : "c1" , name : "mcp__probe__mutate" , arguments : { } } ,
527+ new AbortController ( ) . signal ,
528+ ) ;
529+ expect ( asks ) . toBe ( 1 ) ;
530+ expect ( calls ) . toBe ( 1 ) ;
531+ } finally {
532+ await rm ( cwd , { recursive : true , force : true } ) ;
533+ }
534+ } ) ;
535+
355536test . serial (
356537 "live worker authorization is not evaluated again after policy revocation before runner" ,
357538 async ( ) => {
0 commit comments