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//! **Durable conversations.** [`Session`] holds one instead of firing a task:
158//! open a session over a workspace, take a turn, and the next turn reads the ones
159//! before it. A turn **is** a run — its own trace, budgets, boundary and
160//! checkpoint — so a session survives a crash for the reason a run does, and
161//! [`Session::reopen`] picks it up in a later process from the id alone. The
162//! conversation is an append-only tree: [`Session::branch_from`] takes the next
163//! turn from any earlier one without disturbing what came after it. An observed
164//! turn streams the model's text as [`EventKind::Token`] while it is still being
165//! produced, and a [`Steer`] lets an operator say something else mid-turn or
166//! interrupt — both honoured at the next step boundary, and neither an
167//! authorization: a steer reaches the model as text, and every call it leads to is
168//! checked against the same [`Policy`].
169//!
170//! **Configuration in a file.** [`Config::discover`] reads one `io.toml` across
171//! four scopes — the crate's defaults, a user file, a committed project file, and
172//! a gitignored local one — and projects it onto this API: a [`Policy`], a
173//! [`SandboxConfig`], the run budgets applied through [`Config::apply_to`], the
174//! [`toolchain`] commands, a [`pricing::PriceTable`], and [`McpServer`]s.
175//! `${env:...}` and `${file:...}` keep a credential out of the file, an unknown
176//! key is an error rather than a shrug, and nothing is loaded implicitly — the
177//! caller reads the file, before the run, once, which is what stops an agent
178//! widening the boundary it is running under.
179//! **Documents and images**, behind opt-in features: spreadsheets, Word,
180//! PowerPoint text, PDF, and barcode decoding, each gated on [`Act::Read`] or
181//! [`Act::Write`] against the real path the model named, and verified with
182//! [`Verification::DocumentContains`] rather than a container read as the empty
183//! string; plus image passthrough to any provider whose model accepts one.
184//!
185//! **Git**, as fixed-argv built-ins: status, diff, log, add, and commit (under a
186//! caller-supplied [`Identity`]), so a run ends as a reviewable commit rather
187//! than a working tree someone has to reconstruct. The model supplies paths and
188//! a message, never a subcommand or a flag, so push, fetch, reset and rebase are
189//! unreachable by construction.
190//!
191//! What none of it governs: a stdio MCP server and a registered [`Tool`] both
192//! run outside the sandbox with the privileges of whoever started them. The
193//! harness decides what may *start* and what may be *called* — not what a
194//! started thing then does.
195//!
196//! # Feature flags
197//!
198//! `default = []`. The default build compiles no optional dependency at all.
199//!
200//! | Feature | What it adds |
201//! | --- | --- |
202//! | `media` | Image passthrough to providers that accept images |
203//! | `documents` | Umbrella over the five below |
204//! | `xlsx` | Spreadsheet read, generate, and preserving single-cell edit |
205//! | `docx` | Word read and generate (no in-place edit, deliberately) |
206//! | `pptx` | PowerPoint text extraction (read-only, no writer) |
207//! | `pdf` | PDF generate, extract text, watermark, fill AcroForm fields |
208//! | `barcode` | Barcode and QR decoding from an image |
209//!
210//! # Minimum supported Rust
211//!
212//! **MSRV: Rust 1.88.** The floor comes from `rmcp`, which publishes no
213//! `rust-version` of its own, so cargo cannot catch it at resolve time — on
214//! 1.87 the build fails inside that dependency rather than here.
215//!
216//! # Platform support
217//!
218//! | Platform | Sandbox containment |
219//! | --- | --- |
220//! | macOS | Native, `sandbox-exec` |
221//! | Linux | Native, namespaces and rlimits |
222//! | Windows | Portable floor only |
223//!
224//! The portable floor is the honest floor: on Windows the
225//! [`Backend::WindowsJobObject`] path was designed and never implemented, so a
226//! Windows run reports [`Backend::PortableFloor`] and only the wall-clock cap
227//! fires. [`Cap::Cpu`] and [`Cap::Memory`] rest on unix `rlimit` mechanisms with
228//! no Windows equivalent, and there is no kernel network boundary there either.
229//! The full suite runs on all three in CI.
230//!
231//! # Guides
232//!
233//! Longer prose than a doc comment should carry, one page per capability:
234//!
235//! - [Permissions and approval](https://github.com/initorigin/io-harness/blob/main/docs/guide/permissions.md)
236//! - [Command execution](https://github.com/initorigin/io-harness/blob/main/docs/guide/command-execution.md)
237//! - [Language support](https://github.com/initorigin/io-harness/blob/main/docs/guide/language-support.md)
238//! - [Verification](https://github.com/initorigin/io-harness/blob/main/docs/guide/verification.md)
239//! - [Agent composition](https://github.com/initorigin/io-harness/blob/main/docs/guide/composition.md)
240//! - [Execution sandbox](https://github.com/initorigin/io-harness/blob/main/docs/guide/sandbox.md)
241//! - [Durable runs](https://github.com/initorigin/io-harness/blob/main/docs/guide/durable-runs.md)
242//! - [MCP and network egress](https://github.com/initorigin/io-harness/blob/main/docs/guide/mcp-and-network.md)
243//! - [Tools and skills](https://github.com/initorigin/io-harness/blob/main/docs/guide/tools-and-skills.md)
244//! - [Context and memory](https://github.com/initorigin/io-harness/blob/main/docs/guide/context-and-memory.md)
245//! - [Resilience](https://github.com/initorigin/io-harness/blob/main/docs/guide/resilience.md)
246//! - [Observability and replay](https://github.com/initorigin/io-harness/blob/main/docs/guide/observability.md)
247//! - [Sessions](https://github.com/initorigin/io-harness/blob/main/docs/guide/sessions.md)
248//! - [Configuration](https://github.com/initorigin/io-harness/blob/main/docs/guide/configuration.md)
249//! - [Accounting](https://github.com/initorigin/io-harness/blob/main/docs/guide/accounting.md)
250//! - [Documents](https://github.com/initorigin/io-harness/blob/main/docs/guide/documents.md)
251//! - [Images and git](https://github.com/initorigin/io-harness/blob/main/docs/guide/images-and-git.md)
252//!
253//! [The public contract](https://github.com/initorigin/io-harness/blob/main/docs/CONTRACT.md)
254//! states what is stable, what may change, and the limits that hold today. The
255//! crate is pre-1.0: a minor release may break the contract, and when it does it
256//! is marked in
257//! [CHANGELOG.md](https://github.com/initorigin/io-harness/blob/main/CHANGELOG.md)
258//! with a migration note. That file is where the release history lives.
259//!
260
261// docs.rs builds with every feature on and sets the `docsrs` cfg (see
262// Cargo.toml). This labels each gated item with the feature it needs, so a
263// reader browsing the rendered docs is never shown an item that would not exist
264// in their build without being told why. Nightly-only, and reached only under
265// that cfg — a stable `cargo doc` is unaffected.
266//
267// `doc_cfg`, not `doc_auto_cfg`: the latter was removed in 1.92.0 (rust-lang
268// PR 138907) and merged into `doc_cfg`, which now does the automatic labelling
269// itself. 0.16.1's docs.rs build failed on the removed feature name.
270#![cfg_attr(docsrs, feature(doc_cfg))]
271
272pub mod approve;
273pub mod config;
274pub mod containment;
275pub mod context;
276mod contract;
277mod error;
278pub mod mcp;
279mod net;
280pub mod observe;
281pub mod policy;
282pub mod pricing;
283pub mod provider;
284pub mod resilience;
285mod run;
286pub mod sandbox;
287pub mod session;
288pub mod skills;
289mod state;
290pub mod toolchain;
291pub mod tools;
292mod verify;
293
294pub use approve::{ApproveAll, Approver, Decision, DenyAll, Request, StdinApprover};
295pub use config::Config;
296pub use containment::{Containment, Draw, Ledger, SpawnRefusal};
297pub use context::ContextBudget;
298pub use contract::TaskContract;
299pub use error::{Error, ProviderErrorKind, Result};
300pub use mcp::{McpServer, McpTransport, MCP_TOOL_PREFIX};
301// The `net` module itself stays private, so the default request deadline is
302// surfaced here as well as from each provider module. A caller overriding it with
303// `with_timeout` should be able to name the value they are overriding without
304// reaching into a provider's namespace to find it.
305pub use net::REQUEST_TIMEOUT;
306pub use observe::{EventKind, Flow, Ignore, Observer, RunEvent};
307pub use policy::{Act, Defaults, Effect, Layer, Policy, Rule, Verdict};
308pub use provider::{
309 Anthropic, CompletionRequest, CompletionResponse, OpenAi, OpenRouter, Provider, ToolCall,
310 ToolSpec, Usage,
311};
312#[cfg(feature = "media")]
313pub use provider::{Media, IMAGE_MEDIA_TYPES};
314pub use resilience::{Progress, Progressing, RetryPolicy, StallPolicy};
315// Each entry point has an observed twin: a separate function rather than an extra
316// parameter on the existing seven, so 0.11.0 code compiles unchanged against
317// 0.12.0. The observer is this release's headline, not a reason to break every
318// caller that does not want one.
319pub use run::{
320 resume, resume_from_stored_policy, resume_from_stored_policy_observed, resume_observed,
321 resume_tree, resume_tree_from_stored_policy, resume_tree_from_stored_policy_observed,
322 resume_tree_observed, resume_tree_with_decision, resume_tree_with_decision_observed,
323 resume_with, resume_with_decision, resume_with_decision_observed, resume_with_observed, run,
324 run_observed, run_tree, run_tree_observed, run_with, run_with_observed, RunOutcome, RunResult,
325 SPAWN_TOOL,
326};
327pub use sandbox::{
328 copy_back, select, Backend, Cap, Sandbox, SandboxConfig, SandboxLimits, SandboxOutcome,
329 Selected,
330};
331pub use session::{Session, Steer, SteerInbox, TurnResult};
332pub use skills::{Skill, Skills};
333// `AgentEvent` and `SpawnRow` were `pub` inside this private module but were not
334// re-exported, so `Store::agent_events` and `Store::find_spawn` returned values an
335// external caller could hold and could not name — which made `agent_events`, the
336// only audit of per-step budget draws against the shared tree ledger, unreadable
337// through the public API. Exported in 0.12.0: an observability release cannot ship
338// leaving its own audit table reachable only by opening the SQLite file.
339pub use state::{
340 AgentEvent, CheckpointEvent, ContextEvent, Edit, McpEvent, MemoryEntry, Pending, PolicyEvent,
341 ProviderCall, RunStatus, RunSummary, SandboxEvent, SpawnRow, StepRecord, Store, Turn,
342 BUSY_TIMEOUT, CHECKPOINT_FORMAT, MEMORY_MAX_CHARS, MEMORY_MAX_ENTRIES, MEMORY_MAX_ENTRY_CHARS,
343 SUCCESS_OUTCOME, UNKNOWN_MODEL,
344};
345pub use tools::git::Identity;
346pub use tools::{Tool, ToolFuture, Toolbox, DEFAULT_EXEC_TIMEOUT};
347pub use verify::{ExecGuard, Verification, TEST_BINARY};