Skip to main content

ToolExecutor

Trait ToolExecutor 

Source
pub trait ToolExecutor: Send + Sync {
    // Required method
    fn execute<'life0, 'life1, 'life2, 'async_trait>(
        &'life0 self,
        name: &'life1 str,
        args_json: &'life2 str,
    ) -> Pin<Box<dyn Future<Output = String> + Send + 'async_trait>>
       where Self: 'async_trait,
             'life0: 'async_trait,
             'life1: 'async_trait,
             'life2: 'async_trait;

    // Provided methods
    fn specs(&self) -> Vec<ToolSpec> { ... }
    fn owns(&self, name: &str) -> bool { ... }
    fn needs_approval(&self, _name: &str) -> bool { ... }
    fn pre_dispatch(&self, name: &str, _args_json: &str) -> ToolDecision { ... }
    fn post_dispatch(
        &self,
        _name: &str,
        _args_json: &str,
        _result_json: &str,
    ) -> Option<String> { ... }
    fn cacheable_approval(&self, name: &str) -> bool { ... }
    fn sandbox_would_deny(&self, _name: &str, _args_json: &str) -> bool { ... }
    fn required_capabilities(&self, _name: &str) -> CapabilitySet { ... }
    fn ingests_untrusted_content(&self, name: &str) -> bool { ... }
    fn recover_unadvertised(
        &self,
        _name: &str,
        _args_json: &str,
    ) -> Vec<ToolSpec> { ... }
}
Expand description

Executes a tool call by name, returning a JSON result string. Also advertises the tools it can execute so the provider knows what’s callable.

Required Methods§

Source

