1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
//! # io-harness
//!
//! A production-grade Rust agent harness: run an AI agent from a typed
//! [`TaskContract`] to a *verified* result. Provider-agnostic, embeddable
//! in-process, with a deterministic verification layer.
//!
//! The agent edits one file to meet a [`Verification`] criterion, using the
//! filesystem tool and the OpenRouter provider, persisting every step to
//! rusqlite. v0.2 bounds the run with step, time, and cost (token) budgets,
//! retries transient step failures, records a full trace, adds execution-based
//! verification ([`Verification::CompilesRust`], [`Verification::RustTestPasses`])
//! that compiles the produced file so a substring stub cannot pass, and can
//! [`resume`] an interrupted run.
//!
//! v0.4 adds the permission boundary: a [`Policy`] of layered allow/deny rules
//! decides what the agent may read, write, and execute, enforced in the tool and
//! verification layers rather than the prompt. Anything the policy marks
//! [`Effect::Ask`] goes to an [`Approver`], which may approve (optionally
//! rewriting the action or remembering a rule), deny, or
//! [`Defer`](Decision::Defer) — persisting the pending action so a human can
//! decide after this process has exited, and [`resume_with_decision`] continues
//! it. Every refusal and decision lands in the rusqlite trace, attributed to the
//! rule and layer that produced it.
//!
//! A caller who passes no policy gets [`Policy::permissive`] and the exact 0.3.0
//! behaviour — the boundary is opt-in.
//!
//! v0.5 adds agent composition. [`run_tree`] runs a workspace contract as the
//! root of a tree: the agent gains one tool, [`SPAWN_TOOL`], that launches a
//! contained sub-agent over the same workspace, and its result composes back for
//! the parent's next turn. Children may nest, and many may run at once — the
//! fan-out is bounded by [`Containment::max_concurrent`]. Containment is the
//! safety half: a child inherits its parent's policy and can only *narrow* it
//! ([`Policy::contain`] — allows intersect, denies union, downward at any depth),
//! and the whole tree draws its token spend from one shared [`Ledger`] no spawned
//! [`TaskContract`] can raise, capped by a [`Containment`] handed in at the root.
//! Every spawn, refusal, and budget draw lands in the rusqlite trace, so the tree
//! is a reconstructable graph. Sub-agents are opt-in: [`run`] and [`run_with`]
//! never expose the spawn tool.
//!
//! v0.6 adds the execution [`sandbox`]. Every command the verification gate runs
//! — the `rustc` compile and the test binary it has run since v0.2 — now executes
//! inside an ephemeral [`Sandbox`]: an isolated workdir, resource caps that
//! *kill* rather than throttle ([`SandboxLimits`]), outbound network denied by
//! default, and guaranteed teardown. It is **OS-native and OS-neutral**: one
//! trait with a native backend per platform (macOS `sandbox-exec`, Linux
//! namespaces, Windows Job Objects) over a portable floor that runs everywhere,
//! chosen by [`select`] and recorded in the trace. Sandboxing is the new default
//! and is transparent to verification; a caller who wants the exact v0.5 direct
//! execution opts it off. A configurable network egress allow-list is deferred to
//! v0.8; v0.6 is deny-by-default only.
//!
//! v0.7 makes a run **durable and unattended**. After every completed step the
//! harness commits that step's trace, its budget draw, and a checkpoint marker in
//! one rusqlite transaction, so the committed checkpoint *is* the step's
//! completion marker: a crash leaves either a whole step or none of it. On a
//! restart [`resume`] (single/workspace) and [`resume_tree`] (a whole v0.5 tree)
//! reconstruct the run from the store and continue every agent from its own last
//! committed step — completed steps are skipped, the aggregate [`Ledger`] budget
//! is restored from durable totals (never reset or double-charged), and the time
//! budget counts real wall-clock elapsed across the downtime ([`RunStatus`] and
//! [`Store::run_status`] report where a run stands). Replay is idempotent: an
//! irreversible edit already applied is re-observed, not repeated, and re-running
//! a resume is a no-op. Ephemeral v0.6 sandboxes are never checkpointed — an exec
//! in flight at crash time simply re-runs in a fresh sandbox. A v0.4 approval
//! survives a full process exit and resumes the tree via
//! [`resume_tree_with_decision`]. A resume against a newer-format or missing
//! checkpoint is a typed [`Error::Resume`], never a panic or a half-resume.
//!
//! v0.8 makes the harness **extensible, and its network reach governed**. It is
//! an MCP client: [`TaskContract::with_mcp`] connects [`McpServer`]s — spawned as
//! child processes ([`McpTransport::Stdio`]) or dialled over streamable HTTP
//! ([`McpTransport::Http`]) — and their tools are offered to the model beside the
//! built-ins under `mcp__<server>__<tool>`, so a server can never shadow
//! `write_file`. A capability the crate lacks is added by pointing it at a
//! server, not by forking it. Tool calls carry a timeout, results are size-capped,
//! and one session serves a whole v0.5 tree.
//!
//! Because a configured server is the first thing here that can dial an arbitrary
//! host, the v0.4 policy gains a fourth act: [`Act::Net`]. An outbound connection
//! has a target (`host` or `host:port`) decided by the same deny-first stack that
//! decides paths and binaries — [`Policy::allow_net`], [`Policy::deny_net`],
//! [`Policy::ask_net`] — and *every* connection the harness opens passes one
//! checked entry point before a socket exists. Network defaults to deny; the
//! harness contributes the configured provider's host as a visible layer named
//! `provider`, so a deny-all base still reaches its model and the trace says why.
//! An explicit deny of that host still wins. A v0.5 child inherits its parent's
//! network rules and can only narrow them, and a network `Ask` survives a full
//! restart on the v0.7 durable path.
//!
//! What it does **not** govern: a stdio server is a separate process, and once
//! running it dials what it likes. The harness decides whether it may start (an
//! [`Act::Exec`] check on its binary) and which of its tools may be called (an
//! [`Act::Exec`] check on the namespaced name) — not what it does afterwards.
//!
//! ```no_run
//! use io_harness::{run_with, ApproveAll, McpServer, OpenRouter, Policy, Store,
//! TaskContract, Verification};
//!
//! # async fn mcp_demo() -> io_harness::Result<()> {
//! let contract = TaskContract::workspace(
//! "summarise the repo's README into NOTES.md",
//! "/path/to/repo",
//! Verification::WorkspaceFileContains { file: "NOTES.md".into(), needle: "#".into() },
//! )
//! .with_mcp([McpServer::stdio("files", "my-mcp-file-server")]);
//!
//! // Deny-by-default egress. The provider's own host is allowed by the harness's
//! // `provider` layer; nothing else is reachable, and a stdio server may start
//! // only because the exec rule names it.
//! let policy = Policy::default()
//! .layer("app")
//! .allow_read("*")
//! .allow_write("*")
//! .allow_exec("my-mcp-file-server");
//!
//! let result = run_with(&contract, &OpenRouter::from_env()?, &Store::memory()?,
//! &policy, &ApproveAll).await?;
//! # Ok(())
//! # }
//! ```
//!
//! A refusal is not an error the caller has to catch mid-loop: an out-of-policy
//! *tool call* comes back to the model as an observation it can adapt to, while a
//! denied host or an unstartable server fails the run with [`Error::Refused`] or
//! [`Error::Mcp`] before anything happens.
//!
//! v0.3 adds repository work: [`TaskContract::workspace`] runs a multi-tool loop
//! where the agent greps, finds, reads, and writes several files under one root,
//! verified together ([`Verification::WorkspaceTestPasses`]). It also adds the
//! [`Anthropic`] and [`OpenAi`] providers behind the same [`Provider`] trait —
//! choose one at run construction; the task contract does not change.
//!
//! ```no_run
//! use io_harness::{run_with, ApproveAll, OpenRouter, Policy, Store, TaskContract, Verification};
//!
//! # async fn demo() -> io_harness::Result<()> {
//! let provider = OpenRouter::from_env()?; // OPENROUTER_API_KEY + OPENROUTER_MODEL
//! let store = Store::memory()?;
//! let contract = TaskContract::new(
//! "add a hello function returning 42",
//! "src/hello.rs",
//! Verification::FileContains("fn hello".into()),
//! );
//! // src/ is writable; secrets/ is denied outright and never reaches the approver.
//! let policy = Policy::default()
//! .layer("app")
//! .allow_read("*")
//! .deny_read("secrets/*")
//! .deny_write("secrets/*");
//! let result = run_with(&contract, &provider, &store, &policy, &ApproveAll).await?;
//! println!("{:?}", result.outcome);
//! # Ok(())
//! # }
//! ```
pub use TaskContract;
pub use ;
pub use ;
pub use ;
pub use ;
pub use ;
pub use ;
pub use ;
pub use ;
pub use ;
pub use ;