Expand description
An embeddable agent runtime for Rust. Any task, any provider, in your process — with a permission boundary, a sandbox, and a durable trace you own.
You hand it a TaskContract: the task, the workspace it may touch, and what
it may read, write, run and dial. The harness runs the loop — observe, reason,
act, check, stop — and returns a RunResult whose RunOutcome says why it
stopped, with every step, refusal, and budget draw in a SQLite trace
(Store) you can read afterwards. No daemon and no CLI.
The agent can run the project’s own toolchain, so the language a project is
written in is not this crate’s business. Verification is optional and
language-agnostic: a criterion can be cargo test, npm test, go test ./... or Verification::None when the task has no checkable criterion at
all.
§Quickstart
use io_harness::{run_with, ApproveAll, OpenRouter, Policy, Store, TaskContract, Verification};
#[tokio::main]
async fn main() -> io_harness::Result<()> {
let provider = OpenRouter::from_env()?; // OPENROUTER_API_KEY + OPENROUTER_MODEL
let store = Store::memory()?;
let contract = TaskContract::workspace(
"the test suite is failing; find out why and fix it",
"/path/to/repo",
// The project's own command decides whether the work is done. Nothing
// on this path is Rust-aware.
Verification::Command { argv: vec!["npm".into(), "test".into()], expect_exit: 0 },
);
// src/ is writable, secrets/ is refused outright and never reaches a human,
// and the agent may run the test runner but nothing that publishes.
let policy = Policy::default()
.layer("app")
.allow_read("*")
.allow_write("src/*")
.deny_read("secrets/*")
.allow_exec("npm*")
.deny_exec("npm publish*");
let result = run_with(&contract, &provider, &store, &policy, &ApproveAll).await?;
println!("{:?}", result.outcome);
Ok(())
}run is the same loop with no policy — it uses Policy::permissive,
which enforces nothing. Every entry point has an observed twin taking an
Observer as its last argument (run_with_observed, and so on).
§What it does
The loop. TaskContract::workspace gives the agent grep, find,
read_file, write_file and edit_file across a repository root;
TaskContract::new names one file to edit.
Commands, under the same boundary as everything else. The agent runs the
project’s own build, tests, linter or package manager through an exec tool
(tools::EXEC_TOOL) taking a fixed argv and never a shell string, so there
is no ;, && or $( ) to parse and therefore none to get wrong. Every call
is an Act::Exec check on the program and on the whole argv, so
allow_exec("cargo test*") beside deny_exec("cargo publish*") means what it
reads. A command runs in the workspace with the embedding program’s privileges
and is not sandboxed — see tools::exec for the whole of that bound,
and DEFAULT_EXEC_TIMEOUT for the ceiling on one that wedges.
Verification in any language, or none. Verification::Command runs a
caller-supplied command in the sandbox and asserts its exit status — one
variant covering every language the machine has a toolchain for.
Verification::None is a run with no gate, ended by an assistant turn that
calls no tool and reported as RunOutcome::Finished. What a passing gate
proves is narrower than it reads, and Verification states it exactly.
toolchain::detect tells the agent what this project’s commands
conventionally are, so it does not spend turns finding out.
A permission boundary. A Policy of layered, deny-first Rules
decides what the agent may read, write, execute (Act::Exec) and connect
to (Act::Net), enforced in the tool and verification layers rather than
in the prompt. Anything marked Effect::Ask goes to an Approver, which
may approve (optionally rewriting the action or remembering a rule), deny, or
Defer — persisting the pending action so a human can
decide after this process has exited, with resume_with_decision and
resume_tree_with_decision continuing on that answer. Every refusal is in
the trace, attributed to the rule and the layer that produced it.
Budgets and stop conditions. Steps, wall-clock time, and token spend are
capped by the contract. A whole tree of agents draws from one shared
Ledger that no spawned contract can raise.
Agent composition. run_tree runs a workspace contract as the root of
a tree: the agent gains SPAWN_TOOL, which launches a contained sub-agent
over the same workspace, and the child’s result composes back into the
parent’s next turn. Children nest, and many run at once up to
Containment::max_concurrent. A child inherits its parent’s policy and can
only narrow it (Policy::contain: allows intersect, denies union, at any
depth). run and run_with never expose the spawn tool.
An execution sandbox. Commands the verification gate runs execute in an
ephemeral Sandbox: an isolated workdir, resource caps that kill rather
than throttle (SandboxLimits), outbound network denied by default, and
guaranteed teardown. One trait, a native Backend per platform over a
portable floor that runs everywhere, chosen by select and recorded in the
trace.
Durable, unattended runs. After every completed step the trace, the
budget draw, and a checkpoint commit in one transaction, so a crash leaves
either a whole step or none of it. resume and resume_tree reconstruct
the run from the store and continue every agent from its own last committed
step: completed steps are not re-run, the Ledger is restored from durable
totals rather than reset or double-charged, and an irreversible edit already
applied is re-observed rather than repeated. resume_from_stored_policy
and resume_tree_from_stored_policy read the boundary the run was started
under back out of the store instead of trusting the caller to pass the same
one again — prefer them when the policy is what matters.
Store::run_status and RunStatus report where a run stands; a resume
against a missing or newer-format checkpoint is a typed Error::Resume,
never a panic or a half-resume.
Providers, with fallback. OpenRouter, Anthropic and OpenAi
behind one Provider trait, over the crate’s own HTTP+SSE client.
provider::Fallback moves to the next configured provider when one is down
or rate-limited, and failures are classified (ProviderErrorKind) so a
caller can tell a retryable transport error from a terminal one.
RetryPolicy governs the backoff and StallPolicy detects a run that is
repeating itself rather than progressing.
Extensibility, in-process and out. Implement the object-safe Tool
trait for something the embedding program already does, collect them in a
Toolbox, and register it with TaskContract::with_tools — no second
process, transport, or serialization hop. Or point the harness at MCP servers
with TaskContract::with_mcp: McpServers spawned as child processes
(McpTransport::Stdio) or dialled over streamable HTTP
(McpTransport::Http), offered to the model under mcp__<server>__<tool>
so a server can never shadow a built-in. Either way registration makes a tool
available, not authorized: every call is an Act::Exec check on its
name. TaskContract::with_skills adds Skills — markdown instruction
files that shape how the agent approaches a class of task, loaded through
read_skill as an ordinary policy-checked read,
with no Rust at all.
Context that stays relevant. Each turn is assembled to fit a stated share
of the token budget (ContextBudget): superseded observations are
compacted, and an observation a later write invalidated is re-read rather
than trusted. Durable memory keyed to the workspace (MemoryEntry,
Store::memory_list) survives between runs.
Observation and replay. Register an Observer and be called as the run
happens — RunEvents covering steps, tool calls, approvals, refusals,
spend draws, retries, fallbacks and outcomes — instead of polling the store;
Flow lets an observer ask the run to stop. provider::Record captures
a case and provider::Replay runs it back identically.
Documents and images, behind opt-in features: spreadsheets, Word,
PowerPoint text, PDF, and barcode decoding, each gated on Act::Read or
Act::Write against the real path the model named, and verified with
Verification::DocumentContains rather than a container read as the empty
string; plus image passthrough to any provider whose model accepts one.
Git, as fixed-argv built-ins: status, diff, log, add, and commit (under a
caller-supplied Identity), so a run ends as a reviewable commit rather
than a working tree someone has to reconstruct. The model supplies paths and
a message, never a subcommand or a flag, so push, fetch, reset and rebase are
unreachable by construction.
What none of it governs: a stdio MCP server and a registered Tool both
run outside the sandbox with the privileges of whoever started them. The
harness decides what may start and what may be called — not what a
started thing then does.
§Feature flags
default = []. The default build compiles no optional dependency at all.
| Feature | What it adds |
|---|---|
media | Image passthrough to providers that accept images |
documents | Umbrella over the five below |
xlsx | Spreadsheet read, generate, and preserving single-cell edit |
docx | Word read and generate (no in-place edit, deliberately) |
pptx | PowerPoint text extraction (read-only, no writer) |
pdf | PDF generate, extract text, watermark, fill AcroForm fields |
barcode | Barcode and QR decoding from an image |
§Minimum supported Rust
MSRV: Rust 1.88. The floor comes from rmcp, which publishes no
rust-version of its own, so cargo cannot catch it at resolve time — on
1.87 the build fails inside that dependency rather than here.
§Platform support
| Platform | Sandbox containment |
|---|---|
| macOS | Native, sandbox-exec |
| Linux | Native, namespaces and rlimits |
| Windows | Portable floor only |
The portable floor is the honest floor: on Windows the
Backend::WindowsJobObject path was designed and never implemented, so a
Windows run reports Backend::PortableFloor and only the wall-clock cap
fires. Cap::Cpu and Cap::Memory rest on unix rlimit mechanisms with
no Windows equivalent, and there is no kernel network boundary there either.
The full suite runs on all three in CI.
§Guides
Longer prose than a doc comment should carry, one page per capability:
- Permissions and approval
- Command execution
- Language support
- Verification
- Agent composition
- Execution sandbox
- Durable runs
- MCP and network egress
- Tools and skills
- Context and memory
- Resilience
- Observability and replay
- Documents
- Images and git
The public contract states what is stable, what may change, and the limits that hold today. The crate is pre-1.0: a minor release may break the contract, and when it does it is marked in CHANGELOG.md with a migration note. That file is where the release history lives.
Re-exports§
pub use approve::ApproveAll;pub use approve::Approver;pub use approve::Decision;pub use approve::DenyAll;pub use approve::Request;pub use approve::StdinApprover;pub use containment::Containment;pub use containment::Draw;pub use containment::Ledger;pub use containment::SpawnRefusal;pub use context::ContextBudget;pub use mcp::McpServer;pub use mcp::McpTransport;pub use mcp::MCP_TOOL_PREFIX;pub use observe::EventKind;pub use observe::Flow;pub use observe::Ignore;pub use observe::Observer;pub use observe::RunEvent;pub use policy::Act;pub use policy::Defaults;pub use policy::Effect;pub use policy::Layer;pub use policy::Policy;pub use policy::Rule;pub use policy::Verdict;pub use provider::Anthropic;pub use provider::CompletionRequest;pub use provider::CompletionResponse;pub use provider::OpenAi;pub use provider::OpenRouter;pub use provider::Provider;pub use provider::ToolCall;pub use provider::ToolSpec;pub use provider::Usage;pub use provider::Media;mediapub use provider::IMAGE_MEDIA_TYPES;mediapub use resilience::Progress;pub use resilience::Progressing;pub use resilience::RetryPolicy;pub use resilience::StallPolicy;pub use sandbox::copy_back;pub use sandbox::select;pub use sandbox::Backend;pub use sandbox::Cap;pub use sandbox::Sandbox;pub use sandbox::SandboxConfig;pub use sandbox::SandboxLimits;pub use sandbox::SandboxOutcome;pub use sandbox::Selected;pub use skills::Skill;pub use skills::Skills;pub use tools::git::Identity;pub use tools::Tool;pub use tools::ToolFuture;pub use tools::Toolbox;pub use tools::DEFAULT_EXEC_TIMEOUT;
Modules§
- approve
- The human-approval gate: one trait, three decisions.
- containment
- The containment boundary for a tree of agents.
- context
- The observation log the model sees, and what bounds it.
- mcp
- MCP — tools the harness did not ship, reachable without a fork.
- observe
- Watching a run while it happens.
- policy
- The permission policy: what the agent may read, write, and execute.
- provider
- The provider layer — provider-agnostic by design.
- resilience
- What a run does when things go wrong but nothing has crashed.
- sandbox
- The execution sandbox: model-produced code runs isolated, per run.
- skills
- Skills — instructions that shape how the agent approaches a class of task.
- toolchain
- What kind of project this is, and the commands its ecosystem uses.
- tools
- The tool layer — narrow, typed actions the agent may invoke.
Structs§
- Agent
Event - One event in a tree of agents: a parent spawning a child, a spawn refused by the containment boundary, or a draw against the tree’s shared spend ceiling.
- Checkpoint
Event - One durable checkpoint-lifecycle event: a step was checkpointed, a run was resumed, or an already-committed step was skipped on resume. Together they make a crashed-and-resumed run’s history reconstructable from the store.
- Context
Event - One decision the context assembler made: the section it built for a turn, or a stale read it re-read (or was refused).
- Exec
Guard - What the verification layer is allowed to spawn, and where to record it.
- McpEvent
- One event in the life of an MCP connection: a server connected, a tool it offered, a tool called (with how long it took and whether it worked), or a server disconnected.
- Memory
Entry - One durable memory entry: a fact or decision an agent wrote deliberately, keyed to a workspace rather than to a run.
- Pending
- An action paused awaiting a human decision, persisted so it outlives the process that requested it.
- Policy
Event - One policy event in the trace: an action refused, or a human decision.
- RunResult
- The result of a run, including the persisted run id for audit.
- RunSummary
- What one finished run cost and whether it worked.
- Sandbox
Event - One event in the life of a sandboxed execution: the sandbox created for a run, a command run in it (with the backend that isolated it), a resource cap that killed it, a denied network attempt, or the sandbox torn down.
- Spawn
Row - A persisted spawned-child contract, enough to rebuild and resume that exact child on a tree resume rather than spawning a duplicate.
- Step
Record - One recorded loop step — the full trace entry, as written and read back.
- Store
- A persisted run store. Use
Store::openfor a file, orStore::memoryfor an ephemeral in-memory database. - Task
Contract - A single unit of work handed to the harness.
Enums§
- Error
- Errors io-harness can return from a run.
- Provider
Error Kind - Why a provider call failed, and therefore whether trying it again — or trying a different provider — is worth doing.
- RunOutcome
- Why a run stopped.
- RunStatus
- The durable lifecycle status of a run, so a caller can tell a crashed run
(still
Running) from one paused for a human (Paused) or finished (Completed). OS- and rusqlite-free, so it is safe in the public API. - Verification
- How the harness decides a task is done.
Constants§
- BUSY_
TIMEOUT - How long a contended statement waits for the writer before giving up, set on
every store opened from a file. Without it rusqlite’s default is to fail
immediately with
SQLITE_BUSY, which turns a moment of contention into an error rather than a short wait. - CHECKPOINT_
FORMAT - The checkpoint layout version stamped into
PRAGMA user_version. Bump when the on-disk checkpoint format changes incompatibly. A store whose version is higher than this is from a newer binary and is refused on resume. - MEMORY_
MAX_ CHARS - Most characters one workspace’s entries may total.
- MEMORY_
MAX_ ENTRIES - Most entries one workspace may hold.
- MEMORY_
MAX_ ENTRY_ CHARS - Most characters one entry may hold. A longer value is cut down to this and marked, never refused — a too-long fact is still worth remembering.
- REQUEST_
TIMEOUT - How long one provider request may take before it is abandoned.
- SPAWN_
TOOL - The tool a parent agent calls to spawn a contained sub-agent.
- SUCCESS_
OUTCOME - The one outcome string that means the run did what it was asked.
- TEST_
BINARY - The logical name of the test binary verification builds and runs. Denying it
while allowing
rustcgives compile-only verification: the produced code is type-checked but never executed.
Functions§
- resume
- Resume an interrupted run under its original
run_id. Continues from the step after the last one recorded, reusing the file on disk as the current state — it does not restart from step one. - resume_
from_ stored_ policy - Resume a run under the policy it was started with, read back from the store.
- resume_
from_ stored_ policy_ observed resume_from_stored_policy, reporting toobserveras it happens. Seerun_observed.- resume_
observed resume, reporting toobserveras it happens. Seerun_observed.- resume_
tree - Resume a crashed agent tree under its original root
run_id. Reconstructs the whole 0.5.0 tree from the store: the shared spend ledger is restored from the tree’s durable total spend and agent count (so the resumed tree draws against one continuous ceiling, never a reset one), and the root agent resumes from its last committed step. As it replays its crashed step it adopts the children it had already spawned and resumes each from that child’s own checkpoint (seespawn_child), so every agent in the tree continues where it stopped rather than restarting. - resume_
tree_ from_ stored_ policy - Resume a crashed agent tree under the policy it was started with, read back
from the store —
resume_from_stored_policyfor the 0.5.0 tree loop. - resume_
tree_ from_ stored_ policy_ observed resume_tree_from_stored_policy, reporting toobserveras it happens. Seerun_observed.- resume_
tree_ observed resume_tree, reporting toobserveras it happens. Seerun_observed.- resume_
tree_ with_ decision - Continue an agent tree that paused at
RunOutcome::AwaitingApproval, once a human has decided — the tree counterpart ofresume_with_decision. - resume_
tree_ with_ decision_ observed resume_tree_with_decision, reporting toobserveras it happens. Seerun_observed.- resume_
with - Resume an interrupted run under
policy, routing anything the policy marksEffect::Asktoapprover— the resume twin ofrun_with. - resume_
with_ decision - Continue a run that stopped at
RunOutcome::AwaitingApproval, once a human has decided about the pending action. - resume_
with_ decision_ observed resume_with_decision, reporting toobserveras it happens. Seerun_observed.- resume_
with_ observed resume_with, reporting toobserveras it happens. Seerun_observed.- run
- Run a task contract to a verified result using
providerandstore. - run_
observed run, reporting toobserveras it happens.- run_
tree - Run a workspace contract as the root of an agent tree under
containment. - run_
tree_ observed run_tree, reporting toobserveras it happens. Seerun_observed.- run_
with - Run a task contract under a permission
policy, routing anything the policy marksEffect::Asktoapproverbefore it happens. - run_
with_ observed run_with, reporting toobserveras it happens. Seerun_observed.
Type Aliases§
- Result
- Crate result alias.