Skip to content

apomesh-tool-runner

apomesh-tool-runner is the isolation boundary made executable: a one-shot binary the per-OS ToolSandbox adapters spawn to run exactly one tool invocation inside the OS sandbox. It is the third link in the worker → sandbox → runner execution chain — the point at which tool code finally runs, in its own short-lived process that shares nothing with the worker. Touch it when the subprocess wire contract, the runner’s tool registry, or its resource-limit / env hygiene changes.

  • Layer: worker / tool execution. See Workers for the sandbox isolation chain the runner sits at the end of.
  • Depends on: apomesh-substrate — the one workspace dependency. Through that facade the runner reaches the ToolInput / ToolOutput / ToolRegistry types, the substrate-shipped tools it registers, and the runner_exit exit-code contract (defined in apomesh-substrate-types). The rest are third-party: clap, libc, tokio (current-thread only), tokio-util, serde.
  • Consumed by: no crate links it — it is a binary-only crate. It is located and spawned at runtime, not compiled in, by apomesh-substrate-sandbox’s locate_tool_runner. The two binaries ship side by side (see Usage).
  • The subprocess wire contract — a 4-byte big-endian length prefix followed by a JSON ToolInput on stdin, and the same framing carrying a ToolOutput on stdout when dispatch succeeds. This framing is locked across both sandbox adapters; apomesh-substrate-sandbox speaks the parent half.
  • The transient credential channel — the inbound ToolInput carries a ToolContext whose secrets map holds the per-tenant ForwardedCredential values the orchestrator resolved and the worker forwarded. Each registered tool reads its credential from there at dispatch — never from process env. See The credential plane.
  • The one-shot lifecycle — one process per tool dispatch. It builds a single-threaded Tokio runtime to host the async Tool::dispatch, runs it once, writes the frame, and exits. The sandbox re-spawns the runner for every dispatch.
  • The runner’s tool registrybuild_registry registers the substrate-shipped, credential-less production tools (the native web_search providers, the wrapped client search, and web_fetch); each resolves its credential per dispatch from ToolContext.secrets. A tool the registry lacks surfaces to the worker as tool not found, not a crash. --features test-tools additionally registers reflective in-process tools the sandbox integration tests use.
  • Resource-limit + error hygiene — an optional APOMESH_MEMORY_LIMIT_BYTES is applied as an RLIMIT_AS cap (baseline usage plus budget) before the first allocation; failures travel via distinct exit codes, not parsed stderr, so the parent maps them onto its SandboxError vocabulary.

It does not own the sandbox policy or the spawn mechanics (those live in apomesh-substrate-sandbox), nor the tool implementations themselves (those live in the substrate tool plane, reached through the apomesh-substrate facade). The runner is a host, not the policy or the capability.

This is a binary — its surface is its invocation contract, not an API.

  • CLI — one flag: apomesh-tool-runner --tool-id <ID>. The <ID> must match a tool registered at startup; an unknown tool exits distinctly.
  • stdin — 4-byte BE length + ToolInput JSON (a 64 MiB frame cap guards against a desynced writer). The ToolInput carries the tool args and the optional ToolContext (invoking agent_ref + the resolved secrets).
  • stdout — on success, 4-byte BE length + ToolOutput JSON (the tool result plus an optional cost the worker turns into a CostEvent).
  • Exit codes — the failure classification (BAD_CLI_ARGS, RLIMIT_FAILED, TOOL_NOT_FOUND, INPUT_PARSE_FAILED, TOOL_ERROR, OUTPUT_WRITE_FAILED) is the single source of truth in apomesh-substrate-types (runner_exit), shared with the sandbox-side classifier so the cross-process contract cannot drift. On a TOOL_ERROR exit the runner serializes the ToolError as JSON to stderr for the parent to attach.
  • Cancellation — the runner is killed, not signalled: the sandbox aborts a dispatch by SIGKILL on the child, so the in-process cancel token is passed fresh and never fires.

Source: main.rs.

The runner is never invoked by hand — the sandbox locates and spawns it per dispatch. locate_tool_runner resolves the binary in order:

  1. the APOMESH_TOOL_RUNNER environment variable (explicit override),
  2. an apomesh-tool-runner on PATH,
  3. a sibling of the current executable — the canonical layout when the worker and runner ship in the same artifact.

That third rule is why the deployment ships them together: the worker Dockerfile builds both binaries (cargo build --release --bin apomesh-worker --bin apomesh-tool-runner) and copies each into /usr/local/bin/, so the worker finds the runner as its sibling with no configuration. In a dev checkout the same holds — both land in target/debug/.