Skip to repository content

tenant.openagents/omega

No repository description is available.

OpenAgents Git authority 2026-07-28T00:54:53.514Z 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

73dadebb b23a5d15
diff --git a/Cargo.lock b/Cargo.lock
index d5a3f25a0b..7fa5bd2ea1 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -11993,6 +11993,7 @@ version = "0.1.0"
1199311993 dependencies = [
1199411994  "omega_front_door",
1199511995  "omega_harness",
11996+ "serde",
1199611997 ]
1199711998 
1199811999 [[package]]
diff --git a/OMEGA_DELTAS.md b/OMEGA_DELTAS.md
index 7cb32ca55d..9bccab36ba 100644
--- a/OMEGA_DELTAS.md
+++ b/OMEGA_DELTAS.md
@@ -2075,9 +2075,11 @@ than it sounds, because the harness omega#81's acceptance sentence names —
20752075 - **Upstream Zed:** external agents are attached over ACP or configured as
20762076   custom agent servers. There is no notion of an executor whose own agent can
20772077   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'
2078+- **Which Exo, first.** This is the maintained `OpenAgentsInc/exo` fork of
2079+  `exoharness/exo`, the recursive-self-improvement agent harness. Omega pins
2080+  commit `cd7c0d29db869e953fb7261d8390ca93007d36a6` and tree
2081+  `c61846e3f44daaf445930d1a499432ca9b069306`. The fork contains the ACP
2082+  transport while the upstream change is under review. It is **not** exo labs'
20812083   `exo-explore/exo` cluster-inference appliance, which shares a name and nothing
20822084   else; omega#86 was closed for integrating the wrong one. The pin therefore
20832085   carries the repository as a field a test reads, alongside the commit and the
@@ -2089,9 +2091,8 @@ than it sounds, because the harness omega#81's acceptance sentence names —
20892091   harness whose agent has an unrestricted networked shell and can rebuild
20902092   itself. Exo has no approval prompt anywhere by design — its security model is
20912093   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:
2094+  itself. Omega adds the gate Exo does not have. The turn path runs these checks
2095+  **before** it sends. Each check uses a live observation:
20952096   - **where Exo is** — `EXO_EXOHARNESS_URL` is inherited from the environment,
20962097     and setting it redirects the lane from the state root on disk to an HTTP
20972098     server that has no authentication and full access to Exo's secrets. It is
@@ -2102,26 +2103,22 @@ than it sounds, because the harness omega#81's acceptance sentence names —
21022103     tree, and the pin admits or refuses it;
21032104   - **which bytes** — the binary is measured and compared against the owner's
21042105     `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.
2106+  - **which capability** — Omega reads both `exo agent show` and
2107+    `exo conversation show`. The read includes exact tool-module paths, module
2108+    digests, and agent-level and conversation-level mounts. The normal path
2109+    refuses a self-modifying turn. A person can use a dedicated warning dialog
2110+    to authorize one exact draft. The one-use grant binds the source commit and
2111+    tree, binary digest, agent, conversation, connection generation, objective,
2112+    capability set, tool-module digests, mount paths, turn reference, and
2113+    expiry. Drift, reuse, restart, cancellation, and a different draft refuse.
2114+    Networking is reported and is not refused.
2115+- **Prompts cross ACP, not command-line arguments.** Omega starts
2116+  `exo --root <root> acp <agent> <conversation>`. ACP JSON-RPC carries the
2117+  prompt on standard input. User text cannot become Exo argument syntax.
2118+- **Omega never configures Exo.** The admitted command starts `exo acp` and
2119+  reads capability records. `create`, `update`, `delete`, `mount`, `set`,
2120+  `register`, `configure`, `serve`, and `repl` are unreachable. Exo owns its
2121+  state and its durable turn log.
21252122 - **The executor class, decided rather than defaulted.** `ExecutorClass` is
21262123   closed at three by `OMEGA-AGENT-AC-04`, and it answers *who ran the work*. An
21272124   Exo thread reports `ExternalAcp`. Not `NativeLoop`, which is the first-party
@@ -2132,9 +2129,8 @@ than it sounds, because the harness omega#81's acceptance sentence names —
21322129   fourth model-reachable door into Full Auto authority opened by adding an
21332130   executor, three of which were removed from OpenAgents Desktop the same day.
21342131   `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
2132+  separate process Omega does not own, carrying no run reference. ACP does not
2133+  change the class. A fourth variant was considered and **not
21382134   taken**; the argument is written in `crates/omega_exo_lane` so a later reader
21392135   can disagree with something concrete.
21402136 - **The disclosure names Exo, its executor, and its model** —
@@ -2143,16 +2139,26 @@ than it sounds, because the harness omega#81's acceptance sentence names —
21432139   closed on purpose. `provider` is genuinely absent because Exo's LLM binding
21442140   has no provider field at all, only an optional base URL, and saying "not
21452141   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.
2142+- **The stream is typed.** Exo maps each `ExecutionStreamEvent` to ACP. Omega
2143+  receives live text chunks, tool calls, tool results, and a completion record.
2144+  The completion metadata includes Exo's durable session, turn, and latest-event
2145+  references. ACP cancellation uses cooperative executor cancellation. Exo
2146+  appends a cancellation event and closes the durable turn before it reports
2147+  `Cancelled`.
2148+- **Turn-boundary behavior is explicit.** Exo declares `CannotSteer`. Omega
2149+  queues a mid-turn prompt. It does not guess that Exo can steer, and it does not
2150+  turn a steer request into an implicit cancellation.
2151+- **Self-modification receipts are durable.** Omega writes the one-use authority
2152+  decision and Exo's returned durable references to
2153+  `exo-self-modification-receipts.jsonl`. The thread shows the outcome,
2154+  generation, and latest event reference.
21502155 - **Enforced by:** `the_exo_lane_drives_the_harness_exo_and_not_the_cluster_one`,
21512156   `the_exo_lane_puts_no_user_text_before_the_argument_terminator`,
21522157   `the_exo_lane_exposes_no_endpoint_off_this_machine`,
21532158   `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
2159+  `an_exo_turn_checks_the_pin_and_the_agent_before_it_sends`,
2160+  `an_exo_turn_streams_cancels_and_requires_exact_one_use_authority`, and
2161+  `the_exo_lane_is_reachable_from_omega_agent` in `crates/omega_deltas/`, the 40
21562162   unit checks in `crates/omega_exo_lane/`, and `drives_a_real_exo` in
21572163   `crates/agent_ui/`, which is `#[ignore]`d because it needs a real Exo.
21582164 - **Falsified against a running Exo,** every edit probed before its run. Pointing
@@ -2168,13 +2174,9 @@ than it sounds, because the harness omega#81's acceptance sentence names —
21682174   arm supplies, and the same edit fails it. A second falsification silently
21692175   no-opped through a shell quoting error and was caught by its probe rather than
21702176   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.
2177+- **What this does not cover.** The grant does not give Exo Full Auto
2178+  authority. Omega does not edit Exo configuration. Exo remains an external ACP
2179+  executor and has no engine run reference.
21782180 
21792181 ### OMEGA-DELTA-0043 — `--uninstall` removes the installation, not one file inside it
21802182 
diff --git a/crates/acp_thread/src/acp_thread.rs b/crates/acp_thread/src/acp_thread.rs
index 946a87f2ef..f457fe3afa 100644
--- a/crates/acp_thread/src/acp_thread.rs
+++ b/crates/acp_thread/src/acp_thread.rs
@@ -2374,6 +2374,16 @@ impl AcpThread {
23742374         &self.connection
23752375     }
23762376 
2377+    /// Replace the connection facade while keeping the established ACP
2378+    /// session.
2379+    ///
2380+    /// An adapter can delegate session creation to an inner ACP connection and
2381+    /// then install its policy-enforcing facade here. All later prompts,
2382+    /// cancellation, and executor disclosure must go through that facade.
2383+    pub fn replace_connection(&mut self, connection: Rc<dyn AgentConnection>) {
2384+        self.connection = connection;
2385+    }
2386+
23772387     pub fn action_log(&self) -> &Entity<ActionLog> {
23782388         &self.action_log
23792389     }
diff --git a/crates/agent_servers/src/agent_servers.rs b/crates/agent_servers/src/agent_servers.rs
index 5983ee88d4..27b6242c1b 100644
--- a/crates/agent_servers/src/agent_servers.rs
+++ b/crates/agent_servers/src/agent_servers.rs
@@ -45,6 +45,12 @@ impl AgentServerDelegate {
4545             loading_status: loading_status_tx,
4646         }
4747     }
48+
49+    /// The store that owns external-agent commands for this connection.
50+    #[must_use]
51+    pub fn store(&self) -> &Entity<AgentServerStore> {
52+        &self.store
53+    }
4854 }
4955 
5056 pub trait AgentServer: Send {
diff --git a/crates/agent_ui/src/conversation_view.rs b/crates/agent_ui/src/conversation_view.rs
index 1e8fb8e90c..17a4b63922 100644
--- a/crates/agent_ui/src/conversation_view.rs
+++ b/crates/agent_ui/src/conversation_view.rs
@@ -1026,7 +1026,8 @@ impl ConversationView {
10261026         // server now, so "is this the native agent?" cannot be a bare downcast:
10271027         // a wrapped native agent would read as external and be refused in a
10281028         // shared project.
1029-        if project.read(cx).is_via_collab() && !crate::omega_router::is_native_agent_server(&agent) {
1029+        if project.read(cx).is_via_collab() && !crate::omega_router::is_native_agent_server(&agent)
1030+        {
10301031             return ServerState::LoadError {
10311032                 error: LoadError::Other(
10321033                     "External agents are not yet supported in shared projects.".into(),
@@ -1236,11 +1237,23 @@ impl ConversationView {
12361237             .downcast::<agent::NativeAgentConnection>()
12371238             .map(|native_connection| native_available_skills(&native_connection, &session_id, cx))
12381239             .unwrap_or_default();
1239-        let session_capabilities = Arc::new(RwLock::new(SessionCapabilities::new(
1240-            thread.read(cx).prompt_capabilities(),
1241-            thread.read(cx).available_commands().to_vec(),
1242-            available_skills,
1243-        )));
1240+        let omega_steer_capability = if connection
1241+            .clone()
1242+            .downcast::<crate::omega_exo_connection::ExoHarnessConnection>()
1243+            .is_some()
1244+        {
1245+            omega_front_door::SteerCapability::CannotSteer
1246+        } else {
1247+            omega_front_door::SteerCapability::Unknown
1248+        };
1249+        let session_capabilities = Arc::new(RwLock::new(
1250+            SessionCapabilities::new(
1251+                thread.read(cx).prompt_capabilities(),
1252+                thread.read(cx).available_commands().to_vec(),
1253+                available_skills,
1254+            )
1255+            .with_omega_steer_capability(omega_steer_capability),
1256+        ));
12441257 
12451258         let action_log = thread.read(cx).action_log().clone();
12461259 
@@ -2679,7 +2692,9 @@ impl ConversationView {
26792692                     )
26802693                     .actions_slot(
26812694                         Button::new("session-gone-new-thread", "New Thread")
2682-                            .on_click(|_, window, cx| window.dispatch_action(NewThread.boxed_clone(), cx))
2695+                            .on_click(|_, window, cx| {
2696+                                window.dispatch_action(NewThread.boxed_clone(), cx)
2697+                            })
26832698                             .into_any_element(),
26842699                     )
26852700                     .into_any_element();
diff --git a/crates/agent_ui/src/conversation_view/thread_view.rs b/crates/agent_ui/src/conversation_view/thread_view.rs
index f9b83d520d..7ebd4f03a9 100644
--- a/crates/agent_ui/src/conversation_view/thread_view.rs
+++ b/crates/agent_ui/src/conversation_view/thread_view.rs
@@ -32,9 +32,9 @@ use crate::unicode_confusables;
3232 
3333 use db::kvp::KeyValueStore;
3434 use full_auto_ui::{ThreadRunLink, ThreadRunRecords, project_thread_run_link};
35-use gpui::List;
3635 use gpui::Stateful;
3736 use gpui::TaskExt;
37+use gpui::{List, PromptLevel};
3838 use heapless::Vec as ArrayVec;
3939 use language_model::{
4040     FastModeConfirmation, LanguageModel, LanguageModelEffortLevel, LanguageModelId,
@@ -2257,10 +2257,9 @@ impl ThreadView {
22572257         // Only an external peer negotiates. Omega owns the native loop's stop,
22582258         // and an engine lane's answer does not depend on what the engine can do.
22592259         let capability = match disclosure.class {
2260-            omega_front_door::ExecutorClass::ExternalAcp => self
2261-                .session_capabilities
2262-                .read()
2263-                .omega_steer_capability(),
2260+            omega_front_door::ExecutorClass::ExternalAcp => {
2261+                self.session_capabilities.read().omega_steer_capability()
2262+            }
22642263             _ => omega_front_door::SteerCapability::Unknown,
22652264         };
22662265         omega_front_door::disposition(command, disclosure.class, capability)
@@ -12110,8 +12109,16 @@ impl ThreadView {
1211012109     /// Unconditional, and above the entries rather than beside them, because
1211112110     /// omega#77's falsifier is a thread surface that shows work without naming
1211212111     /// its executor — including an empty thread, which is about to.
12113-    fn render_executor_disclosure(&self, cx: &App) -> impl IntoElement {
12112+    fn render_executor_disclosure(&self, cx: &mut Context<Self>) -> impl IntoElement {
1211412113         let disclosure = self.executor_disclosure(cx);
12114+        let exo = self
12115+            .thread
12116+            .read(cx)
12117+            .connection()
12118+            .clone()
12119+            .downcast::<crate::omega_exo_connection::ExoHarnessConnection>();
12120+        let receipt = exo.as_ref().and_then(|exo| exo.tier_c_receipt());
12121+        let thread = self.thread.clone();
1211512122         h_flex()
1211612123             .w_full()
1211712124             .px_2()
@@ -12130,6 +12137,76 @@ impl ThreadView {
1213012137                     .color(Color::Muted),
1213112138             )
1213212139             .child(div().flex_1())
12140+            .when_some(receipt, |row, receipt| {
12141+                row.child(
12142+                    Label::new(format!(
12143+                        "self-modification {} · generation {} · event {}",
12144+                        receipt.outcome,
12145+                        receipt.observed.generation,
12146+                        receipt.latest_event_id.as_deref().unwrap_or("pending")
12147+                    ))
12148+                    .size(LabelSize::XSmall)
12149+                    .color(Color::Muted),
12150+                )
12151+            })
12152+            .when_some(exo, |row, exo| {
12153+                row.child(
12154+                    Button::new(
12155+                        "omega-exo-authorize-self-modification",
12156+                        "Authorize one self-modifying turn",
12157+                    )
12158+                    .label_size(LabelSize::XSmall)
12159+                    .color(Color::Warning)
12160+                    .on_click(move |_, window, cx| {
12161+                        let (session_id, prompt) = {
12162+                            let thread = thread.read(cx);
12163+                            let prompt = thread
12164+                                .draft_prompt()
12165+                                .filter(|prompt| !prompt.is_empty())
12166+                                .map(<[acp::ContentBlock]>::to_vec);
12167+                            (thread.session_id().clone(), prompt)
12168+                        };
12169+                        let Some(prompt) = prompt else {
12170+                            log::warn!(
12171+                                "omega#87: a self-modification grant needs a non-empty draft"
12172+                            );
12173+                            return;
12174+                        };
12175+                        let objective = crate::omega_exo_connection::exo_prompt_objective(&prompt);
12176+                        let Ok(turn_ref) =
12177+                            crate::omega_exo_connection::exo_turn_ref(&session_id, &prompt)
12178+                        else {
12179+                            log::error!("omega#87: failed to bind the Exo draft to a turn");
12180+                            return;
12181+                        };
12182+                        let exo = Rc::clone(&exo);
12183+                        window
12184+                            .spawn(cx, async move |cx| {
12185+                                let request =
12186+                                    exo.self_modification_request(objective, turn_ref).await?;
12187+                                let detail = format!(
12188+                                    "This grant expires in 60 seconds and applies only to this \
12189+                                     exact draft, Exo generation, source tree, binary, tool \
12190+                                     modules, and read-write mounts.\n\nExact capabilities:\n{:#?}",
12191+                                    request.capabilities
12192+                                );
12193+                                let answer = cx
12194+                                    .prompt(
12195+                                        PromptLevel::Warning,
12196+                                        "Allow this Exo agent to modify itself for one turn?",
12197+                                        Some(&detail),
12198+                                        &["Authorize one turn", "Cancel"],
12199+                                    )
12200+                                    .await?;
12201+                                if answer == 0 {
12202+                                    exo.confirm_self_modification(request)?;
12203+                                }
12204+                                anyhow::Ok(())
12205+                            })
12206+                            .detach_and_log_err(cx);
12207+                    }),
12208+                )
12209+            })
1213312210             .child(self.render_executor_pin(cx))
1213412211     }
1213512212 
@@ -12172,39 +12249,42 @@ impl ThreadView {
1217212249             .menu(move |window, cx| {
1217312250                 let session_id = session_id.clone();
1217412251                 let pinned = pinned.clone();
12175-                Some(ContextMenu::build(window, cx, move |mut menu, _window, _cx| {
12176-                    menu = menu.header("Pin this thread's executor");
12177-                    for class in ExecutorClass::all() {
12178-                        let class = *class;
12179-                        let is_selected =
12180-                            pinned.as_ref().is_some_and(|pin| pin.class == class);
12181-                        let entry = ContextMenuEntry::new(class.token())
12182-                            .toggleable(IconPosition::End, is_selected);
12183-                        menu.push_item(entry.handler({
12184-                            let session_id = session_id.clone();
12252+                Some(ContextMenu::build(
12253+                    window,
12254+                    cx,
12255+                    move |mut menu, _window, _cx| {
12256+                        menu = menu.header("Pin this thread's executor");
12257+                        for class in ExecutorClass::all() {
12258+                            let class = *class;
12259+                            let is_selected = pinned.as_ref().is_some_and(|pin| pin.class == class);
12260+                            let entry = ContextMenuEntry::new(class.token())
12261+                                .toggleable(IconPosition::End, is_selected);
12262+                            menu.push_item(entry.handler({
12263+                                let session_id = session_id.clone();
12264+                                move |_window, _cx| {
12265+                                    let Some(router) = crate::omega_router::active_router() else {
12266+                                        return;
12267+                                    };
12268+                                    router.pin_session(
12269+                                        &session_id,
12270+                                        ExecutorPin::new(class),
12271+                                        PinGesture::ExecutorPinMenuItem,
12272+                                    );
12273+                                }
12274+                            }));
12275+                        }
12276+                        menu = menu.separator();
12277+                        menu.push_item(ContextMenuEntry::new("Unpin").handler({
1218512278                             move |_window, _cx| {
1218612279                                 let Some(router) = crate::omega_router::active_router() else {
1218712280                                     return;
1218812281                                 };
12189-                                router.pin_session(
12190-                                    &session_id,
12191-                                    ExecutorPin::new(class),
12192-                                    PinGesture::ExecutorPinMenuItem,
12193-                                );
12282+                                router.unpin_session(&session_id, PinGesture::ExecutorPinCleared);
1219412283                             }
1219512284                         }));
12196-                    }
12197-                    menu = menu.separator();
12198-                    menu.push_item(ContextMenuEntry::new("Unpin").handler({
12199-                        move |_window, _cx| {
12200-                            let Some(router) = crate::omega_router::active_router() else {
12201-                                return;
12202-                            };
12203-                            router.unpin_session(&session_id, PinGesture::ExecutorPinCleared);
12204-                        }
12205-                    }));
12206-                    menu
12207-                }))
12285+                        menu
12286+                    },
12287+                ))
1220812288             })
1220912289     }
1221012290 }
diff --git a/crates/agent_ui/src/message_editor.rs b/crates/agent_ui/src/message_editor.rs
index 2541b56de2..6977db73be 100644
--- a/crates/agent_ui/src/message_editor.rs
+++ b/crates/agent_ui/src/message_editor.rs
@@ -45,11 +45,17 @@ use util::{ResultExt, debug_panic};
4545 use workspace::{CollaboratorId, Workspace};
4646 use zed_actions::agent::{Chat, PasteRaw};
4747 
48-#[derive(Default)]
4948 pub struct SessionCapabilities {
5049     prompt_capabilities: acp::PromptCapabilities,
5150     available_commands: Vec<acp::AvailableCommand>,
5251     available_skills: Vec<AvailableSkill>,
52+    omega_steer_capability: omega_front_door::SteerCapability,
53+}
54+
55+impl Default for SessionCapabilities {
56+    fn default() -> Self {
57+        Self::new(Default::default(), Vec::new(), Vec::new())
58+    }
5359 }
5460 
5561 impl SessionCapabilities {
@@ -62,9 +68,18 @@ impl SessionCapabilities {
6268             prompt_capabilities,
6369             available_commands,
6470             available_skills,
71+            omega_steer_capability: omega_front_door::SteerCapability::Unknown,
6572         }
6673     }
6774 
75+    pub fn with_omega_steer_capability(
76+        mut self,
77+        capability: omega_front_door::SteerCapability,
78+    ) -> Self {
79+        self.omega_steer_capability = capability;
80+        self
81+    }
82+
6883     pub fn from_acp_commands(
6984         prompt_capabilities: acp::PromptCapabilities,
7085         available_commands: Vec<acp::AvailableCommand>,
@@ -91,15 +106,12 @@ impl SessionCapabilities {
91106     /// `OMEGA-DELTA-0032`. Whether this peer said it can take a message while a
92107     /// turn is running.
93108     ///
94-    /// The Agent Client Protocol has no prompt capability for mid-turn
95-    /// delivery, so today every external peer answers
96-    /// [`SteerCapability::Unknown`](omega_front_door::SteerCapability::Unknown)
97-    /// — and Omega refuses the steer and queues instead of guessing. That is
98-    /// the honest reading: silence is not a declared capability, and the cost
99-    /// of guessing wrong is the user's running turn. When ACP gains the
100-    /// capability, this is the one place that changes.
109+    /// ACP has no standard prompt capability for mid-turn delivery. A peer is
110+    /// `Unknown` unless its integration supplies an exact answer. The Exo ACP
111+    /// transport supplies `CannotSteer`: cancellation is supported, but a new
112+    /// prompt is accepted only after the active turn ends.
101113     pub fn omega_steer_capability(&self) -> omega_front_door::SteerCapability {
102-        omega_front_door::SteerCapability::Unknown
114+        self.omega_steer_capability
103115     }
104116 
105117     pub fn has_slash_completions(&self) -> bool {
@@ -2281,6 +2293,16 @@ mod tests {
22812293     use project::{AgentId, CompletionIntent, Project, ProjectPath};
22822294     use serde_json::{Value, json};
22832295 
2296+    #[test]
2297+    fn exo_can_declare_that_it_cannot_steer_mid_turn() {
2298+        let capabilities = super::SessionCapabilities::default()
2299+            .with_omega_steer_capability(omega_front_door::SteerCapability::CannotSteer);
2300+        assert_eq!(
2301+            capabilities.omega_steer_capability(),
2302+            omega_front_door::SteerCapability::CannotSteer
2303+        );
2304+    }
2305+
22842306     use text::Point;
22852307     use ui::{App, Context, IntoElement, Render, SharedString, Window};
22862308     use util::{path, paths::PathStyle, rel_path::rel_path};
@@ -5521,7 +5543,9 @@ mod tests {
55215543             .decode("iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNkYPhfDwAChwGA60e6kgAAAABJRU5ErkJggg==")
55225544             .expect("decode png");
55235545         let file_name = match extension {
5524-            Some(extension) => format!("omega-agent-ui-test-{}.{}", uuid::Uuid::new_v4(), extension),
5546+            Some(extension) => {
5547+                format!("omega-agent-ui-test-{}.{}", uuid::Uuid::new_v4(), extension)
5548+            }
55255549             None => format!("omega-agent-ui-test-{}", uuid::Uuid::new_v4()),
55265550         };
55275551         let path = std::env::temp_dir().join(file_name);
diff --git a/crates/agent_ui/src/omega_exo_connection.rs b/crates/agent_ui/src/omega_exo_connection.rs
index 861f8a0ed7..93beb3120c 100644
--- a/crates/agent_ui/src/omega_exo_connection.rs
+++ b/crates/agent_ui/src/omega_exo_connection.rs
@@ -1,9 +1,8 @@
1-//! Driving Exo. `OMEGA-DELTA-0042`, omega#87, Tier A.
1+//! Driving Exo. `OMEGA-DELTA-0042`, omega#87.
22 //!
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.
3+//! The law lives in `crates/omega_exo_lane`, which is a leaf. This file starts
4+//! `exo acp` on standard input and output. `AcpConnection` puts each text delta,
5+//! tool call, tool result, and completion record into the existing `AcpThread`.
76 //!
87 //! # How the lane is reached
98 //!
@@ -31,11 +30,9 @@
3130 //!    no-backwards-compatibility house rule.
3231 //! 3. **Which bytes.** The binary is hashed and compared against the owner's
3332 //!    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.
33+//! 4. **Which capability.** Omega reads the agent and conversation. The normal
34+//!    path refuses self-modification. A one-use grant can authorize one exact
35+//!    draft after a visible human confirmation.
3936 //!
4037 //! # What is deliberately absent
4138 //!
@@ -48,38 +45,40 @@
4845 //! is checking.
4946 
5047 use std::any::Any;
51-use std::cell::RefCell;
52-use std::collections::HashMap;
48+use std::cell::{Cell, RefCell};
5349 use std::path::PathBuf;
5450 use std::process::Stdio;
5551 use std::rc::Rc;
52+use std::sync::atomic::{AtomicU64, Ordering};
53+use std::time::{SystemTime, UNIX_EPOCH};
5654 
5755 use acp_thread::{AcpThread, AgentConnection};
58-use action_log::ActionLog;
5956 use agent_client_protocol::schema::v1 as acp;
57+use agent_servers::AcpConnection;
6058 use anyhow::{Context as _, Result, anyhow, bail};
61-use gpui::{App, AppContext as _, Entity, SharedString, Task, WeakEntity};
59+use gpui::{App, AsyncApp, Entity, SharedString, Task, WeakEntity};
6260 use omega_exo_lane::{
63-    EXO_HARNESS_ID, EXO_PIN, ExoAgent, ExoCommand, ExoLaneIdentity, ExoModelBinding, ExoRoot,
64-    ExoTurn, LoopbackEndpoint, ObservedExoCheckout, admits_bytes,
61+    EXO_HARNESS_ID, EXO_PIN, ExoAgent, ExoCommand, ExoConversation, ExoLaneIdentity,
62+    ExoModelBinding, ExoMount, ExoRoot, ExoSelfModificationConsentOrigin, ExoSelfModificationGrant,
63+    ExoSelfModificationGrantRequest, ExoSelfModificationReceipt, LoopbackEndpoint,
64+    ObservedExoCapabilityState, ObservedExoCheckout, ObservedReadWriteMount, ObservedToolModule,
65+    admits_bytes,
6566 };
6667 use omega_harness::MeasuredDigest;
67-use project::{AgentId, Project};
68+use project::{
69+    AgentId, Project,
70+    agent_server_store::{AgentServerCommand, AgentServerStore},
71+};
6872 use util::path_list::PathList;
6973 
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-
7774 /// Where the lane's configuration lives, under the Omega data directory.
7875 const EXO_LANE_FILE: &str = "omega-exo-lane.json";
7976 
8077 /// The schema that file carries.
8178 const EXO_LANE_SCHEMA: &str = "openagents.omega.exo_lane.v1";
8279 
80+static NEXT_EXO_CONNECTION_GENERATION: AtomicU64 = AtomicU64::new(1);
81+
8382 /// Everything Omega needs to reach one Exo install.
8483 ///
8584 /// Every field names something Exo owns. None of them is something Omega
@@ -119,7 +118,9 @@ impl ExoLaneConfig {
119118         let file = std::fs::read_to_string(path).ok()?;
120119         let value: serde_json::Value = serde_json::from_str(&file)
121120             .inspect_err(|error| {
122-                log::warn!("OMEGA-DELTA-0042: the Exo lane file is not JSON ({error}); no Exo lane");
121+                log::warn!(
122+                    "OMEGA-DELTA-0042: the Exo lane file is not JSON ({error}); no Exo lane"
123+                );
123124             })
124125             .ok()?;
125126         if value.get("schema").and_then(serde_json::Value::as_str) != Some(EXO_LANE_SCHEMA) {
@@ -160,26 +161,57 @@ struct ExoDriver {
160161     /// The digest the owner froze for `exo`, if any. Read once at construction
161162     /// from the harness pin ledger; a claim, never a measurement.
162163     frozen_digest: Option<String>,
164+    /// This connection process's generation. A grant cannot survive a
165+    /// reconnect because a new connection receives a new generation.
166+    generation: u64,
167+}
168+
169+#[derive(Clone, Debug, PartialEq, Eq, serde::Deserialize, serde::Serialize)]
170+pub struct ExoTierCReceipt {
171+    pub schema: String,
172+    pub recorded_at_ms: u64,
173+    pub authority: Option<ExoSelfModificationReceipt>,
174+    pub observed: ObservedExoCapabilityState,
175+    pub turn_ref: String,
176+    pub requested_objective: Option<String>,
177+    pub outcome: String,
178+    pub exo_session_id: Option<String>,
179+    pub exo_turn_id: Option<String>,
180+    pub latest_event_id: Option<String>,
181+    pub verification: String,
182+    pub reconnect: String,
183+    pub rollback: String,
163184 }
164185 
165186 /// One Exo install, behind Omega Agent's router.
166187 pub struct ExoHarnessConnection {
167188     driver: Rc<ExoDriver>,
168-    /// The threads this connection built, by session.
169-    sessions: RefCell<HashMap<acp::SessionId, WeakEntity<AcpThread>>>,
189+    acp: Rc<AcpConnection>,
190+    pending_grant: RefCell<Option<ExoSelfModificationGrant>>,
191+    tier_c_receipt: Rc<RefCell<Option<ExoTierCReceipt>>>,
192+    active_tier_c_turn: Rc<Cell<bool>>,
170193 }
171194 
172195 impl ExoHarnessConnection {
173196     /// A connection to the Exo the lane file names.
174197     #[must_use]
175-    pub fn new(config: ExoLaneConfig, frozen_digest: Option<String>) -> Self {
198+    pub fn new(
199+        config: ExoLaneConfig,
200+        frozen_digest: Option<String>,
201+        acp: Rc<AcpConnection>,
202+    ) -> Self {
203+        let previous_receipt = load_latest_tier_c_receipt(&config.agent, &config.conversation);
176204         Self {
177205             driver: Rc::new(ExoDriver {
178206                 config,
179207                 identity: RefCell::new(None),
180208                 frozen_digest,
209+                generation: NEXT_EXO_CONNECTION_GENERATION.fetch_add(1, Ordering::Relaxed),
181210             }),
182-            sessions: RefCell::new(HashMap::new()),
211+            acp,
212+            pending_grant: RefCell::new(None),
213+            tier_c_receipt: Rc::new(RefCell::new(previous_receipt)),
214+            active_tier_c_turn: Rc::new(Cell::new(false)),
183215         }
184216     }
185217 
@@ -198,6 +230,91 @@ impl ExoHarnessConnection {
198230     pub fn config(&self) -> &ExoLaneConfig {
199231         &self.driver.config
200232     }
233+
234+    #[must_use]
235+    pub fn tier_c_receipt(&self) -> Option<ExoTierCReceipt> {
236+        self.tier_c_receipt.borrow().clone()
237+    }
238+
239+    /// Observe the exact Exo capability state for a dedicated confirmation
240+    /// dialog. This does not mint authority.
241+    pub async fn self_modification_request(
242+        &self,
243+        objective: String,
244+        turn_ref: String,
245+    ) -> Result<ExoSelfModificationGrantRequest> {
246+        let observed = self.driver.observe().await?.observed;
247+        let capabilities = observed.requested_capabilities();
248+        if capabilities.is_empty() {
249+            bail!("this Exo agent has no self-modification capability to authorize");
250+        }
251+        Ok(ExoSelfModificationGrantRequest {
252+            objective,
253+            turn_ref,
254+            observed,
255+            capabilities,
256+            expires_at_ms: now_ms().saturating_add(60_000),
257+        })
258+    }
259+
260+    /// Mint the one-use grant after the visible confirmation action returns.
261+    pub fn confirm_self_modification(
262+        &self,
263+        request: ExoSelfModificationGrantRequest,
264+    ) -> Result<()> {
265+        let grant = ExoSelfModificationGrant::mint(
266+            request,
267+            ExoSelfModificationConsentOrigin::HumanConfirmationDialog,
268+            now_ms(),
269+        )
270+        .map_err(|refusal| anyhow!("Exo self-modification grant refused: {refusal:?}"))?;
271+        *self.pending_grant.borrow_mut() = Some(grant);
272+        Ok(())
273+    }
274+}
275+
276+struct ObservedTurn {
277+    observed: ObservedExoCapabilityState,
278+}
279+
280+fn now_ms() -> u64 {
281+    SystemTime::now()
282+        .duration_since(UNIX_EPOCH)
283+        .map_or(0, |duration| {
284+            u64::try_from(duration.as_millis()).unwrap_or(u64::MAX)
285+        })
286+}
287+
288+fn resolve_module_host_path(module: &str, mounts: &[ExoMount]) -> Option<PathBuf> {
289+    let path = std::path::Path::new(module);
290+    if path.is_file() {
291+        return Some(path.to_path_buf());
292+    }
293+    mounts.iter().find_map(|mount| {
294+        let suffix = path.strip_prefix(&mount.mount_path).ok()?;
295+        Some(PathBuf::from(&mount.host_path).join(suffix))
296+    })
297+}
298+
299+#[must_use]
300+pub fn exo_prompt_objective(prompt: &[acp::ContentBlock]) -> String {
301+    prompt
302+        .iter()
303+        .filter_map(|block| match block {
304+            acp::ContentBlock::Text(text) => Some(text.text.as_str()),
305+            _ => None,
306+        })
307+        .collect::<Vec<_>>()
308+        .join("\n")
309+}
310+
311+pub fn exo_turn_ref(session_id: &acp::SessionId, prompt: &[acp::ContentBlock]) -> Result<String> {
312+    let canonical = serde_json::to_vec(&(session_id, prompt))
313+        .context("encoding the exact Exo turn for its authority grant")?;
314+    Ok(format!(
315+        "exo-turn:{}",
316+        MeasuredDigest::measure(&canonical).as_str()
317+    ))
201318 }
202319 
203320 impl ExoDriver {
@@ -213,7 +330,13 @@ impl ExoDriver {
213330             .stdin(Stdio::null())
214331             .output()
215332             .await
216-            .with_context(|| format!("running {} {}", self.config.binary.display(), argv.join(" ")))?;
333+            .with_context(|| {
334+                format!(
335+                    "running {} {}",
336+                    self.config.binary.display(),
337+                    argv.join(" ")
338+                )
339+            })?;
217340         if !output.status.success() {
218341             bail!(
219342                 "exo {} exited {}: {}",
@@ -257,7 +380,7 @@ impl ExoDriver {
257380     }
258381 
259382     /// Refuse an Exo that is not the pinned one.
260-    async fn check_pin(&self) -> Result<()> {
383+    async fn check_pin(&self) -> Result<(ObservedExoCheckout, MeasuredDigest)> {
261384         let git = async |args: &[&str]| -> Result<String> {
262385             let output = smol::process::Command::new("git")
263386                 .arg("-C")
@@ -288,13 +411,16 @@ impl ExoDriver {
288411         let bytes = smol::fs::read(&self.config.binary)
289412             .await
290413             .context("reading the exo binary")?;
291-        admits_bytes(self.frozen_digest.as_deref(), &MeasuredDigest::measure(&bytes))
414+        let digest = MeasuredDigest::measure(&bytes);
415+        admits_bytes(self.frozen_digest.as_deref(), &digest)
292416             .map_err(|mismatch| anyhow!("{mismatch}"))?;
293-        Ok(())
417+        Ok((observed, digest))
294418     }
295419 
296-    /// Read the agent, refuse self-modification, and resolve the disclosure.
297-    async fn check_agent(&self) -> Result<ExoLaneIdentity> {
420+    /// Read the exact agent and conversation capability state.
421+    async fn observe(&self) -> Result<ObservedTurn> {
422+        self.check_endpoint()?;
423+        let (checkout, binary_digest) = self.check_pin().await?;
298424         let shown = self
299425             .run(&ExoCommand::ShowAgent {
300426                 agent: self.config.agent.clone(),
@@ -303,46 +429,66 @@ impl ExoDriver {
303429         let agent = ExoAgent::parse(&shown).map_err(|error| {
304430             anyhow!("{error}; the Omega lane refuses an Exo agent it cannot read")
305431         })?;
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 {
432+        let shown_conversation = self
433+            .run(&ExoCommand::ShowConversation {
324434                 agent: self.config.agent.clone(),
325435                 conversation: self.config.conversation.clone(),
326-                prompt,
327436             })
328437             .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,
438+        let conversation = ExoConversation::parse(&shown_conversation).map_err(|error| {
439+            anyhow!("{error}; the Omega lane refuses an Exo conversation it cannot read")
440+        })?;
441+
442+        let bindings = ExoModelBinding::read_table(&self.run(&ExoCommand::ListModels).await?);
443+        let identity = ExoLaneIdentity::resolve(&agent, &bindings);
444+        *self.identity.borrow_mut() = Some(identity.clone());
445+        let mut mounts = agent.mounts.clone();
446+        mounts.extend(conversation.mounts.clone());
447+        let read_write_mounts = mounts
448+            .iter()
449+            .filter(|mount| mount.read_write)
450+            .map(|mount| ObservedReadWriteMount {
451+                host_path: mount.host_path.clone(),
452+                mount_path: mount.mount_path.clone(),
340453             })
341-            .await
342-        {
343-            log::warn!("OMEGA-DELTA-0042: Exo's durable log could not be read back: {error:#}");
454+            .collect::<Vec<_>>();
455+        let mut tool_modules = Vec::new();
456+        for module in &agent.tool_module_paths {
457+            let host_path = resolve_module_host_path(module, &mounts).ok_or_else(|| {
458+                anyhow!(
459+                    "the Exo tool module {module} is not a readable host file or inside an observed mount"
460+                )
461+            })?;
462+            let bytes = smol::fs::read(&host_path)
463+                .await
464+                .with_context(|| format!("reading Exo tool module {}", host_path.display()))?;
465+            tool_modules.push(ObservedToolModule {
466+                path: module.clone(),
467+                digest: MeasuredDigest::measure(&bytes).as_str().to_owned(),
468+            });
344469         }
345-        Ok(turn)
470+        tool_modules.sort();
471+        let mut read_write_mounts = read_write_mounts;
472+        read_write_mounts.sort();
473+        Ok(ObservedTurn {
474+            observed: ObservedExoCapabilityState {
475+                source_commit: checkout.commit,
476+                source_tree: checkout.tree,
477+                binary_digest: binary_digest.as_str().to_owned(),
478+                agent: agent.slug,
479+                conversation: conversation.slug,
480+                generation: self.generation,
481+                agent_authored_tools: agent.agent_authored_tools,
482+                tool_modules,
483+                read_write_mounts,
484+            },
485+        })
486+    }
487+
488+    /// Re-observe the exact executable and agent immediately before a streamed
489+    /// ACP turn. A self-modifying turn needs a matching one-use grant.
490+    async fn preflight(&self) -> Result<ObservedTurn> {
491+        self.observe().await
346492     }
347493 }
348494 
@@ -352,18 +498,53 @@ impl ExoDriver {
352498 /// `None` and the router registers no external executor — which is the ordinary
353499 /// case, and is why the return type is an `Option` rather than a `Result` that
354500 /// 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)?;
501+pub async fn connect_configured_lane(
502+    lane_path: &std::path::Path,
503+    project: Entity<Project>,
504+    agent_server_store: WeakEntity<AgentServerStore>,
505+    cx: &mut AsyncApp,
506+) -> Result<Option<Rc<dyn AgentConnection>>> {
507+    let Some(config) = ExoLaneConfig::load(lane_path) else {
508+        return Ok(None);
509+    };
358510     let frozen = frozen_exo_digest();
359511     log::info!(
360512         "OMEGA-DELTA-0042: Exo harness lane configured at {} ({} {}), pin {}",
361513         config.root.as_str(),
362514         config.agent,
363515         config.conversation,
364-        if frozen.is_some() { "frozen" } else { "unfrozen" }
516+        if frozen.is_some() {
517+            "frozen"
518+        } else {
519+            "unfrozen"
520+        }
521+    );
522+    let command = AgentServerCommand {
523+        path: config.binary.clone(),
524+        args: vec![
525+            "--root".to_owned(),
526+            config.root.as_str().to_owned(),
527+            "acp".to_owned(),
528+            config.agent.clone(),
529+            config.conversation.clone(),
530+        ],
531+        env: None,
532+    };
533+    let acp = Rc::new(
534+        AcpConnection::stdio(
535+            AgentId::new(EXO_HARNESS_ID),
536+            project,
537+            command,
538+            agent_server_store,
539+            None,
540+            Default::default(),
541+            cx,
542+        )
543+        .await?,
365544     );
366-    Some(Rc::new(ExoHarnessConnection::new(config, frozen)))
545+    Ok(Some(Rc::new(ExoHarnessConnection::new(
546+        config, frozen, acp,
547+    ))))
367548 }
368549 
369550 /// The digest the owner froze for `exo`, from the harness pin ledger.
@@ -380,9 +561,7 @@ fn frozen_exo_digest() -> Option<String> {
380561         .join(omega_harness::HARNESS_PIN_LEDGER_FILE_NAME);
381562     let file = std::fs::read_to_string(path).ok()?;
382563     match omega_harness::decode_harness_pin_ledger(&file) {
383-        Ok(ledger) => ledger
384-            .pin(EXO_HARNESS_ID)
385-            .map(|pin| pin.digest.clone()),
564+        Ok(ledger) => ledger.pin(EXO_HARNESS_ID).map(|pin| pin.digest.clone()),
386565         Err(error) => {
387566             log::warn!("OMEGA-DELTA-0042: the harness pin ledger could not be read ({error})");
388567             None
@@ -390,17 +569,71 @@ fn frozen_exo_digest() -> Option<String> {
390569     }
391570 }
392571 
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,
572+fn meta_value(meta: Option<&acp::Meta>, key: &str) -> Option<String> {
573+    meta.and_then(|meta| meta.get(key))
574+        .and_then(serde_json::Value::as_str)
575+        .map(str::to_owned)
576+}
577+
578+fn tier_c_receipt_path() -> PathBuf {
579+    paths::data_dir()
580+        .join("openagents")
581+        .join("exo-self-modification-receipts.jsonl")
582+}
583+
584+fn load_latest_tier_c_receipt(agent: &str, conversation: &str) -> Option<ExoTierCReceipt> {
585+    std::fs::read_to_string(tier_c_receipt_path())
586+        .ok()?
587+        .lines()
588+        .rev()
589+        .filter_map(|line| serde_json::from_str::<ExoTierCReceipt>(line).ok())
590+        .find(|receipt| {
591+            receipt.observed.agent == agent && receipt.observed.conversation == conversation
401592         })
402-        .collect::<Vec<_>>()
403-        .join("\n")
593+}
594+
595+fn refused_tier_c_receipt(
596+    observed: ObservedExoCapabilityState,
597+    turn_ref: String,
598+    requested_objective: Option<String>,
599+    outcome: String,
600+) -> ExoTierCReceipt {
601+    ExoTierCReceipt {
602+        schema: "openagents.omega.exo_self_modification_receipt.v1".to_owned(),
603+        recorded_at_ms: now_ms(),
604+        authority: None,
605+        observed,
606+        turn_ref,
607+        requested_objective,
608+        outcome,
609+        exo_session_id: None,
610+        exo_turn_id: None,
611+        latest_event_id: None,
612+        verification: "Omega refused the turn before it crossed ACP".to_owned(),
613+        reconnect: "not applicable; the turn did not start".to_owned(),
614+        rollback: "not applicable; the turn did not start".to_owned(),
615+    }
616+}
617+
618+async fn persist_tier_c_receipt(receipt: ExoTierCReceipt) -> Result<()> {
619+    let path = tier_c_receipt_path();
620+    smol::unblock(move || -> Result<()> {
621+        use std::io::Write as _;
622+        if let Some(parent) = path.parent() {
623+            std::fs::create_dir_all(parent)
624+                .with_context(|| format!("creating {}", parent.display()))?;
625+        }
626+        let mut file = std::fs::OpenOptions::new()
627+            .create(true)
628+            .append(true)
629+            .open(&path)
630+            .with_context(|| format!("opening {}", path.display()))?;
631+        serde_json::to_writer(&mut file, &receipt)?;
632+        file.write_all(b"\n")?;
633+        file.sync_data()?;
634+        Ok(())
635+    })
636+    .await
404637 }
405638 
406639 impl AgentConnection for ExoHarnessConnection {
@@ -427,37 +660,15 @@ impl AgentConnection for ExoHarnessConnection {
427660         work_dirs: PathList,
428661         cx: &mut App,
429662     ) -> 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))
663+        let inner_session = self.acp.clone().new_session(project, work_dirs, cx);
664+        let facade: Rc<dyn AgentConnection> = self;
665+        cx.spawn(async move |cx| {
666+            let thread = inner_session.await?;
667+            thread.update(cx, |thread, _| {
668+                thread.replace_connection(facade);
669+            });
670+            Ok(thread)
671+        })
461672     }
462673 
463674     fn auth_methods(&self) -> &[acp::AuthMethod] {
@@ -470,45 +681,159 @@ impl AgentConnection for ExoHarnessConnection {
470681         )))
471682     }
472683 
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`.
684+    fn prompt(
685+        &self,
686+        params: acp::PromptRequest,
687+        cx: &mut App,
688+    ) -> Task<Result<acp::PromptResponse>> {
482689         let driver = Rc::clone(&self.driver);
690+        let acp = Rc::clone(&self.acp);
691+        let pending_grant = self.pending_grant.take();
692+        let receipt_cell = self.tier_c_receipt.clone();
693+        let active_tier_c_turn = self.active_tier_c_turn.clone();
483694         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);
695+            let turn_ref = exo_turn_ref(&params.session_id, &params.prompt)?;
696+            let observed = driver.preflight().await?;
697+            let capabilities = observed.observed.requested_capabilities();
698+            let authority_receipt = if capabilities.is_empty() {
699+                None
700+            } else {
701+                let Some(grant) = pending_grant else {
702+                    let message = "this Exo turn can modify itself; use the dedicated confirmation control for this exact draft";
703+                    let receipt = refused_tier_c_receipt(
704+                        observed.observed.clone(),
705+                        turn_ref.clone(),
706+                        None,
707+                        format!("refused: {message}"),
708+                    );
709+                    *receipt_cell.borrow_mut() = Some(receipt.clone());
710+                    persist_tier_c_receipt(receipt).await?;
711+                    bail!("{message}");
712+                };
713+                let requested_objective = Some(grant.request().objective.clone());
714+                match grant.consume(&observed.observed, &turn_ref, now_ms()) {
715+                    Ok(authority) => Some(authority),
716+                    Err(refusal) => {
717+                        let message = format!(
718+                            "Exo self-modification grant refused: {refusal:?}"
719+                        );
720+                        let receipt = refused_tier_c_receipt(
721+                            observed.observed.clone(),
722+                            turn_ref.clone(),
723+                            requested_objective,
724+                            format!("refused: {refusal:?}"),
725+                        );
726+                        *receipt_cell.borrow_mut() = Some(receipt.clone());
727+                        persist_tier_c_receipt(receipt).await?;
728+                        bail!("{message}");
729+                    }
491730                 }
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))
731+            };
732+            let is_tier_c_turn = authority_receipt.is_some();
733+            if let Some(authority) = authority_receipt {
734+                *receipt_cell.borrow_mut() = Some(ExoTierCReceipt {
735+                    schema: "openagents.omega.exo_self_modification_receipt.v1".to_owned(),
736+                    recorded_at_ms: now_ms(),
737+                    observed: authority.observed.clone(),
738+                    turn_ref: authority.turn_ref.clone(),
739+                    requested_objective: Some(authority.objective.clone()),
740+                    authority: Some(authority),
741+                    outcome: "sent".to_owned(),
742+                    exo_session_id: None,
743+                    exo_turn_id: None,
744+                    latest_event_id: None,
745+                    verification: "waiting for Exo's durable completion receipt".to_owned(),
746+                    reconnect: "not reported by Exo ACP".to_owned(),
747+                    rollback: "not reported by Exo ACP".to_owned(),
748+                });
749+                active_tier_c_turn.set(true);
750+                let receipt = receipt_cell.borrow().clone();
751+                if let Some(receipt) = receipt {
752+                    if let Err(error) = persist_tier_c_receipt(receipt).await {
753+                        active_tier_c_turn.set(false);
754+                        return Err(error);
755+                    }
756+                }
757+            }
758+            let prompt = cx.update(|cx| acp.prompt(params, cx));
759+            let response = prompt.await;
760+            if is_tier_c_turn {
761+                if let Some(receipt) = receipt_cell.borrow_mut().as_mut() {
762+                    match &response {
763+                        Ok(response) => {
764+                            receipt.outcome = match response.stop_reason {
765+                                acp::StopReason::Cancelled => "cancelled",
766+                                _ => "completed",
767+                            }
768+                            .to_owned();
769+                            receipt.recorded_at_ms = now_ms();
770+                            let meta = response.meta.as_ref();
771+                            receipt.exo_session_id = meta_value(meta, "exo.session_id");
772+                            receipt.exo_turn_id = meta_value(meta, "exo.turn_id");
773+                            receipt.latest_event_id = meta_value(meta, "exo.latest_event_id");
774+                            receipt.verification =
775+                                if receipt.latest_event_id.is_some() {
776+                                    "Exo returned its durable latest event reference".to_owned()
777+                                } else {
778+                                    "Exo returned no durable event reference".to_owned()
779+                                };
780+                        }
781+                        Err(error) => {
782+                            receipt.recorded_at_ms = now_ms();
783+                            receipt.outcome = format!("failed: {error}");
784+                            receipt.verification =
785+                                "the ACP turn failed before verification".to_owned();
786+                        }
787+                    }
788+                }
789+                let receipt = receipt_cell.borrow().clone();
790+                let persisted = if let Some(receipt) = receipt {
791+                    persist_tier_c_receipt(receipt).await
792+                } else {
793+                    Ok(())
794+                };
795+                active_tier_c_turn.set(false);
796+                persisted?;
797+            }
798+            response
500799         })
501800     }
502801 
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-        );
802+    fn cancel(&self, session_id: &acp::SessionId, cx: &mut App) {
803+        let cancelled_pending_grant = self.pending_grant.borrow_mut().take().map(|grant| {
804+            let request = grant.request();
805+            refused_tier_c_receipt(
806+                request.observed.clone(),
807+                request.turn_ref.clone(),
808+                Some(request.objective.clone()),
809+                "refused: cancelled before send".to_owned(),
810+            )
811+        });
812+        if let Some(receipt) = cancelled_pending_grant {
813+            *self.tier_c_receipt.borrow_mut() = Some(receipt.clone());
814+            cx.spawn(async move |_| {
815+                if let Err(error) = persist_tier_c_receipt(receipt).await {
816+                    log::error!("omega#87: failed to persist the cancellation receipt: {error}");
817+                }
818+            })
819+            .detach();
820+        } else if self.active_tier_c_turn.get() {
821+            if let Some(receipt) = self.tier_c_receipt.borrow_mut().as_mut() {
822+                receipt.recorded_at_ms = now_ms();
823+                receipt.outcome = "cancellation requested".to_owned();
824+            }
825+            if let Some(receipt) = self.tier_c_receipt.borrow().clone() {
826+                cx.spawn(async move |_| {
827+                    if let Err(error) = persist_tier_c_receipt(receipt).await {
828+                        log::error!(
829+                            "omega#87: failed to persist the cancellation receipt: {error}"
830+                        );
831+                    }
832+                })
833+                .detach();
834+            }
835+        }
836+        self.acp.cancel(session_id, cx);
512837     }
513838 
514839     fn into_any(self: Rc<Self>) -> Rc<dyn Any> {
@@ -571,6 +896,105 @@ mod tests {
571896         }
572897     }
573898 
899+    #[test]
900+    fn a_tier_c_grant_is_bound_to_the_exact_session_and_prompt() {
901+        let session_a = acp::SessionId::new("session-a");
902+        let session_b = acp::SessionId::new("session-b");
903+        let prompt_a = vec![acp::ContentBlock::Text(acp::TextContent::new("edit"))];
904+        let prompt_b = vec![acp::ContentBlock::Text(acp::TextContent::new("edit more"))];
905+        let reference = exo_turn_ref(&session_a, &prompt_a).expect("turn ref");
906+        assert_ne!(
907+            reference,
908+            exo_turn_ref(&session_b, &prompt_a).expect("session-bound ref")
909+        );
910+        assert_ne!(
911+            reference,
912+            exo_turn_ref(&session_a, &prompt_b).expect("prompt-bound ref")
913+        );
914+    }
915+
916+    #[test]
917+    fn a_tier_c_receipt_round_trips_all_authority_and_outcome_fields() {
918+        let observed = ObservedExoCapabilityState {
919+            source_commit: "commit".into(),
920+            source_tree: "tree".into(),
921+            binary_digest: "sha256:binary".into(),
922+            agent: "agent".into(),
923+            conversation: "conversation".into(),
924+            generation: 12,
925+            agent_authored_tools: true,
926+            tool_modules: Vec::new(),
927+            read_write_mounts: Vec::new(),
928+        };
929+        let authority = ExoSelfModificationReceipt {
930+            objective: "Update and verify Exo.".into(),
931+            turn_ref: "turn".into(),
932+            generation: observed.generation,
933+            expires_at_ms: 200,
934+            origin: ExoSelfModificationConsentOrigin::HumanConfirmationDialog,
935+            capabilities: observed.requested_capabilities(),
936+            observed: observed.clone(),
937+        };
938+        let receipt = ExoTierCReceipt {
939+            schema: "openagents.omega.exo_self_modification_receipt.v1".into(),
940+            recorded_at_ms: 150,
941+            authority: Some(authority),
942+            observed,
943+            turn_ref: "turn".into(),
944+            requested_objective: Some("Update and verify Exo.".into()),
945+            outcome: "completed".into(),
946+            exo_session_id: Some("session".into()),
947+            exo_turn_id: Some("turn".into()),
948+            latest_event_id: Some("event".into()),
949+            verification: "verified".into(),
950+            reconnect: "not reported by Exo ACP".into(),
951+            rollback: "not reported by Exo ACP".into(),
952+        };
953+        let encoded = serde_json::to_string(&receipt).expect("serialize receipt");
954+        let decoded = serde_json::from_str::<ExoTierCReceipt>(&encoded).expect("read receipt");
955+        assert_eq!(decoded, receipt);
956+    }
957+
958+    #[test]
959+    fn a_refusal_receipt_has_no_authority_or_durable_exo_turn() {
960+        let observed = ObservedExoCapabilityState {
961+            source_commit: "commit".into(),
962+            source_tree: "tree".into(),
963+            binary_digest: "sha256:binary".into(),
964+            agent: "agent".into(),
965+            conversation: "conversation".into(),
966+            generation: 12,
967+            agent_authored_tools: true,
968+            tool_modules: Vec::new(),
969+            read_write_mounts: Vec::new(),
970+        };
971+        let receipt =
972+            refused_tier_c_receipt(observed, "turn".into(), None, "refused: no grant".into());
973+        assert!(receipt.authority.is_none());
974+        assert!(receipt.exo_turn_id.is_none());
975+        assert_eq!(
976+            receipt.verification,
977+            "Omega refused the turn before it crossed ACP"
978+        );
979+    }
980+
981+    #[test]
982+    fn a_sandbox_module_resolves_only_through_an_observed_mount() {
983+        let mount = ExoMount {
984+            host_path: "/host/exo".into(),
985+            mount_path: "/workspace/exo".into(),
986+            read_write: true,
987+        };
988+        assert_eq!(
989+            resolve_module_host_path("/workspace/exo/tools/guardian.ts", &[mount]),
990+            Some(PathBuf::from("/host/exo/tools/guardian.ts"))
991+        );
992+        assert_eq!(
993+            resolve_module_host_path("/other/tools/guardian.ts", &[]),
994+            None
995+        );
996+    }
997+
574998     /// The lane, against a real Exo. `#[ignore]`d because it needs one: a
575999     /// built `exo` at the pinned commit, a configured agent and conversation,
5761000     /// and whatever credential that agent's model binding resolves to. Exo
@@ -594,9 +1018,6 @@ mod tests {
5941018         let Ok(lane_file) = std::env::var("OMEGA_EXO_LANE_FILE") else {
5951019             panic!("set OMEGA_EXO_LANE_FILE to a lane file; see this test's docs");
5961020         };
597-        let config = ExoLaneConfig::load(std::path::Path::new(&lane_file))
598-            .expect("the lane file names an Exo install");
599-
6001021         // The turn runs a real process against a real model, so this test
6011022         // waits on wall-clock I/O rather than on the deterministic scheduler.
6021023         // That is the point of it: the same await that parks here is what keeps
@@ -605,21 +1026,38 @@ mod tests {
6051026         cx.executor().allow_parking();
6061027         crate::test_support::init_test(cx);
6071028         let fs = fs::FakeFs::new(cx.executor());
608-        let project = Project::test(fs, [], cx).await;
1029+        let lane_path = PathBuf::from(lane_file);
1030+        let process_cwd = lane_path
1031+            .parent()
1032+            .expect("the lane file has a parent")
1033+            .to_path_buf();
1034+        fs.insert_tree(&process_cwd, serde_json::json!({})).await;
1035+        let project = Project::test(fs, [process_cwd.as_path()], cx).await;
6091036 
610-        let connection: Rc<ExoHarnessConnection> =
611-            Rc::new(ExoHarnessConnection::new(config, None));
1037+        let agent_server_store =
1038+            project.read_with(cx, |project, _| project.agent_server_store().downgrade());
1039+        let connect_project = project.clone();
1040+        let connection = cx
1041+            .update(|cx| {
1042+                cx.spawn(async move |cx| {
1043+                    connect_configured_lane(&lane_path, connect_project, agent_server_store, cx)
1044+                        .await
1045+                })
1046+            })
1047+            .await
1048+            .expect("the Exo ACP lane connects")
1049+            .expect("the lane file names an Exo install");
6121050         let thread = cx
6131051             .update(|cx| {
6141052                 connection
6151053                     .clone()
616-                    .new_session(project, PathList::default(), cx)
1054+                    .new_session(project, PathList::new(&[process_cwd]), cx)
6171055             })
6181056             .await
6191057             .expect("a session on the Exo lane");
6201058 
6211059         let session_id = thread.read_with(cx, |thread, _| thread.session_id().clone());
622-        let marker = "OMEGA-EXO-TIER-A";
1060+        let marker = "OMEGA-EXO-ACP-STREAM";
6231061         let request = acp::PromptRequest::new(
6241062             session_id,
6251063             vec![acp::ContentBlock::Text(acp::TextContent::new(format!(
@@ -627,7 +1065,7 @@ mod tests {
6271065             )))],
6281066         );
6291067         let response = cx
630-            .update(|cx| AgentConnection::prompt(connection.as_ref(), request, cx))
1068+            .update(|cx| connection.prompt(request, cx))
6311069             .await
6321070             .expect("Exo ran the turn");
6331071         assert_eq!(response.stop_reason, acp::StopReason::EndTurn);
@@ -642,7 +1080,11 @@ mod tests {
6421080         assert!(disclosure.is_coherent(), "{disclosure:?}");
6431081         assert_eq!(disclosure.class, omega_exo_lane::EXO_EXECUTOR_CLASS);
6441082         assert_eq!(disclosure.run_ref, None);
645-        let identity = connection.identity().expect("Exo told the lane who it is");
1083+        let exo = connection
1084+            .clone()
1085+            .downcast::<ExoHarnessConnection>()
1086+            .expect("the configured lane is Exo");
1087+        let identity = exo.identity().expect("Exo told the lane who it is");
6461088         assert!(disclosure.agent_id.starts_with("exo/"), "{disclosure:?}");
6471089         // The parts that only the Exo arm of `classify_connection` can supply.
6481090         // Without them this assertion set passes on the shared external-agent
@@ -661,19 +1103,4 @@ mod tests {
6611103         println!("executor disclosure: {}", disclosure.label());
6621104         println!("exo identity: {identity:?}");
6631105     }
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-    }
6791106 }
diff --git a/crates/agent_ui/src/omega_router.rs b/crates/agent_ui/src/omega_router.rs
index c693296cbe..c3f23817fe 100644
--- a/crates/agent_ui/src/omega_router.rs
+++ b/crates/agent_ui/src/omega_router.rs
@@ -454,7 +454,8 @@ impl OmegaAgentConnection {
454454     pub fn pin_next_session(&self, pin: Option<ExecutorPin>, gesture: PinGesture) {
455455         log::info!(
456456             "OMEGA-DELTA-0035: the next session is pinned to {} by {}",
457-            pin.as_ref().map_or("nothing".to_owned(), ExecutorPin::token),
457+            pin.as_ref()
458+                .map_or("nothing".to_owned(), ExecutorPin::token),
458459             gesture.token()
459460         );
460461         *self.next_pin.borrow_mut() = pin;
@@ -607,7 +608,9 @@ impl AgentConnection for OmegaAgentConnection {
607608                     decision.explain()
608609                 );
609610             }
610-            self.pins.borrow_mut().extend(pin.map(|pin| (session_id, pin)));
611+            self.pins
612+                .borrow_mut()
613+                .extend(pin.map(|pin| (session_id, pin)));
611614             Ok(thread)
612615         })
613616     }
@@ -632,11 +635,18 @@ impl AgentConnection for OmegaAgentConnection {
632635         self.native.logout(cx)
633636     }
634637 
635-    fn client_user_message_ids(&self, cx: &App) -> Option<Rc<dyn AgentSessionClientUserMessageIds>> {
638+    fn client_user_message_ids(
639+        &self,
640+        cx: &App,
641+    ) -> Option<Rc<dyn AgentSessionClientUserMessageIds>> {
636642         self.native.client_user_message_ids(cx)
637643     }
638644 
639-    fn prompt(&self, params: acp::PromptRequest, cx: &mut App) -> Task<Result<acp::PromptResponse>> {
645+    fn prompt(
646+        &self,
647+        params: acp::PromptRequest,
648+        cx: &mut App,
649+    ) -> Task<Result<acp::PromptResponse>> {
640650         self.executor_for(&params.session_id).prompt(params, cx)
641651     }
642652 
@@ -809,10 +819,11 @@ impl agent_servers::AgentServer for OmegaRouterServer {
809819         project: Entity<Project>,
810820         cx: &mut App,
811821     ) -> Task<Result<Rc<dyn AgentConnection>>> {
812-        let native = self.native.connect(delegate, project, cx);
822+        let agent_server_store = delegate.store().downgrade();
823+        let native = self.native.connect(delegate, project.clone(), cx);
813824         let journal_path = self.journal_path.clone();
814825         let exo_lane_path = self.exo_lane_path.clone();
815-        cx.spawn(async move |_cx| {
826+        cx.spawn(async move |cx| {
816827             let native = native.await?;
817828             let mut router = OmegaAgentConnection::new(native, RouteJournal::at(journal_path));
818829             // `OMEGA-DELTA-0042`, omega#87. The Exo harness lane, when the owner
@@ -822,7 +833,13 @@ impl agent_servers::AgentServer for OmegaRouterServer {
822833             // engine lane. A machine with no Exo registers nothing, and a pin
823834             // to the external executor then falls back visibly with
824835             // `RouteReason::ExternalAcpUnavailable`.
825-            if let Some(exo) = crate::omega_exo_connection::connect_configured_lane(&exo_lane_path)
836+            if let Some(exo) = crate::omega_exo_connection::connect_configured_lane(
837+                &exo_lane_path,
838+                project,
839+                agent_server_store,
840+                cx,
841+            )
842+            .await?
826843             {
827844                 router = router.with_external_acp(exo);
828845             }
@@ -886,7 +903,10 @@ impl agent_servers::AgentServer for OmegaRouterServer {
886903 /// wrong `false`, which is why `omega_deltas` counts the bare downcasts.
887904 #[must_use]
888905 pub fn is_native_agent_server(server: &Rc<dyn agent_servers::AgentServer>) -> bool {
889-    server.clone().downcast::<agent::NativeAgentServer>().is_some()
906+    server
907+        .clone()
908+        .downcast::<agent::NativeAgentServer>()
909+        .is_some()
890910         || server.clone().downcast::<OmegaRouterServer>().is_some()
891911 }
892912 
@@ -909,7 +929,12 @@ pub fn native_connection(
909929     connection
910930         .clone()
911931         .downcast::<OmegaAgentConnection>()
912-        .and_then(|router| router.native.clone().downcast::<agent::NativeAgentConnection>())
932+        .and_then(|router| {
933+            router
934+                .native
935+                .clone()
936+                .downcast::<agent::NativeAgentConnection>()
937+        })
913938 }
914939 
915940 #[cfg(test)]
@@ -1016,10 +1041,7 @@ mod tests {
10161041 
10171042         let reopened = journal_in(&directory);
10181043         assert_eq!(reopened.decision(session).as_ref(), Some(&decision));
1019-        assert_eq!(
1020-            reopened.decisions(),
1021-            vec![(session.to_owned(), decision)]
1022-        );
1044+        assert_eq!(reopened.decisions(), vec![(session.to_owned(), decision)]);
10231045         assert_eq!(
10241046             recorded_route(&acp::SessionId::new(session)),
10251047             Some(RouteReason::EngineUnreachable),
@@ -1059,7 +1081,10 @@ mod tests {
10591081             .with_engine_lane(stub("codex-local"));
10601082         router.observe_capacity(Ok(&ready_capacity()));
10611083 
1062-        let external = router.decide("s-external", Some(ExecutorPin::new(ExecutorClass::ExternalAcp)));
1084+        let external = router.decide(
1085+            "s-external",
1086+            Some(ExecutorPin::new(ExecutorClass::ExternalAcp)),
1087+        );
10631088         assert_eq!(external.chosen, ExecutorClass::ExternalAcp);
10641089         assert_eq!(
10651090             router.executor(external.chosen).agent_id(),
@@ -1179,11 +1204,10 @@ mod tests {
11791204             );
11801205         }
11811206         assert_eq!(
1182-            std::fs::read_to_string(
1183-                directory.path().join("openagents").join(ROUTE_JOURNAL_FILE)
1184-            )
1185-            .unwrap()
1186-            .matches("chosen=").count(),
1207+            std::fs::read_to_string(directory.path().join("openagents").join(ROUTE_JOURNAL_FILE))
1208+                .unwrap()
1209+                .matches("chosen=")
1210+                .count(),
11871211             1,
11881212             "one session must leave one record, not one per decision"
11891213         );
@@ -1226,7 +1250,10 @@ mod tests {
12261250             "a pin a person set must move the turn, or the control is decoration"
12271251         );
12281252         assert_eq!(
1229-            router.journal().decision(session.0.as_ref()).map(|d| d.chosen),
1253+            router
1254+                .journal()
1255+                .decision(session.0.as_ref())
1256+                .map(|d| d.chosen),
12301257             Some(ExecutorClass::ExternalAcp)
12311258         );
12321259     }
@@ -1262,7 +1289,12 @@ mod tests {
12621289             decision.pin.as_ref().map(ExecutorPin::token).as_deref(),
12631290             Some("engine_lane")
12641291         );
1265-        assert!(decision.reason.phrase().contains("fell back to the native loop"));
1292+        assert!(
1293+            decision
1294+                .reason
1295+                .phrase()
1296+                .contains("fell back to the native loop")
1297+        );
12661298         assert_eq!(
12671299             recorded_route(&session),
12681300             Some(decision.reason),
@@ -1309,5 +1341,4 @@ mod tests {
13091341         );
13101342         assert_eq!(router.agent_id(), AgentId::new("omega-agent"));
13111343     }
1312-
13131344 }
diff --git a/crates/omega_deltas/src/omega_deltas.rs b/crates/omega_deltas/src/omega_deltas.rs
index 4aab74ace7..05ead6f607 100644
--- a/crates/omega_deltas/src/omega_deltas.rs
+++ b/crates/omega_deltas/src/omega_deltas.rs
@@ -115,8 +115,7 @@ pub const AGENT_SERVER_STORE_PATH: &str = "crates/project/src/agent_server_store
115115 pub const HARNESS_FRONT_DOOR_PATH: &str = "crates/omega_harness/src/front_door.rs";
116116 
117117 /// OMEGA-DELTA-0033. The page that renders it.
118-pub const EXTERNAL_AGENTS_PAGE_PATH: &str =
119-    "crates/settings_ui/src/pages/external_agents_page.rs";
118+pub const EXTERNAL_AGENTS_PAGE_PATH: &str = "crates/settings_ui/src/pages/external_agents_page.rs";
120119 
121120 /// OMEGA-DELTA-0026. Shipped defaults that would otherwise point a running
122121 /// Omega at one of Zed's production hosts, as
@@ -1493,7 +1492,12 @@ pub fn is_prose(text: &str) -> bool {
14931492 pub fn is_command_form(text: &str, policy: &serde_json::Value) -> bool {
14941493     let words: Vec<&str> = policy["brand"]["words"]
14951494         .as_array()
1496-        .map(|values| values.iter().filter_map(serde_json::Value::as_str).collect())
1495+        .map(|values| {
1496+            values
1497+                .iter()
1498+                .filter_map(serde_json::Value::as_str)
1499+                .collect()
1500+        })
14971501         .unwrap_or_default();
14981502     let tokens: Vec<&str> = text.split_whitespace().collect();
14991503     if tokens.len() < 2 || !words.contains(&tokens[0]) {
@@ -1572,7 +1576,8 @@ pub fn rust_string_literals(source: &str) -> Vec<(usize, String)> {
15721576             // A char literal or a lifetime; neither can hold prose. Only
15731577             // `'"'` has to be stepped over as a unit, so that its quote does
15741578             // not read as the start of a string.
1575-            index += if rest.as_bytes().get(1) == Some(&b'"') && rest.as_bytes().get(2) == Some(&b'\'')
1579+            index += if rest.as_bytes().get(1) == Some(&b'"')
1580+                && rest.as_bytes().get(2) == Some(&b'\'')
15761581             {
15771582                 3
15781583             } else {
@@ -1804,7 +1809,13 @@ pub fn prose_inventory(policy: &serde_json::Value) -> (Vec<ProseLiteral>, ProseR
18041809         let lines: Vec<&str> = source.lines().collect();
18051810         let code: String = lines
18061811             .iter()
1807-            .map(|line| if doc_comment_body(line).is_some() { "" } else { *line })
1812+            .map(|line| {
1813+                if doc_comment_body(line).is_some() {
1814+                    ""
1815+                } else {
1816+                    *line
1817+                }
1818+            })
18081819             .collect::<Vec<_>>()
18091820             .join("\n");
18101821         let schema = derives_any(&code, &["JsonSchema"]);
@@ -1827,9 +1838,10 @@ pub fn prose_inventory(policy: &serde_json::Value) -> (Vec<ProseLiteral>, ProseR
18271838         if brand_hits(source, policy).is_empty() {
18281839             return;
18291840         }
1830-        let relative = normalize_path(path)
1831-            .strip_prefix(&repository)
1832-            .map_or_else(|_| path.display().to_string(), |tail| tail.display().to_string());
1841+        let relative = normalize_path(path).strip_prefix(&repository).map_or_else(
1842+            |_| path.display().to_string(),
1843+            |tail| tail.display().to_string(),
1844+        );
18331845         let is_test_file = is_test_path(&relative);
18341846         let skipped = if is_test_file {
18351847             std::collections::BTreeSet::new()
@@ -1930,7 +1942,6 @@ fn is_test_path(relative: &str) -> bool {
19301942             .any(|segment| normalized.contains(segment))
19311943 }
19321944 
1933-
19341945 // ------ OMEGA-DELTA-0042
19351946 
19361947 /// OMEGA-DELTA-0042. The Exo harness lane's law. A leaf, checkable in a second.
@@ -1953,6 +1964,10 @@ pub const EXO_CONNECTION_PATH: &str = "crates/agent_ui/src/omega_exo_connection.
19531964 /// to get wrong twice, so it is a checked fact rather than a remembered one.
19541965 pub const EXO_HARNESS_UPSTREAM: &str = "exoharness/exo";
19551966 
1967+/// OMEGA-DELTA-0042. The maintained fork can carry the ACP transport while
1968+/// its contribution is under review upstream.
1969+pub const EXO_HARNESS_MAINTAINED_FORK: &str = "OpenAgentsInc/exo";
1970+
19561971 /// OMEGA-DELTA-0042. The other Exo, which must appear nowhere as a target.
19571972 pub const EXO_CLUSTER_UPSTREAM: &str = "exo-explore";
19581973 
@@ -4089,7 +4104,10 @@ mod tests {
40894104             .map(|(line, body)| (*line, body.as_str()))
40904105             .collect();
40914106         assert_eq!(found[0], (3, "first"), "single-line literal and its line");
4092-        assert_eq!(found[1].0, 4, "a continued literal starts on its first line");
4107+        assert_eq!(
4108+            found[1].0, 4,
4109+            "a continued literal starts on its first line"
4110+        );
40934111         assert!(
40944112             found[1].1.contains("continued"),
40954113             "the continuation is part of the literal: {:?}",
@@ -4100,13 +4118,19 @@ mod tests {
41004118             (6, "raw \"quoted\" body"),
41014119             "a raw literal keeps its inner quotes"
41024120         );
4103-        assert_eq!(found[3], (7, "after"), "the line count survives the raw literal");
4121+        assert_eq!(
4122+            found[3],
4123+            (7, "after"),
4124+            "the line count survives the raw literal"
4125+        );
41044126         assert!(
41054127             !found.iter().any(|(_, body)| body.contains("not a literal")),
41064128             "a comment is not a literal"
41074129         );
41084130 
4109-        assert!(is_prose("Click 'Connect' below to start using Ollama in Omega"));
4131+        assert!(is_prose(
4132+            "Click 'Connect' below to start using Ollama in Omega"
4133+        ));
41104134         assert!(!is_prose("crates/zed/src/main.rs"));
41114135         assert!(!is_prose("X-Zed-Predict-Edits-Mode"));
41124136         assert!(
@@ -4224,7 +4248,9 @@ mod tests {
42244248             .lines()
42254249             .filter(|line| {
42264250                 let trimmed = line.trim_start();
4227-                !trimmed.starts_with("//") || trimmed.starts_with("///") || trimmed.starts_with("//!")
4251+                !trimmed.starts_with("//")
4252+                    || trimmed.starts_with("///")
4253+                    || trimmed.starts_with("//!")
42284254             })
42294255             .collect::<Vec<_>>()
42304256             .join("\n");
@@ -5325,12 +5351,13 @@ mod tests {
53255351         let coordinator_path = repository_path(IDENTITY_STARTUP_PATH);
53265352         let coordinator = std::fs::read_to_string(&coordinator_path)
53275353             .unwrap_or_else(|error| panic!("cannot read {}: {error}", coordinator_path.display()));
5328-        let release = function_body(&coordinator, "release_identity_waiters").unwrap_or_else(|| {
5329-            panic!(
5330-                "OMEGA-DELTA-0040: {} no longer has a `release_identity_waiters`.",
5331-                coordinator_path.display()
5332-            )
5333-        });
5354+        let release =
5355+            function_body(&coordinator, "release_identity_waiters").unwrap_or_else(|| {
5356+                panic!(
5357+                    "OMEGA-DELTA-0040: {} no longer has a `release_identity_waiters`.",
5358+                    coordinator_path.display()
5359+                )
5360+            });
53345361         assert!(
53355362             release.contains("finish(Ok(()), cx)"),
53365363             "OMEGA-DELTA-0040: `release_identity_waiters` in {} no longer \
@@ -5364,8 +5391,7 @@ mod tests {
53645391         let panel = std::fs::read_to_string(&panel_path)
53655392             .unwrap_or_else(|error| panic!("cannot read {}: {error}", panel_path.display()));
53665393         assert!(
5367-            panel.contains("observe_capacity(Ok(capacity))")
5368-                && panel.contains("get_capacity()"),
5394+            panel.contains("observe_capacity(Ok(capacity))") && panel.contains("get_capacity()"),
53695395             "OMEGA-DELTA-0035: {} no longer feeds the engine's framed \
53705396              get_capacity answer into the router. Without it every engine-lane \
53715397              pin is decided against a default of \"not running\" whatever \
@@ -5643,7 +5669,6 @@ mod tests {
56435669         );
56445670     }
56455671 
5646-
56475672     // ------------------------------------------------------ OMEGA-DELTA-0033
56485673 
56495674     /// OMEGA-DELTA-0033. The front door renders the decision; it does not make
@@ -5729,7 +5754,8 @@ mod tests {
57295754         );
57305755         assert!(
57315756             rendered.contains("PinControl::Unavailable { reason }")
5732-                && rendered.contains("Tooltip::with_meta(\"Cannot Pin\", None, reason.clone(), cx)"),
5757+                && rendered
5758+                    .contains("Tooltip::with_meta(\"Cannot Pin\", None, reason.clone(), cx)"),
57335759             "OMEGA-DELTA-0033: {} must show the withheld control's reason. A \
57345760              disabled button with no sentence reads as a bug in Omega rather \
57355761              than as a fact about the owner's machine.",
@@ -5749,7 +5775,10 @@ mod tests {
57495775         let filesystem = repository_path(HARNESS_MAINTENANCE_PATH);
57505776         let source = std::fs::read_to_string(&filesystem)
57515777             .unwrap_or_else(|error| panic!("cannot read {}: {error}", filesystem.display()));
5752-        for writer in ["pub async fn pin_installed_harness(", "pub async fn unpin_harness("] {
5778+        for writer in [
5779+            "pub async fn pin_installed_harness(",
5780+            "pub async fn unpin_harness(",
5781+        ] {
57535782             assert!(
57545783                 source.contains(writer),
57555784                 "OMEGA-DELTA-0033: {} no longer offers {writer}. Without it the \
@@ -6358,9 +6387,12 @@ mod tests {
63586387             .expect("EXO_PIN names an upstream")
63596388             .0;
63606389         assert!(
6361-            upstream.ends_with(EXO_HARNESS_UPSTREAM),
6362-            "OMEGA-DELTA-0042: the Exo pin names {upstream}, which is not \
6363-             {EXO_HARNESS_UPSTREAM}. omega#86 made this mistake once."
6390+            [EXO_HARNESS_UPSTREAM, EXO_HARNESS_MAINTAINED_FORK]
6391+                .iter()
6392+                .any(|repository| upstream.ends_with(repository)),
6393+            "OMEGA-DELTA-0042: the Exo pin names {upstream}, which is neither \
6394+             {EXO_HARNESS_UPSTREAM} nor its reviewed maintained fork \
6395+             {EXO_HARNESS_MAINTAINED_FORK}. omega#86 made this mistake once."
63646396         );
63656397 
63666398         for relative in [EXO_LANE_LAW_PATH, EXO_LANE_PIN_PATH, EXO_CONNECTION_PATH] {
@@ -6441,7 +6473,11 @@ mod tests {
64416473     /// added rather than required.
64426474     #[test]
64436475     fn the_exo_lane_exposes_no_endpoint_off_this_machine() {
6444-        for relative in [EXO_CONNECTION_PATH, EXO_LANE_LAW_PATH, EXO_LANE_COMMAND_PATH] {
6476+        for relative in [
6477+            EXO_CONNECTION_PATH,
6478+            EXO_LANE_LAW_PATH,
6479+            EXO_LANE_COMMAND_PATH,
6480+        ] {
64456481             let path = repository_path(relative);
64466482             let source = std::fs::read_to_string(&path)
64476483                 .unwrap_or_else(|error| panic!("cannot read {}: {error}", path.display()));
@@ -6494,10 +6530,15 @@ mod tests {
64946530             "OMEGA-DELTA-0042: `check_endpoint` no longer parses the inherited \
64956531              endpoint through the type that refuses a non-loopback one."
64966532         );
6497-        let turn = function_body(&connection, "drive_turn").expect("the turn path exists");
6533+        let turn = function_body(&connection, "observe").expect("the ACP observation exists");
64986534         assert!(
64996535             turn.contains("self.check_endpoint()"),
6500-            "OMEGA-DELTA-0042: the turn path no longer checks where Exo is."
6536+            "OMEGA-DELTA-0042: the ACP preflight no longer checks where Exo is."
6537+        );
6538+        let prompt = function_body(&connection, "prompt").expect("the ACP prompt path exists");
6539+        assert!(
6540+            prompt.contains("driver.preflight().await") && prompt.contains("acp.prompt(params"),
6541+            "OMEGA-DELTA-0042: the ACP prompt must finish its preflight before it sends."
65016542         );
65026543     }
65036544 
@@ -6538,31 +6579,73 @@ mod tests {
65386579         let path = repository_path(EXO_CONNECTION_PATH);
65396580         let source = std::fs::read_to_string(&path)
65406581             .unwrap_or_else(|error| panic!("cannot read {}: {error}", path.display()));
6541-        let body = function_body(&source, "drive_turn").unwrap_or_else(|| {
6582+        let body = function_body(&source, "observe").unwrap_or_else(|| {
65426583             panic!(
6543-                "OMEGA-DELTA-0042: {} has no `drive_turn`, so nothing in this \
6544-                 file is the turn path any more.",
6584+                "OMEGA-DELTA-0042: {} has no exact Exo observation, so nothing \
6585+                 gates the streamed turn any more.",
65456586                 path.display()
65466587             )
65476588         });
65486589         let pin = body
65496590             .find("self.check_pin()")
65506591             .expect("OMEGA-DELTA-0042: a turn no longer checks which Exo it is driving");
6551-        let agent = body.find("self.check_agent()").expect(
6552-            "OMEGA-DELTA-0042: a turn no longer reads the Exo agent, so it \
6553-             cannot refuse self-modification capability",
6554-        );
6555-        let send = body
6556-            .find("ExoCommand::SendTurn")
6557-            .expect("OMEGA-DELTA-0042: a turn no longer sends");
6558-        assert!(
6559-            pin < agent && agent < send,
6560-            "OMEGA-DELTA-0042: the turn path runs its refusals out of order \
6561-             (pin {pin}, agent {agent}, send {send}). A capability read after \
6562-             the send describes a turn that already happened."
6592+        let agent = body
6593+            .find("ExoCommand::ShowAgent")
6594+            .expect("OMEGA-DELTA-0042: a turn no longer reads the exact Exo agent");
6595+        let conversation = body
6596+            .find("ExoCommand::ShowConversation")
6597+            .expect("OMEGA-DELTA-0042: a turn no longer reads the exact Exo conversation");
6598+        let prompt = function_body(&source, "prompt")
6599+            .expect("OMEGA-DELTA-0042: the streamed ACP prompt path is absent");
6600+        let preflight = prompt
6601+            .find("driver.preflight().await")
6602+            .expect("OMEGA-DELTA-0042: an ACP turn no longer runs its preflight");
6603+        let send = prompt
6604+            .find("acp.prompt(params")
6605+            .expect("OMEGA-DELTA-0042: an ACP turn no longer sends");
6606+        assert!(
6607+            pin < agent && agent < conversation && preflight < send,
6608+            "OMEGA-DELTA-0042: the exact pin, agent, and conversation checks \
6609+             must run before the complete preflight permits the ACP send."
65636610         );
65646611     }
65656612 
6613+    #[test]
6614+    fn an_exo_turn_streams_cancels_and_requires_exact_one_use_authority() {
6615+        let connection_path = repository_path(EXO_CONNECTION_PATH);
6616+        let connection = std::fs::read_to_string(&connection_path)
6617+            .unwrap_or_else(|error| panic!("cannot read {}: {error}", connection_path.display()));
6618+        let compact_connection = without_whitespace(&connection);
6619+        for token in [
6620+            "AcpConnection::stdio",
6621+            "\"acp\".to_owned()",
6622+            "acp.prompt(params",
6623+            "self.acp.cancel(session_id",
6624+            "grant.consume(&observed.observed, &turn_ref",
6625+            "persist_tier_c_receipt",
6626+        ] {
6627+            assert!(
6628+                compact_connection.contains(&without_whitespace(token)),
6629+                "OMEGA-DELTA-0042: the Exo ACP and authority path lost `{token}`"
6630+            );
6631+        }
6632+
6633+        let thread_path = repository_path(THREAD_VIEW_PATH);
6634+        let thread = std::fs::read_to_string(&thread_path)
6635+            .unwrap_or_else(|error| panic!("cannot read {}: {error}", thread_path.display()));
6636+        for token in [
6637+            "omega-exo-authorize-self-modification",
6638+            "Allow this Exo agent to modify itself for one turn?",
6639+            "self_modification_request",
6640+            "confirm_self_modification",
6641+        ] {
6642+            assert!(
6643+                thread.contains(token),
6644+                "OMEGA-DELTA-0042: the visible one-turn Exo confirmation lost `{token}`"
6645+            );
6646+        }
6647+    }
6648+
65666649     /// OMEGA-DELTA-0042. The lane is wired, not merely built.
65676650     ///
65686651     /// omega#78 shipped a router nobody constructed, and `OMEGA-DELTA-0035`
@@ -6740,8 +6823,12 @@ mod tests {
67406823             if trimmed.starts_with("//") {
67416824                 continue;
67426825             }
6743-            for pin_reaching in ["PinGesture", "pin_session(", "pin_next_session(", "ExecutorPin::"]
6744-            {
6826+            for pin_reaching in [
6827+                "PinGesture",
6828+                "pin_session(",
6829+                "pin_next_session(",
6830+                "ExecutorPin::",
6831+            ] {
67456832                 assert!(
67466833                     !trimmed.contains(pin_reaching),
67476834                     "OMEGA-DELTA-0041: the served ACP surface names                      {pin_reaching}. Nothing an external host can reach may                      set a pin: a pin is the only door to an engine lane and                      an engine lane is Full Auto authority, which owner gate 8                      admits only an explicit human action into."
@@ -6781,7 +6868,9 @@ mod tests {
67816868             .expect("crates/agent declares OMEGA_AGENT_ID as a literal");
67826869 
67836870         assert!(
6784-            served.contains(&format!("pub const SERVED_AGENT_ID: &str = \"{declared}\";")),
6871+            served.contains(&format!(
6872+                "pub const SERVED_AGENT_ID: &str = \"{declared}\";"
6873+            )),
67856874             "OMEGA-DELTA-0041: the served ACP surface presents an identity              other than {declared:?}, which is what crates/agent declares. An              attached host would be disclosed an agent that does not exist."
67866875         );
67876876     }
diff --git a/crates/omega_exo_lane/Cargo.toml b/crates/omega_exo_lane/Cargo.toml
index 5716217e1f..3f732f0bf3 100644
--- a/crates/omega_exo_lane/Cargo.toml
+++ b/crates/omega_exo_lane/Cargo.toml
@@ -4,7 +4,7 @@ version = "0.1.0"
44 edition.workspace = true
55 publish.workspace = true
66 license = "GPL-3.0-or-later"
7-description = "The Exo harness lane law: pin, invocation, loopback, turn reading (OMEGA-EXO-01 Tier A)"
7+description = "The Exo harness lane law: pin, ACP transport, authority, and disclosure"
88 
99 [lints]
1010 workspace = true
@@ -16,5 +16,6 @@ doctest = false
1616 [dependencies]
1717 omega_front_door.workspace = true
1818 omega_harness.workspace = true
19+serde.workspace = true
1920 
2021 [dev-dependencies]
diff --git a/crates/omega_exo_lane/src/authority.rs b/crates/omega_exo_lane/src/authority.rs
new file mode 100644
index 0000000000..d147a6947b
--- /dev/null
+++ b/crates/omega_exo_lane/src/authority.rs
@@ -0,0 +1,298 @@
1+//! One-turn authority for Exo self-modification. omega#87, Tier C.
2+//!
3+//! A lane pin does not grant this authority. The host must show the exact
4+//! observed capabilities to a person and mint one grant for one turn. The
5+//! grant is consumed immediately before the prompt crosses ACP.
6+
7+use serde::{Deserialize, Serialize};
8+use std::cell::Cell;
9+
10+/// One exact tool module observed in Exo's configuration.
11+#[derive(Clone, Debug, Deserialize, PartialEq, Eq, PartialOrd, Ord, Serialize)]
12+pub struct ObservedToolModule {
13+    pub path: String,
14+    pub digest: String,
15+}
16+
17+/// One exact read-write mount observed in Exo's effective configuration.
18+#[derive(Clone, Debug, Deserialize, PartialEq, Eq, PartialOrd, Ord, Serialize)]
19+pub struct ObservedReadWriteMount {
20+    pub host_path: String,
21+    pub mount_path: String,
22+}
23+
24+/// Everything that can widen a self-modifying Exo turn.
25+#[derive(Clone, Debug, Deserialize, PartialEq, Eq, Serialize)]
26+pub struct ObservedExoCapabilityState {
27+    pub source_commit: String,
28+    pub source_tree: String,
29+    pub binary_digest: String,
30+    pub agent: String,
31+    pub conversation: String,
32+    pub generation: u64,
33+    pub agent_authored_tools: bool,
34+    pub tool_modules: Vec<ObservedToolModule>,
35+    pub read_write_mounts: Vec<ObservedReadWriteMount>,
36+}
37+
38+/// The closed set of self-modification authority a person can grant.
39+#[derive(Clone, Debug, Deserialize, PartialEq, Eq, PartialOrd, Ord, Serialize)]
40+pub enum ExoSelfModificationCapability {
41+    AgentAuthoredTools,
42+    ToolModule(ObservedToolModule),
43+    ReadWriteMount(ObservedReadWriteMount),
44+}
45+
46+impl ObservedExoCapabilityState {
47+    #[must_use]
48+    pub fn requested_capabilities(&self) -> Vec<ExoSelfModificationCapability> {
49+        let mut capabilities = Vec::new();
50+        if self.agent_authored_tools {
51+            capabilities.push(ExoSelfModificationCapability::AgentAuthoredTools);
52+        }
53+        capabilities.extend(
54+            self.tool_modules
55+                .iter()
56+                .cloned()
57+                .map(ExoSelfModificationCapability::ToolModule),
58+        );
59+        capabilities.extend(
60+            self.read_write_mounts
61+                .iter()
62+                .cloned()
63+                .map(ExoSelfModificationCapability::ReadWriteMount),
64+        );
65+        capabilities
66+    }
67+}
68+
69+/// The exact request shown in the confirmation surface.
70+#[derive(Clone, Debug, Deserialize, PartialEq, Eq, Serialize)]
71+pub struct ExoSelfModificationGrantRequest {
72+    pub objective: String,
73+    pub turn_ref: String,
74+    pub observed: ObservedExoCapabilityState,
75+    pub capabilities: Vec<ExoSelfModificationCapability>,
76+    pub expires_at_ms: u64,
77+}
78+
79+/// Only a visible, dedicated human action can supply this origin.
80+#[derive(Clone, Copy, Debug, Deserialize, PartialEq, Eq, Serialize)]
81+pub enum ExoSelfModificationConsentOrigin {
82+    HumanConfirmationDialog,
83+}
84+
85+/// A one-use authority decision.
86+#[derive(Debug)]
87+pub struct ExoSelfModificationGrant {
88+    request: ExoSelfModificationGrantRequest,
89+    origin: ExoSelfModificationConsentOrigin,
90+    consumed: Cell<bool>,
91+}
92+
93+/// Why a grant did not authorize the send.
94+#[derive(Clone, Copy, Debug, PartialEq, Eq)]
95+pub enum ExoGrantRefusal {
96+    EmptyObjective,
97+    NoSelfModificationRequested,
98+    CapabilityMismatch,
99+    Expired,
100+    ConfigurationDrift,
101+    TurnMismatch,
102+    AlreadyConsumed,
103+}
104+
105+/// Durable input for the host's allow/refuse receipt.
106+#[derive(Clone, Debug, Deserialize, PartialEq, Eq, Serialize)]
107+pub struct ExoSelfModificationReceipt {
108+    pub objective: String,
109+    pub turn_ref: String,
110+    pub generation: u64,
111+    pub expires_at_ms: u64,
112+    pub origin: ExoSelfModificationConsentOrigin,
113+    pub capabilities: Vec<ExoSelfModificationCapability>,
114+    pub observed: ObservedExoCapabilityState,
115+}
116+
117+impl ExoSelfModificationGrant {
118+    /// The exact request this grant can authorize.
119+    #[must_use]
120+    pub fn request(&self) -> &ExoSelfModificationGrantRequest {
121+        &self.request
122+    }
123+
124+    /// Mint a grant after the dedicated confirmation action.
125+    pub fn mint(
126+        request: ExoSelfModificationGrantRequest,
127+        origin: ExoSelfModificationConsentOrigin,
128+        now_ms: u64,
129+    ) -> Result<Self, ExoGrantRefusal> {
130+        if request.objective.trim().is_empty() {
131+            return Err(ExoGrantRefusal::EmptyObjective);
132+        }
133+        let observed = request.observed.requested_capabilities();
134+        if observed.is_empty() {
135+            return Err(ExoGrantRefusal::NoSelfModificationRequested);
136+        }
137+        if request.capabilities != observed {
138+            return Err(ExoGrantRefusal::CapabilityMismatch);
139+        }
140+        if now_ms >= request.expires_at_ms {
141+            return Err(ExoGrantRefusal::Expired);
142+        }
143+        Ok(Self {
144+            request,
145+            origin,
146+            consumed: Cell::new(false),
147+        })
148+    }
149+
150+    /// Consume the grant immediately before the ACP prompt.
151+    pub fn consume(
152+        &self,
153+        current: &ObservedExoCapabilityState,
154+        turn_ref: &str,
155+        now_ms: u64,
156+    ) -> Result<ExoSelfModificationReceipt, ExoGrantRefusal> {
157+        if self.consumed.get() {
158+            return Err(ExoGrantRefusal::AlreadyConsumed);
159+        }
160+        if now_ms >= self.request.expires_at_ms {
161+            return Err(ExoGrantRefusal::Expired);
162+        }
163+        if current != &self.request.observed {
164+            return Err(ExoGrantRefusal::ConfigurationDrift);
165+        }
166+        if turn_ref != self.request.turn_ref {
167+            return Err(ExoGrantRefusal::TurnMismatch);
168+        }
169+        self.consumed.set(true);
170+        Ok(ExoSelfModificationReceipt {
171+            objective: self.request.objective.clone(),
172+            turn_ref: self.request.turn_ref.clone(),
173+            generation: current.generation,
174+            expires_at_ms: self.request.expires_at_ms,
175+            origin: self.origin,
176+            capabilities: self.request.capabilities.clone(),
177+            observed: current.clone(),
178+        })
179+    }
180+}
181+
182+#[cfg(test)]
183+mod tests {
184+    use super::*;
185+
186+    fn observed() -> ObservedExoCapabilityState {
187+        ObservedExoCapabilityState {
188+            source_commit: "commit".into(),
189+            source_tree: "tree".into(),
190+            binary_digest: "sha256:bytes".into(),
191+            agent: "agent".into(),
192+            conversation: "conversation".into(),
193+            generation: 7,
194+            agent_authored_tools: true,
195+            tool_modules: vec![ObservedToolModule {
196+                path: "/tools/guardian.ts".into(),
197+                digest: "sha256:module".into(),
198+            }],
199+            read_write_mounts: vec![ObservedReadWriteMount {
200+                host_path: "/host/exo".into(),
201+                mount_path: "/workspace/exo".into(),
202+            }],
203+        }
204+    }
205+
206+    fn request() -> ExoSelfModificationGrantRequest {
207+        let observed = observed();
208+        ExoSelfModificationGrantRequest {
209+            objective: "Edit, verify, and restart Exo.".into(),
210+            turn_ref: "turn-1".into(),
211+            capabilities: observed.requested_capabilities(),
212+            observed,
213+            expires_at_ms: 200,
214+        }
215+    }
216+
217+    #[test]
218+    fn exact_grant_is_one_use() {
219+        let grant = ExoSelfModificationGrant::mint(
220+            request(),
221+            ExoSelfModificationConsentOrigin::HumanConfirmationDialog,
222+            100,
223+        )
224+        .expect("grant");
225+        assert!(grant.consume(&observed(), "turn-1", 101).is_ok());
226+        assert_eq!(
227+            grant.consume(&observed(), "turn-1", 102),
228+            Err(ExoGrantRefusal::AlreadyConsumed)
229+        );
230+    }
231+
232+    #[test]
233+    fn every_observed_field_is_generation_fenced() {
234+        let mutations: [fn(&mut ObservedExoCapabilityState); 8] = [
235+            |state: &mut ObservedExoCapabilityState| state.source_commit.push('x'),
236+            |state: &mut ObservedExoCapabilityState| state.source_tree.push('x'),
237+            |state: &mut ObservedExoCapabilityState| state.binary_digest.push('x'),
238+            |state: &mut ObservedExoCapabilityState| state.agent.push('x'),
239+            |state: &mut ObservedExoCapabilityState| state.conversation.push('x'),
240+            |state: &mut ObservedExoCapabilityState| state.generation += 1,
241+            |state: &mut ObservedExoCapabilityState| state.tool_modules[0].digest.push('x'),
242+            |state: &mut ObservedExoCapabilityState| {
243+                state.read_write_mounts[0].mount_path.push('x')
244+            },
245+        ];
246+        for mutate in mutations {
247+            let grant = ExoSelfModificationGrant::mint(
248+                request(),
249+                ExoSelfModificationConsentOrigin::HumanConfirmationDialog,
250+                100,
251+            )
252+            .expect("grant");
253+            let mut changed = observed();
254+            mutate(&mut changed);
255+            assert_eq!(
256+                grant.consume(&changed, "turn-1", 101),
257+                Err(ExoGrantRefusal::ConfigurationDrift)
258+            );
259+        }
260+    }
261+
262+    #[test]
263+    fn expired_or_widened_requests_refuse() {
264+        assert!(matches!(
265+            ExoSelfModificationGrant::mint(
266+                request(),
267+                ExoSelfModificationConsentOrigin::HumanConfirmationDialog,
268+                200
269+            ),
270+            Err(ExoGrantRefusal::Expired)
271+        ));
272+        let mut widened = request();
273+        widened.capabilities.pop();
274+        assert!(matches!(
275+            ExoSelfModificationGrant::mint(
276+                widened,
277+                ExoSelfModificationConsentOrigin::HumanConfirmationDialog,
278+                100
279+            ),
280+            Err(ExoGrantRefusal::CapabilityMismatch)
281+        ));
282+    }
283+
284+    #[test]
285+    fn a_grant_cannot_move_to_another_turn() {
286+        let grant = ExoSelfModificationGrant::mint(
287+            request(),
288+            ExoSelfModificationConsentOrigin::HumanConfirmationDialog,
289+            100,
290+        )
291+        .expect("grant");
292+        assert_eq!(
293+            grant.consume(&observed(), "turn-2", 101),
294+            Err(ExoGrantRefusal::TurnMismatch)
295+        );
296+        assert!(grant.consume(&observed(), "turn-1", 102).is_ok());
297+    }
298+}
diff --git a/crates/omega_exo_lane/src/capability.rs b/crates/omega_exo_lane/src/capability.rs
index a9cded1dad..9bbc2e5469 100644
--- a/crates/omega_exo_lane/src/capability.rs
+++ b/crates/omega_exo_lane/src/capability.rs
@@ -8,11 +8,10 @@
88 //! model is sandbox isolation, and Exo's threat model assumes you *want* the
99 //! agent to modify itself.
1010 //!
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.**
11+//! Omega supplies the authority gate Exo lacks. The ordinary lane refuses
12+//! self-modification. A separate, typed, one-use grant can authorize one exact
13+//! turn after a visible human confirmation. The lane never silently enables
14+//! Exo's self-modification tools.
1615 //!
1716 //! # Read the agent, do not assume it
1817 //!
@@ -61,12 +60,31 @@ pub struct ExoAgent {
6160     pub agent_authored_tools: bool,
6261     /// How many TypeScript tool modules are loaded into it.
6362     pub tool_modules: u32,
63+    /// Exact TypeScript tool-module paths reported by Exo.
64+    pub tool_module_paths: Vec<String>,
6465     /// Whether any sandbox mount is read-write.
6566     pub read_write_mount: bool,
67+    /// Exact agent-level sandbox mounts.
68+    pub mounts: Vec<ExoMount>,
6669     /// Whether the agent's sandbox has a network. Reported, never refused.
6770     pub networking: bool,
6871 }
6972 
73+/// A mount observed in an Exo agent or conversation record.
74+#[derive(Clone, Debug, PartialEq, Eq)]
75+pub struct ExoMount {
76+    pub host_path: String,
77+    pub mount_path: String,
78+    pub read_write: bool,
79+}
80+
81+/// Capability-bearing fields from `exo conversation show`.
82+#[derive(Clone, Debug, PartialEq, Eq)]
83+pub struct ExoConversation {
84+    pub slug: String,
85+    pub mounts: Vec<ExoMount>,
86+}
87+
7088 /// Why the lane will not run a turn on this agent.
7189 #[derive(Clone, Copy, Debug, PartialEq, Eq)]
7290 pub enum SelfModification {
@@ -183,13 +201,16 @@ impl ExoAgent {
183201             _ => return Err(ExoAgentReadError::UnreadableField("enable_networking")),
184202         };
185203 
204+        let mounts = mounts(output, "sandbox_mounts")?;
186205         Ok(Self {
187206             slug: field("slug")?.to_owned(),
188207             harness: field("harness")?.to_owned(),
189208             model: field("model")?.to_owned(),
190209             agent_authored_tools: tool_creation,
191210             tool_modules,
192-            read_write_mount: read_write_mount(output)?,
211+            tool_module_paths: tool_module_paths(output, tool_modules)?,
212+            read_write_mount: mounts.iter().any(|mount| mount.read_write),
213+            mounts,
193214             networking,
194215         })
195216     }
@@ -214,38 +235,88 @@ impl ExoAgent {
214235     }
215236 }
216237 
238+impl ExoConversation {
239+    /// Parse the conversation record and retain exact mount paths.
240+    pub fn parse(output: &str) -> Result<Self, ExoAgentReadError> {
241+        let slug = output
242+            .lines()
243+            .find_map(|line| line.strip_prefix("slug:"))
244+            .map(str::trim)
245+            .filter(|value| !value.is_empty())
246+            .ok_or(ExoAgentReadError::MissingField("slug"))?
247+            .to_owned();
248+        Ok(Self {
249+            slug,
250+            mounts: mounts(output, "mounts")?,
251+        })
252+    }
253+
254+    /// Whether the conversation itself can modify a mounted host path.
255+    pub fn admits_lane_turn(&self) -> Result<(), SelfModification> {
256+        if self.mounts.iter().any(|mount| mount.read_write) {
257+            Err(SelfModification::ReadWriteMount)
258+        } else {
259+            Ok(())
260+        }
261+    }
262+}
263+
264+fn tool_module_paths(output: &str, expected: u32) -> Result<Vec<String>, ExoAgentReadError> {
265+    let mut lines = output.lines();
266+    lines
267+        .find(|line| line.starts_with("typescript_tool_modules:"))
268+        .ok_or(ExoAgentReadError::MissingField("typescript_tool_modules"))?;
269+    let paths = lines
270+        .take_while(|line| line.starts_with("  - "))
271+        .map(|line| line.trim_start_matches("  - ").to_owned())
272+        .collect::<Vec<_>>();
273+    if paths.len() != expected as usize {
274+        return Err(ExoAgentReadError::UnreadableField(
275+            "typescript_tool_modules",
276+        ));
277+    }
278+    Ok(paths)
279+}
280+
217281 /// Read the `sandbox_mounts:` block.
218282 ///
219283 /// Exo prints `  none` for an empty set and `  <host> -> <path> (ro)` or
220284 /// `(rw[, internal])` per mount. A block this build cannot read is an error for
221285 /// the same reason a missing field is.
222-fn read_write_mount(output: &str) -> Result<bool, ExoAgentReadError> {
286+fn mounts(output: &str, field: &'static str) -> Result<Vec<ExoMount>, ExoAgentReadError> {
223287     let mut lines = output.lines();
224288     lines
225-        .find(|line| line.trim_end() == "sandbox_mounts:")
226-        .ok_or(ExoAgentReadError::MissingField("sandbox_mounts"))?;
289+        .find(|line| line.trim_end() == format!("{field}:"))
290+        .ok_or(ExoAgentReadError::MissingField(field))?;
227291 
228-    let mut any = false;
292+    let mut mounts = Vec::new();
229293     for line in lines {
230294         let Some(entry) = line.strip_prefix("  ") else {
231295             break;
232296         };
233297         let entry = entry.trim();
234298         if entry == "none" {
235-            return Ok(false);
299+            return Ok(Vec::new());
236300         }
237-        let Some((_, mode)) = entry.rsplit_once('(') else {
238-            return Err(ExoAgentReadError::UnreadableField("sandbox_mounts"));
301+        let Some((paths, mode)) = entry.rsplit_once(" (") else {
302+            return Err(ExoAgentReadError::UnreadableField(field));
239303         };
240304         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-        }
305+        let read_write = match mode.split(',').next().map(str::trim) {
306+            Some("rw") => true,
307+            Some("ro") => false,
308+            _ => return Err(ExoAgentReadError::UnreadableField(field)),
309+        };
310+        let Some((host_path, mount_path)) = paths.split_once(" -> ") else {
311+            return Err(ExoAgentReadError::UnreadableField(field));
312+        };
313+        mounts.push(ExoMount {
314+            host_path: host_path.to_owned(),
315+            mount_path: mount_path.to_owned(),
316+            read_write,
317+        });
246318     }
247-    let _ = any;
248-    Ok(false)
319+    Ok(mounts)
249320 }
250321 
251322 #[cfg(test)]
@@ -295,7 +366,10 @@ braintrust: none
295366     fn the_self_improving_exo_agent_is_refused() {
296367         let self_improving = DRIVEN_AGENT
297368             .replace("tool_creation: disabled", "tool_creation: enabled")
298-            .replace("typescript_tool_modules: 0", "typescript_tool_modules: 1")
369+            .replace(
370+                "typescript_tool_modules: 0",
371+                "typescript_tool_modules: 1\n  - /workspace/exo/examples/exo/guardian-tools.ts",
372+            )
299373             .replace(
300374                 "sandbox_mounts:\n  none",
301375                 "sandbox_mounts:\n  /Users/x/exo -> /workspace/exo (rw)",
@@ -319,7 +393,7 @@ braintrust: none
319393             ),
320394             (
321395                 "typescript_tool_modules: 0",
322-                "typescript_tool_modules: 2",
396+                "typescript_tool_modules: 2\n  - /tools/one.ts\n  - /tools/two.ts",
323397                 SelfModification::ToolModule,
324398             ),
325399             (
@@ -352,13 +426,29 @@ braintrust: none
352426     /// Networking is reported and does not refuse. See the module docs.
353427     #[test]
354428     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");
429+        let agent = ExoAgent::parse(
430+            &DRIVEN_AGENT.replace("enable_networking: false", "enable_networking: true"),
431+        )
432+        .expect("parses");
358433         assert!(agent.networking);
359434         assert_eq!(agent.admits_lane_turn(), Ok(()));
360435     }
361436 
437+    #[test]
438+    fn a_conversation_only_read_write_mount_is_refused() {
439+        let shown = "\
440+slug: omega-lane
441+mounts:
442+  /Users/x/exo -> /workspace/exo (rw)
443+";
444+        let conversation = ExoConversation::parse(shown).expect("conversation parses");
445+        assert_eq!(
446+            conversation.admits_lane_turn(),
447+            Err(SelfModification::ReadWriteMount)
448+        );
449+        assert_eq!(conversation.mounts[0].host_path, "/Users/x/exo");
450+    }
451+
362452     /// The upstream-rename failure, which is the realistic one. A field the
363453     /// decision depends on going missing must not read as "no capability".
364454     #[test]
@@ -389,7 +479,10 @@ braintrust: none
389479     fn an_unreadable_value_is_refused_rather_than_read_as_false() {
390480         for (from, to) in [
391481             ("tool_creation: disabled", "tool_creation: maybe"),
392-            ("typescript_tool_modules: 0", "typescript_tool_modules: some"),
482+            (
483+                "typescript_tool_modules: 0",
484+                "typescript_tool_modules: some",
485+            ),
393486             ("enable_networking: false", "enable_networking: yes"),
394487         ] {
395488             assert!(
diff --git a/crates/omega_exo_lane/src/omega_exo_lane.rs b/crates/omega_exo_lane/src/omega_exo_lane.rs
index 5171d29776..703cc67e6f 100644
--- a/crates/omega_exo_lane/src/omega_exo_lane.rs
+++ b/crates/omega_exo_lane/src/omega_exo_lane.rs
@@ -1,4 +1,4 @@
1-//! The Exo harness lane, Tier A. `OMEGA-DELTA-0042`, omega#87.
1+//! The Exo harness lane. `OMEGA-DELTA-0042`, omega#87.
22 //!
33 //! Omega drives **`exoharness/exo`** — the recursive-self-improvement agent
44 //! harness from the Braintrust orbit — as one more executor lane beneath Omega
@@ -16,21 +16,16 @@
1616 //! router's decision half lives here and its dispatch half lives there: a law
1717 //! that needs GPUI to check is a law nobody checks.
1818 //!
19-//! # Tier A, and what it is not
19+//! # Streaming and self-modification authority
2020 //!
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.
21+//! Exo sends live text, tool calls, tool results, and completion records through
22+//! its ACP standard-input transport. Omega attaches that transport through
23+//! `crates/agent_servers`.
2624 //!
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.
25+//! Self-modification is off by default. A person can authorize one exact turn
26+//! in a dedicated confirmation dialog. The grant binds the source, binary,
27+//! agent, conversation, tool modules, mounts, draft, generation, and expiry.
28+//! The send path consumes the grant once and writes a durable receipt.
3429 //!
3530 //! # Which executor class an Exo thread reports, and why
3631 //!
@@ -91,13 +86,14 @@
9186 //! and it is better than deriving "openai" from the absence of a URL, which
9287 //! would be Omega inventing a fact about somebody else's configuration.
9388 
89+pub mod authority;
9490 pub mod capability;
9591 pub mod command;
9692 pub mod endpoint;
9793 pub mod pin;
9894 pub mod turn;
9995 
100-pub use capability::{ExoAgent, ExoAgentReadError, SelfModification};
96+pub use capability::{ExoAgent, ExoAgentReadError, ExoConversation, ExoMount, SelfModification};
10197 pub use command::{ADMITTED_LANE_ARGV, ARGUMENT_TERMINATOR, ExoArg, ExoCommand, ExoRoot};
10298 pub use endpoint::{EXO_SERVE_DEFAULT_BIND, LoopbackEndpoint, OffLoopback};
10399 pub use pin::{EXO_HARNESS_ID, EXO_PIN, ExoPin, ExoPinMismatch, ObservedExoCheckout, admits_bytes};
@@ -222,7 +218,9 @@ gpt5mini  gpt-5-mini      openai  default
222218             model: "gpt5mini".into(),
223219             agent_authored_tools: false,
224220             tool_modules: 0,
221+            tool_module_paths: Vec::new(),
225222             read_write_mount: false,
223+            mounts: Vec::new(),
226224             networking: false,
227225         }
228226     }
@@ -310,3 +308,8 @@ gpt5mini  gpt-5-mini      openai  default
310308         assert!(identity.disclosure(None).is_coherent());
311309     }
312310 }
311+pub use authority::{
312+    ExoGrantRefusal, ExoSelfModificationCapability, ExoSelfModificationConsentOrigin,
313+    ExoSelfModificationGrant, ExoSelfModificationGrantRequest, ExoSelfModificationReceipt,
314+    ObservedExoCapabilityState, ObservedReadWriteMount, ObservedToolModule,
315+};
diff --git a/crates/omega_exo_lane/src/pin.rs b/crates/omega_exo_lane/src/pin.rs
index ea0097a88a..d939f49fb7 100644
--- a/crates/omega_exo_lane/src/pin.rs
+++ b/crates/omega_exo_lane/src/pin.rs
@@ -43,11 +43,10 @@ pub const EXO_HARNESS_ID: &str = "exo";
4343 
4444 /// The Exo this lane drives.
4545 ///
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.
46+/// **The maintained `OpenAgentsInc/exo` fork of `exoharness/exo`**, the
47+/// recursive-self-improvement agent harness. This is not exo labs'
48+/// `exo-explore/exo` cluster-inference appliance. The fork contains the ACP
49+/// transport that Omega needs while the upstream change is under review.
5150 #[derive(Clone, Copy, Debug, PartialEq, Eq)]
5251 pub struct ExoPin {
5352     /// The repository, in full. See the type documentation for why.
@@ -64,9 +63,9 @@ pub struct ExoPin {
6463 /// `docs/teardowns/2026-07-25-exoharness-exo-teardown.md` and driven for real
6564 /// by omega#87.
6665 pub const EXO_PIN: ExoPin = ExoPin {
67-    upstream: "https://github.com/exoharness/exo",
68-    source_commit: "baa07f6785547080d99bd2a7d3eab6d76b984e35",
69-    source_tree: "0aff9139a166414fa51a09b66ba4785bae05b46b",
66+    upstream: "https://github.com/OpenAgentsInc/exo",
67+    source_commit: "cd7c0d29db869e953fb7261d8390ca93007d36a6",
68+    source_tree: "c61846e3f44daaf445930d1a499432ca9b069306",
7069     version: "0.1.0",
7170 };
7271 
@@ -89,7 +88,7 @@ pub enum ExoPinMismatch {
8988 impl std::fmt::Display for ExoPinMismatch {
9089     fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
9190         formatter.write_str(match self {
92-            Self::Upstream => "the Exo checkout is not exoharness/exo",
91+            Self::Upstream => "the Exo checkout is not the maintained exoharness/exo fork",
9392             Self::Commit => "the Exo checkout is not at the pinned commit",
9493             Self::Tree => "the Exo checkout's tree is not the pinned tree",
9594             Self::Bytes => "the Exo binary is not the bytes frozen in the pin ledger",
@@ -134,7 +133,10 @@ impl ExoPin {
134133         if !same_repository(self.upstream, &observed.upstream) {
135134             return Err(ExoPinMismatch::Upstream);
136135         }
137-        if !self.source_commit.eq_ignore_ascii_case(observed.commit.trim()) {
136+        if !self
137+            .source_commit
138+            .eq_ignore_ascii_case(observed.commit.trim())
139+        {
138140             return Err(ExoPinMismatch::Commit);
139141         }
140142         if !self.source_tree.eq_ignore_ascii_case(observed.tree.trim()) {
@@ -205,7 +207,9 @@ mod tests {
205207         for value in [EXO_PIN.source_commit, EXO_PIN.source_tree] {
206208             assert_eq!(value.len(), 40, "{value} is not a full object id");
207209             assert!(
208-                value.chars().all(|c| c.is_ascii_hexdigit() && !c.is_ascii_uppercase()),
210+                value
211+                    .chars()
212+                    .all(|c| c.is_ascii_hexdigit() && !c.is_ascii_uppercase()),
209213                 "{value} is not a lowercase hexadecimal object id"
210214             );
211215         }
@@ -219,7 +223,7 @@ mod tests {
219223     /// else, and the one this lane drives is the harness.
220224     #[test]
221225     fn the_pin_names_the_harness_exo_and_not_the_cluster_one() {
222-        assert_eq!(EXO_PIN.upstream, "https://github.com/exoharness/exo");
226+        assert_eq!(EXO_PIN.upstream, "https://github.com/OpenAgentsInc/exo");
223227         let cluster = ObservedExoCheckout {
224228             upstream: "https://github.com/exo-explore/exo".into(),
225229             ..pinned_checkout()
@@ -238,10 +242,10 @@ mod tests {
238242     #[test]
239243     fn the_same_repository_written_four_ways_is_the_same_repository() {
240244         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+            "https://github.com/OpenAgentsInc/exo.git",
246+            "https://github.com/OpenAgentsInc/exo/",
247+            "https://GitHub.com/OpenAgentsInc/Exo",
248+            "git@github.com:OpenAgentsInc/exo.git",
245249         ] {
246250             let observed = ObservedExoCheckout {
247251                 upstream: spelling.into(),
Served at tenant.openagents/omega Member data and write actions are omitted.