fix(extensions): wire invokeTool to the extension path as same-tool delegation
Addresses PR review. The initial version put invokeTool on AgentToolContext via ToolContextStore, but the extension execute path (RegisteredToolAdapter) builds its own ExtensionContext and never saw it, so the documented registerTool wrapper use case did not work. It also allowed arbitrary cross-tool targets (bypassing the target's approval policy), used a session-global recursion counter that tripped on concurrent independent delegations, and missed discoverable built-ins that xdev partitioning moves out of the tool array. Rework: - Move invokeTool onto ExtensionContext, and bind it in RegisteredToolAdapter to the tool's own name, so a re-registered built-in actually receives it. - Make delegation same-tool only: invokeTool takes just (params, options) and runs the native built-in of the caller's own name. It cannot reach an arbitrary target, so it cannot escalate past the approval already granted for the call, and the native call is not re-gated. - Track recursion depth per call chain (threaded through invokeNativeTool and createContext) instead of session-global state, so concurrent delegations do not interfere. - Seed the native resolver from the xdev registry when present (it retains discoverable built-ins like browser), else the built-in registry. Replaces the ToolContextStore-level unit test with an end-to-end test that registers a built-in wrapper through the extension/session path and asserts the native tool runs the wrapper's delegated input.
This commit is contained in:
+9
-8
@@ -315,21 +315,22 @@ execute(
|
||||
### Delegating to a native built-in (`ctx.invokeTool`)
|
||||
|
||||
A tool that re-registers a built-in name (e.g. wrapping `write` to add logging or a policy check) can
|
||||
run the original instead of reimplementing it. The `ctx` passed to `execute` carries:
|
||||
run the original instead of reimplementing it. When your registered tool shadows a built-in, the `ctx`
|
||||
passed to `execute` carries:
|
||||
|
||||
```ts
|
||||
ctx.invokeTool?<TDetails>(
|
||||
name: string,
|
||||
params: Record<string, unknown>,
|
||||
options?: { signal?: AbortSignal; onUpdate?: AgentToolUpdateCallback },
|
||||
): Promise<AgentToolResult<TDetails> | undefined>
|
||||
): Promise<AgentToolResult<TDetails>>
|
||||
```
|
||||
|
||||
It runs the **native** built-in of `name` (bypassing your own re-registration, so it does not recurse
|
||||
into your wrapper) and returns its result, including the native tool's own side effects and internal
|
||||
bookkeeping. It resolves to `undefined` when there is no native tool of that name. The invoked tool's
|
||||
approval gate is not re-run — your call already passed approval — and delegation depth is guarded
|
||||
against accidental self-recursion.
|
||||
It runs the **native** built-in of the same name as your tool (delegation is same-tool only, so it
|
||||
cannot reach an arbitrary target or escalate past the approval already granted for this call) and
|
||||
returns its result, including the native tool's own side effects and internal bookkeeping. It is
|
||||
present only when a native built-in of that name exists — `ctx.invokeTool` is `undefined` for a
|
||||
net-new tool that shadows no built-in. The native call is not re-gated, since it is the same tool you
|
||||
are already approved as, and delegation depth is guarded against accidental self-recursion.
|
||||
|
||||
Template:
|
||||
|
||||
|
||||
Reference in New Issue
Block a user