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> { ... }
fn for_worker(
&self,
_scope: &WorkerScope<'_>,
) -> Option<Result<WorkerHandoff, ShareInError>> { ... }
}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§
Sourcefn 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,
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§
Sourcefn specs(&self) -> Vec<ToolSpec>
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.
Sourcefn owns(&self, name: &str) -> bool
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).
Sourcefn needs_approval(&self, _name: &str) -> bool
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.
Sourcefn pre_dispatch(&self, name: &str, _args_json: &str) -> ToolDecision
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.
Sourcefn post_dispatch(
&self,
_name: &str,
_args_json: &str,
_result_json: &str,
) -> Option<String>
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.
Sourcefn cacheable_approval(&self, name: &str) -> bool
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.
Sourcefn sandbox_would_deny(&self, _name: &str, _args_json: &str) -> bool
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.
Sourcefn required_capabilities(&self, _name: &str) -> CapabilitySet
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.
Sourcefn ingests_untrusted_content(&self, name: &str) -> bool
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.
Sourcefn recover_unadvertised(&self, _name: &str, _args_json: &str) -> Vec<ToolSpec>
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.
Sourcefn for_worker(
&self,
_scope: &WorkerScope<'_>,
) -> Option<Result<WorkerHandoff, ShareInError>>
fn for_worker( &self, _scope: &WorkerScope<'_>, ) -> Option<Result<WorkerHandoff, ShareInError>>
Re-root this executor for a delegated worker’s own nested turn
(#2286) and seed it with the parent files _scope requests
(#2295), both keyed by that worker’s delegate call id.
A worker’s nested turn used to reuse the SAME already-composed
executor as its parent, byte-identical execution and all — so two
concurrent workers’ coding-tool calls (file_write, shell_exec, …)
raced on the same workspace paths. An executor that owns a workspace
overrides this to hand back a version of itself scoped to a fresh
subtree keyed by delegate::WorkerScope::worker_id, so concurrent
workers can never clobber each other or read what the parent (or a
sibling worker) wrote.
Because that re-root covers reads too, a worker starts blind to the
parent’s workspace. delegate::WorkerScope::share_in names the
parent files this delegation needs; the implementor copies them into
the worker’s subtree at the same relative paths, refusing anything
delegate::WorkerScope::ceiling does not admit. Seeding lives here,
on the same call as the re-root, because this is the only layer that
knows both the parent root and the worker root — and because a
separate method would be one more thing a wrapper could forget to
forward.
§Returns
None— “this executor has no workspace to re-root”, the correct answer for a proxy, an MCP source, or any other executor whose calls don’t touch a local filesystem at all.Some(Err(_))— this executor owns a workspace but the share-in request was refused. The caller fails the delegation and surfaces the reason; it must NOT fall back to the shared root.Some(Ok(_))— the re-rooted executor and the paths seeded into it.
A None from an executor that DOES own a workspace is not a safe
fallback: the caller reads it as “nothing to re-root” and runs the
worker against the shared conversation root, which is the clobbering
this method exists to prevent. Such an executor must re-root even when
preparing the subtree failed.
A wrapper that owns no workspace but composes over one — the retrieval gate, a spec-narrowing wrapper, any future decorator — must FORWARD this rather than inherit the default: the caller holds the outermost executor, so one silent inheritance anywhere in the chain disables the fencing everywhere below it.
Calling this on an already-re-rooted executor simply nests one level deeper, which stays inside the conversation root and is therefore safe; nothing does today, because delegation depth is capped at one level (a worker never delegates again).
Dyn Compatibility§
This trait is dyn compatible.
In older versions of Rust, dyn compatibility was called "object safety".