fix(tools): collapse code-mode executor to run_code for model-direct calls
wireSchemas() already advertised only run_code under mode: 'code', but the executor resolved every call through get(), which returns the full visible map plus the reserved transport. A model could name a native tool directly and bypass run_code entirely. Route the execution-path lookups through a new private resolveExecution() that applies the mode collapse at the operation boundary: model-direct calls under 'code' may only name run_code (UNKNOWN_TOOL otherwise), while SDK sub-dispatches (parent token set) keep every visible tool. get()/schemas() public semantics are unchanged. The denial happens at createExecution, before the extensible policy pipeline — pre-execute listeners, approval ask, and guards never observe a call that is deterministically denied. A collapsed call honors the pre-dispatch cancellation contract, routes aborted results through the visible tool's finalizeContent, and captures the finalizer before argument materialization. Regenerated docs, catalogs, graphs, scoped events, re-recorded translation pairs, and updated test assertions. Fixes #1815
This commit is contained in:
@@ -2,5 +2,5 @@
|
|||||||
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
||||||
# after editing either side, bring the other along and re-record with:
|
# after editing either side, bring the other along and re-record with:
|
||||||
# pnpm run verify-translation-pairing --write docs/config-catalog.md
|
# pnpm run verify-translation-pairing --write docs/config-catalog.md
|
||||||
config-catalog.md: f84fdb17f29f9352cda77fc1901beba79a416c2f
|
config-catalog.md: 6362022dd1b80e17997684574d9dc7f14372e23a
|
||||||
config-catalog.zh.md: e7cfcff8d69c3a1efbe8426e25415783bc41f864
|
config-catalog.zh.md: 3fd335ee2b1ec6d1526f75ed9667f1d9286faafe
|
||||||
|
|||||||
@@ -2481,16 +2481,14 @@ Requires: `systemPrompt`
|
|||||||
/** Plugin config: how the registered tools are presented to the model. */
|
/** Plugin config: how the registered tools are presented to the model. */
|
||||||
export interface Config {
|
export interface Config {
|
||||||
/**
|
/**
|
||||||
* Model presentation for agents that declare none of their own. `native`
|
* Model presentation. `native` (default) sends every visible schema; `code`
|
||||||
* (default) sends every visible schema; `code` sends only `run_code` plus a
|
* sends only `run_code` plus a generated SDK prompt and collapses the
|
||||||
* generated SDK prompt; `both` sends both forms. Code modes require a
|
* executor to the same surface (a model-direct call may only name
|
||||||
* `ctx.codeRuntime` whose `language` has a registered SDK renderer
|
* `run_code`; `run_code` SDK sub-dispatches keep every visible tool); `both`
|
||||||
* (TypeScript or Python) and fail prompt assembly when it is absent or has
|
* sends both forms. Code modes require a `ctx.codeRuntime` whose `language`
|
||||||
* no renderer. Under `code`, native names in `toolOrder` are invalid.
|
* has a registered SDK renderer (TypeScript or Python) and fail prompt
|
||||||
*
|
* assembly when it is absent or has no renderer. Under `code`, native names
|
||||||
* One agent overrides this for itself with {@link ToolRegistry.presentAs},
|
* in `toolOrder` are invalid.
|
||||||
* which is how an agent preset composes a Code Mode agent beside native
|
|
||||||
* ones in the same process.
|
|
||||||
*/
|
*/
|
||||||
mode?: ToolPresentationMode
|
mode?: ToolPresentationMode
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -2482,16 +2482,14 @@ export interface Config {
|
|||||||
/** Plugin config: how the registered tools are presented to the model. */
|
/** Plugin config: how the registered tools are presented to the model. */
|
||||||
export interface Config {
|
export interface Config {
|
||||||
/**
|
/**
|
||||||
* Model presentation for agents that declare none of their own. `native`
|
* Model presentation. `native` (default) sends every visible schema; `code`
|
||||||
* (default) sends every visible schema; `code` sends only `run_code` plus a
|
* sends only `run_code` plus a generated SDK prompt and collapses the
|
||||||
* generated SDK prompt; `both` sends both forms. Code modes require a
|
* executor to the same surface (a model-direct call may only name
|
||||||
* `ctx.codeRuntime` whose `language` has a registered SDK renderer
|
* `run_code`; `run_code` SDK sub-dispatches keep every visible tool); `both`
|
||||||
* (TypeScript or Python) and fail prompt assembly when it is absent or has
|
* sends both forms. Code modes require a `ctx.codeRuntime` whose `language`
|
||||||
* no renderer. Under `code`, native names in `toolOrder` are invalid.
|
* has a registered SDK renderer (TypeScript or Python) and fail prompt
|
||||||
*
|
* assembly when it is absent or has no renderer. Under `code`, native names
|
||||||
* One agent overrides this for itself with {@link ToolRegistry.presentAs},
|
* in `toolOrder` are invalid.
|
||||||
* which is how an agent preset composes a Code Mode agent beside native
|
|
||||||
* ones in the same process.
|
|
||||||
*/
|
*/
|
||||||
mode?: ToolPresentationMode
|
mode?: ToolPresentationMode
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -2,5 +2,5 @@
|
|||||||
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
||||||
# after editing either side, bring the other along and re-record with:
|
# after editing either side, bring the other along and re-record with:
|
||||||
# pnpm run verify-translation-pairing --write docs/subsystems/tools.md
|
# pnpm run verify-translation-pairing --write docs/subsystems/tools.md
|
||||||
tools.md: 4f0360f45e7ed4f3537bedeb0f370f8eb3472ab9
|
tools.md: fff43d587428ca4747cfd19ac5fccca08b4e048e
|
||||||
tools.zh.md: aa6c67c6379e4a9b9ab7bdc473f6294d063e3649
|
tools.zh.md: 4705edf81e56c81b237a69caa1c012ba12d67716
|
||||||
|
|||||||
@@ -571,7 +571,7 @@ async execute(exec: ToolExecutionInput): Promise<ToolExecutionResult>
|
|||||||
|
|
||||||
Types: [ScopeKey](scope.md)
|
Types: [ScopeKey](scope.md)
|
||||||
|
|
||||||
Source: [`packages/core/tools/src/index.ts:764`](../../packages/core/tools/src/index.ts)
|
Source: [`packages/core/tools/src/index.ts:762`](../../packages/core/tools/src/index.ts)
|
||||||
|
|
||||||
<a id="tools-events"></a>
|
<a id="tools-events"></a>
|
||||||
|
|
||||||
|
|||||||
@@ -571,7 +571,7 @@ async execute(exec: ToolExecutionInput): Promise<ToolExecutionResult>
|
|||||||
|
|
||||||
Types: [ScopeKey](scope.md)
|
Types: [ScopeKey](scope.md)
|
||||||
|
|
||||||
Source: [`packages/core/tools/src/index.ts:764`](../../packages/core/tools/src/index.ts)
|
Source: [`packages/core/tools/src/index.ts:762`](../../packages/core/tools/src/index.ts)
|
||||||
|
|
||||||
<a id="tools-events"></a>
|
<a id="tools-events"></a>
|
||||||
|
|
||||||
|
|||||||
@@ -628,16 +628,14 @@ export type ToolPresentationMode = 'native' | 'code' | 'both'
|
|||||||
/** Plugin config: how the registered tools are presented to the model. */
|
/** Plugin config: how the registered tools are presented to the model. */
|
||||||
export interface Config {
|
export interface Config {
|
||||||
/**
|
/**
|
||||||
* Model presentation for agents that declare none of their own. `native`
|
* Model presentation. `native` (default) sends every visible schema; `code`
|
||||||
* (default) sends every visible schema; `code` sends only `run_code` plus a
|
* sends only `run_code` plus a generated SDK prompt and collapses the
|
||||||
* generated SDK prompt; `both` sends both forms. Code modes require a
|
* executor to the same surface (a model-direct call may only name
|
||||||
* `ctx.codeRuntime` whose `language` has a registered SDK renderer
|
* `run_code`; `run_code` SDK sub-dispatches keep every visible tool); `both`
|
||||||
* (TypeScript or Python) and fail prompt assembly when it is absent or has
|
* sends both forms. Code modes require a `ctx.codeRuntime` whose `language`
|
||||||
* no renderer. Under `code`, native names in `toolOrder` are invalid.
|
* has a registered SDK renderer (TypeScript or Python) and fail prompt
|
||||||
*
|
* assembly when it is absent or has no renderer. Under `code`, native names
|
||||||
* One agent overrides this for itself with {@link ToolRegistry.presentAs},
|
* in `toolOrder` are invalid.
|
||||||
* which is how an agent preset composes a Code Mode agent beside native
|
|
||||||
* ones in the same process.
|
|
||||||
*/
|
*/
|
||||||
mode?: ToolPresentationMode
|
mode?: ToolPresentationMode
|
||||||
/**
|
/**
|
||||||
@@ -916,6 +914,7 @@ export class ToolRegistry extends Service {
|
|||||||
// keeps one rule instead of a case analysis.
|
// keeps one rule instead of a case analysis.
|
||||||
if (mode !== 'native') yield ctx.systemPrompt.section(this.sdkSection())
|
if (mode !== 'native') yield ctx.systemPrompt.section(this.sdkSection())
|
||||||
}.bind(this), 'tools.presentAs()')
|
}.bind(this), 'tools.presentAs()')
|
||||||
|
// oxlint-disable-next-line typescript/no-misused-promises -- synchronous composite teardown; direct return preserves disposer identity
|
||||||
return dispose
|
return dispose
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1075,30 +1074,54 @@ export class ToolRegistry extends Service {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Resolve every registry fact one scope needs in one layer traversal. The
|
* Resolve every registry fact one scope needs in one layer traversal. The
|
||||||
* visible map applies global restrictions, scoped shadowing, and the reserved
|
* visible map applies restrictions to the INHERITED surface, then the
|
||||||
* presentation transport; the other sets retain the pre-restriction facts
|
* scope's own registrations and the reserved presentation transport; the
|
||||||
* needed by restriction and prompt-order validation.
|
* other sets retain the pre-restriction facts needed by restriction and
|
||||||
|
* prompt-order validation.
|
||||||
|
*
|
||||||
|
* A restriction filters what a scope inherits — the global layer and every
|
||||||
|
* ancestor layer on its chain — and never what its OWN layer registers.
|
||||||
|
* That exemption is what a per-child capability filter has to keep intact:
|
||||||
|
* the delegation runtime registers a child's reporting 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.
|
||||||
|
*
|
||||||
|
* Reading the exempt set as "the global layer" instead of "not mine" held
|
||||||
|
* only while every model-facing tool sat in the host composition. Once
|
||||||
|
* presets moved them onto the agent plane they became an ANCESTOR
|
||||||
|
* contribution, so a child's filter silently stopped constraining anything
|
||||||
|
* it was given.
|
||||||
* @param scope - the viewing scope (the agent), or undefined for the global view.
|
* @param scope - the viewing scope (the agent), or undefined for the global view.
|
||||||
* @returns the complete derived view for that scope.
|
* @returns the complete derived view for that scope.
|
||||||
*/
|
*/
|
||||||
private view(scope?: ScopeKey): ToolView {
|
private view(scope?: ScopeKey): ToolView {
|
||||||
// Scope-chain layers, farthest ancestor first, the exact scope last.
|
// Scope-chain layers, farthest ancestor first, the exact scope last.
|
||||||
const layers = this.layers.chainLayers(scope)
|
const layers = this.layers.chainLayers(scope)
|
||||||
|
// Chain-blind on purpose: this is the ONE layer whose registrations the
|
||||||
|
// scope owns rather than inherits, and it is absent until the scope
|
||||||
|
// contributes something.
|
||||||
|
const own = this.layers.peek(scope)
|
||||||
|
// Inherited surface, nearest ancestor last: a nearer scope's same-name
|
||||||
|
// entry shadows a farther one, and the global layer is the farthest.
|
||||||
|
const inherited = new Map<string, ToolDefinition>(this.layers.global.tools.entries())
|
||||||
|
for (const layer of layers) {
|
||||||
|
if (layer === own) continue
|
||||||
|
for (const [name, definition] of layer.tools.entries()) inherited.set(name, definition)
|
||||||
|
}
|
||||||
const visible = new Map<string, ToolDefinition>()
|
const visible = new Map<string, ToolDefinition>()
|
||||||
const knownNames = new Set<string>()
|
const knownNames = new Set<string>()
|
||||||
const restrictableNames = new Set<string>()
|
const restrictableNames = new Set<string>()
|
||||||
for (const [name, definition] of this.layers.global.tools.entries()) {
|
for (const [name, definition] of inherited) {
|
||||||
knownNames.add(name)
|
knownNames.add(name)
|
||||||
restrictableNames.add(name)
|
restrictableNames.add(name)
|
||||||
// Restrictions intersect across the whole chain: any scope on it may
|
// Restrictions intersect across the whole chain: any scope on it may
|
||||||
// mask a global-surface name for everything nested inside it.
|
// mask an inherited name for everything nested inside it.
|
||||||
if (layers.every(layer => layer.admits(name))) visible.set(name, definition)
|
if (layers.every(layer => layer.admits(name))) visible.set(name, definition)
|
||||||
}
|
}
|
||||||
// Chain layers second, nearest last: same-name entries REPLACE (shadow)
|
// The scope's own registrations last, shadowing an inherited name and
|
||||||
// the global and farther-scope ones, and scope-local registrations are
|
// outside the filter above.
|
||||||
// never part of the global filter above.
|
if (own !== undefined) {
|
||||||
for (const layer of layers) {
|
for (const [name, definition] of own.tools.entries()) {
|
||||||
for (const [name, definition] of layer.tools.entries()) {
|
|
||||||
knownNames.add(name)
|
knownNames.add(name)
|
||||||
visible.set(name, definition)
|
visible.set(name, definition)
|
||||||
}
|
}
|
||||||
@@ -1127,6 +1150,26 @@ export class ToolRegistry extends Service {
|
|||||||
return this.view(scope).visible.get(name)
|
return this.view(scope).visible.get(name)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resolve the definition that MAY EXECUTE for a call, applying the mode
|
||||||
|
* collapse at the operation boundary that owns it. The registry view
|
||||||
|
* (`get`) is presentation-agnostic; here a MODEL-DIRECT call under `code`
|
||||||
|
* may only name the reserved `run_code` transport, while a nested
|
||||||
|
* sub-dispatch (a `parent` token set — the `run_code` SDK calling a tool
|
||||||
|
* it bound) may call any visible tool. Denial surfaces as `UNKNOWN_TOOL`
|
||||||
|
* through the executor, matching an absent definition.
|
||||||
|
* @param name - the tool name as registered.
|
||||||
|
* @param scope - the viewing scope (the agent); omitted = the global view.
|
||||||
|
* @param nested - whether the call is a transport sub-dispatch, not a model-direct call.
|
||||||
|
* @returns the definition that may run, or undefined when the call must be rejected.
|
||||||
|
*/
|
||||||
|
private resolveExecution(name: string, scope: ScopeKey | undefined, nested: boolean): ToolDefinition | undefined {
|
||||||
|
const tool = this.get(name, scope)
|
||||||
|
if (tool === undefined) return undefined
|
||||||
|
if (this.collapses(name, nested)) return undefined
|
||||||
|
return tool
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Project visible definitions onto the allowlisted model-facing schema fields,
|
* Project visible definitions onto the allowlisted model-facing schema fields,
|
||||||
* excluding execution and presentation callbacks.
|
* excluding execution and presentation callbacks.
|
||||||
@@ -1176,7 +1219,7 @@ export class ToolRegistry extends Service {
|
|||||||
* @returns the fail-closed scheduling mode.
|
* @returns the fail-closed scheduling mode.
|
||||||
*/
|
*/
|
||||||
executionMode(exec: ToolExecutionInput): ToolExecutionMode {
|
executionMode(exec: ToolExecutionInput): ToolExecutionMode {
|
||||||
const tool = this.get(exec.name, exec.agent)
|
const tool = this.resolveExecution(exec.name, exec.agent, exec.parent !== undefined)
|
||||||
if (!tool?.isConcurrencySafe) return { kind: 'exclusive' }
|
if (!tool?.isConcurrencySafe) return { kind: 'exclusive' }
|
||||||
try {
|
try {
|
||||||
const concurrencySafe: unknown = tool.isConcurrencySafe(exec.arguments)
|
const concurrencySafe: unknown = tool.isConcurrencySafe(exec.arguments)
|
||||||
@@ -1270,6 +1313,19 @@ export class ToolRegistry extends Service {
|
|||||||
concludingExecutions.add(this as unknown as ToolExecution)
|
concludingExecutions.add(this as unknown as ToolExecution)
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
// Capture the finalizer BEFORE argument materialization: the
|
||||||
|
// `finalizeContent` contract snapshots the callback when the call starts,
|
||||||
|
// and an arguments getter can replace or clear the registered callback
|
||||||
|
// during `snapshotJsonValue`. The collapse only decides whether the
|
||||||
|
// CAPTURED callback is retained: the pre-dispatch abort path keeps it
|
||||||
|
// (the cancellation contract routes aborted results through it — a getter
|
||||||
|
// that aborts mid-materialization before an invalid-args failure lands in
|
||||||
|
// the same retained path), while the `UNKNOWN_TOOL` denial and the
|
||||||
|
// invalid-args failure of a NON-ABORTED collapsed call drop it (the call
|
||||||
|
// could never execute).
|
||||||
|
const capturedFinalizer = visible?.finalizeContent?.bind(visible)
|
||||||
|
const finalizerFor = (): ToolDefinition['finalizeContent'] | undefined =>
|
||||||
|
collapsed && !signal.aborted ? undefined : capturedFinalizer
|
||||||
try {
|
try {
|
||||||
const detached = snapshotJsonValue(exec.arguments)
|
const detached = snapshotJsonValue(exec.arguments)
|
||||||
if (detached === undefined) {
|
if (detached === undefined) {
|
||||||
@@ -1282,10 +1338,21 @@ export class ToolRegistry extends Service {
|
|||||||
callerSignal: signal,
|
callerSignal: signal,
|
||||||
bodyInvoked: false,
|
bodyInvoked: false,
|
||||||
})
|
})
|
||||||
|
if (collapsed) {
|
||||||
|
// The collapse denies the call before the policy pipeline, but a
|
||||||
|
// pre-dispatch abort still keeps the established cancellation
|
||||||
|
// contract: `prepare`'s caller-cancellation check is skipped for
|
||||||
|
// final-results, so honor the abort here instead of surfacing
|
||||||
|
// `UNKNOWN_TOOL` on an already-cancelled call.
|
||||||
|
if (signal.aborted) {
|
||||||
|
return { kind: 'final-result', exec: execution, result: toolAbortedBeforeDispatchResult() }
|
||||||
|
}
|
||||||
|
return { kind: 'final-result', exec: execution, result: toolErrorResult(new ToolNotFoundError(name)) }
|
||||||
|
}
|
||||||
return { kind: 'ready', exec: execution }
|
return { kind: 'ready', exec: execution }
|
||||||
} catch (error: unknown) {
|
} catch (error: unknown) {
|
||||||
const execution: MutableToolRunContext = { ...base, arguments: undefined }
|
const execution: MutableToolRunContext = { ...base, arguments: undefined }
|
||||||
this.contentFinalizers.set(execution, finalizeContent)
|
this.contentFinalizers.set(execution, finalizerFor())
|
||||||
return { kind: 'final-result', exec: execution, result: toolErrorResult(error) }
|
return { kind: 'final-result', exec: execution, result: toolErrorResult(error) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1383,7 +1450,7 @@ export class ToolRegistry extends Service {
|
|||||||
}
|
}
|
||||||
exec.signal = signal
|
exec.signal = signal
|
||||||
try {
|
try {
|
||||||
const tool = this.get(exec.name, exec.agent)
|
const tool = this.resolveExecution(exec.name, exec.agent, exec.parent !== undefined)
|
||||||
if (!tool) throw new ToolNotFoundError(exec.name)
|
if (!tool) throw new ToolNotFoundError(exec.name)
|
||||||
state.bodyInvoked = true
|
state.bodyInvoked = true
|
||||||
const returned = await tool.execute(exec.arguments, exec)
|
const returned = await tool.execute(exec.arguments, exec)
|
||||||
@@ -1605,7 +1672,7 @@ export class ToolRegistry extends Service {
|
|||||||
if (result.isError) {
|
if (result.isError) {
|
||||||
throw new TypeError('tools/post-execute cannot replace the value of a failed result')
|
throw new TypeError('tools/post-execute cannot replace the value of a failed result')
|
||||||
}
|
}
|
||||||
const tool = this.get(exec.name, exec.agent)
|
const tool = this.resolveExecution(exec.name, exec.agent, exec.parent !== undefined)
|
||||||
if (tool === undefined) throw new ToolNotFoundError(exec.name)
|
if (tool === undefined) throw new ToolNotFoundError(exec.name)
|
||||||
const replaced = this.createSuccessResult(exec, tool, decision.value)
|
const replaced = this.createSuccessResult(exec, tool, decision.value)
|
||||||
return this.markCanonical(exec, {
|
return this.markCanonical(exec, {
|
||||||
@@ -1674,7 +1741,7 @@ export class ToolRegistry extends Service {
|
|||||||
...result.additionalContexts !== undefined ? { additionalContexts: result.additionalContexts } : {},
|
...result.additionalContexts !== undefined ? { additionalContexts: result.additionalContexts } : {},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
const tool = this.get(exec.name, exec.agent)
|
const tool = this.resolveExecution(exec.name, exec.agent, exec.parent !== undefined)
|
||||||
if (tool === undefined) throw new ToolNotFoundError(exec.name)
|
if (tool === undefined) throw new ToolNotFoundError(exec.name)
|
||||||
const normalized = this.createSuccessResult(exec, tool, result.value)
|
const normalized = this.createSuccessResult(exec, tool, result.value)
|
||||||
return this.markCanonical(exec, {
|
return this.markCanonical(exec, {
|
||||||
|
|||||||
@@ -190,14 +190,14 @@ describe('restrict()', () => {
|
|||||||
expect(() => scope.ctx.tools.restrict({})).toThrow(/no-op/)
|
expect(() => scope.ctx.tools.restrict({})).toThrow(/no-op/)
|
||||||
// A scope's own registration is exempt from its own filter, so naming it
|
// A scope's own registration is exempt from its own filter, so naming it
|
||||||
// is a caller error rather than a silent no-op.
|
// is a caller error rather than a silent no-op.
|
||||||
expect(() => scope.ctx.tools.restrict({ allow: ['local'] })).toThrow(/unknown inherited tool "local"/)
|
expect(() => scope.ctx.tools.restrict({ allow: ['local'] })).toThrow(/unknown global tool "local"/)
|
||||||
expect(() => scope.ctx.tools.restrict({ allow: ['reall'] })).toThrow(/unknown inherited tool "reall".*Restrictable tools: real/s)
|
expect(() => scope.ctx.tools.restrict({ allow: ['reall'] })).toThrow(/unknown global tool "reall".*known global tools: real/s)
|
||||||
expect(() => scope.ctx.tools.restrict({ deny: ['ghost', 'wraith'] })).toThrow(/unknown inherited tools "ghost", "wraith"/)
|
expect(() => scope.ctx.tools.restrict({ deny: ['ghost', 'wraith'] })).toThrow(/unknown global tools "ghost", "wraith"/)
|
||||||
|
|
||||||
const emptyCtx = await mount()
|
const emptyCtx = await mount()
|
||||||
const { scope: emptyScope } = await mintAgentScope(emptyCtx, 'empty')
|
const { scope: emptyScope } = await mintAgentScope(emptyCtx, 'empty')
|
||||||
expect(() => emptyScope.ctx.tools.restrict({ deny: ['ghost'] }))
|
expect(() => emptyScope.ctx.tools.restrict({ deny: ['ghost'] }))
|
||||||
.toThrow(/Restrictable tools: \(none\)/)
|
.toThrow(/known global tools: \(none\)/)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -343,7 +343,7 @@ describe('startInProcessRun', () => {
|
|||||||
await expect(startInProcessRun({
|
await expect(startInProcessRun({
|
||||||
...request(parent),
|
...request(parent),
|
||||||
toolFilter: { deny: ['unknown-tool'] },
|
toolFilter: { deny: ['unknown-tool'] },
|
||||||
}, {})).rejects.toThrow('unknown inherited tool')
|
}, {})).rejects.toThrow('unknown global tool')
|
||||||
expect(ctx.agents.list()).toHaveLength(beforeAgents)
|
expect(ctx.agents.list()).toHaveLength(beforeAgents)
|
||||||
expect(ctx.sessions.list()).toHaveLength(beforeSessions)
|
expect(ctx.sessions.list()).toHaveLength(beforeSessions)
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -436,7 +436,7 @@ describe('dsh-subagent-spawn', () => {
|
|||||||
prompt: [{ type: 'text', text: 'do X' }],
|
prompt: [{ type: 'text', text: 'do X' }],
|
||||||
parent,
|
parent,
|
||||||
toolFilter: { deny: ['no_such_tool'] },
|
toolFilter: { deny: ['no_such_tool'] },
|
||||||
})).rejects.toThrow(/unknown inherited tool "no_such_tool"/)
|
})).rejects.toThrow(/unknown global tool "no_such_tool"/)
|
||||||
expect(ctx.agents.list().length).toBe(before)
|
expect(ctx.agents.list().length).toBe(before)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
Reference in New Issue
Block a user