Răsfoiți Sursa

fix(tool-subagent): a partial toolFilter must not materialize an empty allow-list

ds-review-bot finding: forcing only the OUTER toolFilter key absent left
the nested arrays materializing — a deny-only config gained allow: [],
which means deny-EVERYTHING. The nested arrays now default to undefined
too; an explicit allow: [] (grant-only children) still survives. Pinned by
a capture-provider regression test.
Tianyi Cui 3 luni în urmă
părinte
comite
9ff8720da5

+ 7 - 2
packages/subagent/tool-subagent/src/index.ts

@@ -102,9 +102,14 @@ export const Config: z<Config> = 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(),
 })

+ 26 - 0
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')
+  })
 })