@@ -3,6 +3,7 @@ import { describe, expect, test } from "bun:test";
33import {
44 createFleetMailbox ,
55 createSpawnAgentTool ,
6+ tierGateRequiresTools ,
67 type AgentFleetDeps ,
78} from "./agent-fleet.js" ;
89import { unlimitedAdmissionQueue } from "./admission.js" ;
@@ -31,6 +32,11 @@ const READ_ONLY_PROFILE: AgentProfile = {
3132 capabilities : { mode : "allow" , tools : [ "read_file" ] } ,
3233} ;
3334
35+ const FULL_MOUNT_PROFILE : AgentProfile = {
36+ id : "full-worker" ,
37+ systemPromptRole : "You do anything." ,
38+ } ;
39+
3440function makeDeps (
3541 run : ( params : RunSubAgentParams ) => Promise < RunSubAgentResult > ,
3642 capturedTelemetry : TelemetryEvent [ ] ,
@@ -45,7 +51,7 @@ function makeDeps(
4551 sessions,
4652 fleetRecords : createFleetMailbox ( sessions ) ,
4753 admission : unlimitedAdmissionQueue ( ) ,
48- profiles : [ READ_ONLY_PROFILE ] ,
54+ profiles : [ READ_ONLY_PROFILE , FULL_MOUNT_PROFILE ] ,
4955 telemetry : {
5056 ...NOOP_TELEMETRY ,
5157 capture : ( event : TelemetryEvent ) => {
@@ -192,4 +198,128 @@ describe("spawn_agent requires_tools preflight", () => {
192198 expect ( session ?. requiresTools ) . toBeUndefined ( ) ;
193199 expect ( session ?. snapshotRevision ) . toBeUndefined ( ) ;
194200 } ) ;
201+
202+ test ( "manage_tasks survives dispatch under a narrow allowlist and stamps canonically" , async ( ) => {
203+ const telemetry : TelemetryEvent [ ] = [ ] ;
204+ let seenRequires : readonly string [ ] | undefined ;
205+ const deps = makeDeps ( async ( params ) => {
206+ seenRequires = params . requiresTools ;
207+ return { report : "done" } ;
208+ } , telemetry ) ;
209+ const spawn = createSpawnAgentTool ( deps ) ;
210+
211+ const result = await callSpawn ( spawn , {
212+ description : "plan job" ,
213+ prompt : "track the work" ,
214+ agent : "read-only-worker" ,
215+ requires_tools : [ "manage_tasks" ] ,
216+ } ) ;
217+
218+ expect ( result . isError ) . not . toBe ( true ) ;
219+ const body = JSON . parse ( result . content ) as { agent_id : string } ;
220+ expect ( deps . sessions . get ( body . agent_id ) ?. requiresTools ) . toEqual ( [
221+ "manage_tasks" ,
222+ ] ) ;
223+ expect ( seenRequires ) . toEqual ( [ "manage_tasks" ] ) ;
224+ } ) ;
225+
226+ test ( "update_plan alias survives dispatch and stamps manage_tasks" , async ( ) => {
227+ const telemetry : TelemetryEvent [ ] = [ ] ;
228+ const deps = makeDeps ( async ( ) => ( { report : "done" } ) , telemetry ) ;
229+ const spawn = createSpawnAgentTool ( deps ) ;
230+
231+ const result = await callSpawn ( spawn , {
232+ description : "alias plan job" ,
233+ prompt : "track the work" ,
234+ agent : "read-only-worker" ,
235+ requires_tools : [ "update_plan" ] ,
236+ } ) ;
237+
238+ expect ( result . isError ) . not . toBe ( true ) ;
239+ const body = JSON . parse ( result . content ) as { agent_id : string } ;
240+ expect ( deps . sessions . get ( body . agent_id ) ?. requiresTools ) . toEqual ( [
241+ "manage_tasks" ,
242+ ] ) ;
243+ } ) ;
244+
245+ test ( "leaf requires_tools=[spawn_agent] rejects pre-spawn as missing_tool naming the leaf restriction" , async ( ) => {
246+ const telemetry : TelemetryEvent [ ] = [ ] ;
247+ let runCalled = false ;
248+ const deps = makeDeps ( async ( ) => {
249+ runCalled = true ;
250+ return { report : "done" } ;
251+ } , telemetry ) ;
252+ const spawn = createSpawnAgentTool ( deps ) ;
253+
254+ const result = await callSpawn ( spawn , {
255+ description : "fleet job" ,
256+ prompt : "spawn more workers" ,
257+ agent : "full-worker" ,
258+ requires_tools : [ "spawn_agent" ] ,
259+ } ) ;
260+
261+ expect ( result . isError ) . toBe ( true ) ;
262+ expect ( result . content . startsWith ( "Error:" ) ) . toBe ( true ) ;
263+ expect ( result . content ) . toContain ( "spawn_agent" ) ;
264+ expect ( result . content ) . toContain ( "Tier 3 leaf" ) ;
265+ expect ( result . content ) . toMatch ( / R e - d i s p a t c h | d r o p t h e r e q u i r e m e n t / ) ;
266+ expect ( result . content ) . not . toContain ( "stale_snapshot" ) ;
267+ expect ( runCalled ) . toBe ( false ) ;
268+ expect ( deps . sessions . list ( ) ) . toEqual ( [ ] ) ;
269+ expect ( telemetry ) . toEqual ( [ ] ) ;
270+ } ) ;
271+ } ) ;
272+
273+ describe ( "tierGateRequiresTools" , ( ) => {
274+ test ( "leaf requiring a fleet verb rejects as missing_tool naming the leaf restriction" , ( ) => {
275+ const gated = tierGateRequiresTools ( [ "spawn_agent" ] , "leaf" ) ;
276+ expect ( gated ?. code ) . toBe ( "missing_tool" ) ;
277+ expect ( gated ?. tool ) . toBe ( "spawn_agent" ) ;
278+ expect ( gated ?. detail ) . toContain ( "Tier 3 leaf" ) ;
279+ } ) ;
280+
281+ test ( "orchestrator requiring a fleet verb passes" , ( ) => {
282+ expect (
283+ tierGateRequiresTools ( [ "spawn_agent" ] , "orchestrator" ) ,
284+ ) . toBeUndefined ( ) ;
285+ expect (
286+ tierGateRequiresTools ( [ "spawn_agent" ] , "nested-orchestrator" ) ,
287+ ) . toBeUndefined ( ) ;
288+ } ) ;
289+
290+ test ( "nested-orchestrator requiring fleet discovery rejects pre-spawn" , ( ) => {
291+ const gated = tierGateRequiresTools (
292+ [ "search_agents" ] ,
293+ "nested-orchestrator" ,
294+ ) ;
295+ expect ( gated ?. code ) . toBe ( "missing_tool" ) ;
296+ expect ( gated ?. detail ) . toContain ( "Tier 2" ) ;
297+ } ) ;
298+
299+ test ( "non-leaf requiring the leaf reporting channel rejects pre-spawn" , ( ) => {
300+ for ( const engine of [ "submit_result" , "ask_director" ] ) {
301+ const gated = tierGateRequiresTools ( [ engine ] , "orchestrator" ) ;
302+ expect ( gated ?. code ) . toBe ( "missing_tool" ) ;
303+ expect ( gated ?. tool ) . toBe ( engine ) ;
304+ expect ( gated ?. detail ) . toContain ( "Tier 3 leaf workers only" ) ;
305+ }
306+ } ) ;
307+
308+ test ( "leaf requiring the leaf reporting channel passes" , ( ) => {
309+ expect (
310+ tierGateRequiresTools ( [ "submit_result" , "ask_director" ] , "leaf" ) ,
311+ ) . toBeUndefined ( ) ;
312+ } ) ;
313+
314+ test ( "ordinary tools pass on every tier" , ( ) => {
315+ for ( const tier of [
316+ "leaf" ,
317+ "orchestrator" ,
318+ "nested-orchestrator" ,
319+ ] as const ) {
320+ expect (
321+ tierGateRequiresTools ( [ "read_file" , "manage_tasks" ] , tier ) ,
322+ ) . toBeUndefined ( ) ;
323+ }
324+ } ) ;
195325} ) ;
0 commit comments