Agent tools

The manifest of every tool a cruft agent can call: the harness, filesystem, supply-chain, GitHub, model, and process tools, each with its arguments, results, the flag that grants it, its one pinned network endpoint, and the audit records it writes.

This is the tool manifest for the cruft agent compartment: every tool an agent can call, what grants it, its argument and result shapes, how it behaves under --worker, its timeout, the audit records it emits, and, where a tool reaches the network, the one pinned URL it may touch and how a credential is supplied. For running an agent see the run model; for pinning a policy in a project see projects and policy; for the isolation model underneath see compartments and the CLI reference.

The tool registry is fail-closed

An agent reaches a tool only through callTool(name, args), and only tools you explicitly granted on the command line are reachable. The registry is an allowlist: a tool that was not granted is denied before it runs, the call throws agent tool denied: <name> inside the agent, the run records a tool_denial, and nothing external happens. There is no ambient fallback and no way for the agent to widen its own set. cruft agent doctor states this as "tools":"explicit_registry" and "ambient_authority":"denied_by_default".

Two commands describe the registry without running anything:

cruft agent tool list              # full manifest, one line per tool
cruft agent tool describe <name>   # detail for a single tool

tool list also reports the process-like tools that are deliberately absent (see Unavailable process-like tools).

Each per-tool line carries the same fields:

FieldMeaning
availableWhether the tool is reachable, and the flag that grants it (true means always grantable with --tool=<name>; otherwise the gating flag)
workerBehavior under --worker: true if it works unchanged, otherwise the condition or routing note
timeoutsync (bounded by the turn timeout) or --tool-timeout-ms for async settlement
auditThe audit record kinds the tool can emit
schemaThe args shape and the result shape
credentialFor network tools that accept a token, the flag that supplies it

Granting a tool is two steps: the tool name must be in the registry (--tool=<name>), and any authority the tool needs (a filesystem root, a network cache, a fixture, a credential env) must also be supplied. A name without its authority still fails closed.

Harness tools

These need no external authority and exist to exercise the membrane, budgets, and timeout paths. All three work unchanged under --worker.

ToolGrantArgsResultTimeoutAudit
echo--tool=echoJSON object{ok:true, tool:"echo", echo:<args>}synctool_call, tool_result; tool_denial when not granted
fail--tool=failJSON objecterror (always throws)synctool_call, tool_error
slow--tool=slow{delay_ms?:number, value?:json}{ok, tool, delay_ms, value}--tool-timeout-mstool_call, tool_result, tool_timeout

echo returns its arguments back through the clone boundary, the simplest way to see the membrane at work. fail always throws, exercising the tool_error classification. slow sleeps for delay_ms and settles as a promise, so a delay_ms past --tool-timeout-ms produces a tool_timeout record instead of a result.

Filesystem tools

These read and write only through path capabilities you precollect on the command line. There is no ambient filesystem: a path outside the granted caps is denied, not resolved. All three work under --worker.

ToolGrantArgsResultTimeoutAudit
readFile--fs-read=<path>{path:string}{path, content, bytes}synctool_call, tool_result, tool_denial
listFiles--fs-read=<path>{path?:string}{files:[...]}synctool_call, tool_result, tool_denial
writeArtifact--fs-write=<dir>{path:string, content:string}{path, bytes, hash}synctool_call, tool_result, tool_denial

Read authority is a set of read-only, precollected path caps. Scope it with --fs-read=<path> plus --fs-read-include=<glob> and --fs-read-exclude=<glob>; the run records the resolved slice as a source manifest. A read of an ungranted path emits tool_denial.

Write authority is a single explicit output root supplied with --fs-write=<dir>. writeArtifact returns the written path, the byte count, and an fnv1a64 content hash. A write that escapes the root or overwrites is denied with tool_denial. Durable writes happen only under that root.

Supply-chain tools

These perform one named lookup each against one pinned endpoint. Neither opens general network access. In the same thread the live transport goes to the pinned URL below; under --worker the live transport is routed, and each can instead serve from a persistent cache.

osv.query

  • Grant: --tool=osv.query. --osv-fixture=<json> selects a deterministic fixture instead of live transport.
  • Worker: supported with --osv-fixture or a --named-network-cache-dir persistent cache; worker live transport routed.
  • Args: {package:{ecosystem, name}, version?:string}.
  • Result: an OSV /v1/query-shaped JSON object with a vulns array.
  • Timeout: --tool-timeout-ms.
  • Audit: tool_call, tool_result, tool_denial, tool_error (the last on live transport failure).
  • Pinned URL (same thread): https://api.osv.dev/v1/query. No general network.

npm.metadata

  • Grant: --tool=npm.metadata.
  • Worker: supported with a --named-network-cache-dir persistent cache; worker live transport routed.
  • Args: {package:string}; scoped names use @scope/name.
  • Result: abbreviated npm registry package metadata JSON.
  • Timeout: --tool-timeout-ms.
  • Audit: tool_call, tool_result, tool_denial, tool_error.
  • Pinned URL (same thread): https://registry.npmjs.org/<encoded-package>. No general network.

GitHub read tools

Each of these reads one public GitHub REST endpoint. They share a shape, so the common contract is stated once and the per-tool table gives only the args and the pinned URL.

Shared contract for every GitHub read tool:

  • Grant: --tool=<name>.
  • Worker: supported with a --named-network-cache-dir persistent cache; worker live transport and token forwarding routed. In worker cache mode only matching tokenless persistent-cache envelopes are served.
  • Timeout: --tool-timeout-ms.
  • Audit: tool_call, tool_result, tool_denial, tool_error (the last on live transport failure).
  • Credential: optional host-only bearer token from --github-token-env=<ENV>. The audit records the env name and the credential mode, never the token value. There is no tenant credential exposure.
  • Scope: named lookup only. No repo checkout, git, search, Actions log or artifact download, or general network. The same-thread live transport is pinned to the URL below.

