mirror of
https://github.com/deepseek-ai/deepseek-harness
synced 2026-08-15 21:04:50 +00:00
fix(tools): restrict what a scope inherits, not just the global layer
A restriction was compiled against the global tool layer alone: only global-layer tools were tested against `admits()`, and every chain-layer tool was overlaid unfiltered afterward. That read the exempt set as "the global layer" when what it means is "what this scope registers itself" — two descriptions of the same set only while every model-facing tool sat in the host composition. Moving those rows onto the agent plane separated them. A preset's tools are an ANCESTOR contribution to a joined agent, so a subagent's `toolFilter` stopped constraining anything it was given; and with the global layer empty `restrict()` rejected every name it received as unknown, failing the child outright. With the same tools in the global layer the filter still admits and applies normally, which is what makes this a regression of the move rather than a standing limitation. `view()` now filters everything a scope inherits — the global layer and every ancestor layer on its chain — and exempts only the layer the scope owns. That exemption is load-bearing rather than incidental: the delegation runtime registers a child's `report` and structured-output tools into the child's own layer, and a filter naming the capabilities the child may use must not strip the machinery it answers through. Tool order, and with it prefix-cache reuse, is unchanged: inherited names keep their global-then- ancestor position and own-layer names still come last. The diagnostic said "unknown global tool" while listing what is really the inherited surface; it now names the surface it checks and says why an own-layer name is not restrictable. Fixes #2185
This commit is contained in:
@@ -103,6 +103,21 @@ describe('a child agent composed in-process', () => {
|
||||
await run.dispose()
|
||||
})
|
||||
|
||||
it('honours a tool filter over the preset tools it inherited', async () => {
|
||||
const { ctx, parent } = await setupPresetHost()
|
||||
|
||||
const run = await startInProcessRun(
|
||||
{ ...spawnRequest(parent), toolFilter: { deny: ['preset_only'] } },
|
||||
{},
|
||||
)
|
||||
await run.result
|
||||
|
||||
// The capability filter is the only thing bounding a delegated child, and
|
||||
// every tool it can name now arrives from the preset rather than the host.
|
||||
expect(ctx.tools.schemas(run.localAgent).map(schema => schema.name)).toEqual([])
|
||||
await run.dispose()
|
||||
})
|
||||
|
||||
it('follows a parent that switched preset while blank', async () => {
|
||||
const { ctx, parent } = await setupPresetHost()
|
||||
// A DIFFERENT preset, so the assertion below distinguishes reading the
|
||||
|
||||
@@ -298,7 +298,7 @@ describe('startInProcessRun', () => {
|
||||
await expect(startInProcessRun({
|
||||
...request(parent),
|
||||
toolFilter: { deny: ['unknown-tool'] },
|
||||
}, {})).rejects.toThrow('unknown global tool')
|
||||
}, {})).rejects.toThrow('unknown inherited tool')
|
||||
expect(ctx.agents.list()).toHaveLength(beforeAgents)
|
||||
expect(ctx.sessions.list()).toHaveLength(beforeSessions)
|
||||
})
|
||||
|
||||
@@ -436,7 +436,7 @@ describe('dsh-subagent-spawn', () => {
|
||||
prompt: [{ type: 'text', text: 'do X' }],
|
||||
parent,
|
||||
toolFilter: { deny: ['no_such_tool'] },
|
||||
})).rejects.toThrow(/unknown global tool "no_such_tool"/)
|
||||
})).rejects.toThrow(/unknown inherited tool "no_such_tool"/)
|
||||
expect(ctx.agents.list().length).toBe(before)
|
||||
})
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user