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 coordinate
30617:7649603503856e5148d571eac2766b288a8ff1e9e35d380337a1d2b0015b4f92:omegaMaintainersHidden in public view
References2 branches · 1 tag
Read-only clone
git clone https://openagents.com/git/tenant.openagents/omega.gitRevision diff
73dadebb → b23a5d15diff --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(¶ms.session_id).cloned() else {
477- return Task::ready(Err(anyhow!("no Exo thread for {}", params.session_id.0)));
478- };
479- let prompt = prompt_text(¶ms);
480- // Cloned into the task rather than driven here: the turn must not run
481- // on the thread that draws the window. See `run`.
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(¶ms.session_id, ¶ms.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(¶ms.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(),