Every GitHub reader pins its same-thread transport to a path under https://api.github.com/repos/<owner>/<repo>. The Path column below is that suffix.

ToolArgsResultPath
github.issue.read{owner, repo, number}public issue JSON/issues/<number>
github.pr.read{owner, repo, number}public pull request JSON/pulls/<number>
github.pr.files.list{owner, repo, number}changed-files JSON/pulls/<number>/files
github.release.latest.read{owner, repo}latest release JSON/releases/latest
github.file.read{owner, repo, path, ref?}contents file JSON/contents/<path>?ref=<ref>
github.compare.read{owner, repo, base, head}compare JSON/compare/<base>...<head>
github.commit.read{owner, repo, ref}commit JSON/commits/<ref>
github.repo.read{owner, repo}repo JSON(none, the repo root)
github.workflow.run.read{owner, repo, run_id}workflow run JSON/actions/runs/<run_id>
github.workflow.jobs.list{owner, repo, run_id}workflow jobs JSON/actions/runs/<run_id>/jobs
github.check.runs.list{owner, repo, ref}check runs JSON/commits/<ref>/check-runs

number is a positive integer; run_id accepts a number or a string. github.workflow.jobs.list and github.check.runs.list do not download Actions logs or artifacts.

Network freshness and retry

The supply-chain and GitHub tools can share a persistent cache and a bounded live retry, both replay-visible:

FlagEffect
--named-network-cache-dir=<dir>Enables the persistent cache for the selected tools
--named-network-cache-mode = read-through or offlineServe live-then-cache, or cache only
--named-network-cache-max-age-ms=<n>Maximum served entry age
--named-network-cache-max-entries=<n>Cache size cap
--named-network-retry-attempts=<0..3>Bounded same-thread live retry; backoff and success are audited; nonretryable errors do not retry

Cache use is recorded, so replay never overclaims freshness.

Model tool

model.call

  • Grant: --tool=model.call plus either --model-fixture=<json> for a deterministic fixture, or the live provider: --model-provider=openai.responses with --model-api-key-env=<ENV>.
  • Worker: supported with --model-fixture, or a provider-test host call, or a provider-test --secret=model.call=<ENV>. The live worker async provider transport is routed.
  • Args: {id:string, model?:string, input?:json}.
  • Result: the fixture response object, or provider response JSON.
  • Timeout: --tool-timeout-ms.
  • Audit: tool_call, tool_result, tool_denial, tool_error.
  • Pinned URL (provider): https://api.openai.com/v1/responses. No ambient network.
  • Credential: the provider API key comes from the host env named by --model-api-key-env=<ENV>; the token value is never audited and there is no tenant credential exposure. In worker provider-test mode the --secret metadata is held by the parent, not the agent.

Process tool

process

  • Grant: --process-command=<name=path> and --process-cwd=<dir>. The command is admitted by name mapped to an absolute path; the working directory must be admitted too.
  • Worker: buffered full or summary results are available through a worker host call; worker stream and backpressure are routed. Stream output mode is same-thread only.
  • Args: {command, args?:string[], cwd?, env?, output?:full|summary|stream}.
  • Result, by output mode:
  • full: {exit_code, signal, stdout, stderr, duration_ms}
  • summary: {stdout_summary, stderr_summary, output_mode}
  • stream: {exit_code, signal, stdout, stderr, output_mode:"stream"}
  • Timeout: --tool-timeout-ms; a timeout emits tool_timeout.
  • Audit: tool_call, tool_result, tool_denial (unadmitted command/cwd/env), tool_timeout, process_output_stream, and worker_host_call for worker RPC.
  • Scope: an argv-only supervised child process. There is no shell, no exec/spawn API, and no OS sandbox claim. Additional supporting flags: --process-env=<KEY=value> admits environment entries and --process-output-stream-chunk-bytes=<n> bounds stream chunk size.

Unavailable process-like tools

cruft agent tool list names three process-like tools that are deliberately absent:

unavailable process-like tools:
  shell exec spawn  reason=process_tool_supervisor_required_not_available

There is no shell, exec, or spawn tool. They require a process-tool supervisor that is not available in this build, so they are listed as unavailable rather than silently omitted. The only supervised process path today is the argv-only process tool above, and even it makes no OS-sandbox claim.

The tool membrane

Every callTool crosses a membrane with a fixed contract, the same in the same thread and (where a tool is worker-supported) under --worker:

  • Cloned both directions. Arguments are JSON-cloned into the tool and the result is JSON-cloned back out. No live reference crosses the boundary, so neither side can hand the other a mutable object.
  • Byte-budgeted both directions. Arguments are capped by --max-tool-arg-bytes and results by --max-tool-result-bytes. The boundary enforces the cloned-payload byte budget; the tool is never asked to enforce it. These and the other run budgets are documented on running an agent.
  • Failures are classified. A call resolves into exactly one audited outcome: an invalid argument shape, a denial (tool not in the registry, or a path/command/token not admitted), or a tool error (the tool ran and threw or the live transport failed). The audit record names which, so replay distinguishes "was never allowed" from "ran and failed."

cruft agent doctor reports these as "clone_payload_bytes":"enforced", "tool_payload_budget":"enforced", and the per-tool tool_denial / tool_error records above. Worker-hosted tool state forwarding depends on the full worker membrane, which doctor lists as required_not_available; the per-tool worker= column states exactly what each tool does under --worker today.