Skip to repository content
tenant.openagents/omega
No repository description is available.
OpenAgents Git authority 2026-07-28T00:56:20.343Z Public web read
NIP-34 coordinate
30617:7649603503856e5148d571eac2766b288a8ff1e9e35d380337a1d2b0015b4f92:omegaMaintainersHidden in public view
References2 branches · 1 tag
Read-only clone
git clone https://openagents.com/git/tenant.openagents/omega.gitRevision diff
890f9fa4 → a39c3cd4diff --git a/Cargo.lock b/Cargo.lock
index 7c176df859..5727e53081 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -546,7 +546,9 @@ dependencies = [
546546 "node_runtime",
547547 "notifications",
548548 "omega_effectd",
549+ "omega_exo_lane",
549550 "omega_front_door",
551+ "omega_harness",
550552 "omega_identity",
551553 "ordered-float 2.10.1",
552554 "parking_lot",
@@ -574,6 +576,7 @@ dependencies = [
574576 "serde_json_lenient",
575577 "settings",
576578 "shlex",
579+ "smol",
577580 "streaming_diff",
578581 "task",
579582 "telemetry",
@@ -11983,6 +11986,14 @@ dependencies = [
1198311986 "zed_credentials_provider",
1198411987 ]
1198511988
11989+[[package]]
11990+name = "omega_exo_lane"
11991+version = "0.1.0"
11992+dependencies = [
11993+ "omega_front_door",
11994+ "omega_harness",
11995+]
11996+
1198611997 [[package]]
1198711998 name = "omega_front_door"
1198811999 version = "0.1.0"
diff --git a/Cargo.toml b/Cargo.toml
index 8b6c944f2d..5d1334ebaa 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -149,6 +149,7 @@ members = [
149149 "crates/omega_front_door",
150150 "crates/omega_harness",
151151 "crates/omega_effectd",
152+ "crates/omega_exo_lane",
152153 "crates/full_auto_ui",
153154 "crates/agent_computer_ui",
154155 "crates/workroom_receipts",
@@ -423,6 +424,7 @@ omega_acp_server = { path = "crates/omega_acp_server" }
423424 omega_front_door = { path = "crates/omega_front_door" }
424425 omega_harness = { path = "crates/omega_harness" }
425426 omega_effectd = { path = "crates/omega_effectd" }
427+omega_exo_lane = { path = "crates/omega_exo_lane" }
426428 full_auto_ui = { path = "crates/full_auto_ui" }
427429 agent_computer_ui = { path = "crates/agent_computer_ui" }
428430 workroom_receipts = { path = "crates/workroom_receipts" }
diff --git a/OMEGA_DELTAS.md b/OMEGA_DELTAS.md
index b1c675ee58..357ddc9f18 100644
--- a/OMEGA_DELTAS.md
+++ b/OMEGA_DELTAS.md
@@ -2070,3 +2070,108 @@ than it sounds, because the harness omega#81's acceptance sentence names —
20702070 answered by disclosing where it would route, and dispatching a served turn to
20712071 a real executor is a later decision with its own authority question, not an
20722072 omission. The v1 fleet-backed server (openagents #9179) stays deferred.
2073+### OMEGA-DELTA-0042 — Omega drives Exo as a lane, and supplies the authority gate Exo lacks
2074+
2075+- **Upstream Zed:** external agents are attached over ACP or configured as
2076+ custom agent servers. There is no notion of an executor whose own agent can
2077+ rewrite itself, and nothing in the fork's ancestry has ever had to bound one.
2078+- **Which Exo, first.** This is `exoharness/exo`, the recursive-self-improvement
2079+ agent harness, pinned at `baa07f6785547080d99bd2a7d3eab6d76b984e35` with tree
2080+ `0aff9139a166414fa51a09b66ba4785bae05b46b`. It is **not** exo labs'
2081+ `exo-explore/exo` cluster-inference appliance, which shares a name and nothing
2082+ else; omega#86 was closed for integrating the wrong one. The pin therefore
2083+ carries the repository as a field a test reads, alongside the commit and the
2084+ tree — both, for the same reason `omega-effectd` is pinned by release tag *and*
2085+ asset digest, because a commit id alone is satisfied by a rewritten history in
2086+ a clone nobody re-fetched. Exo declares itself unstable with the written house
2087+ rule "do not write fallback code or handle backwards compatibility."
2088+- **What Omega is integrating, stated plainly.** An unauthenticated local
2089+ harness whose agent has an unrestricted networked shell and can rebuild
2090+ itself. Exo has no approval prompt anywhere by design — its security model is
2091+ sandbox isolation, and its threat model assumes you *want* the agent to modify
2092+ itself. Omega adds the gate Exo does not have, and the gate is four refusals
2093+ the turn path runs **before** it sends, each from a live observation rather
2094+ than from configuration Omega wrote down earlier:
2095+ - **where Exo is** — `EXO_EXOHARNESS_URL` is inherited from the environment,
2096+ and setting it redirects the lane from the state root on disk to an HTTP
2097+ server that has no authentication and full access to Exo's secrets. It is
2098+ parsed through `LoopbackEndpoint`, whose only constructor refuses anything
2099+ that is not this machine, and whose refusal separates `0.0.0.0` and `::`
2100+ from loopback because those are the *plausible* mistakes;
2101+ - **which Exo** — `git` in the checkout answers with a remote, a commit, and a
2102+ tree, and the pin admits or refuses it;
2103+ - **which bytes** — the binary is measured and compared against the owner's
2104+ `omega_harness` pin ledger entry for `exo`, when the owner froze one;
2105+ - **which agent** — `exo agent show` is read, and a turn is refused when the
2106+ agent carries self-modification capability: runtime tool authoring, a tool
2107+ module (which is how `guardian_action` is installed), or a read-write mount
2108+ (which is how Exo's source tree is edited from inside the sandbox). Tier C
2109+ is out of scope, and this is its enforcement rather than a promise about it.
2110+ Networking is *reported and not refused*, because refusing it would refuse
2111+ every useful agent and say nothing about self-modification.
2112+- **User text is never argument syntax.** Exo accepts its global options *after*
2113+ the subcommand, so an unterminated prompt is not a string Exo receives — it is
2114+ Exo's command line. Driven against the pinned binary, a prompt of `--help` on
2115+ an otherwise correct `conversation send` **exits 0, prints Exo's usage text,
2116+ and runs no turn**; a lane without the terminator would have rendered that
2117+ usage text as the model's reply, silently and on a success exit code. Every
2118+ command line the lane can produce is a variant of a closed enum whose exact
2119+ argv shape is written down, and every value reachable from a person or a model
2120+ is emitted after `--`.
2121+- **Omega never configures Exo.** The admitted verbs are one send and four
2122+ reads. `create`, `update`, `delete`, `mount`, `set`, `register`, `configure`,
2123+ `serve`, and `repl` are unreachable — not filtered, unexpressible. The only
2124+ mutation the lane causes is Exo's own record of the turn it just ran.
2125+- **The executor class, decided rather than defaulted.** `ExecutorClass` is
2126+ closed at three by `OMEGA-AGENT-AC-04`, and it answers *who ran the work*. An
2127+ Exo thread reports `ExternalAcp`. Not `NativeLoop`, which is the first-party
2128+ claim and would present an unrestricted shell's output as Omega's own. Not
2129+ `EngineLane`, which *is* Full Auto authority — a record on it must carry a
2130+ `run_ref`, and owner gate 8 admits only an explicit human action into it. Exo
2131+ has no engine run and no receipt, so a lane reporting `EngineLane` would be a
2132+ fourth model-reachable door into Full Auto authority opened by adding an
2133+ executor, three of which were removed from OpenAgents Desktop the same day.
2134+ `ExternalAcp` fits because the class is about the executor and not the wire: a
2135+ separate process Omega does not own, carrying no run reference. Tier B swaps
2136+ the CLI for ACP and the class does not change, which is the check that the
2137+ class was about the right thing. A fourth variant was considered and **not
2138+ taken**; the argument is written in `crates/omega_exo_lane` so a later reader
2139+ can disagree with something concrete.
2140+- **The disclosure names Exo, its executor, and its model** —
2141+ `external_acp · exo/basic · provider not disclosed/gpt-5-mini`, from a real
2142+ turn. No field was added to `ExecutorDisclosure`: the record's field list is
2143+ closed on purpose. `provider` is genuinely absent because Exo's LLM binding
2144+ has no provider field at all, only an optional base URL, and saying "not
2145+ disclosed" is better than deriving `openai` from an absence.
2146+- **Coarse on purpose.** One shot per turn, no live text deltas, tool activity
2147+ after the fact. That is Exo's limit at this pin — its turn streaming is an
2148+ in-process enum consumed by its own REPL and serialised to no transport. Tier
2149+ B lifts it by contributing a transport upstream, and is **not** started here.
2150+- **Enforced by:** `the_exo_lane_drives_the_harness_exo_and_not_the_cluster_one`,
2151+ `the_exo_lane_puts_no_user_text_before_the_argument_terminator`,
2152+ `the_exo_lane_exposes_no_endpoint_off_this_machine`,
2153+ `the_exo_lane_opens_no_path_into_full_auto_authority`,
2154+ `an_exo_turn_checks_the_pin_and_the_agent_before_it_sends`, and
2155+ `the_exo_lane_is_reachable_from_omega_agent` in `crates/omega_deltas/`, the 35
2156+ unit checks in `crates/omega_exo_lane/`, and `drives_a_real_exo` in
2157+ `crates/agent_ui/`, which is `#[ignore]`d because it needs a real Exo.
2158+- **Falsified against a running Exo,** every edit probed before its run. Pointing
2159+ the checkout's remote at `exo-explore/exo` refused the turn by name. Enabling
2160+ `tool_creation` on the live agent refused it. Mounting Exo's own source tree
2161+ `rw` refused it. `EXO_EXOHARNESS_URL=http://100.64.7.9:4766` refused it before
2162+ any Exo process started. Dropping the argument terminator, making the turn
2163+ reader accept any stdout, reporting `EngineLane`, unwiring the lane from the
2164+ router, and moving the capability read after the send each failed their tests.
2165+ **One falsification found a real hole:** removing the disclosure's Exo arm
2166+ left the live test green, because it asserted only the agent id — which the
2167+ fallback also produces. The live test now asserts the model, which only that
2168+ arm supplies, and the same edit fails it. A second falsification silently
2169+ no-opped through a shell quoting error and was caught by its probe rather than
2170+ by its result.
2171+- **What this does not cover.** Cancellation: an Exo turn is one blocking
2172+ process with no interruption point that leaves its durable log consistent, so
2173+ the lane says so in a log line rather than pretending. Steer and queue
2174+ negotiate against a turn boundary the lane cannot see mid-turn, which is
2175+ Tier B's to fix. The lane reads Exo's durable event log after a turn and
2176+ currently only logs a failure to do so, because the turn has already run and
2177+ refusing it afterwards would describe a world that did not happen.
diff --git a/crates/agent_ui/Cargo.toml b/crates/agent_ui/Cargo.toml
index 4e73125210..58978b6b9f 100644
--- a/crates/agent_ui/Cargo.toml
+++ b/crates/agent_ui/Cargo.toml
@@ -80,7 +80,9 @@ menu.workspace = true
8080 multi_buffer.workspace = true
8181 notifications.workspace = true
8282 omega_effectd.workspace = true
83+omega_exo_lane.workspace = true
8384 omega_front_door.workspace = true
85+omega_harness.workspace = true
8486 omega_identity.workspace = true
8587 ordered-float.workspace = true
8688 parking_lot.workspace = true
@@ -100,6 +102,7 @@ sandbox.workspace = true
100102 schemars.workspace = true
101103 search.workspace = true
102104 serde.workspace = true
105+smol.workspace = true
103106 serde_json.workspace = true
104107 serde_json_lenient.workspace = true
105108 settings.workspace = true
diff --git a/crates/agent_ui/src/agent_ui.rs b/crates/agent_ui/src/agent_ui.rs
index 0b2cde6e6b..ba5ab7fa2e 100644
--- a/crates/agent_ui/src/agent_ui.rs
+++ b/crates/agent_ui/src/agent_ui.rs
@@ -24,6 +24,7 @@ mod mode_selector;
2424 mod model_selector;
2525 mod model_selector_popover;
2626 pub mod omega_executor_disclosure;
27+pub mod omega_exo_connection;
2728 pub mod omega_host_bridge;
2829 pub mod omega_router;
2930 pub mod omega_send_queue;
@@ -511,6 +512,18 @@ impl Agent {
511512 } else {
512513 crate::omega_router::RouteJournal::data_dir_path()
513514 },
515+ // OMEGA-DELTA-0042, omega#87. The Exo harness lane's
516+ // configuration. A stateless run gets a path inside the
517+ // temporary directory, which does not exist, so a rendering
518+ // harness never spawns somebody's Exo — the same reasoning as
519+ // the journal above, and the same reason the choice is made
520+ // here rather than inside the router.
521+ if std::env::var("ZED_STATELESS").is_ok() {
522+ std::env::temp_dir()
523+ .join(format!("omega-exo-lane-{}.json", std::process::id()))
524+ } else {
525+ crate::omega_exo_connection::ExoLaneConfig::data_dir_path()
526+ },
514527 )),
515528 Self::Custom { id: name } => {
516529 Rc::new(agent_servers::CustomAgentServer::new(name.clone()))
diff --git a/crates/agent_ui/src/omega_executor_disclosure.rs b/crates/agent_ui/src/omega_executor_disclosure.rs
index 2effaa250f..636aec6c9a 100644
--- a/crates/agent_ui/src/omega_executor_disclosure.rs
+++ b/crates/agent_ui/src/omega_executor_disclosure.rs
@@ -119,6 +119,34 @@ fn classify_connection(
119119 };
120120 }
121121
122+ // `OMEGA-DELTA-0042`, omega#87. The Exo harness lane. Recognised by its
123+ // concrete type for the same reason the native loop is: `agent_id()` on
124+ // this connection is *derived from what Exo said about itself*, so
125+ // classifying by it would let an Exo install decide its own class.
126+ //
127+ // Exo reports `ExternalAcp`. The reasoning — and the argument against a
128+ // fourth class — is in `omega_exo_lane`'s module documentation. What is
129+ // added here over the shared fallback below is the model: Exo does tell
130+ // Omega which model served, so the lane says so instead of "not disclosed".
131+ if let Some(exo) = connection
132+ .clone()
133+ .downcast::<crate::omega_exo_connection::ExoHarnessConnection>()
134+ {
135+ // `None` before Exo has been asked. An identity nobody observed is
136+ // absent, never invented.
137+ let identity = exo.identity();
138+ return ExecutorDisclosure {
139+ class: omega_exo_lane::EXO_EXECUTOR_CLASS,
140+ agent_id: identity
141+ .as_ref()
142+ .map_or_else(|| agent_id.clone(), omega_exo_lane::ExoLaneIdentity::agent_id),
143+ provider: identity.as_ref().and_then(|identity| identity.provider.clone()),
144+ model: identity.and_then(|identity| identity.model),
145+ run_ref: None,
146+ route: crate::omega_router::recorded_route(session_id),
147+ };
148+ }
149+
122150 // `AcpConnection` shares the fallback, but is recognised explicitly so an
123151 // unrecognised connection type leaves a trace instead of passing silently.
124152 if connection.downcast::<AcpConnection>().is_none() {
diff --git a/crates/agent_ui/src/omega_exo_connection.rs b/crates/agent_ui/src/omega_exo_connection.rs
new file mode 100644
index 0000000000..861f8a0ed7
--- /dev/null
+++ b/crates/agent_ui/src/omega_exo_connection.rs
@@ -0,0 +1,679 @@
1+//! Driving Exo. `OMEGA-DELTA-0042`, omega#87, Tier A.
2+//!
3+//! The law lives in `crates/omega_exo_lane`, which is a leaf and can be checked
4+//! in a second. This is the half that needs a process and a thread: it runs the
5+//! `exo` binary, reads what it printed, and pushes the result into an
6+//! `AcpThread` so the existing agent panel renders it like any other lane.
7+//!
8+//! # How the lane is reached
9+//!
10+//! Through the router omega#78 already built, and through nothing else. The Exo
11+//! connection registers as [`OmegaAgentConnection`]'s external executor, so a
12+//! thread reaches it exactly when a person pins `ExecutorClass::ExternalAcp` on
13+//! the disclosure line's pin control — a `PinGesture`, which has no variant for
14+//! a tool call, a slash command, a restored draft, or a model turn. Nothing here
15+//! adds a way to start authority: an unpinned thread still runs on the native
16+//! loop, and when no Exo lane is configured the pin falls back with
17+//! `RouteReason::ExternalAcpUnavailable`, which the user sees on the line.
18+//!
19+//! # What runs before every turn
20+//!
21+//! Four refusals, in order, all of them from live observation rather than from
22+//! configuration Omega wrote down earlier:
23+//!
24+//! 1. **Where Exo is.** `EXO_EXOHARNESS_URL`, which Exo reads from the
25+//! environment and which redirects it from the state root on disk to an
26+//! unauthenticated HTTP server, is parsed through [`LoopbackEndpoint`] and
27+//! refused unless it names this machine.
28+//! 2. **Which Exo.** `git` in the checkout answers with a remote, a commit, and
29+//! a tree, and [`EXO_PIN`] admits or refuses it. This is what stops the
30+//! omega#86 mistake — the other Exo — and an upstream that moved under a
31+//! no-backwards-compatibility house rule.
32+//! 3. **Which bytes.** The binary is hashed and compared against the owner's
33+//! pin ledger entry for `exo`, when the owner froze one.
34+//! 4. **Which agent.** `exo agent show` is read, and a turn is refused when the
35+//! agent carries self-modification capability — runtime tool authoring, a
36+//! tool module (which is how `guardian_action` is installed), or a
37+//! read-write mount. Tier C is out of scope, and this is the enforcement of
38+//! that rather than a promise about it.
39+//!
40+//! # What is deliberately absent
41+//!
42+//! No listener, no bind, no port, no proxy. Omega never stands between Exo's
43+//! unauthenticated endpoint and anything else — [`LoopbackEndpoint`] exists so
44+//! that a future caller who *does* need an address cannot build a non-loopback
45+//! one, and Tier A does not need an address at all, because the CLI talks to
46+//! the state root on disk. `omega_deltas` asserts the absence, because "we did
47+//! not add a listener" is the kind of thing that stays true only while somebody
48+//! is checking.
49+
50+use std::any::Any;
51+use std::cell::RefCell;
52+use std::collections::HashMap;
53+use std::path::PathBuf;
54+use std::process::Stdio;
55+use std::rc::Rc;
56+
57+use acp_thread::{AcpThread, AgentConnection};
58+use action_log::ActionLog;
59+use agent_client_protocol::schema::v1 as acp;
60+use anyhow::{Context as _, Result, anyhow, bail};
61+use gpui::{App, AppContext as _, Entity, SharedString, Task, WeakEntity};
62+use omega_exo_lane::{
63+ EXO_HARNESS_ID, EXO_PIN, ExoAgent, ExoCommand, ExoLaneIdentity, ExoModelBinding, ExoRoot,
64+ ExoTurn, LoopbackEndpoint, ObservedExoCheckout, admits_bytes,
65+};
66+use omega_harness::MeasuredDigest;
67+use project::{AgentId, Project};
68+use util::path_list::PathList;
69+
70+/// How many events the lane reads back from Exo's durable log after a turn.
71+///
72+/// A bound rather than "everything": Exo's log is append-only and a long-lived
73+/// conversation accumulates without limit, and a lane that read all of it to
74+/// render one turn would get slower for the whole life of the conversation.
75+const TOOL_ACTIVITY_EVENT_LIMIT: u32 = 64;
76+
77+/// Where the lane's configuration lives, under the Omega data directory.
78+const EXO_LANE_FILE: &str = "omega-exo-lane.json";
79+
80+/// The schema that file carries.
81+const EXO_LANE_SCHEMA: &str = "openagents.omega.exo_lane.v1";
82+
83+/// Everything Omega needs to reach one Exo install.
84+///
85+/// Every field names something Exo owns. None of them is something Omega
86+/// writes: the binary was built by whoever installed Exo, the checkout is
87+/// theirs, and the state root is Exo's — Omega only ever passes its path on a
88+/// command line. See `ExoRoot`.
89+#[derive(Clone, Debug, PartialEq, Eq)]
90+pub struct ExoLaneConfig {
91+ /// The `exo` binary.
92+ pub binary: PathBuf,
93+ /// The checkout the binary was built from, for the pin check.
94+ pub checkout: PathBuf,
95+ /// Exo's state root.
96+ pub root: ExoRoot,
97+ /// The agent slug the lane sends to.
98+ pub agent: String,
99+ /// The conversation slug the lane sends to.
100+ pub conversation: String,
101+}
102+
103+impl ExoLaneConfig {
104+ /// The lane file's path under the Omega data directory.
105+ #[must_use]
106+ pub fn data_dir_path() -> PathBuf {
107+ paths::data_dir().join("openagents").join(EXO_LANE_FILE)
108+ }
109+
110+ /// Read the lane file, if the owner wrote one.
111+ ///
112+ /// A machine with no Exo has no lane, and that is the ordinary case rather
113+ /// than an error: `None` means the router has no external executor
114+ /// registered and a pin to one falls back visibly. A file that exists and
115+ /// cannot be read is logged and treated as no lane, because a half-read
116+ /// configuration is how a lane ends up pointed at the wrong `.exo`.
117+ #[must_use]
118+ pub fn load(path: &std::path::Path) -> Option<Self> {
119+ let file = std::fs::read_to_string(path).ok()?;
120+ let value: serde_json::Value = serde_json::from_str(&file)
121+ .inspect_err(|error| {
122+ log::warn!("OMEGA-DELTA-0042: the Exo lane file is not JSON ({error}); no Exo lane");
123+ })
124+ .ok()?;
125+ if value.get("schema").and_then(serde_json::Value::as_str) != Some(EXO_LANE_SCHEMA) {
126+ log::warn!("OMEGA-DELTA-0042: the Exo lane file carries an unsupported schema");
127+ return None;
128+ }
129+ let field = |name: &str| {
130+ value
131+ .get(name)
132+ .and_then(serde_json::Value::as_str)
133+ .filter(|value| !value.trim().is_empty())
134+ .map(str::to_owned)
135+ };
136+ Some(Self {
137+ binary: PathBuf::from(field("binary")?),
138+ checkout: PathBuf::from(field("checkout")?),
139+ root: ExoRoot::at(field("root")?),
140+ agent: field("agent")?,
141+ conversation: field("conversation")?,
142+ })
143+ }
144+}
145+
146+/// Everything a turn needs, separated from the connection that owns it.
147+///
148+/// Held behind an `Rc` so a turn can be *moved into the spawned task* rather
149+/// than run on the thread that draws the window. `AgentConnection::prompt`
150+/// takes `&self`, so without this split the only way to reach the Exo process
151+/// from `prompt` would be to run it inline — which is exactly the blocking call
152+/// the workspace's clippy configuration disallows, and it disallows it because
153+/// an Exo turn calls a model and takes seconds.
154+struct ExoDriver {
155+ config: ExoLaneConfig,
156+ /// What Exo said about itself, cached so a disclosure render does not spawn
157+ /// a process. Refreshed by every turn, because the turn re-reads the agent
158+ /// anyway to check its capability.
159+ identity: RefCell<Option<ExoLaneIdentity>>,
160+ /// The digest the owner froze for `exo`, if any. Read once at construction
161+ /// from the harness pin ledger; a claim, never a measurement.
162+ frozen_digest: Option<String>,
163+}
164+
165+/// One Exo install, behind Omega Agent's router.
166+pub struct ExoHarnessConnection {
167+ driver: Rc<ExoDriver>,
168+ /// The threads this connection built, by session.
169+ sessions: RefCell<HashMap<acp::SessionId, WeakEntity<AcpThread>>>,
170+}
171+
172+impl ExoHarnessConnection {
173+ /// A connection to the Exo the lane file names.
174+ #[must_use]
175+ pub fn new(config: ExoLaneConfig, frozen_digest: Option<String>) -> Self {
176+ Self {
177+ driver: Rc::new(ExoDriver {
178+ config,
179+ identity: RefCell::new(None),
180+ frozen_digest,
181+ }),
182+ sessions: RefCell::new(HashMap::new()),
183+ }
184+ }
185+
186+ /// What this lane discloses, as far as it has been able to observe.
187+ ///
188+ /// `None` before the first turn: nothing has asked Exo who it is yet, and
189+ /// inventing an executor name would be the fabrication the disclosure
190+ /// record exists to prevent.
191+ #[must_use]
192+ pub fn identity(&self) -> Option<ExoLaneIdentity> {
193+ self.driver.identity.borrow().clone()
194+ }
195+
196+ /// The lane's configuration.
197+ #[must_use]
198+ pub fn config(&self) -> &ExoLaneConfig {
199+ &self.driver.config
200+ }
201+}
202+
203+impl ExoDriver {
204+ /// Run one `exo` command and return its stdout.
205+ ///
206+ /// Async, and spawned rather than blocking, because an Exo turn calls a
207+ /// model: it takes seconds, and running it on the thread that draws the
208+ /// window would freeze Omega for the length of somebody else's inference.
209+ async fn run(&self, command: &ExoCommand) -> Result<String> {
210+ let argv = command.argv(&self.config.root);
211+ let output = smol::process::Command::new(&self.config.binary)
212+ .args(&argv)
213+ .stdin(Stdio::null())
214+ .output()
215+ .await
216+ .with_context(|| format!("running {} {}", self.config.binary.display(), argv.join(" ")))?;
217+ if !output.status.success() {
218+ bail!(
219+ "exo {} exited {}: {}",
220+ command.shape(),
221+ output.status,
222+ String::from_utf8_lossy(&output.stderr).trim()
223+ );
224+ }
225+ Ok(String::from_utf8_lossy(&output.stdout).into_owned())
226+ }
227+
228+ /// Refuse an Exo that is not on this machine.
229+ ///
230+ /// Exo reads `EXO_EXOHARNESS_URL` from its environment and, when it is set,
231+ /// stops using the state root on disk and talks to an `exo serve` over
232+ /// HTTP instead. That server has **no authentication** — a client may send
233+ /// a bearer token and the server never checks it — and it exposes Exo's
234+ /// whole request protocol, secrets included. Loopback is the entire
235+ /// boundary, and Exo's own documentation says so.
236+ ///
237+ /// Omega never passes that flag; [`ExoCommand`] cannot express it. But the
238+ /// lane inherits Omega's environment, which is how Exo is meant to receive
239+ /// its own configuration, and a variable set there would redirect the lane
240+ /// off-machine without a single Omega command line changing. So the
241+ /// environment is read and parsed through [`LoopbackEndpoint`], whose only
242+ /// constructor refuses anything that is not this machine.
243+ fn check_endpoint(&self) -> Result<()> {
244+ let Ok(url) = std::env::var("EXO_EXOHARNESS_URL") else {
245+ // Unset is the ordinary case and the safe one: the CLI reads the
246+ // state root on disk and no socket exists at all.
247+ return Ok(());
248+ };
249+ let endpoint = LoopbackEndpoint::parse(&url).map_err(|refusal| {
250+ anyhow!(
251+ "{refusal}: the Exo lane will not talk to {url}. Exo's server has no \
252+ authentication and full access to its secrets."
253+ )
254+ })?;
255+ log::info!("OMEGA-DELTA-0042: the Exo lane is pointed at {endpoint}, which is loopback");
256+ Ok(())
257+ }
258+
259+ /// Refuse an Exo that is not the pinned one.
260+ async fn check_pin(&self) -> Result<()> {
261+ let git = async |args: &[&str]| -> Result<String> {
262+ let output = smol::process::Command::new("git")
263+ .arg("-C")
264+ .arg(&self.config.checkout)
265+ .args(args)
266+ .stdin(Stdio::null())
267+ .output()
268+ .await
269+ .context("reading the Exo checkout with git")?;
270+ if !output.status.success() {
271+ bail!("git {args:?} failed in the Exo checkout");
272+ }
273+ Ok(String::from_utf8_lossy(&output.stdout).trim().to_owned())
274+ };
275+ let observed = ObservedExoCheckout {
276+ upstream: git(&["config", "--get", "remote.origin.url"]).await?,
277+ commit: git(&["rev-parse", "HEAD"]).await?,
278+ tree: git(&["rev-parse", "HEAD^{tree}"]).await?,
279+ };
280+ EXO_PIN.admits(&observed).map_err(|mismatch| {
281+ anyhow!(
282+ "{mismatch}: the Exo lane is pinned to {} at {}",
283+ EXO_PIN.upstream,
284+ EXO_PIN.source_commit
285+ )
286+ })?;
287+
288+ let bytes = smol::fs::read(&self.config.binary)
289+ .await
290+ .context("reading the exo binary")?;
291+ admits_bytes(self.frozen_digest.as_deref(), &MeasuredDigest::measure(&bytes))
292+ .map_err(|mismatch| anyhow!("{mismatch}"))?;
293+ Ok(())
294+ }
295+
296+ /// Read the agent, refuse self-modification, and resolve the disclosure.
297+ async fn check_agent(&self) -> Result<ExoLaneIdentity> {
298+ let shown = self
299+ .run(&ExoCommand::ShowAgent {
300+ agent: self.config.agent.clone(),
301+ })
302+ .await?;
303+ let agent = ExoAgent::parse(&shown).map_err(|error| {
304+ anyhow!("{error}; the Omega lane refuses an Exo agent it cannot read")
305+ })?;
306+ agent
307+ .admits_lane_turn()
308+ .map_err(|refusal| anyhow!("{refusal}"))?;
309+
310+ let bindings = ExoModelBinding::read_table(&self.run(&ExoCommand::ListModels).await?);
311+ let identity = ExoLaneIdentity::resolve(&agent, &bindings);
312+ *self.identity.borrow_mut() = Some(identity.clone());
313+ Ok(identity)
314+ }
315+
316+ /// Everything one turn does outside GPUI: the three refusals, the send, and
317+ /// the read-back.
318+ async fn drive_turn(&self, prompt: String) -> Result<ExoTurn> {
319+ self.check_endpoint()?;
320+ self.check_pin().await?;
321+ self.check_agent().await?;
322+ let stdout = self
323+ .run(&ExoCommand::SendTurn {
324+ agent: self.config.agent.clone(),
325+ conversation: self.config.conversation.clone(),
326+ prompt,
327+ })
328+ .await?;
329+ let turn = ExoTurn::read(&stdout).map_err(|error| anyhow!("{error}"))?;
330+ // The durable log is read for its own sake even though the send output
331+ // already carried the tool lines: Exo's log is the record, the printed
332+ // lines are a rendering of it, and a lane that only ever read the
333+ // rendering could not tell a truncated turn from a complete one. Failure
334+ // to read it does not fail the turn, because the turn already ran.
335+ if let Err(error) = self
336+ .run(&ExoCommand::ReadEvents {
337+ agent: self.config.agent.clone(),
338+ conversation: self.config.conversation.clone(),
339+ limit: TOOL_ACTIVITY_EVENT_LIMIT,
340+ })
341+ .await
342+ {
343+ log::warn!("OMEGA-DELTA-0042: Exo's durable log could not be read back: {error:#}");
344+ }
345+ Ok(turn)
346+ }
347+}
348+
349+/// The Exo lane the owner configured, if they configured one.
350+///
351+/// Called once, when the router is built. A machine with no lane file gets
352+/// `None` and the router registers no external executor — which is the ordinary
353+/// case, and is why the return type is an `Option` rather than a `Result` that
354+/// every caller would have to decide to ignore.
355+#[must_use]
356+pub fn connect_configured_lane(lane_path: &std::path::Path) -> Option<Rc<dyn AgentConnection>> {
357+ let config = ExoLaneConfig::load(lane_path)?;
358+ let frozen = frozen_exo_digest();
359+ log::info!(
360+ "OMEGA-DELTA-0042: Exo harness lane configured at {} ({} {}), pin {}",
361+ config.root.as_str(),
362+ config.agent,
363+ config.conversation,
364+ if frozen.is_some() { "frozen" } else { "unfrozen" }
365+ );
366+ Some(Rc::new(ExoHarnessConnection::new(config, frozen)))
367+}
368+
369+/// The digest the owner froze for `exo`, from the harness pin ledger.
370+///
371+/// Read here rather than inside the connection so the connection takes a value
372+/// and stays testable without a filesystem. A ledger that cannot be read is not
373+/// an empty ledger — but it is also not this file's decision, so the absence is
374+/// logged and the pin check proceeds unfrozen, exactly as it does on a machine
375+/// where the owner never froze Exo.
376+fn frozen_exo_digest() -> Option<String> {
377+ let path = paths::data_dir()
378+ .join("openagents")
379+ .join("external_agents")
380+ .join(omega_harness::HARNESS_PIN_LEDGER_FILE_NAME);
381+ let file = std::fs::read_to_string(path).ok()?;
382+ match omega_harness::decode_harness_pin_ledger(&file) {
383+ Ok(ledger) => ledger
384+ .pin(EXO_HARNESS_ID)
385+ .map(|pin| pin.digest.clone()),
386+ Err(error) => {
387+ log::warn!("OMEGA-DELTA-0042: the harness pin ledger could not be read ({error})");
388+ None
389+ }
390+ }
391+}
392+
393+/// The prompt text a request carries, joined.
394+fn prompt_text(request: &acp::PromptRequest) -> String {
395+ request
396+ .prompt
397+ .iter()
398+ .filter_map(|block| match block {
399+ acp::ContentBlock::Text(text) => Some(text.text.as_str()),
400+ _ => None,
401+ })
402+ .collect::<Vec<_>>()
403+ .join("\n")
404+}
405+
406+impl AgentConnection for ExoHarnessConnection {
407+ /// A display label, and only that. omega#77 classifies by downcast because
408+ /// this is renameable; see `omega_executor_disclosure`.
409+ fn agent_id(&self) -> AgentId {
410+ AgentId::new(
411+ self.identity()
412+ .map_or_else(|| EXO_HARNESS_ID.to_owned(), |identity| identity.agent_id()),
413+ )
414+ }
415+
416+ fn telemetry_id(&self) -> SharedString {
417+ EXO_HARNESS_ID.into()
418+ }
419+
420+ fn agent_version(&self) -> Option<SharedString> {
421+ Some(EXO_PIN.version.into())
422+ }
423+
424+ fn new_session(
425+ self: Rc<Self>,
426+ project: Entity<Project>,
427+ work_dirs: PathList,
428+ cx: &mut App,
429+ ) -> Task<Result<Entity<AcpThread>>> {
430+ // The session is Exo's conversation. Tier A binds one Omega thread to
431+ // one Exo conversation rather than minting a new conversation per
432+ // thread, because minting one would mean Omega creating state inside
433+ // `.exo`, which is the thing this lane does not do.
434+ let session_id = acp::SessionId::new(format!(
435+ "exo/{}/{}",
436+ self.driver.config.agent, self.driver.config.conversation
437+ ));
438+ let action_log = cx.new(|_| ActionLog::new(project.clone()));
439+ let thread = cx.new(|cx| {
440+ AcpThread::new(
441+ None,
442+ Some(SharedString::from(format!(
443+ "Exo · {}",
444+ self.driver.config.conversation
445+ ))),
446+ Some(work_dirs),
447+ self.clone(),
448+ project,
449+ action_log,
450+ session_id.clone(),
451+ watch::Receiver::constant(
452+ acp::PromptCapabilities::new().embedded_context(true),
453+ ),
454+ cx,
455+ )
456+ });
457+ self.sessions
458+ .borrow_mut()
459+ .insert(session_id, thread.downgrade());
460+ Task::ready(Ok(thread))
461+ }
462+
463+ fn auth_methods(&self) -> &[acp::AuthMethod] {
464+ &[]
465+ }
466+
467+ fn authenticate(&self, _method: acp::AuthMethodId, _cx: &mut App) -> Task<Result<()>> {
468+ Task::ready(Err(anyhow!(
469+ "Exo holds its own credentials; Omega never edits them"
470+ )))
471+ }
472+
473+ /// One shot. No deltas, by Exo's limit at this pin — see
474+ /// `omega_exo_lane::turn`.
475+ fn prompt(&self, params: acp::PromptRequest, cx: &mut App) -> Task<Result<acp::PromptResponse>> {
476+ let Some(thread) = self.sessions.borrow().get(¶ms.session_id).cloned() else {
477+ return Task::ready(Err(anyhow!("no Exo thread for {}", params.session_id.0)));
478+ };
479+ let prompt = prompt_text(¶ms);
480+ // Cloned into the task rather than driven here: the turn must not run
481+ // on the thread that draws the window. See `run`.
482+ let driver = Rc::clone(&self.driver);
483+ cx.spawn(async move |cx| {
484+ let turn = driver.drive_turn(prompt).await?;
485+ thread.update(cx, |thread, cx| {
486+ for tool in &turn.tools {
487+ let update = acp::SessionUpdate::AgentMessageChunk(acp::ContentChunk::new(
488+ format!("`{}` → {}", tool.name, tool.output).into(),
489+ ));
490+ let _ = thread.handle_session_update(update, cx);
491+ }
492+ let _ = thread.handle_session_update(
493+ acp::SessionUpdate::AgentMessageChunk(acp::ContentChunk::new(
494+ turn.text.clone().into(),
495+ )),
496+ cx,
497+ );
498+ })?;
499+ Ok(acp::PromptResponse::new(acp::StopReason::EndTurn))
500+ })
501+ }
502+
503+ /// Exo's turn is one blocking process. There is nothing to cancel that
504+ /// would leave Exo's durable log consistent, so the lane says so rather
505+ /// than pretending.
506+ fn cancel(&self, session_id: &acp::SessionId, _cx: &mut App) {
507+ log::info!(
508+ "OMEGA-DELTA-0042: cancel is not available on the Exo lane; \
509+ session {} runs one shot per turn",
510+ session_id.0
511+ );
512+ }
513+
514+ fn into_any(self: Rc<Self>) -> Rc<dyn Any> {
515+ self
516+ }
517+}
518+
519+#[cfg(test)]
520+mod tests {
521+ use super::*;
522+
523+ fn lane_file(schema: &str) -> String {
524+ serde_json::json!({
525+ "schema": schema,
526+ "binary": "/opt/exo/target/release/exo",
527+ "checkout": "/opt/exo",
528+ "root": "/opt/exo/.exo",
529+ "agent": "omega-lane",
530+ "conversation": "tier-a",
531+ })
532+ .to_string()
533+ }
534+
535+ fn write(contents: &str) -> (tempfile::TempDir, PathBuf) {
536+ let dir = tempfile::tempdir().expect("a temp dir");
537+ let path = dir.path().join(EXO_LANE_FILE);
538+ std::fs::write(&path, contents).expect("write");
539+ (dir, path)
540+ }
541+
542+ #[test]
543+ fn a_lane_file_names_one_exo_install() {
544+ let (_dir, path) = write(&lane_file(EXO_LANE_SCHEMA));
545+ let config = ExoLaneConfig::load(&path).expect("a lane");
546+ assert_eq!(config.agent, "omega-lane");
547+ assert_eq!(config.conversation, "tier-a");
548+ assert_eq!(config.root.as_str(), "/opt/exo/.exo");
549+ }
550+
551+ /// A machine with no Exo has no lane. This is the ordinary case, and it
552+ /// must not be an error.
553+ #[test]
554+ fn no_lane_file_means_no_lane() {
555+ let dir = tempfile::tempdir().expect("a temp dir");
556+ assert_eq!(ExoLaneConfig::load(&dir.path().join(EXO_LANE_FILE)), None);
557+ }
558+
559+ /// A file this build cannot read is no lane rather than a partially trusted
560+ /// one. A lane pointed at the wrong `.exo` is worse than no lane.
561+ #[test]
562+ fn an_unreadable_lane_file_is_no_lane() {
563+ for contents in [
564+ "not json".to_owned(),
565+ lane_file("openagents.omega.exo_lane.v0"),
566+ serde_json::json!({ "schema": EXO_LANE_SCHEMA, "agent": "a" }).to_string(),
567+ lane_file(EXO_LANE_SCHEMA).replace("omega-lane", ""),
568+ ] {
569+ let (_dir, path) = write(&contents);
570+ assert_eq!(ExoLaneConfig::load(&path), None, "{contents}");
571+ }
572+ }
573+
574+ /// The lane, against a real Exo. `#[ignore]`d because it needs one: a
575+ /// built `exo` at the pinned commit, a configured agent and conversation,
576+ /// and whatever credential that agent's model binding resolves to. Exo
577+ /// `#[ignore]`s its own heavy integration cells for the same reason.
578+ ///
579+ /// Point it at an install with the same lane file the product reads:
580+ ///
581+ /// ```text
582+ /// OMEGA_EXO_LANE_FILE=/path/to/omega-exo-lane.json \
583+ /// cargo test -p agent_ui drives_a_real_exo -- --ignored --nocapture
584+ /// ```
585+ ///
586+ /// This is the acceptance path end to end: the router's external executor
587+ /// builds a thread, a turn runs through Exo's CLI, the reply lands in the
588+ /// thread, and the thread discloses who ran it.
589+ #[gpui::test]
590+ #[ignore = "needs a local Exo install at the pinned commit"]
591+ async fn drives_a_real_exo(cx: &mut gpui::TestAppContext) {
592+ use crate::omega_executor_disclosure::ThreadExecutorDisclosure as _;
593+
594+ let Ok(lane_file) = std::env::var("OMEGA_EXO_LANE_FILE") else {
595+ panic!("set OMEGA_EXO_LANE_FILE to a lane file; see this test's docs");
596+ };
597+ let config = ExoLaneConfig::load(std::path::Path::new(&lane_file))
598+ .expect("the lane file names an Exo install");
599+
600+ // The turn runs a real process against a real model, so this test
601+ // waits on wall-clock I/O rather than on the deterministic scheduler.
602+ // That is the point of it: the same await that parks here is what keeps
603+ // the window drawing in production, and the workspace's clippy
604+ // configuration disallows the blocking call that would not park.
605+ cx.executor().allow_parking();
606+ crate::test_support::init_test(cx);
607+ let fs = fs::FakeFs::new(cx.executor());
608+ let project = Project::test(fs, [], cx).await;
609+
610+ let connection: Rc<ExoHarnessConnection> =
611+ Rc::new(ExoHarnessConnection::new(config, None));
612+ let thread = cx
613+ .update(|cx| {
614+ connection
615+ .clone()
616+ .new_session(project, PathList::default(), cx)
617+ })
618+ .await
619+ .expect("a session on the Exo lane");
620+
621+ let session_id = thread.read_with(cx, |thread, _| thread.session_id().clone());
622+ let marker = "OMEGA-EXO-TIER-A";
623+ let request = acp::PromptRequest::new(
624+ session_id,
625+ vec![acp::ContentBlock::Text(acp::TextContent::new(format!(
626+ "Reply with exactly the word {marker} and nothing else."
627+ )))],
628+ );
629+ let response = cx
630+ .update(|cx| AgentConnection::prompt(connection.as_ref(), request, cx))
631+ .await
632+ .expect("Exo ran the turn");
633+ assert_eq!(response.stop_reason, acp::StopReason::EndTurn);
634+
635+ let rendered = thread.read_with(cx, |thread, cx| thread.to_markdown(cx));
636+ assert!(
637+ rendered.contains(marker),
638+ "the Exo reply did not reach the thread: {rendered}"
639+ );
640+
641+ let disclosure = thread.read_with(cx, |thread, cx| thread.omega_executor_disclosure(cx));
642+ assert!(disclosure.is_coherent(), "{disclosure:?}");
643+ assert_eq!(disclosure.class, omega_exo_lane::EXO_EXECUTOR_CLASS);
644+ assert_eq!(disclosure.run_ref, None);
645+ let identity = connection.identity().expect("Exo told the lane who it is");
646+ assert!(disclosure.agent_id.starts_with("exo/"), "{disclosure:?}");
647+ // The parts that only the Exo arm of `classify_connection` can supply.
648+ // Without them this assertion set passes on the shared external-agent
649+ // fallback, which discloses no model at all — and a disclosure that
650+ // says "model not disclosed" about an executor that *did* disclose one
651+ // is the silent failure this test exists to catch. It was silent here
652+ // once, on 2026-07-26, until this line was added.
653+ assert_eq!(
654+ disclosure.model, identity.model,
655+ "the disclosure dropped the model Exo reported: {disclosure:?}"
656+ );
657+ assert!(
658+ disclosure.model.is_some(),
659+ "Exo reported a model and the thread did not disclose it: {disclosure:?}"
660+ );
661+ println!("executor disclosure: {}", disclosure.label());
662+ println!("exo identity: {identity:?}");
663+ }
664+
665+ /// The prompt reaches Exo as text and nothing else. A content block this
666+ /// build does not carry is dropped rather than rendered into the command
667+ /// line as a debug string.
668+ #[test]
669+ fn only_text_reaches_exos_command_line() {
670+ let request = acp::PromptRequest::new(
671+ acp::SessionId::new("exo/omega-lane/tier-a"),
672+ vec![
673+ acp::ContentBlock::Text(acp::TextContent::new(String::from("first"))),
674+ acp::ContentBlock::Text(acp::TextContent::new(String::from("second"))),
675+ ],
676+ );
677+ assert_eq!(prompt_text(&request), "first\nsecond");
678+ }
679+}
diff --git a/crates/agent_ui/src/omega_router.rs b/crates/agent_ui/src/omega_router.rs
index b5223ac5c6..c693296cbe 100644
--- a/crates/agent_ui/src/omega_router.rs
+++ b/crates/agent_ui/src/omega_router.rs
@@ -766,15 +766,24 @@ pub struct OmegaRouterServer {
766766 /// Where decisions are written down. Chosen by the caller; see
767767 /// [`RouteJournal::data_dir_path`].
768768 journal_path: PathBuf,
769+ /// Where the Exo harness lane is configured, if the owner configured one.
770+ /// `OMEGA-DELTA-0042`. Chosen by the caller for the same reason the journal
771+ /// path is: a harness must not read the owner's real data directory.
772+ exo_lane_path: PathBuf,
769773 }
770774
771775 impl OmegaRouterServer {
772776 /// A router server over the native agent server, journalling to `journal_path`.
773777 #[must_use]
774- pub fn new(native: agent::NativeAgentServer, journal_path: PathBuf) -> Self {
778+ pub fn new(
779+ native: agent::NativeAgentServer,
780+ journal_path: PathBuf,
781+ exo_lane_path: PathBuf,
782+ ) -> Self {
775783 Self {
776784 native,
777785 journal_path,
786+ exo_lane_path,
778787 }
779788 }
780789
@@ -802,12 +811,22 @@ impl agent_servers::AgentServer for OmegaRouterServer {
802811 ) -> Task<Result<Rc<dyn AgentConnection>>> {
803812 let native = self.native.connect(delegate, project, cx);
804813 let journal_path = self.journal_path.clone();
814+ let exo_lane_path = self.exo_lane_path.clone();
805815 cx.spawn(async move |_cx| {
806816 let native = native.await?;
807- let router = Rc::new(OmegaAgentConnection::new(
808- native,
809- RouteJournal::at(journal_path),
810- ));
817+ let mut router = OmegaAgentConnection::new(native, RouteJournal::at(journal_path));
818+ // `OMEGA-DELTA-0042`, omega#87. The Exo harness lane, when the owner
819+ // configured one. Registered as the external executor rather than
820+ // as its own class: see `omega_exo_lane`'s module docs for why an
821+ // Exo thread reports `ExternalAcp` and why it must not report an
822+ // engine lane. A machine with no Exo registers nothing, and a pin
823+ // to the external executor then falls back visibly with
824+ // `RouteReason::ExternalAcpUnavailable`.
825+ if let Some(exo) = crate::omega_exo_connection::connect_configured_lane(&exo_lane_path)
826+ {
827+ router = router.with_external_acp(exo);
828+ }
829+ let router = Rc::new(router);
811830 publish_active_router(&router);
812831 Ok(router as Rc<dyn AgentConnection>)
813832 })
diff --git a/crates/omega_deltas/src/omega_deltas.rs b/crates/omega_deltas/src/omega_deltas.rs
index e79510afa8..26f6a4e286 100644
--- a/crates/omega_deltas/src/omega_deltas.rs
+++ b/crates/omega_deltas/src/omega_deltas.rs
@@ -69,6 +69,7 @@ pub const ENFORCED_DELTAS: &[&str] = &[
6969 "OMEGA-DELTA-0039",
7070 "OMEGA-DELTA-0040",
7171 "OMEGA-DELTA-0041",
72+ "OMEGA-DELTA-0042",
7273 ];
7374
7475 /// OMEGA-DELTA-0036. The uninstall script embedded in the shipped `cli`.
@@ -1865,6 +1866,81 @@ fn is_test_path(relative: &str) -> bool {
18651866 }
18661867
18671868
1869+// ------ OMEGA-DELTA-0042
1870+
1871+/// OMEGA-DELTA-0042. The Exo harness lane's law. A leaf, checkable in a second.
1872+pub const EXO_LANE_LAW_PATH: &str = "crates/omega_exo_lane/src/omega_exo_lane.rs";
1873+
1874+/// OMEGA-DELTA-0042. Which Exo the lane admits.
1875+pub const EXO_LANE_PIN_PATH: &str = "crates/omega_exo_lane/src/pin.rs";
1876+
1877+/// OMEGA-DELTA-0042. Every command line the lane can produce.
1878+pub const EXO_LANE_COMMAND_PATH: &str = "crates/omega_exo_lane/src/command.rs";
1879+
1880+/// OMEGA-DELTA-0042. The half that spawns a process and builds a thread.
1881+pub const EXO_CONNECTION_PATH: &str = "crates/agent_ui/src/omega_exo_connection.rs";
1882+
1883+/// OMEGA-DELTA-0042. The two Exos, which share only a name.
1884+///
1885+/// omega#86 was closed for targeting the wrong one — exo labs'
1886+/// `exo-explore/exo` cluster-inference appliance — and omega#87 supersedes it
1887+/// with `exoharness/exo`, the agent harness. A repository name is a cheap thing
1888+/// to get wrong twice, so it is a checked fact rather than a remembered one.
1889+pub const EXO_HARNESS_UPSTREAM: &str = "exoharness/exo";
1890+
1891+/// OMEGA-DELTA-0042. The other Exo, which must appear nowhere as a target.
1892+pub const EXO_CLUSTER_UPSTREAM: &str = "exo-explore";
1893+
1894+/// OMEGA-DELTA-0042. Placeholders in `ADMITTED_LANE_ARGV` that stand for text
1895+/// reachable from a person or a model.
1896+///
1897+/// Exo accepts its global options *after* the subcommand, so an unterminated
1898+/// prompt is not a string Exo receives — it is Exo's command line. Driven
1899+/// against the pinned Exo, a prompt of `--help` exits 0, prints usage, and runs
1900+/// no turn.
1901+pub const EXO_LANE_USER_TEXT_SLOTS: &[&str] = &["<agent>", "<conversation>", "<prompt>"];
1902+
1903+/// OMEGA-DELTA-0042. Vocabulary that would mean Omega is standing between Exo's
1904+/// unauthenticated endpoint and something else.
1905+///
1906+/// `exo serve` has no authentication and full secret access; its own
1907+/// documentation says loopback is the entire boundary. Tier A needs no address
1908+/// at all — the CLI reaches the state root on disk — so any of this appearing in
1909+/// the lane is a surface nobody asked for.
1910+pub const EXO_OFF_MACHINE_TOKENS: &[(&str, &str)] = &[
1911+ ("a listener", "TcpListener"),
1912+ ("a listener", "bind("),
1913+ ("a listener", "0.0.0.0"),
1914+ ("a proxy", "proxy"),
1915+ ("a bearer token Exo never checks", "bearer"),
1916+];
1917+
1918+/// OMEGA-DELTA-0042. Flags that would point Exo somewhere other than the state
1919+/// root on disk.
1920+///
1921+/// Checked against the *argv table* rather than the whole lane, because the
1922+/// lane has to be able to name `EXO_EXOHARNESS_URL` in order to refuse an
1923+/// off-loopback one it inherited from the environment. Omega passing the flag
1924+/// and Omega refusing the variable are opposite acts that share a spelling.
1925+pub const EXO_REDIRECTING_FLAGS: &[&str] = &["exoharness-url", "--url", "serve", "bearer-env"];
1926+
1927+/// OMEGA-DELTA-0042, and owner gate 8 behind it.
1928+///
1929+/// *No model-initiated path may start Full Auto authority.* Three such paths
1930+/// were removed from OpenAgents Desktop on 2026-07-25, one of them a rename of
1931+/// another. An Exo agent has an unrestricted networked shell and can rebuild
1932+/// itself, which makes it exactly the caller that gate exists for — so adding an
1933+/// executor lane must not open a fourth door. The lane names none of this
1934+/// vocabulary, and the check is that it cannot start naming it quietly.
1935+pub const EXO_FULL_AUTO_TOKENS: &[&str] = &[
1936+ "LaunchOrigin",
1937+ "full_auto",
1938+ "FullAuto",
1939+ "PinGesture",
1940+ "EngineLane",
1941+ "run_ref: Some",
1942+];
1943+
18681944 // ------ OMEGA-DELTA-0032
18691945
18701946 /// OMEGA-DELTA-0032. The law that decides what a send during a turn does.
@@ -4799,6 +4875,31 @@ mod tests {
47994875 None
48004876 }
48014877
4878+ /// OMEGA-DELTA-0042. A source file with its test module removed.
4879+ ///
4880+ /// Every scan below asks "does the shipped lane name this?", and the tests
4881+ /// beside it must be free to name exactly the things it refuses — a test
4882+ /// that asserts the wrong Exo is rejected has to write the wrong Exo down.
4883+ /// Scanning the whole file would make those tests unwritable, which is the
4884+ /// wrong pressure: it would push the refusals out of the file rather than
4885+ /// into it.
4886+ fn production_source(source: &str) -> &str {
4887+ source
4888+ .split_once("\n#[cfg(test)]")
4889+ .map_or(source, |(before, _)| before)
4890+ }
4891+
4892+ /// OMEGA-DELTA-0042. Whether a token is named in code rather than in prose.
4893+ fn named_in_code(source: &str, token: &str) -> bool {
4894+ production_source(source)
4895+ .lines()
4896+ .filter(|line| line.contains(token))
4897+ .any(|line| {
4898+ let line = line.trim_start();
4899+ !line.starts_with("//") && !line.starts_with("*")
4900+ })
4901+ }
4902+
48024903 /// OMEGA-DELTA-0034. The front door works with no project open.
48034904 ///
48044905 /// Checked in both directions. Upstream's guard restored on a front-door
@@ -5979,6 +6080,271 @@ mod tests {
59796080 It is a field that reads like an observation, so it has to be one."
59806081 );
59816082 }
6083+
6084+ // ------ OMEGA-DELTA-0042
6085+
6086+ /// OMEGA-DELTA-0042. The Exo the lane drives is the agent harness.
6087+ ///
6088+ /// omega#86 was closed for integrating exo labs' cluster-inference
6089+ /// appliance, which shares a name with the harness and nothing else. The
6090+ /// pin therefore carries the repository as a *field*, and this reads it, so
6091+ /// the distinction survives somebody skimming a doc comment.
6092+ #[test]
6093+ fn the_exo_lane_drives_the_harness_exo_and_not_the_cluster_one() {
6094+ let path = repository_path(EXO_LANE_PIN_PATH);
6095+ let source = std::fs::read_to_string(&path)
6096+ .unwrap_or_else(|error| panic!("cannot read {}: {error}", path.display()));
6097+ let upstream = source
6098+ .split_once("upstream: \"")
6099+ .and_then(|(_, rest)| rest.split_once('"'))
6100+ .expect("EXO_PIN names an upstream")
6101+ .0;
6102+ assert!(
6103+ upstream.ends_with(EXO_HARNESS_UPSTREAM),
6104+ "OMEGA-DELTA-0042: the Exo pin names {upstream}, which is not \
6105+ {EXO_HARNESS_UPSTREAM}. omega#86 made this mistake once."
6106+ );
6107+
6108+ for relative in [EXO_LANE_LAW_PATH, EXO_LANE_PIN_PATH, EXO_CONNECTION_PATH] {
6109+ let path = repository_path(relative);
6110+ let source = std::fs::read_to_string(&path)
6111+ .unwrap_or_else(|error| panic!("cannot read {}: {error}", path.display()));
6112+ let targeted = named_in_code(&source, EXO_CLUSTER_UPSTREAM);
6113+ assert!(
6114+ !targeted,
6115+ "OMEGA-DELTA-0042: {} names {EXO_CLUSTER_UPSTREAM} outside a \
6116+ comment, which would make the wrong Exo a target rather than \
6117+ a warning.",
6118+ path.display()
6119+ );
6120+ }
6121+ }
6122+
6123+ /// OMEGA-DELTA-0042. No text from outside Omega can become an Exo flag.
6124+ ///
6125+ /// Read off the written shapes rather than off the builder, because the
6126+ /// shapes are what a reviewer reads. Exo takes its global options after the
6127+ /// subcommand, so a shape that put `<prompt>` before the terminator would
6128+ /// hand the command line to whoever typed the prompt — and at this pin that
6129+ /// failure is *silent*: `--help` as a prompt exits 0 with usage text and no
6130+ /// turn.
6131+ #[test]
6132+ fn the_exo_lane_puts_no_user_text_before_the_argument_terminator() {
6133+ let path = repository_path(EXO_LANE_COMMAND_PATH);
6134+ let source = std::fs::read_to_string(&path)
6135+ .unwrap_or_else(|error| panic!("cannot read {}: {error}", path.display()));
6136+ let table = source
6137+ .split_once("pub const ADMITTED_LANE_ARGV")
6138+ .and_then(|(_, rest)| rest.split_once("\n];"))
6139+ .expect("the admitted argv table is present")
6140+ .0;
6141+
6142+ let mut shapes = 0usize;
6143+ for shape in table.split(" (\n").skip(1) {
6144+ shapes += 1;
6145+ let tokens: Vec<&str> = shape
6146+ .match_indices('"')
6147+ .map(|(offset, _)| offset)
6148+ .collect::<Vec<_>>()
6149+ .chunks_exact(2)
6150+ .map(|pair| &shape[pair[0] + 1..pair[1]])
6151+ .collect();
6152+ let terminator = tokens.iter().position(|token| *token == "--");
6153+ for slot in EXO_LANE_USER_TEXT_SLOTS {
6154+ let Some(at) = tokens.iter().position(|token| token == slot) else {
6155+ continue;
6156+ };
6157+ let Some(terminator) = terminator else {
6158+ panic!(
6159+ "OMEGA-DELTA-0042: an admitted Exo command carries {slot} \
6160+ and emits no argument terminator, so the value is Exo's \
6161+ command line rather than its input: {tokens:?}"
6162+ );
6163+ };
6164+ assert!(
6165+ at > terminator,
6166+ "OMEGA-DELTA-0042: an admitted Exo command puts {slot} \
6167+ before the argument terminator: {tokens:?}"
6168+ );
6169+ }
6170+ }
6171+ assert!(
6172+ shapes >= 4,
6173+ "OMEGA-DELTA-0042: the admitted argv table parsed as {shapes} \
6174+ shapes, so this check is reading nothing."
6175+ );
6176+ }
6177+
6178+ /// OMEGA-DELTA-0042. Omega never puts Exo on a network.
6179+ ///
6180+ /// Exo's one server has no authentication and full access to its secrets;
6181+ /// loopback is the entire boundary and Exo's own documentation says so.
6182+ /// Tier A needs no address at all, so anything here is a surface that was
6183+ /// added rather than required.
6184+ #[test]
6185+ fn the_exo_lane_exposes_no_endpoint_off_this_machine() {
6186+ for relative in [EXO_CONNECTION_PATH, EXO_LANE_LAW_PATH, EXO_LANE_COMMAND_PATH] {
6187+ let path = repository_path(relative);
6188+ let source = std::fs::read_to_string(&path)
6189+ .unwrap_or_else(|error| panic!("cannot read {}: {error}", path.display()));
6190+ for (what, token) in EXO_OFF_MACHINE_TOKENS {
6191+ assert!(
6192+ !named_in_code(&source, token),
6193+ "OMEGA-DELTA-0042: {} names {what} (`{token}`) outside a \
6194+ comment. Exo's endpoint is unauthenticated and Omega must \
6195+ never proxy it off-machine.",
6196+ path.display()
6197+ );
6198+ }
6199+ }
6200+
6201+ // The flags that would redirect Exo, checked where they would have to
6202+ // appear to do any harm: the command lines Omega actually builds.
6203+ let command_path = repository_path(EXO_LANE_COMMAND_PATH);
6204+ let command = std::fs::read_to_string(&command_path)
6205+ .unwrap_or_else(|error| panic!("cannot read {}: {error}", command_path.display()));
6206+ let table = command
6207+ .split_once("pub const ADMITTED_LANE_ARGV")
6208+ .and_then(|(_, rest)| rest.split_once("\n];"))
6209+ .expect("the admitted argv table is present")
6210+ .0;
6211+ for flag in EXO_REDIRECTING_FLAGS {
6212+ assert!(
6213+ !table.contains(flag),
6214+ "OMEGA-DELTA-0042: an admitted Exo command line carries \
6215+ `{flag}`, which points Exo away from the state root on disk \
6216+ and at a server with no authentication."
6217+ );
6218+ }
6219+
6220+ // And the positive half: the lane refuses an off-loopback endpoint it
6221+ // inherited. Without this the check above is satisfied by a lane that
6222+ // simply never looked.
6223+ let connection_path = repository_path(EXO_CONNECTION_PATH);
6224+ let connection = std::fs::read_to_string(&connection_path)
6225+ .unwrap_or_else(|error| panic!("cannot read {}: {error}", connection_path.display()));
6226+ let body = function_body(&connection, "check_endpoint").unwrap_or_else(|| {
6227+ panic!(
6228+ "OMEGA-DELTA-0042: {} no longer checks where Exo is. \
6229+ `EXO_EXOHARNESS_URL` in the inherited environment redirects \
6230+ the lane off-machine with no Omega command line changing.",
6231+ connection_path.display()
6232+ )
6233+ });
6234+ assert!(
6235+ body.contains("EXO_EXOHARNESS_URL") && body.contains("LoopbackEndpoint::parse"),
6236+ "OMEGA-DELTA-0042: `check_endpoint` no longer parses the inherited \
6237+ endpoint through the type that refuses a non-loopback one."
6238+ );
6239+ let turn = function_body(&connection, "drive_turn").expect("the turn path exists");
6240+ assert!(
6241+ turn.contains("self.check_endpoint()"),
6242+ "OMEGA-DELTA-0042: the turn path no longer checks where Exo is."
6243+ );
6244+ }
6245+
6246+ /// OMEGA-DELTA-0042, and owner gate 8 behind it.
6247+ ///
6248+ /// Adding an executor lane must not open a fourth model-initiated path into
6249+ /// Full Auto authority. The lane reaches nothing that starts a run: it names
6250+ /// no launch origin, constructs no pin gesture, and never writes a run
6251+ /// reference — a record that carried one would be claiming engine-lane
6252+ /// authority Exo does not have.
6253+ #[test]
6254+ fn the_exo_lane_opens_no_path_into_full_auto_authority() {
6255+ for relative in [EXO_CONNECTION_PATH, EXO_LANE_LAW_PATH] {
6256+ let path = repository_path(relative);
6257+ let source = std::fs::read_to_string(&path)
6258+ .unwrap_or_else(|error| panic!("cannot read {}: {error}", path.display()));
6259+ for token in EXO_FULL_AUTO_TOKENS {
6260+ let named = named_in_code(&source, token);
6261+ assert!(
6262+ !named,
6263+ "OMEGA-DELTA-0042: {} names `{token}` in code. An Exo agent \
6264+ has an unrestricted networked shell; it is exactly the \
6265+ caller owner gate 8 exists for.",
6266+ path.display()
6267+ );
6268+ }
6269+ }
6270+ }
6271+
6272+ /// OMEGA-DELTA-0042. Every turn is gated before it is sent.
6273+ ///
6274+ /// Three refusals, in order, and the order matters: an agent read after the
6275+ /// send would report a capability the turn already used. Checked on the
6276+ /// function body, because the calls existing somewhere in a file is not the
6277+ /// same claim as the turn path running them.
6278+ #[test]
6279+ fn an_exo_turn_checks_the_pin_and_the_agent_before_it_sends() {
6280+ let path = repository_path(EXO_CONNECTION_PATH);
6281+ let source = std::fs::read_to_string(&path)
6282+ .unwrap_or_else(|error| panic!("cannot read {}: {error}", path.display()));
6283+ let body = function_body(&source, "drive_turn").unwrap_or_else(|| {
6284+ panic!(
6285+ "OMEGA-DELTA-0042: {} has no `drive_turn`, so nothing in this \
6286+ file is the turn path any more.",
6287+ path.display()
6288+ )
6289+ });
6290+ let pin = body
6291+ .find("self.check_pin()")
6292+ .expect("OMEGA-DELTA-0042: a turn no longer checks which Exo it is driving");
6293+ let agent = body.find("self.check_agent()").expect(
6294+ "OMEGA-DELTA-0042: a turn no longer reads the Exo agent, so it \
6295+ cannot refuse self-modification capability",
6296+ );
6297+ let send = body
6298+ .find("ExoCommand::SendTurn")
6299+ .expect("OMEGA-DELTA-0042: a turn no longer sends");
6300+ assert!(
6301+ pin < agent && agent < send,
6302+ "OMEGA-DELTA-0042: the turn path runs its refusals out of order \
6303+ (pin {pin}, agent {agent}, send {send}). A capability read after \
6304+ the send describes a turn that already happened."
6305+ );
6306+ }
6307+
6308+ /// OMEGA-DELTA-0042. The lane is wired, not merely built.
6309+ ///
6310+ /// omega#78 shipped a router nobody constructed, and `OMEGA-DELTA-0035`
6311+ /// exists because of it. The same failure is available here: a lane with a
6312+ /// law, a connection, and tests, reachable by nobody.
6313+ #[test]
6314+ fn the_exo_lane_is_reachable_from_omega_agent() {
6315+ let router_path = repository_path(ROUTER_DISPATCH_PATH);
6316+ let router = std::fs::read_to_string(&router_path)
6317+ .unwrap_or_else(|error| panic!("cannot read {}: {error}", router_path.display()));
6318+ assert!(
6319+ router.contains("omega_exo_connection::connect_configured_lane")
6320+ && router.contains("with_external_acp(exo)"),
6321+ "OMEGA-DELTA-0042: {} no longer registers the Exo lane as the \
6322+ router's external executor, so a pin to it can never be honoured.",
6323+ router_path.display()
6324+ );
6325+
6326+ let factory_path = repository_path(AGENT_SERVER_FACTORY_PATH);
6327+ let factory = std::fs::read_to_string(&factory_path)
6328+ .unwrap_or_else(|error| panic!("cannot read {}: {error}", factory_path.display()));
6329+ assert!(
6330+ factory.contains("omega_exo_connection::ExoLaneConfig::data_dir_path()"),
6331+ "OMEGA-DELTA-0042: {} no longer hands the router the Exo lane's \
6332+ configuration path, so the lane is never found.",
6333+ factory_path.display()
6334+ );
6335+
6336+ let disclosure_path = repository_path(EXECUTOR_DISCLOSURE_BINDING_PATH);
6337+ let disclosure = std::fs::read_to_string(&disclosure_path)
6338+ .unwrap_or_else(|error| panic!("cannot read {}: {error}", disclosure_path.display()));
6339+ assert!(
6340+ disclosure.contains("downcast::<crate::omega_exo_connection::ExoHarnessConnection>()"),
6341+ "OMEGA-DELTA-0042: {} no longer recognises the Exo connection by \
6342+ its concrete type. `agent_id()` on that connection is derived from \
6343+ what Exo said about itself, so classifying by it would let an Exo \
6344+ install choose its own executor class.",
6345+ disclosure_path.display()
6346+ );
6347+ }
59826348 // ---------------------------------------------------------------------
59836349 // OMEGA-DELTA-0041 — Omega Agent served over ACP on a loopback socket
59846350 // ---------------------------------------------------------------------
diff --git a/crates/omega_exo_lane/Cargo.toml b/crates/omega_exo_lane/Cargo.toml
new file mode 100644
index 0000000000..5716217e1f
--- /dev/null
+++ b/crates/omega_exo_lane/Cargo.toml
@@ -0,0 +1,20 @@
1+[package]
2+name = "omega_exo_lane"
3+version = "0.1.0"
4+edition.workspace = true
5+publish.workspace = true
6+license = "GPL-3.0-or-later"
7+description = "The Exo harness lane law: pin, invocation, loopback, turn reading (OMEGA-EXO-01 Tier A)"
8+
9+[lints]
10+workspace = true
11+
12+[lib]
13+path = "src/omega_exo_lane.rs"
14+doctest = false
15+
16+[dependencies]
17+omega_front_door.workspace = true
18+omega_harness.workspace = true
19+
20+[dev-dependencies]
diff --git a/crates/omega_exo_lane/src/capability.rs b/crates/omega_exo_lane/src/capability.rs
new file mode 100644
index 0000000000..a9cded1dad
--- /dev/null
+++ b/crates/omega_exo_lane/src/capability.rs
@@ -0,0 +1,401 @@
1+//! What the Exo agent behind this lane is allowed to be. `OMEGA-DELTA-0042`,
2+//! omega#87.
3+//!
4+//! Exo's differentiator is that its agent can rewrite itself. The flagship Exo
5+//! agent runs unrestricted networked `shell` in a sandbox with Exo's own source
6+//! tree mounted read-write, plus a `guardian_action` tool that builds and
7+//! restarts Exo. There is no approval prompt anywhere in Exo — the security
8+//! model is sandbox isolation, and Exo's threat model assumes you *want* the
9+//! agent to modify itself.
10+//!
11+//! Omega supplies the authority gate Exo lacks. Tier C — surfacing that
12+//! self-improvement loop — is out of scope and stays out; it needs its own
13+//! packet, explicit typed authority, and per-run owner consent. What this file
14+//! does is the negative half that Tier A owes: **the lane never silently enables
15+//! Exo's self-modification tools.**
16+//!
17+//! # Read the agent, do not assume it
18+//!
19+//! "Never enables" is easy and nearly worthless on its own: the lane emits no
20+//! configuring command (see [`crate::command`]), so of course it enables
21+//! nothing. The capability that matters is the one the agent *already has*,
22+//! configured by whoever set Exo up — and an Omega lane pointed at the flagship
23+//! self-improving agent would be surfacing exactly the capability this packet
24+//! excludes, without Omega having enabled anything.
25+//!
26+//! So the lane reads `exo agent show` before it sends a turn and refuses when
27+//! the agent carries self-modification capability. That is a live observation of
28+//! the agent that is about to run, not an assertion about Omega's own argv.
29+//!
30+//! Three capabilities are refused, and each is one of the three things
31+//! `docs/RSI.md` and the flagship agent's own configuration use to rewrite Exo:
32+//!
33+//! * **agent-authored tools** — `tool_creation: enabled`, the agent writing
34+//! TypeScript into `.exo/agent-tools/` at runtime;
35+//! * **a tool module** — `typescript_tool_modules: N` where `N > 0`, which is
36+//! how `guardian_action` is installed (`examples/exo/guardian-tools.ts`);
37+//! * **a read-write mount** — a `sandbox_mounts` entry in `rw` mode, which is
38+//! how Exo's source tree gets into the sandbox to be edited.
39+//!
40+//! Networking is deliberately **not** in that list. An agent with a shell and a
41+//! network is a high-capability agent, and it is also what any useful coding
42+//! agent is; refusing it would make the lane refuse everything and would say
43+//! nothing about self-modification. It is reported, so a reader sees it, and it
44+//! does not refuse.
45+
46+/// What `exo agent show` said about the agent this lane points at.
47+///
48+/// Every field is read out of Exo's own output. Nothing here is a default this
49+/// file invented: a record Omega could not parse becomes
50+/// [`ExoAgentReadError`], not a permissive record.
51+#[derive(Clone, Debug, PartialEq, Eq)]
52+pub struct ExoAgent {
53+ /// The agent slug, as Exo reports it.
54+ pub slug: String,
55+ /// The executor Exo will run the turn with: `basic`, `rlm`, `typescript`,
56+ /// `codex`, `claude-code`, `cursor`, or a module path.
57+ pub harness: String,
58+ /// The model binding the agent resolves to.
59+ pub model: String,
60+ /// Whether the agent may author its own tools at runtime.
61+ pub agent_authored_tools: bool,
62+ /// How many TypeScript tool modules are loaded into it.
63+ pub tool_modules: u32,
64+ /// Whether any sandbox mount is read-write.
65+ pub read_write_mount: bool,
66+ /// Whether the agent's sandbox has a network. Reported, never refused.
67+ pub networking: bool,
68+}
69+
70+/// Why the lane will not run a turn on this agent.
71+#[derive(Clone, Copy, Debug, PartialEq, Eq)]
72+pub enum SelfModification {
73+ /// `tool_creation: enabled`.
74+ AgentAuthoredTools,
75+ /// One or more `--tool-module` modules, the `guardian_action` shape.
76+ ToolModule,
77+ /// A read-write sandbox mount, the source-tree shape.
78+ ReadWriteMount,
79+}
80+
81+impl SelfModification {
82+ /// The stable token this refusal is recorded under.
83+ #[must_use]
84+ pub const fn token(self) -> &'static str {
85+ match self {
86+ Self::AgentAuthoredTools => "agent_authored_tools",
87+ Self::ToolModule => "tool_module",
88+ Self::ReadWriteMount => "read_write_mount",
89+ }
90+ }
91+
92+ /// Every capability the lane refuses, in declaration order.
93+ #[must_use]
94+ pub const fn all() -> &'static [Self] {
95+ &[
96+ Self::AgentAuthoredTools,
97+ Self::ToolModule,
98+ Self::ReadWriteMount,
99+ ]
100+ }
101+}
102+
103+impl std::fmt::Display for SelfModification {
104+ fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
105+ formatter.write_str(match self {
106+ Self::AgentAuthoredTools => {
107+ "this Exo agent may write its own tools at runtime; the Omega lane does not \
108+ surface Exo's self-modification"
109+ }
110+ Self::ToolModule => {
111+ "this Exo agent has a tool module loaded, which is how guardian_action is \
112+ installed; the Omega lane does not surface Exo's self-modification"
113+ }
114+ Self::ReadWriteMount => {
115+ "this Exo agent has a read-write mount, which is how Exo's source tree is \
116+ edited from inside the sandbox; the Omega lane does not surface Exo's \
117+ self-modification"
118+ }
119+ })
120+ }
121+}
122+
123+impl std::error::Error for SelfModification {}
124+
125+/// Why `exo agent show` could not be read.
126+#[derive(Clone, Copy, Debug, PartialEq, Eq)]
127+pub enum ExoAgentReadError {
128+ /// A field the decision depends on was not in the output.
129+ MissingField(&'static str),
130+ /// A field was present and this build could not read its value.
131+ UnreadableField(&'static str),
132+}
133+
134+impl std::fmt::Display for ExoAgentReadError {
135+ fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
136+ match self {
137+ Self::MissingField(field) => {
138+ write!(formatter, "exo agent show did not report {field}")
139+ }
140+ Self::UnreadableField(field) => {
141+ write!(formatter, "exo agent show reported an unreadable {field}")
142+ }
143+ }
144+ }
145+}
146+
147+impl std::error::Error for ExoAgentReadError {}
148+
149+impl ExoAgent {
150+ /// Read `exo agent show` output.
151+ ///
152+ /// Fails closed on every shape it does not recognise. Exo's house rule is
153+ /// that it does not keep backwards compatibility, so the realistic failure
154+ /// is not a malicious record — it is upstream renaming `tool_creation` in a
155+ /// commit nobody read, and a parser that defaulted the missing field to
156+ /// `false` would answer "no self-modification" about an agent it could no
157+ /// longer see. That is the one wrong answer this whole file exists to
158+ /// prevent, so an absent field is an error and never a default.
159+ ///
160+ /// # Errors
161+ ///
162+ /// [`ExoAgentReadError`] when a load-bearing field is missing or unreadable.
163+ pub fn parse(output: &str) -> Result<Self, ExoAgentReadError> {
164+ let field = |name: &'static str| -> Result<&str, ExoAgentReadError> {
165+ output
166+ .lines()
167+ .find_map(|line| line.strip_prefix(name)?.strip_prefix(':'))
168+ .map(str::trim)
169+ .ok_or(ExoAgentReadError::MissingField(name))
170+ };
171+
172+ let tool_creation = match field("tool_creation")? {
173+ "enabled" => true,
174+ "disabled" => false,
175+ _ => return Err(ExoAgentReadError::UnreadableField("tool_creation")),
176+ };
177+ let tool_modules: u32 = field("typescript_tool_modules")?
178+ .parse()
179+ .map_err(|_| ExoAgentReadError::UnreadableField("typescript_tool_modules"))?;
180+ let networking = match field("enable_networking")? {
181+ "true" => true,
182+ "false" => false,
183+ _ => return Err(ExoAgentReadError::UnreadableField("enable_networking")),
184+ };
185+
186+ Ok(Self {
187+ slug: field("slug")?.to_owned(),
188+ harness: field("harness")?.to_owned(),
189+ model: field("model")?.to_owned(),
190+ agent_authored_tools: tool_creation,
191+ tool_modules,
192+ read_write_mount: read_write_mount(output)?,
193+ networking,
194+ })
195+ }
196+
197+ /// Whether the lane may run a turn on this agent.
198+ ///
199+ /// # Errors
200+ ///
201+ /// The first self-modification capability found, in
202+ /// [`SelfModification::all`] order.
203+ pub fn admits_lane_turn(&self) -> Result<(), SelfModification> {
204+ if self.agent_authored_tools {
205+ return Err(SelfModification::AgentAuthoredTools);
206+ }
207+ if self.tool_modules > 0 {
208+ return Err(SelfModification::ToolModule);
209+ }
210+ if self.read_write_mount {
211+ return Err(SelfModification::ReadWriteMount);
212+ }
213+ Ok(())
214+ }
215+}
216+
217+/// Read the `sandbox_mounts:` block.
218+///
219+/// Exo prints ` none` for an empty set and ` <host> -> <path> (ro)` or
220+/// `(rw[, internal])` per mount. A block this build cannot read is an error for
221+/// the same reason a missing field is.
222+fn read_write_mount(output: &str) -> Result<bool, ExoAgentReadError> {
223+ let mut lines = output.lines();
224+ lines
225+ .find(|line| line.trim_end() == "sandbox_mounts:")
226+ .ok_or(ExoAgentReadError::MissingField("sandbox_mounts"))?;
227+
228+ let mut any = false;
229+ for line in lines {
230+ let Some(entry) = line.strip_prefix(" ") else {
231+ break;
232+ };
233+ let entry = entry.trim();
234+ if entry == "none" {
235+ return Ok(false);
236+ }
237+ let Some((_, mode)) = entry.rsplit_once('(') else {
238+ return Err(ExoAgentReadError::UnreadableField("sandbox_mounts"));
239+ };
240+ let mode = mode.trim_end_matches(')');
241+ match mode.split(',').next().map(str::trim) {
242+ Some("rw") => return Ok(true),
243+ Some("ro") => any = true,
244+ _ => return Err(ExoAgentReadError::UnreadableField("sandbox_mounts")),
245+ }
246+ }
247+ let _ = any;
248+ Ok(false)
249+}
250+
251+#[cfg(test)]
252+mod tests {
253+ use super::*;
254+
255+ /// Real `exo agent show` output, captured from the pinned Exo at
256+ /// `baa07f67` on 2026-07-25. Not a hand-written fixture: this is what the
257+ /// binary printed for the agent the lane was driven against.
258+ const DRIVEN_AGENT: &str = "\
259+id: 019f9cc4-0bf8-7fa2-8a48-eb84460661c1
260+slug: omega-lane
261+name: OmegaLane
262+harness: basic
263+typescript_module: none
264+typescript_tool_modules: 0
265+tool_creation: disabled
266+sandbox_image: default
267+sandbox_provider: apple-container
268+sandbox_scope: conversation
269+enable_networking: false
270+sandbox_mounts:
271+ none
272+model: gpt5mini
273+max_output_tokens: none
274+max_tool_round_trips: none
275+braintrust: none
276+";
277+
278+ #[test]
279+ fn the_agent_the_lane_was_driven_against_is_admitted() {
280+ let agent = ExoAgent::parse(DRIVEN_AGENT).expect("real output parses");
281+ assert_eq!(agent.slug, "omega-lane");
282+ assert_eq!(agent.harness, "basic");
283+ assert_eq!(agent.model, "gpt5mini");
284+ assert!(!agent.agent_authored_tools);
285+ assert_eq!(agent.tool_modules, 0);
286+ assert!(!agent.read_write_mount);
287+ assert!(!agent.networking);
288+ assert_eq!(agent.admits_lane_turn(), Ok(()));
289+ }
290+
291+ /// The flagship Exo agent, which is the thing this refusal exists for: the
292+ /// source tree mounted read-write, `guardian-tools.ts` loaded, and runtime
293+ /// tool authoring on.
294+ #[test]
295+ fn the_self_improving_exo_agent_is_refused() {
296+ let self_improving = DRIVEN_AGENT
297+ .replace("tool_creation: disabled", "tool_creation: enabled")
298+ .replace("typescript_tool_modules: 0", "typescript_tool_modules: 1")
299+ .replace(
300+ "sandbox_mounts:\n none",
301+ "sandbox_mounts:\n /Users/x/exo -> /workspace/exo (rw)",
302+ );
303+ let agent = ExoAgent::parse(&self_improving).expect("parses");
304+ assert_eq!(
305+ agent.admits_lane_turn(),
306+ Err(SelfModification::AgentAuthoredTools)
307+ );
308+ }
309+
310+ /// Each capability refuses on its own, so removing any one of the three
311+ /// checks fails a test rather than being masked by the other two.
312+ #[test]
313+ fn each_self_modification_capability_refuses_on_its_own() {
314+ let cases = [
315+ (
316+ "tool_creation: disabled",
317+ "tool_creation: enabled",
318+ SelfModification::AgentAuthoredTools,
319+ ),
320+ (
321+ "typescript_tool_modules: 0",
322+ "typescript_tool_modules: 2",
323+ SelfModification::ToolModule,
324+ ),
325+ (
326+ "sandbox_mounts:\n none",
327+ "sandbox_mounts:\n /Users/x/exo -> /workspace/exo (rw, internal)",
328+ SelfModification::ReadWriteMount,
329+ ),
330+ ];
331+ assert_eq!(cases.len(), SelfModification::all().len());
332+ for (from, to, expected) in cases {
333+ let agent = ExoAgent::parse(&DRIVEN_AGENT.replace(from, to)).expect("parses");
334+ assert_eq!(agent.admits_lane_turn(), Err(expected), "{to}");
335+ }
336+ }
337+
338+ /// A read-only mount is not self-modification. Refusing it would make the
339+ /// gate refuse an ordinary working directory and say nothing about Exo
340+ /// rewriting itself.
341+ #[test]
342+ fn a_read_only_mount_is_not_self_modification() {
343+ let agent = ExoAgent::parse(&DRIVEN_AGENT.replace(
344+ "sandbox_mounts:\n none",
345+ "sandbox_mounts:\n /Users/x/project -> /workspace/project (ro)",
346+ ))
347+ .expect("parses");
348+ assert!(!agent.read_write_mount);
349+ assert_eq!(agent.admits_lane_turn(), Ok(()));
350+ }
351+
352+ /// Networking is reported and does not refuse. See the module docs.
353+ #[test]
354+ fn a_networked_agent_is_reported_and_not_refused() {
355+ let agent =
356+ ExoAgent::parse(&DRIVEN_AGENT.replace("enable_networking: false", "enable_networking: true"))
357+ .expect("parses");
358+ assert!(agent.networking);
359+ assert_eq!(agent.admits_lane_turn(), Ok(()));
360+ }
361+
362+ /// The upstream-rename failure, which is the realistic one. A field the
363+ /// decision depends on going missing must not read as "no capability".
364+ #[test]
365+ fn a_renamed_field_refuses_to_parse_rather_than_defaulting_to_permissive() {
366+ for field in [
367+ "tool_creation",
368+ "typescript_tool_modules",
369+ "sandbox_mounts",
370+ "harness",
371+ "model",
372+ "slug",
373+ "enable_networking",
374+ ] {
375+ let renamed = DRIVEN_AGENT
376+ .lines()
377+ .filter(|line| !line.starts_with(&format!("{field}:")))
378+ .collect::<Vec<_>>()
379+ .join("\n");
380+ let read = ExoAgent::parse(&renamed);
381+ assert!(
382+ read.is_err(),
383+ "dropping {field} still parsed, and the record would claim no capability: {read:?}"
384+ );
385+ }
386+ }
387+
388+ #[test]
389+ fn an_unreadable_value_is_refused_rather_than_read_as_false() {
390+ for (from, to) in [
391+ ("tool_creation: disabled", "tool_creation: maybe"),
392+ ("typescript_tool_modules: 0", "typescript_tool_modules: some"),
393+ ("enable_networking: false", "enable_networking: yes"),
394+ ] {
395+ assert!(
396+ ExoAgent::parse(&DRIVEN_AGENT.replace(from, to)).is_err(),
397+ "{to}"
398+ );
399+ }
400+ }
401+}
diff --git a/crates/omega_exo_lane/src/command.rs b/crates/omega_exo_lane/src/command.rs
new file mode 100644
index 0000000000..cdbbf85c61
--- /dev/null
+++ b/crates/omega_exo_lane/src/command.rs
@@ -0,0 +1,445 @@
1+//! Every command the Exo lane is allowed to run. `OMEGA-DELTA-0042`, omega#87.
2+//!
3+//! Omega owns Exo's process the way it owns any external tool, and it **never
4+//! edits Exo's `.exo` root, agents, secrets, or configuration** — the same law
5+//! it obeys around a Codex home. The way that law is kept is not a review
6+//! convention: it is that there is no way to express a configuring command.
7+//! [`ExoCommand`] is a closed enum of four operations, [`ExoCommand::argv`] is
8+//! total over it, and [`ADMITTED_LANE_ARGV`] states each resulting shape
9+//! exactly. A fifth operation does not compile until somebody writes its shape
10+//! down, and writing it down is the record that a person decided it belonged.
11+//!
12+//! The set is deliberately small: send one turn, read the durable log, read the
13+//! conversation, read the agent, read the model bindings. Four of the five are
14+//! reads. The one that writes is `conversation send`, and what it writes is
15+//! Exo's own record of the turn it just ran — Exo mutating Exo, which is the
16+//! only mutation this lane causes and the only one it could cause without
17+//! ceasing to be a lane.
18+//!
19+//! # The argument terminator is load-bearing
20+//!
21+//! Exo's global options are accepted **after** the subcommand: `exo
22+//! conversation send --harness cursor <agent> <conv> <prompt>` is a valid
23+//! invocation that changes which executor runs the turn. So a prompt is not
24+//! merely a string Exo receives — without a terminator it is *argument syntax*,
25+//! and the person typing it is writing Exo's command line.
26+//!
27+//! Driven against the pinned Exo, a prompt of `--help` on an otherwise correct
28+//! `conversation send` **exits 0, prints Exo's usage text, and runs no turn.**
29+//! A lane without the terminator would have read that usage text back as the
30+//! model's reply. With `--` it is delivered as text and a real turn runs.
31+//!
32+//! So every value that could have come from a person or a model is emitted
33+//! after `--`, and [`ExoArg`] makes that checkable rather than hoped for:
34+//! arguments carry whether they are literal, Omega's own configuration, or
35+//! text from outside, and `user_text_cannot_become_an_exo_flag` fails if one of
36+//! the last kind is ever emitted before the terminator.
37+
38+/// Exo's state root, as a value Omega only ever *names*.
39+///
40+/// There is no method here that writes, creates, removes, or opens anything.
41+/// That is the type's entire job: an `ExoRoot` can be passed to a command
42+/// builder and cannot be used to touch the directory it names. Omega's process
43+/// never writes inside `.exo`; only the `exo` binary does.
44+#[derive(Clone, Debug, PartialEq, Eq)]
45+pub struct ExoRoot(String);
46+
47+impl ExoRoot {
48+ /// Name an existing Exo state root.
49+ #[must_use]
50+ pub fn at(path: impl Into<String>) -> Self {
51+ Self(path.into())
52+ }
53+
54+ /// The path, for putting on a command line.
55+ #[must_use]
56+ pub fn as_str(&self) -> &str {
57+ &self.0
58+ }
59+}
60+
61+/// One argument, and where it came from.
62+///
63+/// The provenance is the point. See the module documentation.
64+#[derive(Clone, Debug, PartialEq, Eq)]
65+pub enum ExoArg {
66+ /// A literal this file wrote.
67+ Fixed(&'static str),
68+ /// A value from Omega's own lane configuration — the state root, a limit.
69+ /// Not literal, but not reachable from a turn either.
70+ Config(String),
71+ /// A value that could have been typed by a person or produced by a model.
72+ /// Never emitted before the terminator.
73+ UserText(String),
74+}
75+
76+impl ExoArg {
77+ /// The argument as it goes on the command line.
78+ #[must_use]
79+ pub fn value(&self) -> &str {
80+ match self {
81+ Self::Fixed(value) => value,
82+ Self::Config(value) | Self::UserText(value) => value,
83+ }
84+ }
85+
86+ /// Whether this argument came from outside Omega's own configuration.
87+ #[must_use]
88+ pub const fn is_user_text(&self) -> bool {
89+ matches!(self, Self::UserText(_))
90+ }
91+}
92+
93+/// The terminator, after which Exo stops reading arguments as options.
94+pub const ARGUMENT_TERMINATOR: &str = "--";
95+
96+/// Every command the lane may run, and the exact argv each produces.
97+///
98+/// A closed list, in the same spirit as `EXECUTOR_DISCLOSURE_FIELDS`: the
99+/// property that matters is "these shapes and nothing else". A denylist of
100+/// dangerous flags would be a guess about which flags upstream will add next,
101+/// in a project whose written house rule is that it does not keep backwards
102+/// compatibility. Placeholders are `<name>`; every other token is literal.
103+pub const ADMITTED_LANE_ARGV: &[(&str, &[&str])] = &[
104+ (
105+ "send_turn",
106+ &[
107+ "--root",
108+ "<root>",
109+ "conversation",
110+ "send",
111+ "--",
112+ "<agent>",
113+ "<conversation>",
114+ "<prompt>",
115+ ],
116+ ),
117+ (
118+ "read_events",
119+ &[
120+ "--root",
121+ "<root>",
122+ "conversation",
123+ "events",
124+ "--limit",
125+ "<limit>",
126+ "--",
127+ "<agent>",
128+ "<conversation>",
129+ ],
130+ ),
131+ (
132+ "show_conversation",
133+ &[
134+ "--root",
135+ "<root>",
136+ "conversation",
137+ "show",
138+ "--",
139+ "<agent>",
140+ "<conversation>",
141+ ],
142+ ),
143+ (
144+ "show_agent",
145+ &["--root", "<root>", "agent", "show", "--", "<agent>"],
146+ ),
147+ // No terminator, because this command carries no value from outside Omega.
148+ // A terminator here would be decoration, and decoration in a security shape
149+ // teaches a reader that the terminator is a habit rather than a mechanism.
150+ ("list_models", &["--root", "<root>", "model", "list"]),
151+];
152+
153+/// The five things the lane does.
154+#[derive(Clone, Debug, PartialEq, Eq)]
155+pub enum ExoCommand {
156+ /// Run one turn. The whole of Tier A's execution: one shot, no stream.
157+ SendTurn {
158+ agent: String,
159+ conversation: String,
160+ prompt: String,
161+ },
162+ /// Read the durable event log, newest page, for tool activity after the
163+ /// fact.
164+ ReadEvents {
165+ agent: String,
166+ conversation: String,
167+ limit: u32,
168+ },
169+ /// Read a conversation's record, for the model it resolves to.
170+ ShowConversation {
171+ agent: String,
172+ conversation: String,
173+ },
174+ /// Read an agent's record, for its executor and its capability set. This is
175+ /// what [`crate::capability`] refuses a turn on.
176+ ShowAgent { agent: String },
177+ /// Read the model bindings, so the disclosure can name the model that
178+ /// actually served rather than the local alias the agent record carries.
179+ ListModels,
180+}
181+
182+impl ExoCommand {
183+ /// The token this command is recorded and checked under.
184+ #[must_use]
185+ pub const fn shape(&self) -> &'static str {
186+ match self {
187+ Self::SendTurn { .. } => "send_turn",
188+ Self::ReadEvents { .. } => "read_events",
189+ Self::ShowConversation { .. } => "show_conversation",
190+ Self::ShowAgent { .. } => "show_agent",
191+ Self::ListModels => "list_models",
192+ }
193+ }
194+
195+ /// The arguments, with their provenance.
196+ ///
197+ /// Total: every variant produces a shape, and each one is stated in
198+ /// [`ADMITTED_LANE_ARGV`].
199+ #[must_use]
200+ pub fn args(&self, root: &ExoRoot) -> Vec<ExoArg> {
201+ let mut args = vec![
202+ ExoArg::Fixed("--root"),
203+ ExoArg::Config(root.as_str().to_owned()),
204+ ];
205+ match self {
206+ Self::SendTurn {
207+ agent,
208+ conversation,
209+ prompt,
210+ } => {
211+ args.extend([
212+ ExoArg::Fixed("conversation"),
213+ ExoArg::Fixed("send"),
214+ ExoArg::Fixed(ARGUMENT_TERMINATOR),
215+ ExoArg::UserText(agent.clone()),
216+ ExoArg::UserText(conversation.clone()),
217+ ExoArg::UserText(prompt.clone()),
218+ ]);
219+ }
220+ Self::ReadEvents {
221+ agent,
222+ conversation,
223+ limit,
224+ } => {
225+ args.extend([
226+ ExoArg::Fixed("conversation"),
227+ ExoArg::Fixed("events"),
228+ ExoArg::Fixed("--limit"),
229+ ExoArg::Config(limit.to_string()),
230+ ExoArg::Fixed(ARGUMENT_TERMINATOR),
231+ ExoArg::UserText(agent.clone()),
232+ ExoArg::UserText(conversation.clone()),
233+ ]);
234+ }
235+ Self::ShowConversation {
236+ agent,
237+ conversation,
238+ } => {
239+ args.extend([
240+ ExoArg::Fixed("conversation"),
241+ ExoArg::Fixed("show"),
242+ ExoArg::Fixed(ARGUMENT_TERMINATOR),
243+ ExoArg::UserText(agent.clone()),
244+ ExoArg::UserText(conversation.clone()),
245+ ]);
246+ }
247+ Self::ShowAgent { agent } => {
248+ args.extend([
249+ ExoArg::Fixed("agent"),
250+ ExoArg::Fixed("show"),
251+ ExoArg::Fixed(ARGUMENT_TERMINATOR),
252+ ExoArg::UserText(agent.clone()),
253+ ]);
254+ }
255+ Self::ListModels => {
256+ args.extend([ExoArg::Fixed("model"), ExoArg::Fixed("list")]);
257+ }
258+ }
259+ args
260+ }
261+
262+ /// The command line, as strings.
263+ #[must_use]
264+ pub fn argv(&self, root: &ExoRoot) -> Vec<String> {
265+ self.args(root)
266+ .iter()
267+ .map(|arg| arg.value().to_owned())
268+ .collect()
269+ }
270+
271+ /// One of every variant, filled with placeholder values.
272+ ///
273+ /// The exhaustiveness handle: a new variant that is not added here fails
274+ /// `every_variant_has_a_written_shape`, and a new variant that *is* added
275+ /// here fails until its shape is written into [`ADMITTED_LANE_ARGV`].
276+ #[must_use]
277+ pub fn every_shape() -> Vec<Self> {
278+ vec![
279+ Self::SendTurn {
280+ agent: "<agent>".into(),
281+ conversation: "<conversation>".into(),
282+ prompt: "<prompt>".into(),
283+ },
284+ Self::ReadEvents {
285+ agent: "<agent>".into(),
286+ conversation: "<conversation>".into(),
287+ limit: 0,
288+ },
289+ Self::ShowConversation {
290+ agent: "<agent>".into(),
291+ conversation: "<conversation>".into(),
292+ },
293+ Self::ShowAgent {
294+ agent: "<agent>".into(),
295+ },
296+ Self::ListModels,
297+ ]
298+ }
299+}
300+
301+#[cfg(test)]
302+mod tests {
303+ use super::*;
304+
305+ fn root() -> ExoRoot {
306+ ExoRoot::at("<root>")
307+ }
308+
309+ /// The shapes, asserted exactly. Not "contains no dangerous flag" — every
310+ /// token, in order, against a written list.
311+ ///
312+ /// A `<name>` token in the written shape asserts a *slot*: that position
313+ /// must be filled from a value, and a literal appearing there fails. Every
314+ /// other token asserts itself. So the check covers both what the command
315+ /// line says and which parts of it this file decided rather than received.
316+ #[test]
317+ fn every_variant_has_a_written_shape() {
318+ let shapes = ExoCommand::every_shape();
319+ assert_eq!(
320+ shapes.len(),
321+ ADMITTED_LANE_ARGV.len(),
322+ "a command variant exists with no written argv shape, or the other way round"
323+ );
324+ for command in &shapes {
325+ let (_, expected) = ADMITTED_LANE_ARGV
326+ .iter()
327+ .find(|(name, _)| *name == command.shape())
328+ .unwrap_or_else(|| panic!("{} has no written shape", command.shape()));
329+ let args = command.args(&root());
330+ assert_eq!(args.len(), expected.len(), "{}", command.shape());
331+ for (index, (arg, token)) in args.iter().zip(expected.iter()).enumerate() {
332+ if token.starts_with('<') {
333+ assert!(
334+ !matches!(arg, ExoArg::Fixed(_)),
335+ "{} argv[{index}] is a literal where the shape says {token}",
336+ command.shape()
337+ );
338+ } else {
339+ assert_eq!(
340+ arg,
341+ &ExoArg::Fixed(token),
342+ "{} argv[{index}]",
343+ command.shape()
344+ );
345+ }
346+ }
347+ }
348+ }
349+
350+ /// The security law, stated over the whole closed set rather than over the
351+ /// arguments one call site happened to build.
352+ ///
353+ /// A command that carries no text from outside needs no terminator; a
354+ /// command that carries any must put all of it after one. Both halves are
355+ /// checked, so neither "forgot the terminator" nor "added user text after
356+ /// the fact" can land quietly.
357+ #[test]
358+ fn user_text_cannot_become_an_exo_flag() {
359+ for command in ExoCommand::every_shape() {
360+ let args = command.args(&root());
361+ let terminator = args
362+ .iter()
363+ .position(|arg| arg == &ExoArg::Fixed(ARGUMENT_TERMINATOR));
364+ let Some(terminator) = terminator else {
365+ assert!(
366+ !args.iter().any(ExoArg::is_user_text),
367+ "{} carries text from outside Omega and emits no argument terminator",
368+ command.shape()
369+ );
370+ continue;
371+ };
372+ for (index, arg) in args.iter().enumerate() {
373+ assert!(
374+ !(arg.is_user_text() && index <= terminator),
375+ "{} puts text from outside Omega at argv[{index}], before the terminator",
376+ command.shape()
377+ );
378+ }
379+ }
380+ }
381+
382+ /// The prompt that proved this against the real binary: `--help` on a
383+ /// `conversation send` exits 0, prints usage, and runs no turn. After the
384+ /// terminator it is a prompt.
385+ #[test]
386+ fn a_flag_shaped_prompt_is_still_a_prompt() {
387+ let command = ExoCommand::SendTurn {
388+ agent: "omega-lane".into(),
389+ conversation: "tier-a".into(),
390+ prompt: "--help".into(),
391+ };
392+ let argv = command.argv(&root());
393+ let terminator = argv
394+ .iter()
395+ .position(|arg| arg == ARGUMENT_TERMINATOR)
396+ .expect("terminated");
397+ assert_eq!(argv.last().map(String::as_str), Some("--help"));
398+ assert!(argv.len() - 1 > terminator);
399+ }
400+
401+ /// Omega never configures Exo. The admitted verbs are three reads and one
402+ /// send, and the ones that are absent are absent by name so a later edit
403+ /// that adds `agent update` has to delete a line of this test to pass.
404+ #[test]
405+ fn the_lane_can_express_no_command_that_configures_exo() {
406+ let admitted: Vec<&str> = ADMITTED_LANE_ARGV
407+ .iter()
408+ .map(|(_, argv)| argv[3]) // --root <root> <group> <verb>
409+ .collect();
410+ assert_eq!(admitted, ["send", "events", "show", "show", "list"]);
411+ for forbidden in [
412+ "create", "update", "delete", "mount", "set", "register", "configure", "serve", "repl",
413+ "adapters", "fork", "run",
414+ ] {
415+ for (name, argv) in ADMITTED_LANE_ARGV {
416+ assert!(
417+ !argv.contains(&forbidden),
418+ "{name} can reach Exo's {forbidden}"
419+ );
420+ }
421+ }
422+ }
423+
424+ /// The state root is named and never opened. If `ExoRoot` ever grows a way
425+ /// to write, this is the test that has to be deleted for it to land.
426+ #[test]
427+ fn the_exo_root_is_a_name_and_not_a_handle() {
428+ let root = ExoRoot::at("/tmp/exo/.exo");
429+ assert_eq!(root.as_str(), "/tmp/exo/.exo");
430+ let source = include_str!("command.rs");
431+ let impl_block = source
432+ .split_once("impl ExoRoot {")
433+ .and_then(|(_, rest)| rest.split_once("\n}\n"))
434+ .expect("ExoRoot has an impl block")
435+ .0;
436+ for writing in [
437+ "fs::", "File", "create", "remove", "write", "OpenOptions", "std::io",
438+ ] {
439+ assert!(
440+ !impl_block.contains(writing),
441+ "ExoRoot grew a way to touch the directory it names: {writing}"
442+ );
443+ }
444+ }
445+}
diff --git a/crates/omega_exo_lane/src/endpoint.rs b/crates/omega_exo_lane/src/endpoint.rs
new file mode 100644
index 0000000000..a86b1a1728
--- /dev/null
+++ b/crates/omega_exo_lane/src/endpoint.rs
@@ -0,0 +1,273 @@
1+//! Exo stays on the machine it runs on. `OMEGA-DELTA-0042`, omega#87.
2+//!
3+//! `exo serve` is a single unary HTTP endpoint with **no authentication**. Its
4+//! own documentation says so: a client may send a bearer token and the server
5+//! never checks it. It exposes the full 53-variant request protocol, which
6+//! includes reading secrets. Loopback is the entire boundary, and Exo knows it.
7+//!
8+//! Omega therefore treats "where Exo listens" and "where Omega talks to Exo" as
9+//! typed values that cannot be built out of a non-loopback address, rather than
10+//! as strings a caller is trusted to have checked. The refuse list in the
11+//! teardown is stated as a law here: *do not expose Exo's unauthenticated HTTP
12+//! endpoint or agent-cli socket beyond loopback through any Omega surface.*
13+//!
14+//! # Why a parser rather than a check
15+//!
16+//! A check is something a call site can forget. [`LoopbackEndpoint`] has one
17+//! constructor, it takes a string, and it fails. Every value of the type is a
18+//! loopback address because there is no other way to make one — the same
19+//! discipline `MeasuredDigest` uses for bytes.
20+//!
21+//! # What "loopback" admits
22+//!
23+//! `127.0.0.0/8`, `::1`, and the literal name `localhost`. Nothing else. In
24+//! particular `0.0.0.0` and `::` are refused, and those are the two that matter:
25+//! they are the *plausible* mistakes. They read as "local" and they mean "every
26+//! interface on this machine", which on a laptop that joins a Tailnet or a café
27+//! network publishes an unauthenticated agent with a shell to that network.
28+
29+/// An address Omega will let Exo be reached at.
30+///
31+/// Loopback by construction. See the module documentation.
32+#[derive(Clone, Debug, PartialEq, Eq)]
33+pub struct LoopbackEndpoint {
34+ host: String,
35+ port: Option<u16>,
36+}
37+
38+/// Why an address was refused.
39+#[derive(Clone, Copy, Debug, PartialEq, Eq)]
40+pub enum OffLoopback {
41+ /// The address names every interface — `0.0.0.0` or `::`.
42+ EveryInterface,
43+ /// The address names a specific host that is not this machine.
44+ RemoteHost,
45+ /// The string is not an address this build can read. Unreadable is refused
46+ /// rather than assumed local, which is the fail-closed direction.
47+ Unreadable,
48+}
49+
50+impl std::fmt::Display for OffLoopback {
51+ fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
52+ formatter.write_str(match self {
53+ Self::EveryInterface => {
54+ "that address publishes Exo on every interface, and Exo has no authentication"
55+ }
56+ Self::RemoteHost => "Omega only reaches Exo on this machine",
57+ Self::Unreadable => "that is not an address Omega can read as loopback",
58+ })
59+ }
60+}
61+
62+impl std::error::Error for OffLoopback {}
63+
64+/// Where `exo serve` listens when nobody says otherwise.
65+///
66+/// Exo's own default, restated here so the lane's default is a value in Omega's
67+/// source rather than a behaviour inherited from an unstable upstream.
68+pub const EXO_SERVE_DEFAULT_BIND: &str = "127.0.0.1:4766";
69+
70+impl LoopbackEndpoint {
71+ /// Read an address, admitting only loopback.
72+ ///
73+ /// # Errors
74+ ///
75+ /// [`OffLoopback`] for anything that is not this machine.
76+ pub fn parse(address: &str) -> Result<Self, OffLoopback> {
77+ let address = address.trim();
78+ if address.is_empty() {
79+ return Err(OffLoopback::Unreadable);
80+ }
81+
82+ // A URL is reduced to its authority before anything else, so
83+ // `http://127.0.0.1:4766/request` and `127.0.0.1:4766` are the same
84+ // decision. A scheme this build does not know is unreadable, not
85+ // assumed harmless.
86+ let authority = match address.split_once("://") {
87+ Some((scheme, rest)) => {
88+ if !matches!(scheme, "http" | "https") {
89+ return Err(OffLoopback::Unreadable);
90+ }
91+ rest.split(['/', '?', '#']).next().unwrap_or(rest)
92+ }
93+ None => address,
94+ };
95+ if authority.contains('@') {
96+ // Userinfo can hide the real host behind an `@`. Refuse rather than
97+ // pick a side of it.
98+ return Err(OffLoopback::Unreadable);
99+ }
100+
101+ let (host, port) = split_host_port(authority)?;
102+ match classify_host(&host) {
103+ HostKind::Loopback => Ok(Self { host, port }),
104+ HostKind::EveryInterface => Err(OffLoopback::EveryInterface),
105+ HostKind::Remote => Err(OffLoopback::RemoteHost),
106+ HostKind::Unreadable => Err(OffLoopback::Unreadable),
107+ }
108+ }
109+
110+ /// The host, as read.
111+ #[must_use]
112+ pub fn host(&self) -> &str {
113+ &self.host
114+ }
115+
116+ /// The port, when the address carried one.
117+ #[must_use]
118+ pub const fn port(&self) -> Option<u16> {
119+ self.port
120+ }
121+}
122+
123+impl std::fmt::Display for LoopbackEndpoint {
124+ fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
125+ match self.port {
126+ Some(port) if self.host.contains(':') => write!(formatter, "[{}]:{port}", self.host),
127+ Some(port) => write!(formatter, "{}:{port}", self.host),
128+ None => formatter.write_str(&self.host),
129+ }
130+ }
131+}
132+
133+enum HostKind {
134+ Loopback,
135+ EveryInterface,
136+ Remote,
137+ Unreadable,
138+}
139+
140+fn split_host_port(authority: &str) -> Result<(String, Option<u16>), OffLoopback> {
141+ if let Some(rest) = authority.strip_prefix('[') {
142+ let (host, tail) = rest.split_once(']').ok_or(OffLoopback::Unreadable)?;
143+ let port = match tail.strip_prefix(':') {
144+ Some(port) => Some(port.parse().map_err(|_| OffLoopback::Unreadable)?),
145+ None if tail.is_empty() => None,
146+ None => return Err(OffLoopback::Unreadable),
147+ };
148+ return Ok((host.to_ascii_lowercase(), port));
149+ }
150+ // A bare IPv6 literal carries colons and no port.
151+ if authority.matches(':').count() > 1 {
152+ return Ok((authority.to_ascii_lowercase(), None));
153+ }
154+ match authority.split_once(':') {
155+ Some((host, port)) => Ok((
156+ host.to_ascii_lowercase(),
157+ Some(port.parse().map_err(|_| OffLoopback::Unreadable)?),
158+ )),
159+ None => Ok((authority.to_ascii_lowercase(), None)),
160+ }
161+}
162+
163+fn classify_host(host: &str) -> HostKind {
164+ if host == "localhost" {
165+ return HostKind::Loopback;
166+ }
167+ if host == "0.0.0.0" || host == "::" || host == "[::]" {
168+ return HostKind::EveryInterface;
169+ }
170+ if host == "::1" {
171+ return HostKind::Loopback;
172+ }
173+ let octets: Vec<&str> = host.split('.').collect();
174+ if octets.len() == 4 && octets.iter().all(|part| part.parse::<u8>().is_ok()) {
175+ return if octets[0] == "127" {
176+ HostKind::Loopback
177+ } else {
178+ HostKind::Remote
179+ };
180+ }
181+ if host.contains(':') {
182+ // Any other IPv6 literal. `::ffff:0.0.0.0` and friends are deliberately
183+ // not decoded into their embedded IPv4 form: a lane that tried would be
184+ // making a routing judgement, and refusing is the fail-closed answer.
185+ return HostKind::Remote;
186+ }
187+ if host.is_empty() {
188+ return HostKind::Unreadable;
189+ }
190+ HostKind::Remote
191+}
192+
193+#[cfg(test)]
194+mod tests {
195+ use super::*;
196+
197+ #[test]
198+ fn exos_own_default_bind_is_loopback_and_is_admitted() {
199+ let endpoint = LoopbackEndpoint::parse(EXO_SERVE_DEFAULT_BIND).expect("loopback");
200+ assert_eq!(endpoint.host(), "127.0.0.1");
201+ assert_eq!(endpoint.port(), Some(4766));
202+ assert_eq!(endpoint.to_string(), EXO_SERVE_DEFAULT_BIND);
203+ }
204+
205+ #[test]
206+ fn every_loopback_spelling_is_admitted() {
207+ for spelling in [
208+ "127.0.0.1",
209+ "127.0.0.1:4766",
210+ "127.1.2.3:4766",
211+ "localhost:4766",
212+ "LOCALHOST",
213+ "::1",
214+ "[::1]:4766",
215+ "http://127.0.0.1:4766/request",
216+ "http://localhost:4766",
217+ ] {
218+ assert!(
219+ LoopbackEndpoint::parse(spelling).is_ok(),
220+ "{spelling} is loopback"
221+ );
222+ }
223+ }
224+
225+ /// The two plausible mistakes. Both read as "local" and neither is.
226+ #[test]
227+ fn binding_every_interface_is_refused() {
228+ for spelling in ["0.0.0.0:4766", "0.0.0.0", "::", "[::]:4766"] {
229+ assert_eq!(
230+ LoopbackEndpoint::parse(spelling),
231+ Err(OffLoopback::EveryInterface),
232+ "{spelling}"
233+ );
234+ }
235+ }
236+
237+ #[test]
238+ fn a_remote_host_is_refused() {
239+ for spelling in [
240+ "10.0.0.4:4766",
241+ "100.64.7.9:4766",
242+ "192.168.1.20",
243+ "exo.example.com:4766",
244+ "http://exo.openagents.com/request",
245+ "[fd7a:115c::1]:4766",
246+ ] {
247+ assert_eq!(
248+ LoopbackEndpoint::parse(spelling),
249+ Err(OffLoopback::RemoteHost),
250+ "{spelling}"
251+ );
252+ }
253+ }
254+
255+ /// A host hidden behind userinfo, an unknown scheme, and an empty string
256+ /// are refused rather than guessed at. Unreadable fails closed.
257+ #[test]
258+ fn an_address_this_build_cannot_read_is_refused_rather_than_assumed_local() {
259+ for spelling in [
260+ "http://127.0.0.1@evil.example.com/request",
261+ "ssh://127.0.0.1:4766",
262+ "",
263+ " ",
264+ "127.0.0.1:notaport",
265+ ] {
266+ assert_eq!(
267+ LoopbackEndpoint::parse(spelling),
268+ Err(OffLoopback::Unreadable),
269+ "{spelling:?}"
270+ );
271+ }
272+ }
273+}
diff --git a/crates/omega_exo_lane/src/omega_exo_lane.rs b/crates/omega_exo_lane/src/omega_exo_lane.rs
new file mode 100644
index 0000000000..5171d29776
--- /dev/null
+++ b/crates/omega_exo_lane/src/omega_exo_lane.rs
@@ -0,0 +1,312 @@
1+//! The Exo harness lane, Tier A. `OMEGA-DELTA-0042`, omega#87.
2+//!
3+//! Omega drives **`exoharness/exo`** — the recursive-self-improvement agent
4+//! harness from the Braintrust orbit — as one more executor lane beneath Omega
5+//! Agent, beside the native loop and the ACP agents. It is **not** exo labs'
6+//! `exo-explore/exo` cluster-inference appliance; omega#86 was closed for
7+//! targeting the wrong one, and [`EXO_PIN`] names the repository so the
8+//! distinction is a field rather than a sentence.
9+//!
10+//! This crate is the **law**, and it is a leaf: no GPUI, no process, no
11+//! filesystem, no clock. It decides which Exo is admitted ([`pin`]), which
12+//! command lines exist ([`command`]), where Exo may be reached ([`endpoint`]),
13+//! which Exo agents may be run ([`capability`]), and what a turn's output means
14+//! ([`turn`]). The half that spawns a process and builds a thread lives in
15+//! `crates/agent_ui/src/omega_exo_connection.rs`, for the same reason the
16+//! router's decision half lives here and its dispatch half lives there: a law
17+//! that needs GPUI to check is a law nobody checks.
18+//!
19+//! # Tier A, and what it is not
20+//!
21+//! Tier A is coarse by design: one shot per turn, no live text deltas, tool
22+//! activity visible after the fact. That is Exo's limit at this pin — its turn
23+//! streaming exists only as an in-process enum consumed by its own REPL and
24+//! serialised to no transport at all. Tier A proves the lane, the identity
25+//! binding, and the disclosure before any wire work.
26+//!
27+//! **Tier B** — a streaming transport contributed to Exo, attached through
28+//! `crates/agent_servers` like `codex-acp` — is not started here. Exo's adapter
29+//! types are a closed Rust enum, so it is an upstream contribution or a
30+//! maintained fork, not a configuration.
31+//!
32+//! **Tier C** — Exo's self-modification — is out of scope and stays out.
33+//! [`capability`] is the part of that exclusion this tier owes.
34+//!
35+//! # Which executor class an Exo thread reports, and why
36+//!
37+//! [`ExecutorClass`] is closed at three variants by `OMEGA-AGENT-AC-04`, and it
38+//! answers one question: **who ran the work.** An Exo thread reports
39+//! [`ExecutorClass::ExternalAcp`].
40+//!
41+//! * Not [`ExecutorClass::NativeLoop`]. That is the first-party claim — Omega's
42+//! own in-process loop in `crates/agent`. Presenting somebody else's output
43+//! as Omega's own is the dishonest attribution omega#77 exists to stop, and
44+//! this would be its worst case: Exo's agent has an unrestricted networked
45+//! shell.
46+//! * Not [`ExecutorClass::EngineLane`]. An engine lane *is* Full Auto
47+//! authority. `ExecutorDisclosure::is_coherent` requires a `run_ref` on one,
48+//! and the router will not route an unpinned thread to one because owner gate
49+//! 8 admits only an explicit human action into that authority. Exo has no
50+//! engine run, no run reference, and no receipt. A lane that reported
51+//! `EngineLane` would be a new door into Full Auto authority reachable by
52+//! adding an executor — a fourth of exactly the kind that were removed from
53+//! OpenAgents Desktop on 2026-07-25. An Exo agent with a shell is precisely
54+//! the caller that gate exists for.
55+//! * So [`ExecutorClass::ExternalAcp`]. Its documentation says "an external ACP
56+//! agent reached through `crates/agent_servers`", and Tier A reaches Exo over
57+//! its CLI rather than over ACP — so the fit is worth stating rather than
58+//! assuming. The class is about the *executor*, not the *wire*: a separate
59+//! process, outside Omega, that Omega does not own, that reports no model
60+//! through `AgentConnection::model_selector`, and that must carry no
61+//! `run_ref` — which is exactly what `is_coherent` requires of this class and
62+//! exactly what is true of Exo. Tier B swaps the CLI for ACP and **the class
63+//! does not change**, which is the test that the class was about the right
64+//! thing.
65+//!
66+//! A fourth variant was considered and not taken. It would need a revision of
67+//! `OMEGA-AGENT-AC-04`, and the argument for it would have to be that the wire
68+//! is part of who ran the work. It is not: the wire is why the lane is coarse,
69+//! and the coarseness is visible to the reader without a class, because no text
70+//! delta ever arrives. That argument is written here so a later reader can
71+//! disagree with something concrete rather than re-derive it.
72+//!
73+//! # The disclosure names Exo, its executor, and its model
74+//!
75+//! [`ExecutorDisclosure`] is a record of parts a label renders, never a stored
76+//! label — omega#74's binding condition, held mechanically by
77+//! `EXECUTOR_DISCLOSURE_FIELDS`. So the lane adds no field. It fills the ones
78+//! that exist:
79+//!
80+//! | Part | Exo |
81+//! | --- | --- |
82+//! | `class` | `ExternalAcp`, for the reasons above |
83+//! | `agent_id` | `exo/<executor>` — the process and the executor inside it |
84+//! | `provider` | the model binding's base URL, or *not disclosed* |
85+//! | `model` | the upstream model the binding resolves to |
86+//! | `run_ref` | `None`; Exo holds no Omega run authority |
87+//!
88+//! `provider` is genuinely `None` for a default binding, because Exo's LLM
89+//! binding has no provider field at all — only an optional base URL. Saying
90+//! "not disclosed" is the honest third option omega#77 added for exactly this,
91+//! and it is better than deriving "openai" from the absence of a URL, which
92+//! would be Omega inventing a fact about somebody else's configuration.
93+
94+pub mod capability;
95+pub mod command;
96+pub mod endpoint;
97+pub mod pin;
98+pub mod turn;
99+
100+pub use capability::{ExoAgent, ExoAgentReadError, SelfModification};
101+pub use command::{ADMITTED_LANE_ARGV, ARGUMENT_TERMINATOR, ExoArg, ExoCommand, ExoRoot};
102+pub use endpoint::{EXO_SERVE_DEFAULT_BIND, LoopbackEndpoint, OffLoopback};
103+pub use pin::{EXO_HARNESS_ID, EXO_PIN, ExoPin, ExoPinMismatch, ObservedExoCheckout, admits_bytes};
104+pub use turn::{ExoToolActivity, ExoTurn, NotATurn};
105+
106+use omega_front_door::{ExecutorClass, ExecutorDisclosure, RouteReason};
107+
108+/// The executor class an Exo thread reports. See the module documentation for
109+/// why this is the one, and why it is not a fourth.
110+pub const EXO_EXECUTOR_CLASS: ExecutorClass = ExecutorClass::ExternalAcp;
111+
112+/// One row of `exo model list`.
113+#[derive(Clone, Debug, PartialEq, Eq)]
114+pub struct ExoModelBinding {
115+ /// The local binding name, which is what an agent record refers to.
116+ pub name: String,
117+ /// The model the binding actually resolves to upstream.
118+ pub upstream_model: String,
119+ /// The base URL, when the binding names one. `None` for Exo's default,
120+ /// which carries no provider information at all.
121+ pub base_url: Option<String>,
122+}
123+
124+impl ExoModelBinding {
125+ /// Read `exo model list` output.
126+ ///
127+ /// The table is elastic-tab-expanded, so columns are separated by runs of
128+ /// two or more spaces. A row this build cannot split into four columns is
129+ /// skipped rather than guessed at: a half-read binding would produce a
130+ /// disclosure naming a model nobody configured.
131+ #[must_use]
132+ pub fn read_table(stdout: &str) -> Vec<Self> {
133+ stdout
134+ .lines()
135+ .skip(1)
136+ .filter_map(|line| {
137+ let columns: Vec<&str> = line
138+ .split(" ")
139+ .map(str::trim)
140+ .filter(|column| !column.is_empty())
141+ .collect();
142+ let [name, upstream_model, _secret, base_url] = columns.as_slice() else {
143+ return None;
144+ };
145+ Some(Self {
146+ name: (*name).to_owned(),
147+ upstream_model: (*upstream_model).to_owned(),
148+ base_url: (*base_url != "default").then(|| (*base_url).to_owned()),
149+ })
150+ })
151+ .collect()
152+ }
153+}
154+
155+/// Everything a thread on this lane discloses.
156+///
157+/// Assembled from what Exo reported about itself: [`ExoAgent`] from
158+/// `agent show`, and the matching [`ExoModelBinding`] from `model list`. A
159+/// binding Exo did not report is *absent*, never invented.
160+#[derive(Clone, Debug, PartialEq, Eq)]
161+pub struct ExoLaneIdentity {
162+ /// The executor Exo runs the turn with: `basic`, `rlm`, `typescript`,
163+ /// `codex`, `claude-code`, `cursor`, or a module path.
164+ pub executor: String,
165+ /// The model the agent's binding resolves to upstream, when Exo reported a
166+ /// binding for it.
167+ pub model: Option<String>,
168+ /// The binding's base URL, when it has one.
169+ pub provider: Option<String>,
170+}
171+
172+impl ExoLaneIdentity {
173+ /// Bind an agent to the model bindings Exo reported.
174+ #[must_use]
175+ pub fn resolve(agent: &ExoAgent, bindings: &[ExoModelBinding]) -> Self {
176+ let binding = bindings.iter().find(|binding| binding.name == agent.model);
177+ Self {
178+ executor: agent.harness.clone(),
179+ model: binding.map(|binding| binding.upstream_model.clone()),
180+ provider: binding.and_then(|binding| binding.base_url.clone()),
181+ }
182+ }
183+
184+ /// The identifier a thread on this lane presents.
185+ ///
186+ /// One string, because [`ExecutorDisclosure`] has one field for it and the
187+ /// field list is closed on purpose. Both halves are facts Exo reported: the
188+ /// process is `exo`, and the executor inside it is what `agent show` said.
189+ #[must_use]
190+ pub fn agent_id(&self) -> String {
191+ format!("exo/{}", self.executor)
192+ }
193+
194+ /// The disclosure record for a thread on this lane.
195+ #[must_use]
196+ pub fn disclosure(&self, route: Option<RouteReason>) -> ExecutorDisclosure {
197+ ExecutorDisclosure {
198+ class: EXO_EXECUTOR_CLASS,
199+ agent_id: self.agent_id(),
200+ provider: self.provider.clone(),
201+ model: self.model.clone(),
202+ run_ref: None,
203+ route,
204+ }
205+ }
206+}
207+
208+#[cfg(test)]
209+mod tests {
210+ use super::*;
211+
212+ /// Real `exo model list` output from the pinned Exo, 2026-07-25.
213+ const DRIVEN_MODELS: &str = "\
214+MODEL UPSTREAM_MODEL SECRET BASE_URL
215+gpt5mini gpt-5-mini openai default
216+";
217+
218+ fn driven_agent() -> ExoAgent {
219+ ExoAgent {
220+ slug: "omega-lane".into(),
221+ harness: "basic".into(),
222+ model: "gpt5mini".into(),
223+ agent_authored_tools: false,
224+ tool_modules: 0,
225+ read_write_mount: false,
226+ networking: false,
227+ }
228+ }
229+
230+ #[test]
231+ fn the_model_table_that_was_driven_reads_back() {
232+ let bindings = ExoModelBinding::read_table(DRIVEN_MODELS);
233+ assert_eq!(
234+ bindings,
235+ vec![ExoModelBinding {
236+ name: "gpt5mini".into(),
237+ upstream_model: "gpt-5-mini".into(),
238+ base_url: None,
239+ }]
240+ );
241+ }
242+
243+ /// The disclosure line, on the identity that was actually driven. It names
244+ /// Exo, the executor inside it, and the model — and it says the provider is
245+ /// not disclosed rather than inventing one.
246+ #[test]
247+ fn an_exo_thread_names_exo_its_executor_and_its_model() {
248+ let identity =
249+ ExoLaneIdentity::resolve(&driven_agent(), &ExoModelBinding::read_table(DRIVEN_MODELS));
250+ let disclosure = identity.disclosure(Some(RouteReason::PinHonored));
251+ assert!(disclosure.is_coherent());
252+
253+ let line = disclosure.label();
254+ assert!(line.contains("exo/"), "{line}");
255+ assert!(line.contains("basic"), "{line}");
256+ assert!(line.contains("gpt-5-mini"), "{line}");
257+ assert!(line.contains("provider not disclosed"), "{line}");
258+ assert!(line.starts_with(EXO_EXECUTOR_CLASS.token()), "{line}");
259+ }
260+
261+ /// The class decision, as an oracle rather than a comment. An Exo thread
262+ /// must never claim Omega's own loop, and must never claim engine-lane
263+ /// authority.
264+ #[test]
265+ fn an_exo_thread_claims_neither_omegas_own_loop_nor_full_auto_authority() {
266+ assert_ne!(EXO_EXECUTOR_CLASS, ExecutorClass::NativeLoop);
267+ assert_ne!(EXO_EXECUTOR_CLASS, ExecutorClass::EngineLane);
268+ let identity =
269+ ExoLaneIdentity::resolve(&driven_agent(), &ExoModelBinding::read_table(DRIVEN_MODELS));
270+ let disclosure = identity.disclosure(Some(RouteReason::PinHonored));
271+ assert_eq!(disclosure.run_ref, None, "Exo holds no Omega run authority");
272+
273+ // The same record with engine-lane authority claimed is incoherent, so
274+ // the coherence law is what stops it rather than this lane's manners.
275+ let forged = ExecutorDisclosure {
276+ class: ExecutorClass::EngineLane,
277+ ..disclosure
278+ };
279+ assert!(!forged.is_coherent());
280+ }
281+
282+ /// A binding Exo did not report leaves the model absent. The alternative —
283+ /// falling back to the agent's local alias — would put `gpt5mini` in a
284+ /// field that means "the model that served the turn".
285+ #[test]
286+ fn an_unreported_binding_leaves_the_model_undisclosed() {
287+ let identity = ExoLaneIdentity::resolve(&driven_agent(), &[]);
288+ assert_eq!(identity.model, None);
289+ let disclosure = identity.disclosure(None);
290+ assert!(disclosure.is_coherent());
291+ assert!(disclosure.label().contains("model not disclosed"));
292+ }
293+
294+ #[test]
295+ fn a_binding_with_a_base_url_discloses_it_as_the_provider() {
296+ let bindings = ExoModelBinding::read_table(
297+ "MODEL UPSTREAM_MODEL SECRET BASE_URL\n\
298+ glm z-ai/glm-5.2 or https://openrouter.ai/api/v1\n",
299+ );
300+ let agent = ExoAgent {
301+ model: "glm".into(),
302+ ..driven_agent()
303+ };
304+ let identity = ExoLaneIdentity::resolve(&agent, &bindings);
305+ assert_eq!(
306+ identity.provider.as_deref(),
307+ Some("https://openrouter.ai/api/v1")
308+ );
309+ assert_eq!(identity.model.as_deref(), Some("z-ai/glm-5.2"));
310+ assert!(identity.disclosure(None).is_coherent());
311+ }
312+}
diff --git a/crates/omega_exo_lane/src/pin.rs b/crates/omega_exo_lane/src/pin.rs
new file mode 100644
index 0000000000..ea0097a88a
--- /dev/null
+++ b/crates/omega_exo_lane/src/pin.rs
@@ -0,0 +1,287 @@
1+//! Which Exo. `OMEGA-DELTA-0042`, omega#87.
2+//!
3+//! Exo declares itself unstable and writes the house rule "do not write
4+//! fallback code or handle backwards compatibility" into its own `AGENTS.md`.
5+//! `0.1.0`, no tags, nothing published. A lane that said "whatever `exo` is on
6+//! `PATH`" would be a lane whose behaviour is decided by somebody else's next
7+//! push.
8+//!
9+//! So the lane names an exact commit, and it names the tree that commit points
10+//! at. Both, for the same reason `omega-effectd` is pinned by release tag *and*
11+//! asset digest in `script/prove-omega-rc-install`: a commit id alone is
12+//! satisfied by a force-pushed branch that still answers to the same forty
13+//! characters in a clone nobody re-fetched, and a tree alone produces a refusal
14+//! nobody can act on.
15+//!
16+//! # What this file does and does not pin
17+//!
18+//! [`EXO_PIN`] is an **identity**: upstream, commit, tree, version. It is the
19+//! same on every machine, it is checked in, and it is what a reader compares a
20+//! clone against.
21+//!
22+//! The **bytes that actually run** are a different fact, because Exo has no
23+//! release artifact — the install path is `curl setup.sh | bash`, which clones
24+//! and builds from source, so two hosts at the same commit hold two different
25+//! binaries. Those bytes go through the mechanism that already exists for
26+//! exactly this: [`omega_harness::HarnessPinLedger`], under the harness id
27+//! [`EXO_HARNESS_ID`], holding a [`MeasuredDigest`] the host computed from the
28+//! binary it is about to run.
29+//!
30+//! [`MeasuredDigest`]: omega_harness::MeasuredDigest
31+//!
32+//! That split is the whole point. The identity is a claim anyone can check
33+//! against upstream; the digest is a measurement only this host can make. They
34+//! must not share a type and they must not share a file.
35+
36+use omega_harness::MeasuredDigest;
37+
38+/// The harness id the Exo lane is frozen under in the pin ledger.
39+///
40+/// The same namespace `codex-acp` uses, because Exo is the same kind of thing:
41+/// an external harness Omega runs and does not own.
42+pub const EXO_HARNESS_ID: &str = "exo";
43+
44+/// The Exo this lane drives.
45+///
46+/// **`exoharness/exo`, the recursive-self-improvement agent harness** — not exo
47+/// labs' `exo-explore/exo` cluster-inference appliance, which shares only a
48+/// name. omega#86 was closed for targeting the wrong one. The upstream URL is
49+/// part of the pin so the distinction is a field a test can read rather than a
50+/// sentence in a doc.
51+#[derive(Clone, Copy, Debug, PartialEq, Eq)]
52+pub struct ExoPin {
53+ /// The repository, in full. See the type documentation for why.
54+ pub upstream: &'static str,
55+ /// The exact commit the lane was audited and driven against.
56+ pub source_commit: &'static str,
57+ /// The tree that commit points at.
58+ pub source_tree: &'static str,
59+ /// The workspace version at that commit, for a message a person can read.
60+ pub version: &'static str,
61+}
62+
63+/// The pin. Audited in the openagents teardown
64+/// `docs/teardowns/2026-07-25-exoharness-exo-teardown.md` and driven for real
65+/// by omega#87.
66+pub const EXO_PIN: ExoPin = ExoPin {
67+ upstream: "https://github.com/exoharness/exo",
68+ source_commit: "baa07f6785547080d99bd2a7d3eab6d76b984e35",
69+ source_tree: "0aff9139a166414fa51a09b66ba4785bae05b46b",
70+ version: "0.1.0",
71+};
72+
73+/// Why a local Exo checkout is not the pinned one.
74+#[derive(Clone, Copy, Debug, PartialEq, Eq)]
75+pub enum ExoPinMismatch {
76+ /// The clone is a different repository. This is the omega#86 failure: the
77+ /// other Exo.
78+ Upstream,
79+ /// The checkout is at a different commit.
80+ Commit,
81+ /// The checkout is at the pinned commit and the tree underneath it is not
82+ /// the pinned tree — a rewritten history, a dirty tree, or a substituted
83+ /// object store.
84+ Tree,
85+ /// The bytes about to run are not the bytes the owner froze.
86+ Bytes,
87+}
88+
89+impl std::fmt::Display for ExoPinMismatch {
90+ fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
91+ formatter.write_str(match self {
92+ Self::Upstream => "the Exo checkout is not exoharness/exo",
93+ Self::Commit => "the Exo checkout is not at the pinned commit",
94+ Self::Tree => "the Exo checkout's tree is not the pinned tree",
95+ Self::Bytes => "the Exo binary is not the bytes frozen in the pin ledger",
96+ })
97+ }
98+}
99+
100+impl std::error::Error for ExoPinMismatch {}
101+
102+/// What a host observed about a local Exo checkout.
103+///
104+/// Every field is something the host read: `git config --get remote.origin.url`,
105+/// `git rev-parse HEAD`, `git rev-parse HEAD^{tree}`. A record of observations,
106+/// which is why it is a separate type from [`ExoPin`], which is a record of
107+/// decisions.
108+#[derive(Clone, Debug, PartialEq, Eq)]
109+pub struct ObservedExoCheckout {
110+ /// The remote the checkout came from.
111+ pub upstream: String,
112+ /// `HEAD`.
113+ pub commit: String,
114+ /// `HEAD^{tree}`.
115+ pub tree: String,
116+}
117+
118+impl ExoPin {
119+ /// Whether an observed checkout is the Exo this lane admits.
120+ ///
121+ /// Upstream is compared after normalising a trailing `.git` and a trailing
122+ /// slash, because `https://github.com/exoharness/exo.git` and
123+ /// `https://github.com/exoharness/exo` are the same repository and refusing
124+ /// one of them would be a cosmetic refusal. `git@github.com:exoharness/exo`
125+ /// is *not* normalised into the same string; it is compared by its
126+ /// `owner/name` suffix, which is the part that carries the identity.
127+ ///
128+ /// # Errors
129+ ///
130+ /// Returns the first thing that differed, most-identifying first: a
131+ /// checkout of the wrong repository is reported as the wrong repository
132+ /// even though its commit also differs.
133+ pub fn admits(&self, observed: &ObservedExoCheckout) -> Result<(), ExoPinMismatch> {
134+ if !same_repository(self.upstream, &observed.upstream) {
135+ return Err(ExoPinMismatch::Upstream);
136+ }
137+ if !self.source_commit.eq_ignore_ascii_case(observed.commit.trim()) {
138+ return Err(ExoPinMismatch::Commit);
139+ }
140+ if !self.source_tree.eq_ignore_ascii_case(observed.tree.trim()) {
141+ return Err(ExoPinMismatch::Tree);
142+ }
143+ Ok(())
144+ }
145+}
146+
147+/// Whether the bytes this host measured are the bytes the owner froze.
148+///
149+/// Takes the frozen digest as a `&str` and the live one as a [`MeasuredDigest`]
150+/// for the reason that type exists: a pin read back from a file is a recorded
151+/// claim about a past measurement, and this process did not make it.
152+///
153+/// An **unfrozen** Exo is admitted, and that is deliberate rather than an
154+/// oversight. `codex-acp` behaves the same way: a pin is the owner freezing a
155+/// version, not a precondition for running anything at all. What is refused is
156+/// a *disagreement* — the case where the owner said "these bytes" and the host
157+/// is holding different ones.
158+///
159+/// # Errors
160+///
161+/// [`ExoPinMismatch::Bytes`] when a pin exists and the measurement differs.
162+pub fn admits_bytes(frozen: Option<&str>, measured: &MeasuredDigest) -> Result<(), ExoPinMismatch> {
163+ match frozen {
164+ Some(frozen) if !measured.matches_recorded(frozen) => Err(ExoPinMismatch::Bytes),
165+ Some(_) | None => Ok(()),
166+ }
167+}
168+
169+/// Whether two remote strings name the same repository.
170+fn same_repository(pinned: &str, observed: &str) -> bool {
171+ normalize_remote(pinned) == normalize_remote(observed)
172+}
173+
174+/// `owner/name`, lowercased, with the transport, host, trailing slash and
175+/// `.git` suffix removed.
176+fn normalize_remote(remote: &str) -> String {
177+ let remote = remote.trim().trim_end_matches('/');
178+ let remote = remote.strip_suffix(".git").unwrap_or(remote);
179+ let tail = remote.rsplit(['/', ':']).take(2).collect::<Vec<_>>();
180+ tail.into_iter()
181+ .rev()
182+ .collect::<Vec<_>>()
183+ .join("/")
184+ .to_ascii_lowercase()
185+}
186+
187+#[cfg(test)]
188+mod tests {
189+ use super::*;
190+
191+ fn pinned_checkout() -> ObservedExoCheckout {
192+ ObservedExoCheckout {
193+ upstream: EXO_PIN.upstream.to_string(),
194+ commit: EXO_PIN.source_commit.to_string(),
195+ tree: EXO_PIN.source_tree.to_string(),
196+ }
197+ }
198+
199+ /// The commit and tree are forty hex characters each. A pin that had been
200+ /// filled in with a branch name, a tag, or a truncated id would name
201+ /// something that can move, which is the whole thing the pin exists to
202+ /// stop.
203+ #[test]
204+ fn the_pin_names_an_exact_commit_and_an_exact_tree() {
205+ for value in [EXO_PIN.source_commit, EXO_PIN.source_tree] {
206+ assert_eq!(value.len(), 40, "{value} is not a full object id");
207+ assert!(
208+ value.chars().all(|c| c.is_ascii_hexdigit() && !c.is_ascii_uppercase()),
209+ "{value} is not a lowercase hexadecimal object id"
210+ );
211+ }
212+ assert_ne!(
213+ EXO_PIN.source_commit, EXO_PIN.source_tree,
214+ "a commit and the tree it points at are different objects"
215+ );
216+ }
217+
218+ /// omega#86's failure, as a test. The two Exos share a name and nothing
219+ /// else, and the one this lane drives is the harness.
220+ #[test]
221+ fn the_pin_names_the_harness_exo_and_not_the_cluster_one() {
222+ assert_eq!(EXO_PIN.upstream, "https://github.com/exoharness/exo");
223+ let cluster = ObservedExoCheckout {
224+ upstream: "https://github.com/exo-explore/exo".into(),
225+ ..pinned_checkout()
226+ };
227+ assert_eq!(EXO_PIN.admits(&cluster), Err(ExoPinMismatch::Upstream));
228+ }
229+
230+ #[test]
231+ fn the_pinned_checkout_is_admitted() {
232+ assert_eq!(EXO_PIN.admits(&pinned_checkout()), Ok(()));
233+ }
234+
235+ /// A remote that differs only in `.git`, a trailing slash, case, or SSH
236+ /// versus HTTPS transport is the same repository, and refusing it would be
237+ /// a refusal about punctuation.
238+ #[test]
239+ fn the_same_repository_written_four_ways_is_the_same_repository() {
240+ for spelling in [
241+ "https://github.com/exoharness/exo.git",
242+ "https://github.com/exoharness/exo/",
243+ "https://GitHub.com/ExoHarness/Exo",
244+ "git@github.com:exoharness/exo.git",
245+ ] {
246+ let observed = ObservedExoCheckout {
247+ upstream: spelling.into(),
248+ ..pinned_checkout()
249+ };
250+ assert_eq!(EXO_PIN.admits(&observed), Ok(()), "{spelling}");
251+ }
252+ }
253+
254+ /// A moved branch that keeps the commit id it answers to is exactly the
255+ /// substitution a commit-only pin cannot see, so the tree is checked too.
256+ #[test]
257+ fn the_pinned_commit_over_a_different_tree_is_refused() {
258+ let rewritten = ObservedExoCheckout {
259+ tree: "0000000000000000000000000000000000000000".into(),
260+ ..pinned_checkout()
261+ };
262+ assert_eq!(EXO_PIN.admits(&rewritten), Err(ExoPinMismatch::Tree));
263+ }
264+
265+ #[test]
266+ fn a_different_commit_is_refused() {
267+ let moved = ObservedExoCheckout {
268+ commit: "1111111111111111111111111111111111111111".into(),
269+ ..pinned_checkout()
270+ };
271+ assert_eq!(EXO_PIN.admits(&moved), Err(ExoPinMismatch::Commit));
272+ }
273+
274+ /// The bytes half. An unfrozen harness runs; a frozen one that changed
275+ /// underneath the owner does not.
276+ #[test]
277+ fn frozen_bytes_that_changed_are_refused_and_unfrozen_bytes_run() {
278+ let measured = MeasuredDigest::measure(b"the exo binary this host built");
279+ assert_eq!(admits_bytes(None, &measured), Ok(()));
280+ assert_eq!(admits_bytes(Some(measured.as_str()), &measured), Ok(()));
281+ let other = MeasuredDigest::measure(b"a different exo binary");
282+ assert_eq!(
283+ admits_bytes(Some(other.as_str()), &measured),
284+ Err(ExoPinMismatch::Bytes)
285+ );
286+ }
287+}
diff --git a/crates/omega_exo_lane/src/turn.rs b/crates/omega_exo_lane/src/turn.rs
new file mode 100644
index 0000000000..5206660ed1
--- /dev/null
+++ b/crates/omega_exo_lane/src/turn.rs
@@ -0,0 +1,243 @@
1+//! Reading what Exo said. `OMEGA-DELTA-0042`, omega#87.
2+//!
3+//! Tier A is coarse on purpose. Exo's turn streaming exists only as an
4+//! in-process `ExecutionStreamEvent` enum consumed by its own terminal REPL and
5+//! serialised to no transport whatsoever, so a host application gets one shot
6+//! per turn, no live text deltas, and tool activity only after the fact. That
7+//! is a limit of Exo at this pin, not a shortcut here, and Tier B is where it
8+//! is lifted — by contributing a transport to Exo, whose adapter types are a
9+//! closed Rust enum.
10+//!
11+//! What `exo conversation send` prints is the messages the turn appended, one
12+//! per line, each prefixed with a compact clock and a role:
13+//!
14+//! ```text
15+//! [04:52:12] user: Reply with exactly the word OMEGA-EXO-TIER-A and nothing else.
16+//! [04:52:12] assistant: [reasoning]
17+//! [04:52:12] assistant: OMEGA-EXO-TIER-A
18+//! ```
19+//!
20+//! # A turn is not "whatever came back on stdout"
21+//!
22+//! This is the parser's real job, and the reason it refuses rather than doing
23+//! its best. Driven against the pinned Exo, `conversation send` with a prompt of
24+//! `--help` **exits 0 and prints Exo's usage text**, because Exo accepts its
25+//! global options after the subcommand and consumed the prompt as one.
26+//! [`crate::command`] makes that unreachable with an argument terminator — but a
27+//! reader that treated any successful stdout as a reply would have rendered a
28+//! usage message as the model's answer, and it would have done so on exit 0
29+//! with nothing to see in a log.
30+//!
31+//! So [`ExoTurn::read`] requires the shape of a turn — at least one assistant
32+//! line — and returns [`NotATurn`] otherwise. Two independent guards against one
33+//! silent failure, because the failure was silent.
34+
35+/// What Exo produced for one turn.
36+#[derive(Clone, Debug, PartialEq, Eq)]
37+pub struct ExoTurn {
38+ /// The assistant's text, reasoning parts removed, lines joined.
39+ pub text: String,
40+ /// Tool activity, in the order Exo printed it. Visible after the fact; see
41+ /// the module documentation.
42+ pub tools: Vec<ExoToolActivity>,
43+}
44+
45+/// One tool result Exo recorded during the turn.
46+#[derive(Clone, Debug, PartialEq, Eq)]
47+pub struct ExoToolActivity {
48+ /// The tool's name, as Exo reports it.
49+ pub name: String,
50+ /// The rendered result. Exo shows a preview here; the full result is an
51+ /// artifact in its durable log.
52+ pub output: String,
53+}
54+
55+/// Why stdout was not a turn.
56+#[derive(Clone, Copy, Debug, PartialEq, Eq)]
57+pub enum NotATurn {
58+ /// Nothing on stdout carried a role at all. Exo's usage text lands here,
59+ /// which is the case this exists for.
60+ NoMessages,
61+ /// Messages were printed and none of them were the assistant's. A turn
62+ /// that echoed the user and said nothing is not a reply.
63+ NoAssistantMessage,
64+}
65+
66+impl std::fmt::Display for NotATurn {
67+ fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
68+ formatter.write_str(match self {
69+ Self::NoMessages => "Exo printed no messages, so this turn produced no reply",
70+ Self::NoAssistantMessage => "Exo printed no assistant message for this turn",
71+ })
72+ }
73+}
74+
75+impl std::error::Error for NotATurn {}
76+
77+/// The prefix Exo puts on a reasoning part.
78+const REASONING_PREFIX: &str = "[reasoning]";
79+
80+impl ExoTurn {
81+ /// Read `exo conversation send` output.
82+ ///
83+ /// # Errors
84+ ///
85+ /// [`NotATurn`] when the output does not have the shape of a turn.
86+ pub fn read(stdout: &str) -> Result<Self, NotATurn> {
87+ let mut saw_message = false;
88+ let mut text_lines: Vec<String> = Vec::new();
89+ let mut tools = Vec::new();
90+ let mut in_assistant = false;
91+
92+ for line in stdout.lines() {
93+ match strip_clock(line) {
94+ Some(body) => {
95+ saw_message = true;
96+ in_assistant = false;
97+ if let Some(assistant) = body.strip_prefix("assistant: ") {
98+ in_assistant = true;
99+ push_assistant(&mut text_lines, assistant);
100+ } else if let Some(tool) = body.strip_prefix("tool ") {
101+ if let Some((name, output)) = tool.split_once(": ") {
102+ tools.push(ExoToolActivity {
103+ name: name.to_owned(),
104+ output: output.to_owned(),
105+ });
106+ }
107+ }
108+ }
109+ // A continuation of the previous message: assistant text with a
110+ // newline in it. Only kept while an assistant message is open,
111+ // so a multi-line tool result cannot leak into the reply.
112+ None if in_assistant => text_lines.push(line.to_owned()),
113+ None => {}
114+ }
115+ }
116+
117+ if !saw_message {
118+ return Err(NotATurn::NoMessages);
119+ }
120+ if text_lines.is_empty() {
121+ return Err(NotATurn::NoAssistantMessage);
122+ }
123+ Ok(Self {
124+ text: text_lines.join("\n").trim().to_owned(),
125+ tools,
126+ })
127+ }
128+}
129+
130+/// `[04:52:12] rest` → `rest`.
131+///
132+/// Matched by shape rather than by regex: eleven characters, brackets at the
133+/// ends of the clock, digits and colons between. A line Exo did not print with
134+/// a clock is a continuation, not a message.
135+fn strip_clock(line: &str) -> Option<&str> {
136+ let rest = line.strip_prefix('[')?;
137+ let (clock, rest) = rest.split_once("] ")?;
138+ if clock.len() != 8 || !clock.chars().all(|c| c.is_ascii_digit() || c == ':') {
139+ return None;
140+ }
141+ Some(rest)
142+}
143+
144+/// Keep the assistant's words and drop the parts that are not words.
145+///
146+/// Reasoning is dropped rather than rendered: at this pin Exo prints it as a
147+/// bare `[reasoning]` marker with the text usually empty, and showing an empty
148+/// marker as the model's reply would be worse than showing nothing. Tool-call
149+/// and tool-result parts are dropped from the *text* because they are carried
150+/// as [`ExoToolActivity`] instead, so a reader does not see them twice.
151+fn push_assistant(text_lines: &mut Vec<String>, content: &str) {
152+ let content = content.trim();
153+ if content.is_empty() || content == REASONING_PREFIX {
154+ return;
155+ }
156+ if content.starts_with(REASONING_PREFIX)
157+ || content.starts_with("[tool_call ")
158+ || content.starts_with("[tool_result ")
159+ {
160+ return;
161+ }
162+ text_lines.push(content.to_owned());
163+}
164+
165+#[cfg(test)]
166+mod tests {
167+ use super::*;
168+
169+ /// Exactly what the pinned Exo printed for the turn that proved this lane,
170+ /// on 2026-07-25. Copied from the terminal, not composed here.
171+ const DRIVEN_TURN: &str = "\
172+[04:52:12] user: Reply with exactly the word OMEGA-EXO-TIER-A and nothing else.
173+[04:52:12] assistant: [reasoning]
174+[04:52:12] assistant: OMEGA-EXO-TIER-A
175+";
176+
177+ #[test]
178+ fn the_turn_that_was_actually_driven_reads_back() {
179+ let turn = ExoTurn::read(DRIVEN_TURN).expect("a real turn");
180+ assert_eq!(turn.text, "OMEGA-EXO-TIER-A");
181+ assert!(turn.tools.is_empty());
182+ }
183+
184+ /// The silent failure, as a test. Exo's usage text on exit 0 must not be
185+ /// readable as a reply.
186+ #[test]
187+ fn exos_usage_text_is_not_a_turn() {
188+ let usage = "\
189+Usage: exo conversation send [OPTIONS] <AGENT> <CONVERSATION> <PROMPT>
190+
191+Arguments:
192+ <AGENT>
193+ <CONVERSATION>
194+ <PROMPT>
195+";
196+ assert_eq!(ExoTurn::read(usage), Err(NotATurn::NoMessages));
197+ }
198+
199+ #[test]
200+ fn a_turn_with_no_assistant_message_is_refused() {
201+ let echoed = "[04:52:12] user: hello\n";
202+ assert_eq!(ExoTurn::read(echoed), Err(NotATurn::NoAssistantMessage));
203+ }
204+
205+ #[test]
206+ fn tool_activity_is_read_and_kept_out_of_the_reply_text() {
207+ let with_tools = "\
208+[04:52:12] user: list the directory
209+[04:52:13] assistant: [tool_call shell] {\"command\":\"ls\"}
210+[04:52:14] tool shell: Cargo.toml
211+[04:52:15] assistant: There is one file.
212+";
213+ let turn = ExoTurn::read(with_tools).expect("a turn");
214+ assert_eq!(turn.text, "There is one file.");
215+ assert_eq!(
216+ turn.tools,
217+ vec![ExoToolActivity {
218+ name: "shell".into(),
219+ output: "Cargo.toml".into(),
220+ }]
221+ );
222+ }
223+
224+ /// A reply with a newline in it arrives as an unprefixed continuation line.
225+ #[test]
226+ fn a_multi_line_reply_keeps_its_lines() {
227+ let multi = "[04:52:12] assistant: first\nsecond\n";
228+ assert_eq!(ExoTurn::read(multi).expect("a turn").text, "first\nsecond");
229+ }
230+
231+ /// A multi-line *tool* result must not run on into the reply, or a tool
232+ /// that printed a paragraph would be attributed to the model.
233+ #[test]
234+ fn a_multi_line_tool_result_does_not_leak_into_the_reply() {
235+ let leaky = "\
236+[04:52:14] tool shell: line one
237+line two
238+[04:52:15] assistant: done
239+";
240+ let turn = ExoTurn::read(leaky).expect("a turn");
241+ assert_eq!(turn.text, "done");
242+ }
243+}