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 coordinate30617:7649603503856e5148d571eac2766b288a8ff1e9e35d380337a1d2b0015b4f92:omega
MaintainersHidden in public view
References2 branches · 1 tag
Read-only clonegit clone https://openagents.com/git/tenant.openagents/omega.git

Revision diff

890f9fa4 a39c3cd4
diff --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(&params.session_id).cloned() else {
477+            return Task::ready(Err(anyhow!("no Exo thread for {}", params.session_id.0)));
478+        };
479+        let prompt = prompt_text(&params);
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+}
Served at tenant.openagents/omega Member data and write actions are omitted.