Skip to main content

Crate io_harness

Crate io_harness 

Source
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.

FeatureWhat it adds
mediaImage passthrough to providers that accept images
documentsUmbrella over the five below
xlsxSpreadsheet read, generate, and preserving single-cell edit
docxWord read and generate (no in-place edit, deliberately)
pptxPowerPoint text extraction (read-only, no writer)
pdfPDF generate, extract text, watermark, fill AcroForm fields
barcodeBarcode 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

PlatformSandbox containment
macOSNative, sandbox-exec
LinuxNative, namespaces and rlimits
WindowsPortable 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:

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;media
pub use provider::IMAGE_MEDIA_TYPES;media
pub 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§

AgentEvent
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.
CheckpointEvent
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.
ContextEvent
One decision the context assembler made: the section it built for a turn, or a stale read it re-read (or was refused).
ExecGuard
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.
MemoryEntry
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.
PolicyEvent
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.
SandboxEvent
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.
SpawnRow
A persisted spawned-child contract, enough to rebuild and resume that exact child on a tree resume rather than spawning a duplicate.
StepRecord
One recorded loop step — the full trace entry, as written and read back.
Store
A persisted run store. Use Store::open for a file, or Store::memory for an ephemeral in-memory database.
TaskContract
A single unit of work handed to the harness.

Enums§

Error
Errors io-harness can return from a run.
ProviderErrorKind
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 rustc gives 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 to observer as it happens. See run_observed.
resume_observed
resume, reporting to observer as it happens. See run_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 (see spawn_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_policy for the 0.5.0 tree loop.
resume_tree_from_stored_policy_observed
resume_tree_from_stored_policy, reporting to observer as it happens. See run_observed.
resume_tree_observed
resume_tree, reporting to observer as it happens. See run_observed.
resume_tree_with_decision
Continue an agent tree that paused at RunOutcome::AwaitingApproval, once a human has decided — the tree counterpart of resume_with_decision.
resume_tree_with_decision_observed
resume_tree_with_decision, reporting to observer as it happens. See run_observed.
resume_with
Resume an interrupted run under policy, routing anything the policy marks Effect::Ask to approver — the resume twin of run_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 to observer as it happens. See run_observed.
resume_with_observed
resume_with, reporting to observer as it happens. See run_observed.
run
Run a task contract to a verified result using provider and store.
run_observed
run, reporting to observer as it happens.
run_tree
Run a workspace contract as the root of an agent tree under containment.
run_tree_observed
run_tree, reporting to observer as it happens. See run_observed.
run_with
Run a task contract under a permission policy, routing anything the policy marks Effect::Ask to approver before it happens.
run_with_observed
run_with, reporting to observer as it happens. See run_observed.

Type Aliases§

Result
Crate result alias.