Skip to main content

io_harness/
lib.rs

1//! An embeddable agent runtime for Rust. Any task, any provider, in your process
2//! — with a permission boundary, a sandbox, and a durable trace you own.
3//!
4//! You hand it a [`TaskContract`]: the task, the workspace it may touch, and what
5//! it may read, write, run and dial. The harness runs the loop — observe, reason,
6//! act, check, stop — and returns a [`RunResult`] whose [`RunOutcome`] says why it
7//! stopped, with every step, refusal, and budget draw in a SQLite trace
8//! ([`Store`]) you can read afterwards. No daemon and no CLI.
9//!
10//! The agent can run the project's own toolchain, so the language a project is
11//! written in is not this crate's business. [`Verification`] is optional and
12//! language-agnostic: a criterion can be `cargo test`, `npm test`, `go test
13//! ./...` or [`Verification::None`] when the task has no checkable criterion at
14//! all.
15//!
16//! # Quickstart
17//!
18//! ```no_run
19//! use io_harness::{run_with, ApproveAll, OpenRouter, Policy, Store, TaskContract, Verification};
20//!
21//! #[tokio::main]
22//! async fn main() -> io_harness::Result<()> {
23//!     let provider = OpenRouter::from_env()?; // OPENROUTER_API_KEY + OPENROUTER_MODEL
24//!     let store = Store::memory()?;
25//!
26//!     let contract = TaskContract::workspace(
27//!         "the test suite is failing; find out why and fix it",
28//!         "/path/to/repo",
29//!         // The project's own command decides whether the work is done. Nothing
30//!         // on this path is Rust-aware.
31//!         Verification::Command { argv: vec!["npm".into(), "test".into()], expect_exit: 0 },
32//!     );
33//!
34//!     // src/ is writable, secrets/ is refused outright and never reaches a human,
35//!     // and the agent may run the test runner but nothing that publishes.
36//!     let policy = Policy::default()
37//!         .layer("app")
38//!         .allow_read("*")
39//!         .allow_write("src/*")
40//!         .deny_read("secrets/*")
41//!         .allow_exec("npm*")
42//!         .deny_exec("npm publish*");
43//!
44//!     let result = run_with(&contract, &provider, &store, &policy, &ApproveAll).await?;
45//!     println!("{:?}", result.outcome);
46//!     Ok(())
47//! }
48//! ```
49//!
50//! [`run`] is the same loop with no policy — it uses [`Policy::permissive`],
51//! which enforces nothing. Every entry point has an observed twin taking an
52//! [`Observer`] as its last argument ([`run_with_observed`], and so on).
53//!
54//! # What it does
55//!
56//! **The loop.** [`TaskContract::workspace`] gives the agent `grep`, `find`,
57//! `read_file`, `write_file` and `edit_file` across a repository root;
58//! [`TaskContract::new`] names one file to edit.
59//!
60//! **Commands, under the same boundary as everything else.** The agent runs the
61//! project's own build, tests, linter or package manager through an `exec` tool
62//! ([`tools::EXEC_TOOL`]) taking a fixed argv and never a shell string, so there
63//! is no `;`, `&&` or `$( )` to parse and therefore none to get wrong. Every call
64//! is an [`Act::Exec`] check on the program *and* on the whole argv, so
65//! `allow_exec("cargo test*")` beside `deny_exec("cargo publish*")` means what it
66//! reads. A command runs in the workspace with the embedding program's privileges
67//! and is **not** sandboxed — see [`tools::exec`] for the whole of that bound,
68//! and [`DEFAULT_EXEC_TIMEOUT`] for the ceiling on one that wedges.
69//!
70//! **Verification in any language, or none.** [`Verification::Command`] runs a
71//! caller-supplied command in the sandbox and asserts its exit status — one
72//! variant covering every language the machine has a toolchain for.
73//! [`Verification::None`] is a run with no gate, ended by an assistant turn that
74//! calls no tool and reported as [`RunOutcome::Finished`]. What a passing gate
75//! proves is narrower than it reads, and [`Verification`] states it exactly.
76//! [`toolchain::detect`] tells the agent what this project's commands
77//! conventionally are, so it does not spend turns finding out.
78//!
79//! **A permission boundary.** A [`Policy`] of layered, deny-first [`Rule`]s
80//! decides what the agent may read, write, execute ([`Act::Exec`]) and connect
81//! to ([`Act::Net`]), enforced in the tool and verification layers rather than
82//! in the prompt. Anything marked [`Effect::Ask`] goes to an [`Approver`], which
83//! may approve (optionally rewriting the action or remembering a rule), deny, or
84//! [`Defer`](Decision::Defer) — persisting the pending action so a human can
85//! decide after this process has exited, with [`resume_with_decision`] and
86//! [`resume_tree_with_decision`] continuing on that answer. Every refusal is in
87//! the trace, attributed to the rule and the layer that produced it.
88//!
89//! **Budgets and stop conditions.** Steps, wall-clock time, and token spend are
90//! capped by the contract. A whole tree of agents draws from one shared
91//! [`Ledger`] that no spawned contract can raise.
92//!
93//! **Agent composition.** [`run_tree`] runs a workspace contract as the root of
94//! a tree: the agent gains [`SPAWN_TOOL`], which launches a contained sub-agent
95//! over the same workspace, and the child's result composes back into the
96//! parent's next turn. Children nest, and many run at once up to
97//! [`Containment::max_concurrent`]. A child inherits its parent's policy and can
98//! only narrow it ([`Policy::contain`]: allows intersect, denies union, at any
99//! depth). [`run`] and [`run_with`] never expose the spawn tool.
100//!
101//! **An execution sandbox.** Commands the verification gate runs execute in an
102//! ephemeral [`Sandbox`]: an isolated workdir, resource caps that *kill* rather
103//! than throttle ([`SandboxLimits`]), outbound network denied by default, and
104//! guaranteed teardown. One trait, a native [`Backend`] per platform over a
105//! portable floor that runs everywhere, chosen by [`select`] and recorded in the
106//! trace.
107//!
108//! **Durable, unattended runs.** After every completed step the trace, the
109//! budget draw, and a checkpoint commit in one transaction, so a crash leaves
110//! either a whole step or none of it. [`resume`] and [`resume_tree`] reconstruct
111//! the run from the store and continue every agent from its own last committed
112//! step: completed steps are not re-run, the [`Ledger`] is restored from durable
113//! totals rather than reset or double-charged, and an irreversible edit already
114//! applied is re-observed rather than repeated. [`resume_from_stored_policy`]
115//! and [`resume_tree_from_stored_policy`] read the boundary the run was started
116//! under back out of the store instead of trusting the caller to pass the same
117//! one again — prefer them when the policy is what matters.
118//! [`Store::run_status`] and [`RunStatus`] report where a run stands; a resume
119//! against a missing or newer-format checkpoint is a typed [`Error::Resume`],
120//! never a panic or a half-resume.
121//!
122//! **Providers, with fallback.** [`OpenRouter`], [`Anthropic`] and [`OpenAi`]
123//! behind one [`Provider`] trait, over the crate's own HTTP+SSE client.
124//! [`provider::Fallback`] moves to the next configured provider when one is down
125//! or rate-limited, and failures are classified ([`ProviderErrorKind`]) so a
126//! caller can tell a retryable transport error from a terminal one.
127//! [`RetryPolicy`] governs the backoff and [`StallPolicy`] detects a run that is
128//! repeating itself rather than progressing.
129//!
130//! **Extensibility, in-process and out.** Implement the object-safe [`Tool`]
131//! trait for something the embedding program already does, collect them in a
132//! [`Toolbox`], and register it with [`TaskContract::with_tools`] — no second
133//! process, transport, or serialization hop. Or point the harness at MCP servers
134//! with [`TaskContract::with_mcp`]: [`McpServer`]s spawned as child processes
135//! ([`McpTransport::Stdio`]) or dialled over streamable HTTP
136//! ([`McpTransport::Http`]), offered to the model under `mcp__<server>__<tool>`
137//! so a server can never shadow a built-in. Either way registration makes a tool
138//! *available*, not authorized: every call is an [`Act::Exec`] check on its
139//! name. [`TaskContract::with_skills`] adds [`Skills`] — markdown instruction
140//! files that shape how the agent approaches a class of task, loaded through
141//! [`read_skill`](tools::READ_SKILL_TOOL) as an ordinary policy-checked read,
142//! with no Rust at all.
143//!
144//! **Context that stays relevant.** Each turn is assembled to fit a stated share
145//! of the token budget ([`ContextBudget`]): superseded observations are
146//! compacted, and an observation a later write invalidated is re-read rather
147//! than trusted. Durable memory keyed to the workspace ([`MemoryEntry`],
148//! [`Store::memory_list`]) survives between runs.
149//!
150//! **Observation and replay.** Register an [`Observer`] and be called as the run
151//! happens — [`RunEvent`]s covering steps, tool calls, approvals, refusals,
152//! spend draws, retries, fallbacks and outcomes — instead of polling the store;
153//! [`Flow`] lets an observer ask the run to stop. [`provider::Record`] captures
154//! a case and [`provider::Replay`] runs it back identically.
155//!
156//!
157//! **Configuration in a file.** [`Config::discover`] reads one `io.toml` across
158//! four scopes — the crate's defaults, a user file, a committed project file, and
159//! a gitignored local one — and projects it onto this API: a [`Policy`], a
160//! [`SandboxConfig`], the run budgets applied through [`Config::apply_to`], the
161//! [`toolchain`] commands, a [`pricing::PriceTable`], and [`McpServer`]s.
162//! `${env:...}` and `${file:...}` keep a credential out of the file, an unknown
163//! key is an error rather than a shrug, and nothing is loaded implicitly — the
164//! caller reads the file, before the run, once, which is what stops an agent
165//! widening the boundary it is running under.
166//! **Documents and images**, behind opt-in features: spreadsheets, Word,
167//! PowerPoint text, PDF, and barcode decoding, each gated on [`Act::Read`] or
168//! [`Act::Write`] against the real path the model named, and verified with
169//! [`Verification::DocumentContains`] rather than a container read as the empty
170//! string; plus image passthrough to any provider whose model accepts one.
171//!
172//! **Git**, as fixed-argv built-ins: status, diff, log, add, and commit (under a
173//! caller-supplied [`Identity`]), so a run ends as a reviewable commit rather
174//! than a working tree someone has to reconstruct. The model supplies paths and
175//! a message, never a subcommand or a flag, so push, fetch, reset and rebase are
176//! unreachable by construction.
177//!
178//! What none of it governs: a stdio MCP server and a registered [`Tool`] both
179//! run outside the sandbox with the privileges of whoever started them. The
180//! harness decides what may *start* and what may be *called* — not what a
181//! started thing then does.
182//!
183//! # Feature flags
184//!
185//! `default = []`. The default build compiles no optional dependency at all.
186//!
187//! | Feature | What it adds |
188//! | --- | --- |
189//! | `media` | Image passthrough to providers that accept images |
190//! | `documents` | Umbrella over the five below |
191//! | `xlsx` | Spreadsheet read, generate, and preserving single-cell edit |
192//! | `docx` | Word read and generate (no in-place edit, deliberately) |
193//! | `pptx` | PowerPoint text extraction (read-only, no writer) |
194//! | `pdf` | PDF generate, extract text, watermark, fill AcroForm fields |
195//! | `barcode` | Barcode and QR decoding from an image |
196//!
197//! # Minimum supported Rust
198//!
199//! **MSRV: Rust 1.88.** The floor comes from `rmcp`, which publishes no
200//! `rust-version` of its own, so cargo cannot catch it at resolve time — on
201//! 1.87 the build fails inside that dependency rather than here.
202//!
203//! # Platform support
204//!
205//! | Platform | Sandbox containment |
206//! | --- | --- |
207//! | macOS | Native, `sandbox-exec` |
208//! | Linux | Native, namespaces and rlimits |
209//! | Windows | Portable floor only |
210//!
211//! The portable floor is the honest floor: on Windows the
212//! [`Backend::WindowsJobObject`] path was designed and never implemented, so a
213//! Windows run reports [`Backend::PortableFloor`] and only the wall-clock cap
214//! fires. [`Cap::Cpu`] and [`Cap::Memory`] rest on unix `rlimit` mechanisms with
215//! no Windows equivalent, and there is no kernel network boundary there either.
216//! The full suite runs on all three in CI.
217//!
218//! # Guides
219//!
220//! Longer prose than a doc comment should carry, one page per capability:
221//!
222//! - [Permissions and approval](https://github.com/initorigin/io-harness/blob/main/docs/guide/permissions.md)
223//! - [Command execution](https://github.com/initorigin/io-harness/blob/main/docs/guide/command-execution.md)
224//! - [Language support](https://github.com/initorigin/io-harness/blob/main/docs/guide/language-support.md)
225//! - [Verification](https://github.com/initorigin/io-harness/blob/main/docs/guide/verification.md)
226//! - [Agent composition](https://github.com/initorigin/io-harness/blob/main/docs/guide/composition.md)
227//! - [Execution sandbox](https://github.com/initorigin/io-harness/blob/main/docs/guide/sandbox.md)
228//! - [Durable runs](https://github.com/initorigin/io-harness/blob/main/docs/guide/durable-runs.md)
229//! - [MCP and network egress](https://github.com/initorigin/io-harness/blob/main/docs/guide/mcp-and-network.md)
230//! - [Tools and skills](https://github.com/initorigin/io-harness/blob/main/docs/guide/tools-and-skills.md)
231//! - [Context and memory](https://github.com/initorigin/io-harness/blob/main/docs/guide/context-and-memory.md)
232//! - [Resilience](https://github.com/initorigin/io-harness/blob/main/docs/guide/resilience.md)
233//! - [Observability and replay](https://github.com/initorigin/io-harness/blob/main/docs/guide/observability.md)
234//! - [Configuration](https://github.com/initorigin/io-harness/blob/main/docs/guide/configuration.md)
235//! - [Accounting](https://github.com/initorigin/io-harness/blob/main/docs/guide/accounting.md)
236//! - [Documents](https://github.com/initorigin/io-harness/blob/main/docs/guide/documents.md)
237//! - [Images and git](https://github.com/initorigin/io-harness/blob/main/docs/guide/images-and-git.md)
238//!
239//! [The public contract](https://github.com/initorigin/io-harness/blob/main/docs/CONTRACT.md)
240//! states what is stable, what may change, and the limits that hold today. The
241//! crate is pre-1.0: a minor release may break the contract, and when it does it
242//! is marked in
243//! [CHANGELOG.md](https://github.com/initorigin/io-harness/blob/main/CHANGELOG.md)
244//! with a migration note. That file is where the release history lives.
245//!
246
247// docs.rs builds with every feature on and sets the `docsrs` cfg (see
248// Cargo.toml). This labels each gated item with the feature it needs, so a
249// reader browsing the rendered docs is never shown an item that would not exist
250// in their build without being told why. Nightly-only, and reached only under
251// that cfg — a stable `cargo doc` is unaffected.
252//
253// `doc_cfg`, not `doc_auto_cfg`: the latter was removed in 1.92.0 (rust-lang
254// PR 138907) and merged into `doc_cfg`, which now does the automatic labelling
255// itself. 0.16.1's docs.rs build failed on the removed feature name.
256#![cfg_attr(docsrs, feature(doc_cfg))]
257
258pub mod approve;
259pub mod config;
260pub mod containment;
261pub mod context;
262mod contract;
263mod error;
264pub mod mcp;
265mod net;
266pub mod observe;
267pub mod policy;
268pub mod pricing;
269pub mod provider;
270pub mod resilience;
271mod run;
272pub mod sandbox;
273pub mod skills;
274mod state;
275pub mod toolchain;
276pub mod tools;
277mod verify;
278
279pub use approve::{ApproveAll, Approver, Decision, DenyAll, Request, StdinApprover};
280pub use config::Config;
281pub use containment::{Containment, Draw, Ledger, SpawnRefusal};
282pub use context::ContextBudget;
283pub use contract::TaskContract;
284pub use error::{Error, ProviderErrorKind, Result};
285pub use mcp::{McpServer, McpTransport, MCP_TOOL_PREFIX};
286// The `net` module itself stays private, so the default request deadline is
287// surfaced here as well as from each provider module. A caller overriding it with
288// `with_timeout` should be able to name the value they are overriding without
289// reaching into a provider's namespace to find it.
290pub use net::REQUEST_TIMEOUT;
291pub use observe::{EventKind, Flow, Ignore, Observer, RunEvent};
292pub use policy::{Act, Defaults, Effect, Layer, Policy, Rule, Verdict};
293pub use provider::{
294    Anthropic, CompletionRequest, CompletionResponse, OpenAi, OpenRouter, Provider, ToolCall,
295    ToolSpec, Usage,
296};
297#[cfg(feature = "media")]
298pub use provider::{Media, IMAGE_MEDIA_TYPES};
299pub use resilience::{Progress, Progressing, RetryPolicy, StallPolicy};
300// Each entry point has an observed twin: a separate function rather than an extra
301// parameter on the existing seven, so 0.11.0 code compiles unchanged against
302// 0.12.0. The observer is this release's headline, not a reason to break every
303// caller that does not want one.
304pub use run::{
305    resume, resume_from_stored_policy, resume_from_stored_policy_observed, resume_observed,
306    resume_tree, resume_tree_from_stored_policy, resume_tree_from_stored_policy_observed,
307    resume_tree_observed, resume_tree_with_decision, resume_tree_with_decision_observed,
308    resume_with, resume_with_decision, resume_with_decision_observed, resume_with_observed, run,
309    run_observed, run_tree, run_tree_observed, run_with, run_with_observed, RunOutcome, RunResult,
310    SPAWN_TOOL,
311};
312pub use sandbox::{
313    copy_back, select, Backend, Cap, Sandbox, SandboxConfig, SandboxLimits, SandboxOutcome,
314    Selected,
315};
316pub use skills::{Skill, Skills};
317// `AgentEvent` and `SpawnRow` were `pub` inside this private module but were not
318// re-exported, so `Store::agent_events` and `Store::find_spawn` returned values an
319// external caller could hold and could not name — which made `agent_events`, the
320// only audit of per-step budget draws against the shared tree ledger, unreadable
321// through the public API. Exported in 0.12.0: an observability release cannot ship
322// leaving its own audit table reachable only by opening the SQLite file.
323pub use state::{
324    AgentEvent, CheckpointEvent, ContextEvent, Edit, McpEvent, MemoryEntry, Pending, PolicyEvent,
325    ProviderCall, RunStatus, RunSummary, SandboxEvent, SpawnRow, StepRecord, Store, BUSY_TIMEOUT,
326    CHECKPOINT_FORMAT, MEMORY_MAX_CHARS, MEMORY_MAX_ENTRIES, MEMORY_MAX_ENTRY_CHARS,
327    SUCCESS_OUTCOME, UNKNOWN_MODEL,
328};
329pub use tools::git::Identity;
330pub use tools::{Tool, ToolFuture, Toolbox, DEFAULT_EXEC_TIMEOUT};
331pub use verify::{ExecGuard, Verification, TEST_BINARY};