diff --git a/packages/subagent/tool-subagent/src/index.ts b/packages/subagent/tool-subagent/src/index.ts index 7cf6f0ddbb..35c23c7fed 100644 --- a/packages/subagent/tool-subagent/src/index.ts +++ b/packages/subagent/tool-subagent/src/index.ts @@ -102,9 +102,14 @@ export const Config: z = z.object({ // deny-everything, silently. Force the omitted key to stay absent (the same // shape discipline as SystemPrompt's toolOrder); the cast is needed because // .default() expects the object type. + // The NESTED arrays get the same treatment as the object itself: a partial + // filter ({deny: […]}) must not materialize allow: [] beside it — an empty + // allow-list means deny-EVERYTHING, so the materialized default would turn + // a deny-one config into deny-all. An EXPLICIT allow: [] (grant-only + // children) survives, since only the omitted key defaults to undefined. toolFilter: z.object({ - allow: z.array(z.string()), - deny: z.array(z.string()), + allow: z.array(z.string()).default(undefined as unknown as string[]), + deny: z.array(z.string()).default(undefined as unknown as string[]), }).default(undefined as unknown as { allow: string[]; deny: string[] }), maxDepth: z.number(), }) diff --git a/packages/subagent/tool-subagent/tests/tool-subagent.spec.ts b/packages/subagent/tool-subagent/tests/tool-subagent.spec.ts index 9510df53ab..5fd2ace838 100644 --- a/packages/subagent/tool-subagent/tests/tool-subagent.spec.ts +++ b/packages/subagent/tool-subagent/tests/tool-subagent.spec.ts @@ -484,4 +484,30 @@ describe('dsh-tool-subagent', () => { expect(seen?.toolFilter).toMatchObject({ deny: ['subagent'] }) expect(seen?.maxDepth).toBe(2) }) + + it('a partial toolFilter (deny only) does not materialize an empty allow-list (deny-all trap)', async () => { + let seen: { toolFilter?: { allow?: string[]; deny?: string[] } } | undefined + const ctx = new Context() + await ctx.plugin(SystemPrompt) + await ctx.plugin(ToolRegistry) + await ctx.plugin(SubagentService) + ctx.subagents.registerProvider({ + name: 'capture3', + capabilities: { outputSchema: false, depthLimit: false, toolFilter: true, persona: false }, + inheritsParentContext: false, + start: (request) => { + seen = request + return { + id: AgentId('capture3-child'), + result: Promise.resolve({ output: [{ type: 'text', text: 'ok' }], stopReason: 'completed' as const }), + cancel() {}, + dispose: async () => {}, + } + }, + }) + await ctx.plugin(tool, { provider: 'capture3', toolFilter: { deny: ['subagent'] } }) + await callSubagent(ctx, { description: 'd', prompt: 'p' }) + expect(seen?.toolFilter).toEqual({ deny: ['subagent'] }) + expect(seen?.toolFilter).not.toHaveProperty('allow') + }) })