fn execute<'life0, 'life1, 'life2, 'async_trait>( &'life0 self, name: &'life1 str, args_json: &'life2 str, ) -> Pin<Box<dyn Future<Output = String> + Send + 'async_trait>>
where Self: 'async_trait, 'life0: 'async_trait, 'life1: 'async_trait, 'life2: 'async_trait,

Run name with JSON args_json; return a JSON result.

Provided Methods§

Source

fn specs(&self) -> Vec<ToolSpec>

Specs for the tools this executor knows how to run. The default returns an empty list — the model won’t be told about any tools, so it won’t emit tool_calls. Real registries override this.

Source

fn owns(&self, name: &str) -> bool

Whether this executor advertises a tool named name.

Used by composite/registry executors to route a call to its owning source without materialising every source’s full Self::specs on the hot path. The default derives the answer from Self::specs; executors that cache or compute specs lazily should override with a cheaper check (e.g. a name lookup that avoids cloning the spec list).

Source

fn needs_approval(&self, _name: &str) -> bool

Whether name requires explicit human approval before Self::execute may run. The default is false — pure / read-only tools shouldn’t trigger an approval gate. Override for sensitive tools (writes, code execution, network reach, anything with side effects).

When this returns true, run_turn does NOT call Self::execute. Instead it surfaces the unexecuted tool calls via TurnResult::pending_approvals; the caller is responsible for persisting an approval_request event, waiting for a (cryptographically signed) approval_response, and re-driving the loop on the next turn.

Source

fn pre_dispatch(&self, name: &str, _args_json: &str) -> ToolDecision

The dispatch-time policy decision for a call, seeing BOTH the tool name AND its arguments (#67). This is the argument-aware gate the turn loop consults before every execution — richer than the name-only Self::needs_approval, so a policy can allow read foo.txt but deny read /etc/shadow.

The default DERIVES the decision from Self::needs_approval — a gated tool maps to ToolDecision::RequireApproval, everything else to ToolDecision::Allow — so an executor that only implements the name-only check keeps working unchanged and adopting the richer decision is opt-in. Executors override this to gate, rewrite, deny, or inject on arguments.

Source

fn post_dispatch( &self, _name: &str, _args_json: &str, _result_json: &str, ) -> Option<String>

Optionally rewrite a tool’s RESULT before it re-enters the model’s context (#67, #540) — the place to redact a secret from output or enrich it. Some(new) replaces the result; None (the default) leaves it unchanged. A redaction is recorded as a distinct signed event, so the substitution is transparent in the audit log, never silent.

Source

fn cacheable_approval(&self, name: &str) -> bool

Whether a single human approval for name may be remembered for the rest of a conversation session (per-caller) and reused for later calls of the tool. This is the authoritative gate for session-scoped approval (run_turn only honors a remembered approval when this returns true), so a non-idempotent tool can never have its approval cached.

Like Self::owns, the default DERIVES the answer from the tool’s ToolSpec::cacheable_approval annotation via Self::specs — the single source of truth. Composing executors that already delegate specs() therefore inherit the correct policy automatically and must NOT re-delegate this (forgetting to, in two nested wrappers, was a real bug). Only an executor whose specs() is intentionally INCOMPLETE (i.e. it hides some tools it can still execute) should override, and then it should delegate to its base, mirroring how it delegates Self::needs_approval.

Source

fn sandbox_would_deny(&self, _name: &str, _args_json: &str) -> bool

Whether running name with args_json would be DENIED by the sandbox before any side effect, so the call should ESCALATE to a human approval (an unsandboxed retry) instead of executing and returning a flat denial (graduated approval, #301).

The default is false — no executor escalates. A sandbox-aware registry overrides it to recognize the denials it can predict purely (e.g. a path-bearing destructive tool whose target escapes the workspace root). run_turn_with consults this ONLY when RunTurnOptions::escalate_sandbox_denials is set, and treats a true exactly like Self::needs_approval: the call pauses via the same whole-batch approval gate (no side effect, atomicity preserved), so the strong sandbox runs everything it can and a human is asked only for what it would otherwise block.

Source

fn required_capabilities(&self, _name: &str) -> CapabilitySet

The capabilities a call to name requires (#592) — the executor’s one gate-facing classification surface, derived from the tool’s spec annotations plus what the executor knows about the tool’s registry provenance (see polyc_capability::required_capabilities).

The default is the full privileged set (polyc_capability::CapabilitySet::all), fail closed: an executor that does not classify its tools — a plain stub, a wrapper that forgot to delegate — never lets a call through with less than everything required, so an unknown tool cannot slip past the gate under taint. Real registries override this with the derived set; composing executors delegate to the owning source (mirroring Self::owns) so the hot path avoids materialising spec catalogs.

Taint-immune classification (fixed-connector read) is earned only by operator registration — registry provenance, never a connector’s self-declared annotation hints alone.

Source

fn ingests_untrusted_content(&self, name: &str) -> bool

Whether name’s RESULT carries untrusted-provenance content — the taint SOURCE predicate: “did content of open-world, attacker-influenceable provenance enter the transcript”. NOT the dual of the required-capability surface — that asks what a call may do outbound; this asks what its result brings in.

This is the MCP openWorldHint — “the tool may interact with an open world of external entities”. A tool with open_world = true seeds the untrusted-content taint when its result is in context. The default DERIVES it from the tool’s ToolSpec::open_world annotation via Self::specs (the single source of truth, exactly like Self::cacheable_approval), so both built-in and connector tools are classified by the SAME declared property rather than a hardcoded name list. The built-in web fetchers carry open_world = true; a dialed connector carries whatever its openWorldHint declared at connect. untrusted_content_in_context consults this per tool-result already in context; a plain executor (StubTools) advertises no specs, so it ingests nothing untrusted.

Source

fn recover_unadvertised(&self, _name: &str, _args_json: &str) -> Vec<ToolSpec>

Attempts in-turn recovery for a tool call that named no advertised tool — the fuzzy-match escape hatch (#582, invariant 9). The inputs are the raw facts of the failed call, mirroring Self::execute: the called (hallucinated) name and its args_json. How they become a retrieval query is the implementor’s business — the executor owns the ranking pipeline. Returns full specs for the closest not-yet-advertised tools in the executor’s catalog, matched FUZZILY — never by exact-name lookup, because a model that needs an unoffered capability hallucinates a plausible name rather than abstaining — for run_turn_with to append to the turn’s advertised set.

The default returns nothing, so the hatch is inert for every executor that does not opt in: an unadvertised call then resolves to the ordinary unknown-tool result, byte-for-byte today’s behavior. The turn loop consults this only when RunTurnOptions::escape_hatch is set, and at most once per turn.

Dyn Compatibility§

This trait is dyn compatible.

In older versions of Rust, dyn compatibility was called "object safety".

Implementors§