fix(tools): add collapses() method and fix createExecution collapse logic

This commit is contained in:
Chinesezjc
2026-08-11 22:40:36 +08:00
parent 24dd48b133
commit 1e78513806
+23 -3
View File
@@ -1250,6 +1250,19 @@ export class ToolRegistry extends Service {
}
}
/**
* Whether the `code` mode collapse denies a model-direct call: only the
* reserved `run_code` transport may be named. Nested sub-dispatches (a
* `parent` token set) bypass the collapse. One home for the
* security-relevant predicate, shared by {@link resolveExecution} and
* {@link createExecution} so the two can never drift apart.
* @param name - the tool name as registered.
* @param nested - whether the call is a transport sub-dispatch, not a model-direct call.
*/
private collapses(name: string, nested: boolean): boolean {
return !nested && this.defaultMode === 'code' && name !== RUN_CODE_NAME
}
/**
* Execute through pre-policy, guards, around-dispatch, post-policy,
* definition-owned content finalization, and final notification. Tool and
@@ -1295,8 +1308,15 @@ export class ToolRegistry extends Service {
const agent = exec.agent
const parent = exec.parent
const signal = exec.signal
const definition = this.get(name, agent)
const finalizeContent = definition?.finalizeContent?.bind(definition)
// Distinguish a mode-collapsed call (visible in the scope, denied only by
// the `code` collapse) from a genuinely unknown tool. A collapsed call is
// deterministically denied, so it terminates BEFORE the extensible policy
// pipeline: pre-execute listeners, approval `ask`, and guards must never
// observe — or worse, approve — a call that can only fail. An unknown tool
// keeps the historical dispatch-stage `UNKNOWN_TOOL` path so policy
// listeners still see every name that reaches the registry.
const visible = this.get(name, agent)
const collapsed = visible !== undefined && this.collapses(name, parent !== undefined)
const concludingExecutions = this.concludingExecutions
const base = {
token,
@@ -1333,7 +1353,7 @@ export class ToolRegistry extends Service {
}
const execution: MutableToolRunContext = { ...base, arguments: deepFreeze(detached) }
this.deferredContexts.set(execution, deferredContexts)
this.contentFinalizers.set(execution, finalizeContent)
this.contentFinalizers.set(execution, finalizerFor())
this.cancellationStates.set(execution, {
callerSignal: signal,
bodyInvoked: false,