Skip to repository content
tenant.openagents/omega
No repository description is available.
OpenAgents Git authority 2026-07-28T01:03:26.928Z 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
06059dd5 → 890f9fa4diff --git a/Cargo.lock b/Cargo.lock
index 50691fddbc..7c176df859 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -11933,6 +11933,18 @@ dependencies = [
1193311933 "settings",
1193411934 ]
1193511935
11936+[[package]]
11937+name = "omega_acp_server"
11938+version = "0.1.0"
11939+dependencies = [
11940+ "agent-client-protocol",
11941+ "futures 0.3.32",
11942+ "log",
11943+ "omega_front_door",
11944+ "serde_json",
11945+ "smol",
11946+]
11947+
1193611948 [[package]]
1193711949 name = "omega_deltas"
1193811950 version = "0.1.0"
@@ -11954,7 +11966,9 @@ dependencies = [
1195411966 "gpui",
1195511967 "http_client",
1195611968 "libc",
11969+ "log",
1195711970 "nostr",
11971+ "omega_acp_server",
1195811972 "omega_identity",
1195911973 "paths",
1196011974 "rand 0.9.4",
diff --git a/Cargo.toml b/Cargo.toml
index 443d9664d0..8b6c944f2d 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -145,6 +145,7 @@ members = [
145145 "crates/ollama",
146146 "crates/omega_identity",
147147 "crates/omega_deltas",
148+ "crates/omega_acp_server",
148149 "crates/omega_front_door",
149150 "crates/omega_harness",
150151 "crates/omega_effectd",
@@ -418,6 +419,7 @@ oauth_callback_server = { path = "crates/oauth_callback_server" }
418419 ollama = { path = "crates/ollama" }
419420 omega_identity = { path = "crates/omega_identity" }
420421 omega_deltas = { path = "crates/omega_deltas" }
422+omega_acp_server = { path = "crates/omega_acp_server" }
421423 omega_front_door = { path = "crates/omega_front_door" }
422424 omega_harness = { path = "crates/omega_harness" }
423425 omega_effectd = { path = "crates/omega_effectd" }
diff --git a/OMEGA_DELTAS.md b/OMEGA_DELTAS.md
index c97d7697e5..b1c675ee58 100644
--- a/OMEGA_DELTAS.md
+++ b/OMEGA_DELTAS.md
@@ -1984,3 +1984,89 @@ than it sounds, because the harness omega#81's acceptance sentence names —
19841984 onboarding-surface decision (omega#9) and not this delta's to take. This delta
19851985 also does not cover *what* onboarding asks for, or the editor-setup journey,
19861986 which reaches the welcome page rather than the front door and is unchanged.
1987+
1988+### OMEGA-DELTA-0041 — Omega Agent is attachable over ACP, on a loopback socket that is off by default and read-only
1989+
1990+- **Upstream Zed:** an ACP client only. `crates/agent_servers` reaches *out* to
1991+ external agents and every `ConnectionTo<…>` in the tree is a
1992+ `ConnectionTo<Agent>`. Nothing in upstream implements the agent role, so no
1993+ external host can attach to the editor and use the agent it is configured
1994+ with.
1995+- **Omega:** Omega Agent — the router, not a raw provider — can be served over
1996+ ACP to an external host on `127.0.0.1`. An attaching host initialises,
1997+ creates a session, prompts it, and is answered with the same disclosed
1998+ routing an in-app thread carries. omega#82's recursive composability, in the
1999+ direction upstream does not have.
2000+- **Off by default, and that is asserted rather than intended.**
2001+ `OMEGA_ACP_SERVER` must be exactly `1`. `true`, `yes`, `on`, `01`, ` 1` and an
2002+ unset variable are each off, with a typed `OffReason` rather than a bool, and
2003+ `the_served_acp_surface_is_off_unless_the_flag_is_exact` fails on a
2004+ truthy-tolerant read such as `to_lowercase()` or `parse::<bool>()`. A listener
2005+ that is on by default is a different product.
2006+- **Loopback by construction, not by configuration.** `LoopbackHost::new`
2007+ refuses anything but `127.0.0.1` and `::1`, and refuses a *name* rather than
2008+ resolving it — resolution is the step at which `localhost` can be made to mean
2009+ something routable. `LoopbackAcpServer::bind` takes that type, so reaching a
2010+ routable interface needs a new type rather than a new setting.
2011+- **Read-only, inside the authority partition rather than beside it.** This is
2012+ an **unauthenticated** model-driven surface — `authMethods` is empty, so it
2013+ carries no bearer at all and is structurally weaker than the Desktop MCP
2014+ surface. Owner gate 8 (*no model-initiated path can start Full Auto
2015+ authority; only an explicit human action can, wherever that action lives*)
2016+ therefore reaches it directly, and three model-callable Full Auto starts were
2017+ removed from OpenAgents Desktop the same day. So the surface does not sit
2018+ beside the partition: `SERVED_SURFACE` is the four methods a host can reach,
2019+ every one of them observation, and `UNEXPOSED_AUTHORITY` classifies every
2020+ authority-bearing control with the typed refusal a host attempting it gets —
2021+ checked in **both directions** against `FULL_AUTO_AFFORDANCES` and
2022+ `PinGesture::all()`, so adding a Full Auto control fails this crate's tests
2023+ until somebody says what a served host is told. Anything not listed is refused
2024+ by the absence of a dispatch entry, not by a branch someone has to remember.
2025+- **A served session can never reach an engine lane.** A pin is the only door to
2026+ one, setting a pin requires a `PinGesture`, and no variant of that enum is
2027+ reachable over a socket. `a_served_session_can_never_reach_an_engine_lane`
2028+ proves the consequence against every engine state the router can be shown —
2029+ including an engine with idle lanes and an executor registered — so "the
2030+ engine happened to be down" is not what is doing the work.
2031+- **Ingress is a record beside the disclosure, not a fourth executor class.**
2032+ `ExecutorClass` answers *who ran the work*; serving over ACP changes *who
2033+ asked*. Reusing `ExternalAcp` would make a served session claim an external
2034+ agent did work Omega did, and make one token mean opposite things depending on
2035+ which side of the socket the reader stands on. `OMEGA-AGENT-AC-04` therefore
2036+ stands unrevised at three classes and `SessionOrigin` carries the ingress.
2037+ Both records cross the wire with **exactly** their declared fields —
2038+ `EXECUTOR_DISCLOSURE_FIELDS` and `SESSION_ORIGIN_FIELDS`, asserted exactly
2039+ rather than against a denylist, because a denylist that fails on `label`
2040+ passes for `line`, `text`, `summary`, and `caption`.
2041+- **GPUI never opens the socket.** `crates/omega_acp_server` declares no
2042+ dependency on GPUI, `workspace`, `project`, `agent_ui`, or `ui`, and the only
2043+ production caller of `start_if_enabled` is `crates/omega_effectd` — the
2044+ supervisor layer. Both are checked by scanning the tree rather than asserted
2045+ in prose.
2046+- **Enforced by:** `the_served_acp_surface_is_off_unless_the_flag_is_exact`,
2047+ `only_the_supervisor_opens_the_served_acp_socket`,
2048+ `nothing_over_the_served_acp_surface_can_take_a_pin`,
2049+ `the_served_surface_presents_the_first_party_agent_id`, and
2050+ `the_supervisor_starts_the_served_surface_before_it_resolves_the_engine` in
2051+ `crates/omega_deltas`; the fourteen checks in `crates/omega_acp_server`,
2052+ including one that drives the real loopback socket with the **upstream** ACP
2053+ SDK client and reads the disclosure back off it; and
2054+ `the_disclosure_record_holds_no_rendered_label` plus
2055+ `the_origin_record_holds_no_rendered_label` in `crates/omega_front_door`.
2056+- **A served prompt ends its turn, and says what it did not do.** ACP has a
2057+ `refusal` stop reason and it is the wrong one: it means *the prompt and
2058+ everything after it will not be included in the next prompt*, and stock Zed
2059+ 1.12.0 implements that literally — it dropped the turn and showed a refusal
2060+ banner with **no disclosure at all**. That was watched happening before this
2061+ shape was chosen. The turn genuinely ends, so it says `end_turn`, and what did
2062+ not happen is in the message the operator reads and in the typed record beside
2063+ it.
2064+- **What this does not cover.** The listener runs in the Omega process under the
2065+ supervisor's control, **not** inside the packaged `@openagentsinc/omega-effectd`
2066+ daemon; that daemon lives in the openagents repository and this packet is
2067+ scoped to omega. What is enforced here is the property omega#82's falsifier
2068+ names — the bind lives in a crate the UI layer cannot reach, and the start
2069+ lives in the supervisor. The surface also does not *execute*: a served turn is
2070+ answered by disclosing where it would route, and dispatching a served turn to
2071+ a real executor is a later decision with its own authority question, not an
2072+ omission. The v1 fleet-backed server (openagents #9179) stays deferred.
diff --git a/crates/omega_acp_server/Cargo.toml b/crates/omega_acp_server/Cargo.toml
new file mode 100644
index 0000000000..a22b82ffca
--- /dev/null
+++ b/crates/omega_acp_server/Cargo.toml
@@ -0,0 +1,31 @@
1+[package]
2+name = "omega_acp_server"
3+version = "0.1.0"
4+edition.workspace = true
5+publish.workspace = true
6+license = "GPL-3.0-or-later"
7+
8+[lints]
9+workspace = true
10+
11+[lib]
12+path = "src/omega_acp_server.rs"
13+doctest = false
14+
15+# The listener has a binary of its own so it can be supervised as a child, and
16+# so a conformance run against an external ACP host does not need a windowing
17+# system. `omega-effectd` owns the lifecycle in a shipped build; see the module
18+# docs on why the socket is deliberately not in a GPUI crate.
19+[[bin]]
20+name = "omega-acp-server"
21+path = "src/main.rs"
22+
23+[dependencies]
24+agent-client-protocol.workspace = true
25+log.workspace = true
26+omega_front_door.workspace = true
27+serde_json.workspace = true
28+
29+[dev-dependencies]
30+futures.workspace = true
31+smol.workspace = true
diff --git a/crates/omega_acp_server/src/main.rs b/crates/omega_acp_server/src/main.rs
new file mode 100644
index 0000000000..f9d3854ea8
--- /dev/null
+++ b/crates/omega_acp_server/src/main.rs
@@ -0,0 +1,69 @@
1+//! The loopback ACP server as a supervised child. `OMEGA-DELTA-0041`,
2+//! omega#82.
3+//!
4+//! `omega-effectd` owns the lifecycle in a shipped build, by calling
5+//! [`omega_acp_server::start_if_enabled`] from `crates/omega_effectd`. This
6+//! binary exists so the same code path can be **driven by a real external ACP
7+//! host** without a windowing system: an external host attaches through a
8+//! stdio-to-TCP bridge, and what it talks to is the library above, not a
9+//! demonstration copy of it.
10+//!
11+//! It is off by the same flag and binds through the same
12+//! [`omega_acp_server::LoopbackHost`], so running it proves the shipped
13+//! behaviour rather than a parallel one. With the flag unset it prints why it
14+//! is not listening and exits, which is the default-off property observable
15+//! from a shell.
16+//!
17+//! # Attaching an external ACP host
18+//!
19+//! ACP is spoken over a subprocess's stdio, so an external host reaches a
20+//! socket through a one-line bridge. This is exactly how stock Zed 1.12.0 was
21+//! driven against it on 2026-07-26:
22+//!
23+//! ```text
24+//! OMEGA_ACP_SERVER=1 OMEGA_ACP_SERVER_PORT=8282 omega-acp-server
25+//! ```
26+//!
27+//! then, in the external host's settings:
28+//!
29+//! ```json
30+//! "agent_servers": {
31+//! "omega-served": {
32+//! "command": "/bin/sh",
33+//! "args": ["-c", "exec /usr/bin/nc 127.0.0.1 8282"]
34+//! }
35+//! }
36+//! ```
37+//!
38+//! Opening a thread on that agent initialises, creates a session, and shows the
39+//! disclosure the served turn renders. The headless equivalent —
40+//! the **upstream** ACP SDK client over the same loopback socket — is
41+//! `the_upstream_acp_client_reads_the_disclosure_off_a_real_socket` in the
42+//! library, so the exit is reproducible without a windowing system.
43+
44+fn main() {
45+ match omega_acp_server::start_if_enabled() {
46+ omega_acp_server::StartOutcome::NotStarted(reason) => {
47+ println!(
48+ "omega-acp-server: not listening ({}). Set {}={} to serve Omega \
49+ Agent over ACP on loopback.",
50+ reason.token(),
51+ omega_acp_server::ENABLE_FLAG,
52+ omega_acp_server::ENABLE_VALUE,
53+ );
54+ }
55+ omega_acp_server::StartOutcome::Listening(address) => {
56+ // Printed on stdout so a bridge script can read the port back
57+ // without parsing a log.
58+ println!("omega-acp-server: listening on {address}");
59+ // The listener serves on its own thread; park this one on it.
60+ loop {
61+ std::thread::park();
62+ }
63+ }
64+ omega_acp_server::StartOutcome::Failed(error) => {
65+ eprintln!("omega-acp-server: could not listen: {error}");
66+ std::process::exit(1);
67+ }
68+ }
69+}
diff --git a/crates/omega_acp_server/src/omega_acp_server.rs b/crates/omega_acp_server/src/omega_acp_server.rs
new file mode 100644
index 0000000000..bcf20ab59e
--- /dev/null
+++ b/crates/omega_acp_server/src/omega_acp_server.rs
@@ -0,0 +1,1644 @@
1+//! Omega Agent, served over ACP on a loopback socket. `OMEGA-DELTA-0041`,
2+//! omega#82.
3+//!
4+//! Omega is already an ACP **client**: `crates/agent_servers` reaches out to
5+//! external agents. This crate is the other direction. An external ACP host —
6+//! stock Zed, another fork, any conformant client — can attach to Omega and get
7+//! Omega Agent, the router, with the same disclosed routing an in-app thread
8+//! gets. Recursive composability, which is what omega#82 asks for.
9+//!
10+//! Four properties hold this surface down, and each is structural rather than
11+//! a runtime check something could forget to call.
12+//!
13+//! # 1. Default off
14+//!
15+//! [`enablement`] is an **exact** match on `"1"`. `"true"`, `"yes"`, `"on"`,
16+//! `" 1"`, and an unset variable are all off, and the caller gets a typed
17+//! [`OffReason`] rather than a bool. A listener that is on by default is a
18+//! different product from one that is off by default, so the default is
19+//! asserted rather than assumed.
20+//!
21+//! # 2. Loopback only
22+//!
23+//! [`LoopbackHost`] is a **construction invariant**, not a configuration check.
24+//! There is no way to hold a bind address that is not `127.0.0.1` or `::1`,
25+//! because the only constructor refuses anything else, and
26+//! [`LoopbackAcpServer::bind`] takes a `LoopbackHost` rather than a string. A
27+//! routable interface cannot be reached by editing a setting; it needs a new
28+//! type.
29+//!
30+//! # 3. Read-only, inside the authority partition rather than beside it
31+//!
32+//! This is an **unauthenticated** model-driven surface: [`AUTH_METHODS`] is
33+//! empty, so it carries no bearer at all and is structurally weaker than the
34+//! Desktop MCP surface. Owner gate 8 — *no model-initiated path can start Full
35+//! Auto authority; only an explicit human action can, wherever that action
36+//! lives* — therefore reaches it directly.
37+//!
38+//! So the partition is the design. [`SERVED_SURFACE`] is every method an
39+//! attached host can reach, each classified, and every one of them is an
40+//! [`SurfaceAuthority::Observation`]. [`UNEXPOSED_AUTHORITY`] is the other
41+//! half: every control in Omega that grants Full Auto authority or mutates run
42+//! state, listed with the typed refusal a host attempting it receives. That
43+//! list is checked **against the existing ledgers** — every
44+//! [`omega_front_door::FULL_AUTO_AFFORDANCES`] element id and every
45+//! [`omega_front_door::PinGesture`] must appear in it — so a new Full Auto
46+//! control fails this crate's tests until somebody classifies it. Adding a
47+//! control cannot silently open a door here.
48+//!
49+//! Anything not in [`SERVED_SURFACE`] is refused: [`served_method`] is the only
50+//! dispatch table, so deny-by-default is the absence of an entry rather than a
51+//! branch someone has to remember to write.
52+//!
53+//! A served prompt is answered by **disclosing**, not by executing. Nothing is
54+//! dispatched to any executor, so the answer's stop reason is the protocol's
55+//! own word for it, `refusal`.
56+//!
57+//! # 4. The served agent is the router, and it can never reach an engine lane
58+//!
59+//! [`served_route`] calls [`omega_front_door::route`] — the same routing law an
60+//! in-app thread is decided by — with a pin of `None`. A pin is the only door
61+//! to an engine lane, an engine lane *is* Full Auto authority, and setting one
62+//! requires an `omega_front_door::PinGesture`, every variant of which is a
63+//! visible control a person operates. There is no variant for "an external host
64+//! asked", so the served surface cannot take a pin, and
65+//! `a_served_session_can_never_reach_an_engine_lane` proves the consequence
66+//! against every engine state the router can be shown.
67+//!
68+//! # Where the socket lives
69+//!
70+//! Not in a GPUI crate. This crate depends on `omega_front_door` and the pinned
71+//! ACP schema and on **no** part of GPUI, and a check in `crates/omega_deltas`
72+//! fails if a listener appears in `crates/agent_ui`, `crates/full_auto_ui`, or
73+//! `crates/zed`. The lifecycle owner in a shipped build is `omega-effectd`:
74+//! [`start_if_enabled`] is called from `crates/omega_effectd`, which is the
75+//! supervisor layer, and nowhere else.
76+//!
77+//! **What this does not do,** stated rather than discovered: the listener runs
78+//! in the Omega process under the supervisor's control, not inside the packaged
79+//! `@openagentsinc/omega-effectd` daemon. The daemon lives in the openagents
80+//! repository and this packet is scoped to omega. The property omega#82's
81+//! falsifier names — *GPUI owns the socket* — is what is enforced here, by
82+//! keeping the bind in a crate GPUI cannot reach and the start in the
83+//! supervisor.
84+
85+use std::io::{BufRead, BufReader, Write};
86+use std::net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr, TcpListener, TcpStream};
87+
88+use agent_client_protocol::schema::v1::{AGENT_METHOD_NAMES, CLIENT_METHOD_NAMES};
89+use omega_front_door::{
90+ EngineReadiness, EngineUnreachable, ExecutorDisclosure, RouteDecision, RouteInputs,
91+ SessionOrigin, route,
92+};
93+use serde_json::{Value, json};
94+
95+// -------------------------------------------------------------------------
96+// Default off
97+// -------------------------------------------------------------------------
98+
99+/// The environment variable that turns the loopback ACP server on.
100+pub const ENABLE_FLAG: &str = "OMEGA_ACP_SERVER";
101+
102+/// The environment variable that pins the port. Optional; `0` means ephemeral.
103+pub const PORT_FLAG: &str = "OMEGA_ACP_SERVER_PORT";
104+
105+/// The one value of [`ENABLE_FLAG`] that turns the server on.
106+///
107+/// Exact. `"true"`, `"yes"`, `"on"`, `"01"` and `" 1"` are all off. A flag that
108+/// accepts anything truthy is a flag whose default nobody can state.
109+pub const ENABLE_VALUE: &str = "1";
110+
111+/// Why the server is off.
112+#[derive(Debug, Clone, Copy, PartialEq, Eq)]
113+pub enum OffReason {
114+ /// The flag is not set at all. This is the shipped default.
115+ FlagUnset,
116+ /// The flag is set to something other than exactly [`ENABLE_VALUE`].
117+ FlagNotExactlyOne,
118+}
119+
120+impl OffReason {
121+ /// The stable token this reason is recorded under.
122+ #[must_use]
123+ pub const fn token(self) -> &'static str {
124+ match self {
125+ Self::FlagUnset => "flag_unset",
126+ Self::FlagNotExactlyOne => "flag_not_exactly_one",
127+ }
128+ }
129+}
130+
131+/// Whether the loopback ACP server runs.
132+#[derive(Debug, Clone, Copy, PartialEq, Eq)]
133+pub enum Enablement {
134+ /// Off, with the reason.
135+ Off(OffReason),
136+ /// On, because the flag said exactly [`ENABLE_VALUE`].
137+ On,
138+}
139+
140+impl Enablement {
141+ /// Whether a listener should be opened.
142+ #[must_use]
143+ pub const fn is_on(self) -> bool {
144+ matches!(self, Self::On)
145+ }
146+}
147+
148+/// Read the enablement from the flag's value.
149+///
150+/// Takes the value rather than reading the environment, so the default can be
151+/// asserted without a process.
152+#[must_use]
153+pub fn enablement(flag: Option<&str>) -> Enablement {
154+ match flag {
155+ None => Enablement::Off(OffReason::FlagUnset),
156+ Some(ENABLE_VALUE) => Enablement::On,
157+ Some(_) => Enablement::Off(OffReason::FlagNotExactlyOne),
158+ }
159+}
160+
161+// -------------------------------------------------------------------------
162+// Loopback only
163+// -------------------------------------------------------------------------
164+
165+/// A bind address that is loopback, proven by construction.
166+///
167+/// There is no way to build one that is not, so nothing downstream has to
168+/// check. `LoopbackAcpServer::bind` takes this type rather than a string, which
169+/// is what makes "never binds a routable interface" a property of the type
170+/// system instead of a code review.
171+#[derive(Debug, Clone, Copy, PartialEq, Eq)]
172+pub struct LoopbackHost(IpAddr);
173+
174+/// Why an address was refused as a bind target.
175+#[derive(Debug, Clone, PartialEq, Eq)]
176+pub enum BindRefusal {
177+ /// The text is not an IP address at all. A hostname is refused rather than
178+ /// resolved: resolution is the step where `localhost` can be made to mean
179+ /// something routable.
180+ NotAnAddress(String),
181+ /// The address parses and is not loopback.
182+ NotLoopback(IpAddr),
183+}
184+
185+impl BindRefusal {
186+ /// The stable token this refusal is recorded under.
187+ #[must_use]
188+ pub const fn token(&self) -> &'static str {
189+ match self {
190+ Self::NotAnAddress(_) => "not_an_address",
191+ Self::NotLoopback(_) => "not_loopback",
192+ }
193+ }
194+}
195+
196+impl std::fmt::Display for BindRefusal {
197+ fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
198+ match self {
199+ Self::NotAnAddress(raw) => write!(
200+ formatter,
201+ "{raw:?} is not an IP address. The Omega ACP server binds a \
202+ literal loopback address and never resolves a name."
203+ ),
204+ Self::NotLoopback(address) => write!(
205+ formatter,
206+ "{address} is not a loopback address. The Omega ACP server is \
207+ an unauthenticated surface and never binds a routable \
208+ interface."
209+ ),
210+ }
211+ }
212+}
213+
214+impl LoopbackHost {
215+ /// The shipped bind address.
216+ pub const DEFAULT: Self = Self(IpAddr::V4(Ipv4Addr::LOCALHOST));
217+
218+ /// Take a loopback address, or refuse.
219+ ///
220+ /// # Errors
221+ ///
222+ /// [`BindRefusal`] when the text is not an IP address, or is an address
223+ /// that is not loopback.
224+ pub fn new(raw: &str) -> Result<Self, BindRefusal> {
225+ let address: IpAddr = raw
226+ .parse()
227+ .map_err(|_| BindRefusal::NotAnAddress(raw.to_owned()))?;
228+ if address == IpAddr::V4(Ipv4Addr::LOCALHOST) || address == IpAddr::V6(Ipv6Addr::LOCALHOST)
229+ {
230+ Ok(Self(address))
231+ } else {
232+ Err(BindRefusal::NotLoopback(address))
233+ }
234+ }
235+
236+ /// The address itself.
237+ #[must_use]
238+ pub const fn address(self) -> IpAddr {
239+ self.0
240+ }
241+}
242+
243+// -------------------------------------------------------------------------
244+// The authority partition
245+// -------------------------------------------------------------------------
246+
247+/// What an attached host is refused, and why.
248+///
249+/// Typed rather than a sentence, so a caller can branch on it and so the same
250+/// refusal reads the same on the wire and in a test.
251+#[derive(Debug, Clone, Copy, PartialEq, Eq)]
252+pub enum ServedRefusal {
253+ /// A prompt was answered by disclosing where it would route, and dispatched
254+ /// to nothing.
255+ ExecutesNothing,
256+ /// A run may not be started over this surface.
257+ StartsNoRun,
258+ /// A run's state may not be changed over this surface.
259+ MutatesNoRun,
260+ /// An executor pin may not be set or cleared over this surface.
261+ SetsNoPin,
262+ /// An account or credential may not be connected or revoked over this
263+ /// surface.
264+ GrantsNoCredential,
265+ /// The method is not part of the served surface at all.
266+ NotExposed,
267+}
268+
269+impl ServedRefusal {
270+ /// Every refusal, in declaration order.
271+ #[must_use]
272+ pub const fn all() -> &'static [Self] {
273+ &[
274+ Self::ExecutesNothing,
275+ Self::StartsNoRun,
276+ Self::MutatesNoRun,
277+ Self::SetsNoPin,
278+ Self::GrantsNoCredential,
279+ Self::NotExposed,
280+ ]
281+ }
282+
283+ /// The stable token this refusal is recorded under.
284+ #[must_use]
285+ pub const fn token(self) -> &'static str {
286+ match self {
287+ Self::ExecutesNothing => "served_executes_nothing",
288+ Self::StartsNoRun => "served_starts_no_run",
289+ Self::MutatesNoRun => "served_mutates_no_run",
290+ Self::SetsNoPin => "served_sets_no_pin",
291+ Self::GrantsNoCredential => "served_grants_no_credential",
292+ Self::NotExposed => "served_method_not_exposed",
293+ }
294+ }
295+
296+ /// What the attached host is told, in its operator's terms.
297+ ///
298+ /// Derived on every call; nothing stores it.
299+ #[must_use]
300+ pub const fn message(self) -> &'static str {
301+ match self {
302+ Self::ExecutesNothing => {
303+ "Omega Agent served over ACP is read-only. This turn was routed \
304+ and disclosed, and dispatched to no executor."
305+ }
306+ Self::StartsNoRun => {
307+ "A run cannot be started over the served ACP surface. Only an \
308+ explicit human action in Omega starts Full Auto authority."
309+ }
310+ Self::MutatesNoRun => {
311+ "Run state cannot be changed over the served ACP surface. The \
312+ engine remains the sole run authority and Omega's own controls \
313+ are the only way to reach it."
314+ }
315+ Self::SetsNoPin => {
316+ "An executor pin cannot be set over the served ACP surface. A \
317+ pin is the only door to an engine lane and it is set by a \
318+ human gesture on a visible control."
319+ }
320+ Self::GrantsNoCredential => {
321+ "An account cannot be connected or revoked over the served ACP \
322+ surface. This surface is unauthenticated and grants nothing."
323+ }
324+ Self::NotExposed => {
325+ "This method is not part of the served ACP surface. The surface \
326+ is deny-by-default: what is not listed is refused."
327+ }
328+ }
329+ }
330+}
331+
332+/// What an attached host may do with a served method.
333+#[derive(Debug, Clone, Copy, PartialEq, Eq)]
334+pub enum SurfaceAuthority {
335+ /// Reads a record or mints one. Grants nothing and changes no run.
336+ ///
337+ /// Every entry of [`SERVED_SURFACE`] is this, and
338+ /// `the_whole_served_surface_is_observation` fails if one is not.
339+ Observation,
340+}
341+
342+/// Which handler a served method reaches.
343+///
344+/// The dispatch is driven by [`SERVED_SURFACE`] rather than by a `match` on a
345+/// string, so a method with no entry in the table has no handler to reach and
346+/// deny-by-default is the absence of a row.
347+#[derive(Debug, Clone, Copy, PartialEq, Eq)]
348+pub enum ServedMethodKind {
349+ /// `initialize`.
350+ Initialize,
351+ /// `session/new`.
352+ NewSession,
353+ /// `session/prompt`.
354+ Prompt,
355+ /// `session/cancel`, a notification.
356+ Cancel,
357+}
358+
359+/// One method an attached host may reach, and what it is allowed to do.
360+#[derive(Debug, Clone, Copy, PartialEq, Eq)]
361+pub struct ServedMethod {
362+ /// The ACP method name, taken from the pinned schema's own constants so a
363+ /// protocol rename is a compile error rather than a silent 404.
364+ pub method: &'static str,
365+ /// Which handler it reaches.
366+ pub kind: ServedMethodKind,
367+ /// What it is allowed to do.
368+ pub authority: SurfaceAuthority,
369+}
370+
371+/// Every method the served surface exposes.
372+///
373+/// Deliberately short. `session/load`, `session/resume`, `session/fork`,
374+/// `session/set_mode`, `session/set_config_option`, `session/delete`,
375+/// `authenticate`, and `logout` are all absent, and absence is the refusal:
376+/// [`served_method`] returns `None` and the caller answers
377+/// [`ServedRefusal::NotExposed`].
378+pub const SERVED_SURFACE: &[ServedMethod] = &[
379+ ServedMethod {
380+ method: AGENT_METHOD_NAMES.initialize,
381+ kind: ServedMethodKind::Initialize,
382+ authority: SurfaceAuthority::Observation,
383+ },
384+ ServedMethod {
385+ method: AGENT_METHOD_NAMES.session_new,
386+ kind: ServedMethodKind::NewSession,
387+ authority: SurfaceAuthority::Observation,
388+ },
389+ ServedMethod {
390+ method: AGENT_METHOD_NAMES.session_prompt,
391+ kind: ServedMethodKind::Prompt,
392+ authority: SurfaceAuthority::Observation,
393+ },
394+ ServedMethod {
395+ method: AGENT_METHOD_NAMES.session_cancel,
396+ kind: ServedMethodKind::Cancel,
397+ authority: SurfaceAuthority::Observation,
398+ },
399+];
400+
401+/// The entry for a method, or `None` if the surface does not expose it.
402+#[must_use]
403+pub fn served_method(method: &str) -> Option<&'static ServedMethod> {
404+ SERVED_SURFACE.iter().find(|entry| entry.method == method)
405+}
406+
407+/// One Omega control that grants authority or mutates run state, and is
408+/// deliberately not on the served surface.
409+#[derive(Debug, Clone, Copy, PartialEq, Eq)]
410+pub struct UnexposedControl {
411+ /// The control's own identifier: a `full_auto_ui` element id, or an
412+ /// `omega_front_door::PinGesture` token.
413+ pub control: &'static str,
414+ /// What an attached host attempting it is told.
415+ pub refusal: ServedRefusal,
416+}
417+
418+/// Every authority-bearing control in Omega, and the refusal a served host gets
419+/// for it.
420+///
421+/// This is the half of the partition that makes the served surface *inside* it
422+/// rather than beside it. `every_authority_bearing_control_is_classified`
423+/// checks this list against the two ledgers that already exist —
424+/// [`omega_front_door::FULL_AUTO_AFFORDANCES`] and
425+/// [`omega_front_door::PinGesture::all`] — in both directions, so adding a Full
426+/// Auto control or a pin gesture fails this crate until it is classified here,
427+/// and removing one fails until the stale row goes.
428+pub const UNEXPOSED_AUTHORITY: &[UnexposedControl] = &[
429+ UnexposedControl {
430+ control: "full-auto-panel",
431+ refusal: ServedRefusal::NotExposed,
432+ },
433+ UnexposedControl {
434+ control: "full-auto-openagents-connect",
435+ refusal: ServedRefusal::GrantsNoCredential,
436+ },
437+ UnexposedControl {
438+ control: "full-auto-openagents-disconnect",
439+ refusal: ServedRefusal::GrantsNoCredential,
440+ },
441+ UnexposedControl {
442+ control: "full-auto-provider-account",
443+ refusal: ServedRefusal::GrantsNoCredential,
444+ },
445+ UnexposedControl {
446+ control: "full-auto-advanced-toggle",
447+ refusal: ServedRefusal::NotExposed,
448+ },
449+ UnexposedControl {
450+ control: "full-auto-start",
451+ refusal: ServedRefusal::StartsNoRun,
452+ },
453+ UnexposedControl {
454+ control: "full-auto-cancel",
455+ refusal: ServedRefusal::NotExposed,
456+ },
457+ UnexposedControl {
458+ control: "full-auto-pause",
459+ refusal: ServedRefusal::MutatesNoRun,
460+ },
461+ UnexposedControl {
462+ control: "full-auto-resume",
463+ refusal: ServedRefusal::MutatesNoRun,
464+ },
465+ UnexposedControl {
466+ control: "full-auto-handoff",
467+ refusal: ServedRefusal::MutatesNoRun,
468+ },
469+ UnexposedControl {
470+ control: "full-auto-retry",
471+ refusal: ServedRefusal::MutatesNoRun,
472+ },
473+ UnexposedControl {
474+ control: "full-auto-stop",
475+ refusal: ServedRefusal::MutatesNoRun,
476+ },
477+ UnexposedControl {
478+ control: "full-auto-new",
479+ refusal: ServedRefusal::StartsNoRun,
480+ },
481+ UnexposedControl {
482+ control: "full-auto-evidence-chain",
483+ refusal: ServedRefusal::NotExposed,
484+ },
485+ UnexposedControl {
486+ control: "full-auto-monitor",
487+ refusal: ServedRefusal::NotExposed,
488+ },
489+ UnexposedControl {
490+ control: "full-auto-monitor-new",
491+ refusal: ServedRefusal::StartsNoRun,
492+ },
493+ UnexposedControl {
494+ control: "full-auto-run-row",
495+ refusal: ServedRefusal::NotExposed,
496+ },
497+ UnexposedControl {
498+ control: "executor_pin_menu_item",
499+ refusal: ServedRefusal::SetsNoPin,
500+ },
501+ UnexposedControl {
502+ control: "executor_pin_cleared",
503+ refusal: ServedRefusal::SetsNoPin,
504+ },
505+];
506+
507+// -------------------------------------------------------------------------
508+// The served session
509+// -------------------------------------------------------------------------
510+
511+/// The agent identity the served surface presents.
512+///
513+/// The same identity an in-app thread discloses, because it is the same agent.
514+/// `the_served_surface_presents_the_first_party_agent_id` in `omega_deltas`
515+/// checks this against `crates/agent`'s own `OMEGA_AGENT_ID`, so the two cannot
516+/// drift into a served surface that claims to be something else.
517+pub const SERVED_AGENT_ID: &str = "Omega Agent";
518+
519+/// The authentication methods the served surface offers.
520+///
521+/// Empty, and empty on purpose. There is no credential to present, so there is
522+/// no credential to steal or to mistake for authority. That is also why the
523+/// surface must be read-only: an unauthenticated surface that could act would
524+/// be a hole with no gate in front of it.
525+pub const AUTH_METHODS: &[&str] = &[];
526+
527+/// The routing inputs a served session is decided from.
528+///
529+/// The pin is `None` and there is no way to make it anything else from this
530+/// crate: setting a pin requires an `omega_front_door::PinGesture`, and no
531+/// variant of that enum is reachable over a socket. The engine is reported
532+/// unreachable because the served surface does not hold the supervisor's
533+/// answer — and it does not matter, which
534+/// `a_served_session_can_never_reach_an_engine_lane` proves by showing the
535+/// decision is identical for every engine state.
536+#[must_use]
537+pub fn served_inputs() -> RouteInputs {
538+ RouteInputs {
539+ pin: None,
540+ engine: EngineReadiness::Unreachable(EngineUnreachable::NotRunning),
541+ external_acp: None,
542+ engine_lane: None,
543+ }
544+}
545+
546+/// The route decision a served session gets.
547+///
548+/// The same [`omega_front_door::route`] law an in-app thread is decided by, so
549+/// an attached host gets Omega Agent's disclosed routing rather than a raw
550+/// provider.
551+#[must_use]
552+pub fn served_route() -> RouteDecision {
553+ route(&served_inputs())
554+}
555+
556+/// The disclosure a served session carries.
557+///
558+/// A typed record. The provider and model are `None` — *not disclosed*, which
559+/// is the honest answer, because the served surface dispatches to no executor
560+/// and so no provider served anything.
561+#[must_use]
562+pub fn served_disclosure(decision: &RouteDecision) -> ExecutorDisclosure {
563+ ExecutorDisclosure {
564+ class: decision.chosen,
565+ agent_id: SERVED_AGENT_ID.to_owned(),
566+ provider: None,
567+ model: None,
568+ run_ref: None,
569+ route: Some(decision.disclosed_route()),
570+ }
571+}
572+
573+/// One session an external host reached over the loopback server.
574+#[derive(Debug, Clone, PartialEq, Eq)]
575+pub struct ServedSession {
576+ /// The ACP session id this session was minted under.
577+ pub session_id: String,
578+ /// Where it was reached from. A separate record from the disclosure,
579+ /// because ingress and execution are different facts.
580+ pub origin: SessionOrigin,
581+ /// Where a turn on it would be routed.
582+ pub decision: RouteDecision,
583+ /// What it says about the executor. Typed; a label renders it.
584+ pub disclosure: ExecutorDisclosure,
585+}
586+
587+/// The `_meta` key the served records are published under.
588+pub const SERVED_SESSION_META_KEY: &str = "openagents.omega.served_session.v1";
589+
590+/// The disclosure record as it goes on the wire.
591+///
592+/// Keys are exactly `omega_front_door::EXECUTOR_DISCLOSURE_FIELDS`, and
593+/// `the_served_disclosure_is_a_record_not_a_label` asserts that exactly. A
594+/// rendered line on the wire under any name — `label`, `line`, `text`,
595+/// `summary` — fails, because the binding condition of the owner's identity
596+/// decision is that disclosure travels as a record a label renders.
597+#[must_use]
598+pub fn disclosure_meta(disclosure: &ExecutorDisclosure) -> Value {
599+ json!({
600+ "class": disclosure.class.token(),
601+ "agent_id": disclosure.agent_id,
602+ "provider": disclosure.provider,
603+ "model": disclosure.model,
604+ "run_ref": disclosure.run_ref,
605+ "route": disclosure.route.map(|route| route.token()),
606+ })
607+}
608+
609+/// The origin record as it goes on the wire. Keys exactly
610+/// `omega_front_door::SESSION_ORIGIN_FIELDS`.
611+#[must_use]
612+pub fn origin_meta(origin: &SessionOrigin) -> Value {
613+ json!({
614+ "ingress": origin.ingress.token(),
615+ "host_name": origin.host_name,
616+ "host_version": origin.host_version,
617+ "authenticated": origin.authenticated,
618+ })
619+}
620+
621+/// The `_meta` block a served **turn** publishes.
622+///
623+/// The session's records, plus the typed refusal saying the turn reached no
624+/// executor. The refusal lives here rather than in the stop reason on purpose:
625+/// ACP's `refusal` stop reason means *the prompt and everything after it will
626+/// not be included in the next prompt*, and stock Zed implements that by
627+/// dropping the turn from the thread — which took the disclosure with it, so
628+/// the operator saw a refusal banner and no disclosure at all. That was watched
629+/// happening against Zed 1.12.0 before this shape was chosen. The turn genuinely
630+/// ended, so it says `end_turn`, and what did **not** happen is stated in the
631+/// message the operator reads and in the record beside it.
632+#[must_use]
633+pub fn served_turn_meta(session: &ServedSession) -> Value {
634+ let mut meta = served_meta(session);
635+ if let Some(block) = meta
636+ .get_mut(SERVED_SESSION_META_KEY)
637+ .and_then(Value::as_object_mut)
638+ {
639+ block.insert(
640+ "refusal".to_owned(),
641+ Value::String(ServedRefusal::ExecutesNothing.token().to_owned()),
642+ );
643+ }
644+ meta
645+}
646+
647+/// The `_meta` block a served session publishes.
648+#[must_use]
649+pub fn served_meta(session: &ServedSession) -> Value {
650+ json!({
651+ SERVED_SESSION_META_KEY: {
652+ "disclosure": disclosure_meta(&session.disclosure),
653+ "origin": origin_meta(&session.origin),
654+ "readOnly": true,
655+ "refusals": ServedRefusal::all()
656+ .iter()
657+ .map(|refusal| refusal.token())
658+ .collect::<Vec<_>>(),
659+ }
660+ })
661+}
662+
663+/// What a served turn says back, rendered from the typed records.
664+///
665+/// Derived on every call. Nothing stores it, which is the same discipline
666+/// `ExecutorDisclosure::label` is held to — the wire carries the record in
667+/// `_meta` and this rendering in the message the operator reads.
668+#[must_use]
669+pub fn served_turn_text(session: &ServedSession) -> String {
670+ format!(
671+ "Omega Agent, served over ACP.\n\n\
672+ executor: {}\n\
673+ origin: {}\n\n\
674+ {}",
675+ session.disclosure.label(),
676+ session.origin.label(),
677+ ServedRefusal::ExecutesNothing.message(),
678+ )
679+}
680+
681+// -------------------------------------------------------------------------
682+// The connection
683+// -------------------------------------------------------------------------
684+
685+/// The JSON-RPC error code for a method the surface does not expose.
686+const METHOD_NOT_FOUND: i64 = -32601;
687+
688+/// The JSON-RPC error code for a request this surface cannot read.
689+const INVALID_REQUEST: i64 = -32600;
690+
691+/// The ACP protocol version this surface speaks.
692+const PROTOCOL_VERSION: u16 = 1;
693+
694+/// One attached host.
695+///
696+/// Holds the sessions it minted and what it said about itself. It holds no run
697+/// state, no policy state, and no credential, because there is none to hold.
698+#[derive(Debug)]
699+pub struct ServedConnection {
700+ /// A stable label for this connection, used to mint session ids without a
701+ /// clock or a random source.
702+ connection_ref: String,
703+ /// Sessions minted on this connection, in mint order.
704+ sessions: Vec<ServedSession>,
705+ /// What the host said about itself at `initialize`.
706+ host_name: Option<String>,
707+ /// The host's version, where it gave one.
708+ host_version: Option<String>,
709+}
710+
711+impl ServedConnection {
712+ /// A connection with nothing on it yet.
713+ #[must_use]
714+ pub fn new(connection_ref: impl Into<String>) -> Self {
715+ Self {
716+ connection_ref: connection_ref.into(),
717+ sessions: Vec::new(),
718+ host_name: None,
719+ host_version: None,
720+ }
721+ }
722+
723+ /// The sessions minted on this connection.
724+ #[must_use]
725+ pub fn sessions(&self) -> &[ServedSession] {
726+ &self.sessions
727+ }
728+
729+ /// One session by id.
730+ #[must_use]
731+ pub fn session(&self, session_id: &str) -> Option<&ServedSession> {
732+ self.sessions
733+ .iter()
734+ .find(|session| session.session_id == session_id)
735+ }
736+
737+ /// The origin every session on this connection carries.
738+ #[must_use]
739+ pub fn origin(&self) -> SessionOrigin {
740+ SessionOrigin::loopback_acp(self.host_name.clone(), self.host_version.clone())
741+ }
742+
743+ /// Handle one newline-framed JSON-RPC message, and return the lines to
744+ /// write back in order.
745+ ///
746+ /// Notifications the surface emits come before the response they belong to,
747+ /// which is the order ACP requires for a prompt turn.
748+ pub fn handle_line(&mut self, line: &str) -> Vec<String> {
749+ let Ok(message) = serde_json::from_str::<Value>(line) else {
750+ return vec![error_line(
751+ &Value::Null,
752+ INVALID_REQUEST,
753+ "The Omega ACP server reads newline-framed JSON-RPC.",
754+ ServedRefusal::NotExposed,
755+ )];
756+ };
757+ let id = message.get("id").cloned();
758+ let Some(method) = message.get("method").and_then(Value::as_str) else {
759+ // A response to a request. This surface sends none, so there is
760+ // nothing this can be an answer to and nothing to say back.
761+ return Vec::new();
762+ };
763+ let params = message.get("params").cloned().unwrap_or(Value::Null);
764+
765+ let Some(entry) = served_method(method) else {
766+ log::info!(
767+ "OMEGA-DELTA-0041: served surface refused {method}: {}",
768+ ServedRefusal::NotExposed.token()
769+ );
770+ return match id {
771+ Some(id) => vec![error_line(
772+ &id,
773+ METHOD_NOT_FOUND,
774+ ServedRefusal::NotExposed.message(),
775+ ServedRefusal::NotExposed,
776+ )],
777+ // A notification gets no answer, by JSON-RPC. It is still
778+ // refused: nothing happened.
779+ None => Vec::new(),
780+ };
781+ };
782+
783+ match entry.kind {
784+ ServedMethodKind::Initialize => {
785+ self.remember_host(¶ms);
786+ id.map(|id| vec![result_line(&id, self.initialize_result())])
787+ .unwrap_or_default()
788+ }
789+ ServedMethodKind::NewSession => {
790+ let session = self.mint_session();
791+ let result = json!({
792+ "sessionId": session.session_id,
793+ "_meta": served_meta(&session),
794+ });
795+ self.sessions.push(session);
796+ id.map(|id| vec![result_line(&id, result)]).unwrap_or_default()
797+ }
798+ ServedMethodKind::Prompt => {
799+ let session_id = params
800+ .get("sessionId")
801+ .and_then(Value::as_str)
802+ .unwrap_or_default()
803+ .to_owned();
804+ let Some(session) = self.session(&session_id).cloned() else {
805+ return match id {
806+ Some(id) => vec![error_line(
807+ &id,
808+ INVALID_REQUEST,
809+ "No such session on this connection.",
810+ ServedRefusal::NotExposed,
811+ )],
812+ None => Vec::new(),
813+ };
814+ };
815+ let mut lines = vec![notification_line(
816+ CLIENT_METHOD_NAMES.session_update,
817+ json!({
818+ "sessionId": session.session_id,
819+ "update": {
820+ "sessionUpdate": "agent_message_chunk",
821+ "content": { "type": "text", "text": served_turn_text(&session) },
822+ },
823+ "_meta": served_turn_meta(&session),
824+ }),
825+ )];
826+ if let Some(id) = id {
827+ lines.push(result_line(
828+ &id,
829+ json!({
830+ // The turn ended, having disclosed. Not ACP's
831+ // `refusal`, which means the turn is dropped from
832+ // the thread — stock Zed implements that literally
833+ // and the disclosure went with it. What did not
834+ // happen is in the message and in `_meta`.
835+ "stopReason": "end_turn",
836+ "_meta": served_turn_meta(&session),
837+ }),
838+ ));
839+ }
840+ lines
841+ }
842+ // Nothing is ever in flight, because nothing is ever dispatched.
843+ // A cancel is therefore already satisfied, and a notification gets
844+ // no answer.
845+ ServedMethodKind::Cancel => Vec::new(),
846+ }
847+ }
848+
849+ fn remember_host(&mut self, params: &Value) {
850+ let info = params.get("clientInfo");
851+ self.host_name = info
852+ .and_then(|info| info.get("name"))
853+ .and_then(Value::as_str)
854+ .filter(|name| !name.is_empty())
855+ .map(str::to_owned);
856+ self.host_version = info
857+ .and_then(|info| info.get("version"))
858+ .and_then(Value::as_str)
859+ .filter(|version| !version.is_empty())
860+ .map(str::to_owned);
861+ }
862+
863+ fn initialize_result(&self) -> Value {
864+ json!({
865+ "protocolVersion": PROTOCOL_VERSION,
866+ "agentCapabilities": {
867+ "loadSession": false,
868+ "promptCapabilities": {
869+ "image": false,
870+ "audio": false,
871+ "embeddedContext": false,
872+ },
873+ },
874+ "authMethods": AUTH_METHODS,
875+ "agentInfo": { "name": SERVED_AGENT_ID, "version": env!("CARGO_PKG_VERSION") },
876+ })
877+ }
878+
879+ fn mint_session(&self) -> ServedSession {
880+ let decision = served_route();
881+ // No clock and no random source: the id is the connection's own label
882+ // and a count. Two identical runs mint identical ids, which is the same
883+ // discipline the route journal is held to.
884+ let session_id = format!("omega-served-{}-{}", self.connection_ref, self.sessions.len());
885+ ServedSession {
886+ session_id,
887+ origin: self.origin(),
888+ disclosure: served_disclosure(&decision),
889+ decision,
890+ }
891+ }
892+}
893+
894+fn result_line(id: &Value, result: Value) -> String {
895+ json!({ "jsonrpc": "2.0", "id": id, "result": result }).to_string()
896+}
897+
898+fn notification_line(method: &str, params: Value) -> String {
899+ json!({ "jsonrpc": "2.0", "method": method, "params": params }).to_string()
900+}
901+
902+fn error_line(id: &Value, code: i64, message: &str, refusal: ServedRefusal) -> String {
903+ json!({
904+ "jsonrpc": "2.0",
905+ "id": id,
906+ "error": {
907+ "code": code,
908+ "message": message,
909+ "data": { "refusal": refusal.token() },
910+ },
911+ })
912+ .to_string()
913+}
914+
915+// -------------------------------------------------------------------------
916+// The listener
917+// -------------------------------------------------------------------------
918+
919+/// A bound loopback ACP listener.
920+#[derive(Debug)]
921+pub struct LoopbackAcpServer {
922+ listener: TcpListener,
923+}
924+
925+impl LoopbackAcpServer {
926+ /// Bind the listener.
927+ ///
928+ /// Takes a [`LoopbackHost`], so there is no way to ask this for a routable
929+ /// interface.
930+ ///
931+ /// # Errors
932+ ///
933+ /// The underlying bind error.
934+ pub fn bind(host: LoopbackHost, port: u16) -> std::io::Result<Self> {
935+ let listener = TcpListener::bind(SocketAddr::new(host.address(), port))?;
936+ Ok(Self { listener })
937+ }
938+
939+ /// Where it is listening.
940+ ///
941+ /// # Errors
942+ ///
943+ /// The underlying socket error.
944+ pub fn local_addr(&self) -> std::io::Result<SocketAddr> {
945+ self.listener.local_addr()
946+ }
947+
948+ /// Serve connections until the listener fails.
949+ ///
950+ /// One thread per attached host. There is no shared mutable state between
951+ /// connections, because a served session holds nothing an operator could
952+ /// change.
953+ pub fn serve(self) {
954+ let mut accepted = 0usize;
955+ loop {
956+ match self.listener.accept() {
957+ Ok((stream, peer)) => {
958+ let connection_ref = format!("{accepted}");
959+ accepted += 1;
960+ log::info!("OMEGA-DELTA-0041: served ACP connection from {peer}");
961+ std::thread::spawn(move || serve_stream(stream, &connection_ref));
962+ }
963+ Err(error) => {
964+ log::error!("OMEGA-DELTA-0041: the served ACP listener stopped: {error:#}");
965+ return;
966+ }
967+ }
968+ }
969+ }
970+}
971+
972+/// Serve one attached host over an already-accepted stream.
973+pub fn serve_stream(stream: TcpStream, connection_ref: &str) {
974+ let Ok(write_half) = stream.try_clone() else {
975+ return;
976+ };
977+ let mut connection = ServedConnection::new(connection_ref);
978+ let reader = BufReader::new(stream);
979+ let mut writer = write_half;
980+ for line in reader.lines() {
981+ let Ok(line) = line else { return };
982+ if line.trim().is_empty() {
983+ continue;
984+ }
985+ for outgoing in connection.handle_line(&line) {
986+ if writeln!(writer, "{outgoing}").is_err() || writer.flush().is_err() {
987+ return;
988+ }
989+ }
990+ }
991+}
992+
993+/// What [`start_if_enabled`] did.
994+#[derive(Debug)]
995+pub enum StartOutcome {
996+ /// Nothing was bound. The shipped default.
997+ NotStarted(OffReason),
998+ /// A listener is bound at this address and is serving on its own thread.
999+ Listening(SocketAddr),
1000+ /// The flag asked for a listener and one could not be opened.
1001+ Failed(String),
1002+}
1003+
1004+/// Start the server if, and only if, the flag says exactly `1`.
1005+///
1006+/// The only production call site is `crates/omega_effectd`, which is the
1007+/// supervisor layer. GPUI never calls this and `omega_deltas` fails if it
1008+/// starts.
1009+pub fn start_if_enabled() -> StartOutcome {
1010+ let flag = std::env::var(ENABLE_FLAG).ok();
1011+ match enablement(flag.as_deref()) {
1012+ Enablement::Off(reason) => StartOutcome::NotStarted(reason),
1013+ Enablement::On => {
1014+ let port = std::env::var(PORT_FLAG)
1015+ .ok()
1016+ .and_then(|port| port.parse::<u16>().ok())
1017+ .unwrap_or(0);
1018+ match LoopbackAcpServer::bind(LoopbackHost::DEFAULT, port) {
1019+ Ok(server) => match server.local_addr() {
1020+ Ok(address) => {
1021+ log::info!(
1022+ "OMEGA-DELTA-0041: Omega Agent is served over ACP on {address} \
1023+ (loopback, unauthenticated, read-only)"
1024+ );
1025+ std::thread::spawn(move || server.serve());
1026+ StartOutcome::Listening(address)
1027+ }
1028+ Err(error) => StartOutcome::Failed(error.to_string()),
1029+ },
1030+ Err(error) => StartOutcome::Failed(error.to_string()),
1031+ }
1032+ }
1033+ }
1034+}
1035+
1036+#[cfg(test)]
1037+mod tests {
1038+ use super::*;
1039+ use agent_client_protocol::schema::v1::{
1040+ InitializeResponse, NewSessionResponse, PromptResponse, SessionNotification, StopReason,
1041+ };
1042+ use omega_front_door::{
1043+ EXECUTOR_DISCLOSURE_FIELDS, EngineLane, ExecutorClass, FULL_AUTO_AFFORDANCES, Ingress,
1044+ LaneState, PinGesture, RouteReason, SESSION_ORIGIN_FIELDS,
1045+ };
1046+ use std::collections::BTreeSet;
1047+ use std::io::{BufRead, BufReader, Write};
1048+ use std::net::TcpStream;
1049+
1050+ fn initialized(host: &str) -> ServedConnection {
1051+ let mut connection = ServedConnection::new("test");
1052+ connection.handle_line(
1053+ &json!({
1054+ "jsonrpc": "2.0", "id": 0, "method": "initialize",
1055+ "params": {
1056+ "protocolVersion": 1,
1057+ "clientInfo": { "name": host, "version": "1.12.0" },
1058+ },
1059+ })
1060+ .to_string(),
1061+ );
1062+ connection
1063+ }
1064+
1065+ fn new_session(connection: &mut ServedConnection) -> Value {
1066+ let lines = connection.handle_line(
1067+ &json!({
1068+ "jsonrpc": "2.0", "id": 1, "method": "session/new",
1069+ "params": { "cwd": "/tmp", "mcpServers": [] },
1070+ })
1071+ .to_string(),
1072+ );
1073+ assert_eq!(lines.len(), 1);
1074+ serde_json::from_str::<Value>(&lines[0]).expect("a JSON line")
1075+ }
1076+
1077+ /// The shipped default is off, and the flag is exact.
1078+ ///
1079+ /// A listener that is on by default is a different product. This is the
1080+ /// check that says which product this is.
1081+ #[test]
1082+ fn the_server_is_off_unless_the_flag_says_exactly_one() {
1083+ assert_eq!(enablement(None), Enablement::Off(OffReason::FlagUnset));
1084+ assert!(!enablement(None).is_on());
1085+
1086+ for truthy_looking in ["true", "TRUE", "yes", "on", "0", "01", " 1", "1 ", "", "11"] {
1087+ assert_eq!(
1088+ enablement(Some(truthy_looking)),
1089+ Enablement::Off(OffReason::FlagNotExactlyOne),
1090+ "{truthy_looking:?} turned the loopback ACP server on. The \
1091+ flag is an exact match on {ENABLE_VALUE:?} so that the \
1092+ default can be stated rather than guessed."
1093+ );
1094+ }
1095+
1096+ assert_eq!(enablement(Some(ENABLE_VALUE)), Enablement::On);
1097+ }
1098+
1099+ /// A routable bind address is refused by construction.
1100+ #[test]
1101+ fn a_routable_bind_address_is_refused_by_construction() {
1102+ for routable in ["0.0.0.0", "192.168.1.10", "10.0.0.1", "::", "8.8.8.8"] {
1103+ let refused = LoopbackHost::new(routable).expect_err(
1104+ "the Omega ACP server is unauthenticated and must never bind a \
1105+ routable interface",
1106+ );
1107+ assert_eq!(refused.token(), "not_loopback");
1108+ }
1109+
1110+ // A name is refused rather than resolved: resolution is the step where
1111+ // `localhost` can be pointed somewhere else.
1112+ assert_eq!(
1113+ LoopbackHost::new("localhost").expect_err("names are refused").token(),
1114+ "not_an_address"
1115+ );
1116+
1117+ assert_eq!(
1118+ LoopbackHost::new("127.0.0.1").expect("loopback").address(),
1119+ IpAddr::V4(Ipv4Addr::LOCALHOST)
1120+ );
1121+ assert_eq!(
1122+ LoopbackHost::new("::1").expect("loopback").address(),
1123+ IpAddr::V6(Ipv6Addr::LOCALHOST)
1124+ );
1125+ assert_eq!(LoopbackHost::DEFAULT.address(), IpAddr::V4(Ipv4Addr::LOCALHOST));
1126+ }
1127+
1128+ /// Every method the surface exposes is observation, and the surface is
1129+ /// exactly these four.
1130+ #[test]
1131+ fn the_whole_served_surface_is_observation() {
1132+ let methods: Vec<&str> = SERVED_SURFACE.iter().map(|entry| entry.method).collect();
1133+ assert_eq!(
1134+ methods,
1135+ ["initialize", "session/new", "session/prompt", "session/cancel"],
1136+ "the served surface grew or lost a method. It is deny-by-default, \
1137+ so every addition is a deliberate edit to this list — and an \
1138+ addition that can mutate run state or grant authority is owner \
1139+ gate 8 broken through a socket."
1140+ );
1141+ for entry in SERVED_SURFACE {
1142+ assert_eq!(
1143+ entry.authority,
1144+ SurfaceAuthority::Observation,
1145+ "{} is on an unauthenticated surface and is not observation",
1146+ entry.method
1147+ );
1148+ }
1149+ }
1150+
1151+ /// The served surface is inside the authority partition, not beside it.
1152+ ///
1153+ /// Checked against the ledgers that already exist, in both directions, so a
1154+ /// new Full Auto control or a new pin gesture fails here until somebody
1155+ /// says what a served host attempting it is told.
1156+ #[test]
1157+ fn every_authority_bearing_control_is_classified() {
1158+ let classified: BTreeSet<&str> = UNEXPOSED_AUTHORITY
1159+ .iter()
1160+ .map(|control| control.control)
1161+ .collect();
1162+ let mut required: BTreeSet<&str> = FULL_AUTO_AFFORDANCES
1163+ .iter()
1164+ .map(|affordance| affordance.element_id)
1165+ .collect();
1166+ required.extend(PinGesture::all().iter().map(|gesture| gesture.token()));
1167+
1168+ assert!(
1169+ !required.is_empty(),
1170+ "the ledgers reached nothing; this partition check would be vacuous"
1171+ );
1172+ let unclassified: Vec<&&str> = required.difference(&classified).collect();
1173+ assert!(
1174+ unclassified.is_empty(),
1175+ "authority-bearing controls with no entry in UNEXPOSED_AUTHORITY: \
1176+ {unclassified:?}. The loopback ACP surface is an unauthenticated \
1177+ model-driven surface and must sit *inside* the authority \
1178+ partition. A control nobody classified is a fourth hole."
1179+ );
1180+ let stale: Vec<&&str> = classified.difference(&required).collect();
1181+ assert!(
1182+ stale.is_empty(),
1183+ "UNEXPOSED_AUTHORITY names controls that no longer exist: \
1184+ {stale:?}. A partition that outlives its source stops being \
1185+ evidence."
1186+ );
1187+
1188+ // Nothing that grants authority may be answerable over the socket.
1189+ let served: BTreeSet<&str> = SERVED_SURFACE.iter().map(|entry| entry.method).collect();
1190+ for control in UNEXPOSED_AUTHORITY {
1191+ assert!(
1192+ !served.contains(control.control),
1193+ "{} is both classified as unexposed and served",
1194+ control.control
1195+ );
1196+ }
1197+ }
1198+
1199+ /// A served session is routed by the router, and never reaches an engine
1200+ /// lane — whatever the engine says.
1201+ ///
1202+ /// This is owner gate 8 at the socket. A pin is the only door to an engine
1203+ /// lane, setting one requires a `PinGesture`, and no variant of that enum
1204+ /// is reachable from a socket. The consequence is checked against every
1205+ /// engine state the router can be shown, so "at capacity" or "no lanes" is
1206+ /// not what is doing the work.
1207+ #[test]
1208+ fn a_served_session_can_never_reach_an_engine_lane() {
1209+ let decision = served_route();
1210+ assert_eq!(decision.chosen, ExecutorClass::NativeLoop);
1211+ assert_eq!(decision.reason, RouteReason::UnpinnedDefault);
1212+ assert!(decision.is_coherent());
1213+
1214+ for engine in [
1215+ EngineReadiness::Unreachable(EngineUnreachable::NotRunning),
1216+ EngineReadiness::Unreachable(EngineUnreachable::Timeout),
1217+ EngineReadiness::Unreachable(EngineUnreachable::ProtocolError),
1218+ EngineReadiness::Answered {
1219+ active_run_count: 0,
1220+ active_run_limit: 8,
1221+ lanes: vec![
1222+ EngineLane::new("claude-local", LaneState::Available),
1223+ EngineLane::new("codex-local", LaneState::Available),
1224+ ],
1225+ },
1226+ ] {
1227+ let inputs = RouteInputs {
1228+ engine,
1229+ external_acp: Some("codex-acp".to_owned()),
1230+ engine_lane: Some("codex-local".to_owned()),
1231+ ..served_inputs()
1232+ };
1233+ let decided = route(&inputs);
1234+ assert_eq!(
1235+ decided.chosen,
1236+ ExecutorClass::NativeLoop,
1237+ "a served session reached an executor other than the native \
1238+ loop. Nothing over the socket may take a pin, and a pin is \
1239+ the only door to Full Auto authority."
1240+ );
1241+ assert!(decided.lane_ref.is_none());
1242+ }
1243+ }
1244+
1245+ /// The disclosure crosses the wire as a record with exactly the fields the
1246+ /// record has, and the rendered line is derived beside it.
1247+ #[test]
1248+ fn the_served_disclosure_is_a_record_not_a_label() {
1249+ let mut connection = initialized("Zed");
1250+ let response = new_session(&mut connection);
1251+ let meta = &response["result"]["_meta"][SERVED_SESSION_META_KEY];
1252+
1253+ let disclosure = meta["disclosure"]
1254+ .as_object()
1255+ .expect("the disclosure travels as an object");
1256+ let mut keys: Vec<&str> = disclosure.keys().map(String::as_str).collect();
1257+ keys.sort_unstable();
1258+ let mut expected: Vec<&str> = EXECUTOR_DISCLOSURE_FIELDS.to_vec();
1259+ expected.sort_unstable();
1260+ assert_eq!(
1261+ keys, expected,
1262+ "the disclosure on the wire must carry exactly the record's \
1263+ fields. A rendered line under any name — label, line, text, \
1264+ summary — breaks the condition the owner admitted the identity \
1265+ decision on, and an exact key set is what catches the names \
1266+ nobody anticipated."
1267+ );
1268+
1269+ let origin = meta["origin"].as_object().expect("the origin travels as an object");
1270+ let mut origin_keys: Vec<&str> = origin.keys().map(String::as_str).collect();
1271+ origin_keys.sort_unstable();
1272+ let mut expected_origin: Vec<&str> = SESSION_ORIGIN_FIELDS.to_vec();
1273+ expected_origin.sort_unstable();
1274+ assert_eq!(origin_keys, expected_origin);
1275+
1276+ assert_eq!(disclosure["class"], "native_loop");
1277+ assert_eq!(disclosure["agent_id"], SERVED_AGENT_ID);
1278+ assert!(disclosure["provider"].is_null());
1279+ assert!(disclosure["model"].is_null());
1280+ assert_eq!(disclosure["route"], "unpinned_default");
1281+ assert_eq!(origin["ingress"], "loopback_acp");
1282+ assert_eq!(origin["host_name"], "Zed");
1283+ assert_eq!(origin["authenticated"], false);
1284+ }
1285+
1286+ /// A served prompt is answered by disclosing, and dispatched to nothing.
1287+ #[test]
1288+ fn a_served_prompt_executes_nothing_and_says_so() {
1289+ let mut connection = initialized("Zed");
1290+ let session = new_session(&mut connection);
1291+ let session_id = session["result"]["sessionId"].as_str().expect("a session id");
1292+
1293+ let lines = connection.handle_line(
1294+ &json!({
1295+ "jsonrpc": "2.0", "id": 2, "method": "session/prompt",
1296+ "params": {
1297+ "sessionId": session_id,
1298+ "prompt": [{ "type": "text", "text": "run the build and push it" }],
1299+ },
1300+ })
1301+ .to_string(),
1302+ );
1303+ assert_eq!(lines.len(), 2, "one update then one response");
1304+
1305+ let update: Value = serde_json::from_str(&lines[0]).expect("JSON");
1306+ assert_eq!(update["method"], "session/update");
1307+ let text = update["params"]["update"]["content"]["text"]
1308+ .as_str()
1309+ .expect("the disclosure is rendered for the operator");
1310+ assert!(text.contains("native_loop · Omega Agent"), "{text}");
1311+ assert!(text.contains("routed: unpinned"), "{text}");
1312+ assert!(text.contains("loopback_acp · Zed 1.12.0 · unauthenticated"), "{text}");
1313+ assert!(text.contains("dispatched to no executor"), "{text}");
1314+
1315+ let response: Value = serde_json::from_str(&lines[1]).expect("JSON");
1316+ assert_eq!(
1317+ response["result"]["stopReason"], "end_turn",
1318+ "the turn ended having disclosed. ACP's `refusal` stop reason \
1319+ means the turn is dropped from the thread, and stock Zed 1.12.0 \
1320+ implements that literally — the operator saw a refusal banner \
1321+ and no disclosure at all, which is the exact failure this packet \
1322+ exists to avoid."
1323+ );
1324+ assert_eq!(
1325+ response["result"]["_meta"][SERVED_SESSION_META_KEY]["refusal"],
1326+ ServedRefusal::ExecutesNothing.token(),
1327+ "the turn ended without executing and the record must say so, \
1328+ since the stop reason no longer can"
1329+ );
1330+ assert_eq!(
1331+ update["params"]["_meta"][SERVED_SESSION_META_KEY]["refusal"],
1332+ ServedRefusal::ExecutesNothing.token()
1333+ );
1334+ }
1335+
1336+ /// Deny-by-default. Everything the surface does not list is refused, and
1337+ /// the refusal names which one it is.
1338+ #[test]
1339+ fn every_unexposed_method_is_watched_refusing() {
1340+ let mut connection = initialized("Zed");
1341+ // `session/new` first, so a refusal cannot be mistaken for "there was
1342+ // no session anyway".
1343+ let session = new_session(&mut connection);
1344+ let session_id = session["result"]["sessionId"]
1345+ .as_str()
1346+ .expect("a session id")
1347+ .to_owned();
1348+
1349+ for (index, (method, params)) in [
1350+ ("session/load", json!({ "sessionId": session_id, "cwd": "/tmp" })),
1351+ ("session/resume", json!({ "sessionId": session_id })),
1352+ ("session/delete", json!({ "sessionId": session_id })),
1353+ ("session/set_mode", json!({ "sessionId": session_id, "modeId": "yolo" })),
1354+ (
1355+ "session/set_config_option",
1356+ json!({ "sessionId": session_id, "configId": "a", "value": true }),
1357+ ),
1358+ ("authenticate", json!({ "methodId": "anything" })),
1359+ ("logout", json!({})),
1360+ ("fs/write_text_file", json!({ "path": "/tmp/x", "content": "x" })),
1361+ ("terminal/create", json!({ "command": "sh" })),
1362+ ("full_auto/start", json!({ "objective": "ship it" })),
1363+ ("omega/pin_executor", json!({ "class": "engine_lane" })),
1364+ ]
1365+ .into_iter()
1366+ .enumerate()
1367+ {
1368+ let lines = connection.handle_line(
1369+ &json!({ "jsonrpc": "2.0", "id": 100 + index, "method": method, "params": params })
1370+ .to_string(),
1371+ );
1372+ assert_eq!(lines.len(), 1, "{method} answered something other than one refusal");
1373+ let answer: Value = serde_json::from_str(&lines[0]).expect("JSON");
1374+ assert!(
1375+ answer.get("result").is_none(),
1376+ "{method} was answered with a result: {answer}"
1377+ );
1378+ assert_eq!(answer["error"]["code"], METHOD_NOT_FOUND, "{method}");
1379+ assert_eq!(
1380+ answer["error"]["data"]["refusal"],
1381+ ServedRefusal::NotExposed.token(),
1382+ "{method} was refused without naming which refusal it was"
1383+ );
1384+ }
1385+
1386+ // Nothing the attached host attempted minted a session, changed one, or
1387+ // left any trace at all.
1388+ assert_eq!(connection.sessions().len(), 1);
1389+ assert_eq!(connection.sessions()[0].decision.chosen, ExecutorClass::NativeLoop);
1390+ }
1391+
1392+ /// Every refusal has a distinct token and a message that says what did not
1393+ /// happen.
1394+ #[test]
1395+ fn every_refusal_states_what_did_not_happen() {
1396+ let tokens: BTreeSet<&str> = ServedRefusal::all()
1397+ .iter()
1398+ .map(|refusal| refusal.token())
1399+ .collect();
1400+ assert_eq!(tokens.len(), ServedRefusal::all().len());
1401+ for refusal in ServedRefusal::all() {
1402+ assert!(
1403+ refusal.message().len() > 60,
1404+ "{} is stated in a phrase, which is not stated",
1405+ refusal.token()
1406+ );
1407+ }
1408+ }
1409+
1410+ /// The surface offers no credential to present.
1411+ #[test]
1412+ fn the_served_surface_is_unauthenticated_and_says_so() {
1413+ let mut connection = ServedConnection::new("test");
1414+ let lines = connection.handle_line(
1415+ &json!({
1416+ "jsonrpc": "2.0", "id": 0, "method": "initialize",
1417+ "params": { "protocolVersion": 1 },
1418+ })
1419+ .to_string(),
1420+ );
1421+ let answer: Value = serde_json::from_str(&lines[0]).expect("JSON");
1422+ assert_eq!(answer["result"]["authMethods"], json!([]));
1423+ assert_eq!(answer["result"]["agentCapabilities"]["loadSession"], false);
1424+ assert!(AUTH_METHODS.is_empty());
1425+
1426+ // A host that said nothing about itself is disclosed as having said
1427+ // nothing, not as anonymous-but-fine.
1428+ let session = new_session(&mut connection);
1429+ let origin = &session["result"]["_meta"][SERVED_SESSION_META_KEY]["origin"];
1430+ assert!(origin["host_name"].is_null());
1431+ assert_eq!(origin["authenticated"], false);
1432+ }
1433+
1434+ /// Every message this surface writes reads back as the pinned ACP schema
1435+ /// type it claims to be.
1436+ ///
1437+ /// The conformance link. The wire is built as JSON so the *exact* key set
1438+ /// can be asserted; this is what stops that freedom from drifting away from
1439+ /// the protocol Zed actually speaks.
1440+ #[test]
1441+ fn every_answer_deserialises_as_its_pinned_schema_type() {
1442+ let mut connection = initialized("Zed");
1443+
1444+ let initialize = connection.handle_line(
1445+ &json!({ "jsonrpc": "2.0", "id": 0, "method": "initialize", "params": {} }).to_string(),
1446+ );
1447+ let initialize: Value = serde_json::from_str(&initialize[0]).expect("JSON");
1448+ let parsed: InitializeResponse =
1449+ serde_json::from_value(initialize["result"].clone()).expect("an InitializeResponse");
1450+ assert!(parsed.auth_methods.is_empty());
1451+
1452+ let session = new_session(&mut connection);
1453+ let parsed: NewSessionResponse =
1454+ serde_json::from_value(session["result"].clone()).expect("a NewSessionResponse");
1455+ let session_id = parsed.session_id.0.to_string();
1456+
1457+ let lines = connection.handle_line(
1458+ &json!({
1459+ "jsonrpc": "2.0", "id": 2, "method": "session/prompt",
1460+ "params": { "sessionId": session_id, "prompt": [{ "type": "text", "text": "hi" }] },
1461+ })
1462+ .to_string(),
1463+ );
1464+ let update: Value = serde_json::from_str(&lines[0]).expect("JSON");
1465+ let _: SessionNotification =
1466+ serde_json::from_value(update["params"].clone()).expect("a SessionNotification");
1467+ let response: Value = serde_json::from_str(&lines[1]).expect("JSON");
1468+ let parsed: PromptResponse =
1469+ serde_json::from_value(response["result"].clone()).expect("a PromptResponse");
1470+ assert_eq!(parsed.stop_reason, StopReason::EndTurn);
1471+ }
1472+
1473+ /// The whole thing over a real loopback socket, end to end.
1474+ #[test]
1475+ fn a_real_client_attaches_over_loopback_and_is_disclosed_to() {
1476+ let server = LoopbackAcpServer::bind(LoopbackHost::DEFAULT, 0).expect("binds loopback");
1477+ let address = server.local_addr().expect("an address");
1478+ assert!(address.ip().is_loopback());
1479+ std::thread::spawn(move || server.serve());
1480+
1481+ let stream = TcpStream::connect(address).expect("connects");
1482+ let mut writer = stream.try_clone().expect("a write half");
1483+ let mut reader = BufReader::new(stream);
1484+ let send = |message: Value, writer: &mut TcpStream| {
1485+ writeln!(writer, "{message}").expect("writes");
1486+ writer.flush().expect("flushes");
1487+ };
1488+ let read = |reader: &mut BufReader<TcpStream>| {
1489+ let mut line = String::new();
1490+ reader.read_line(&mut line).expect("reads");
1491+ serde_json::from_str::<Value>(&line).expect("JSON")
1492+ };
1493+
1494+ send(
1495+ json!({
1496+ "jsonrpc": "2.0", "id": 0, "method": "initialize",
1497+ "params": {
1498+ "protocolVersion": 1,
1499+ "clientInfo": { "name": "conformance-client", "version": "0.1.0" },
1500+ },
1501+ }),
1502+ &mut writer,
1503+ );
1504+ assert_eq!(read(&mut reader)["result"]["authMethods"], json!([]));
1505+
1506+ send(
1507+ json!({ "jsonrpc": "2.0", "id": 1, "method": "session/new", "params": { "cwd": "/tmp" } }),
1508+ &mut writer,
1509+ );
1510+ let session = read(&mut reader);
1511+ let session_id = session["result"]["sessionId"].as_str().expect("id").to_owned();
1512+ assert_eq!(
1513+ session["result"]["_meta"][SERVED_SESSION_META_KEY]["origin"]["ingress"],
1514+ "loopback_acp"
1515+ );
1516+
1517+ send(
1518+ json!({
1519+ "jsonrpc": "2.0", "id": 2, "method": "session/prompt",
1520+ "params": { "sessionId": session_id, "prompt": [{ "type": "text", "text": "hi" }] },
1521+ }),
1522+ &mut writer,
1523+ );
1524+ let update = read(&mut reader);
1525+ assert!(
1526+ update["params"]["update"]["content"]["text"]
1527+ .as_str()
1528+ .expect("text")
1529+ .contains("native_loop · Omega Agent")
1530+ );
1531+ let answered = read(&mut reader);
1532+ assert_eq!(answered["result"]["stopReason"], "end_turn");
1533+ assert_eq!(
1534+ answered["result"]["_meta"][SERVED_SESSION_META_KEY]["refusal"],
1535+ ServedRefusal::ExecutesNothing.token()
1536+ );
1537+
1538+ // And the mutation attempt, over the same real socket.
1539+ send(
1540+ json!({ "jsonrpc": "2.0", "id": 3, "method": "full_auto/start", "params": {} }),
1541+ &mut writer,
1542+ );
1543+ assert_eq!(
1544+ read(&mut reader)["error"]["data"]["refusal"],
1545+ ServedRefusal::NotExposed.token()
1546+ );
1547+ }
1548+
1549+ /// An **upstream** ACP client, over a real loopback socket, sees the
1550+ /// disclosure.
1551+ ///
1552+ /// The client here is `agent_client_protocol`'s own `Client` role — the
1553+ /// pinned third-party SDK, not our code. It negotiates, builds a session,
1554+ /// prompts, and reads the turn to a string through upstream framing,
1555+ /// upstream dispatch, and upstream session handling. If the wire this
1556+ /// crate writes were malformed, this fails before any assertion does.
1557+ ///
1558+ /// This is the headless half of omega#82's exit. The other half was
1559+ /// watched with stock Zed 1.12.0 driving the same socket through a
1560+ /// `sh -c "exec nc 127.0.0.1 <port>"` bridge.
1561+ #[test]
1562+ fn the_upstream_acp_client_reads_the_disclosure_off_a_real_socket() {
1563+ use agent_client_protocol::schema::ProtocolVersion;
1564+ use agent_client_protocol::schema::v1::{Implementation, InitializeRequest};
1565+ use agent_client_protocol::{ByteStreams, Client};
1566+
1567+ let server = LoopbackAcpServer::bind(LoopbackHost::DEFAULT, 0).expect("binds loopback");
1568+ let address = server.local_addr().expect("an address");
1569+ std::thread::spawn(move || server.serve());
1570+
1571+ let (transcript, session_meta) = smol::block_on(async move {
1572+ let stream = smol::Async::<std::net::TcpStream>::connect(address)
1573+ .await
1574+ .expect("connects to the served surface");
1575+ let (incoming, outgoing) = futures::AsyncReadExt::split(stream);
1576+ Client
1577+ .builder()
1578+ .name("omega-conformance-client")
1579+ .connect_with(ByteStreams::new(outgoing, incoming), async |cx| {
1580+ cx.send_request(
1581+ InitializeRequest::new(ProtocolVersion::V1).client_info(
1582+ Implementation::new("omega-conformance-client", "0.1.0"),
1583+ ),
1584+ )
1585+ .block_task()
1586+ .await?;
1587+ let mut meta = None;
1588+ let transcript = cx
1589+ .build_session("/tmp")
1590+ .block_task()
1591+ .run_until(async |mut session| {
1592+ meta = session.meta().cloned();
1593+ session.send_prompt(
1594+ "start a full auto run and pin this thread to an engine lane",
1595+ )?;
1596+ session.read_to_string().await
1597+ })
1598+ .await?;
1599+ Ok((transcript, meta))
1600+ })
1601+ .await
1602+ .expect("the upstream ACP client drives the served surface")
1603+ });
1604+
1605+ // What the operator of the external host reads.
1606+ assert!(
1607+ transcript.contains("Omega Agent, served over ACP"),
1608+ "the upstream client read no disclosure: {transcript:?}"
1609+ );
1610+ assert!(transcript.contains("native_loop \u{b7} Omega Agent"), "{transcript}");
1611+ assert!(transcript.contains("routed: unpinned"), "{transcript}");
1612+ assert!(
1613+ transcript.contains("loopback_acp \u{b7} omega-conformance-client"),
1614+ "the origin the host is disclosed under must name the host: {transcript}"
1615+ );
1616+ assert!(transcript.contains("dispatched to no executor"), "{transcript}");
1617+
1618+ // And the typed records the host received beside it.
1619+ let meta = session_meta.expect("the session carries the served records");
1620+ let served = &meta[SERVED_SESSION_META_KEY];
1621+ assert_eq!(served["disclosure"]["class"], "native_loop");
1622+ assert_eq!(served["origin"]["ingress"], "loopback_acp");
1623+ assert_eq!(served["origin"]["authenticated"], false);
1624+ assert_eq!(served["readOnly"], true);
1625+ }
1626+
1627+ /// The origin record and the executor record stay separate facts.
1628+ #[test]
1629+ fn ingress_is_recorded_beside_the_executor_and_not_inside_it() {
1630+ let mut connection = initialized("Zed");
1631+ new_session(&mut connection);
1632+ let session = &connection.sessions()[0];
1633+ assert!(session.origin.is_coherent());
1634+ assert!(session.disclosure.is_coherent());
1635+ assert_eq!(session.origin.ingress, Ingress::LoopbackAcp);
1636+ assert_eq!(
1637+ session.disclosure.class,
1638+ ExecutorClass::NativeLoop,
1639+ "the executor class answers who ran the work, and serving over ACP \
1640+ changes only who asked"
1641+ );
1642+ assert_ne!(session.disclosure.class, ExecutorClass::ExternalAcp);
1643+ }
1644+}
diff --git a/crates/omega_deltas/src/omega_deltas.rs b/crates/omega_deltas/src/omega_deltas.rs
index 183292876e..e79510afa8 100644
--- a/crates/omega_deltas/src/omega_deltas.rs
+++ b/crates/omega_deltas/src/omega_deltas.rs
@@ -68,6 +68,7 @@ pub const ENFORCED_DELTAS: &[&str] = &[
6868 "OMEGA-DELTA-0038",
6969 "OMEGA-DELTA-0039",
7070 "OMEGA-DELTA-0040",
71+ "OMEGA-DELTA-0041",
7172 ];
7273
7374 /// OMEGA-DELTA-0036. The uninstall script embedded in the shipped `cli`.
@@ -325,6 +326,18 @@ pub const NEW_THREAD_CHORD_NARROW_CONTEXTS: &[&str] = &[
325326 "Terminal",
326327 ];
327328
329+/// OMEGA-DELTA-0041. The crate that owns the served ACP socket.
330+pub const ACP_SERVER_PATH: &str = "crates/omega_acp_server/src/omega_acp_server.rs";
331+
332+/// OMEGA-DELTA-0041. That crate's manifest, which must not reach GPUI.
333+pub const ACP_SERVER_MANIFEST_PATH: &str = "crates/omega_acp_server/Cargo.toml";
334+
335+/// OMEGA-DELTA-0041. The supervisor layer that owns the listener's lifecycle.
336+pub const EFFECTD_PATH: &str = "crates/omega_effectd/src/omega_effectd.rs";
337+
338+/// OMEGA-DELTA-0041. Where the first-party agent identity is declared.
339+pub const NATIVE_AGENT_IDENTITY_PATH: &str = "crates/agent/src/agent.rs";
340+
328341 /// OMEGA-DELTA-0040. The startup path that opens Omega's first window.
329342 pub const STARTUP_PATH: &str = "crates/zed/src/main.rs";
330343
@@ -5966,4 +5979,208 @@ mod tests {
59665979 It is a field that reads like an observation, so it has to be one."
59675980 );
59685981 }
5982+ // ---------------------------------------------------------------------
5983+ // OMEGA-DELTA-0041 — Omega Agent served over ACP on a loopback socket
5984+ // ---------------------------------------------------------------------
5985+
5986+ /// OMEGA-DELTA-0041. The served ACP surface is off unless the flag says
5987+ /// exactly `1`, and the source says so.
5988+ ///
5989+ /// `omega_acp_server` proves the behaviour with its own unit test. This
5990+ /// checks the *shape* the behaviour depends on, in the file, because a
5991+ /// later edit that made the flag truthy-tolerant — `is_some`,
5992+ /// `unwrap_or("1")`, a `parse::<bool>()` — would keep every one of that
5993+ /// crate's tests passing for the values it happened to list while turning
5994+ /// a listener on for values it did not.
5995+ #[test]
5996+ fn the_served_acp_surface_is_off_unless_the_flag_is_exact() {
5997+ let source = std::fs::read_to_string(repository_path(ACP_SERVER_PATH))
5998+ .expect("the served ACP surface is readable");
5999+
6000+ assert!(
6001+ source.contains("None => Enablement::Off(OffReason::FlagUnset)"),
6002+ "OMEGA-DELTA-0041: an unset OMEGA_ACP_SERVER must be off. A listener that is on by default is a different product."
6003+ );
6004+ assert!(
6005+ source.contains("Some(ENABLE_VALUE) => Enablement::On")
6006+ && source.contains("Some(_) => Enablement::Off(OffReason::FlagNotExactlyOne)"),
6007+ "OMEGA-DELTA-0041: the enable flag must be an exact match on ENABLE_VALUE with everything else off. A truthy-tolerant flag is a flag whose default nobody can state."
6008+ );
6009+ for tolerant in [
6010+ "to_lowercase()",
6011+ "eq_ignore_ascii_case",
6012+ "parse::<bool>",
6013+ "unwrap_or(ENABLE_VALUE)",
6014+ "flag.is_some()",
6015+ ] {
6016+ assert!(
6017+ !source.contains(tolerant),
6018+ "OMEGA-DELTA-0041: the enable flag reads {tolerant}, which widens the set of values that open an unauthenticated socket."
6019+ );
6020+ }
6021+ }
6022+
6023+ /// OMEGA-DELTA-0041. The socket is the supervisor's, and GPUI cannot reach
6024+ /// the crate that opens it.
6025+ ///
6026+ /// omega#82's falsifier is *GPUI owns the socket*. Two things make that
6027+ /// false and both are checked here rather than asserted in prose: the
6028+ /// crate that binds declares no GPUI dependency at all, and the only
6029+ /// production caller of `start_if_enabled` is `crates/omega_effectd`.
6030+ #[test]
6031+ fn only_the_supervisor_opens_the_served_acp_socket() {
6032+ let manifest = std::fs::read_to_string(repository_path(ACP_SERVER_MANIFEST_PATH))
6033+ .expect("the served ACP surface's manifest is readable");
6034+ for reaching_into_the_app in [
6035+ "gpui.workspace",
6036+ "workspace.workspace",
6037+ "agent_ui.workspace",
6038+ "project.workspace",
6039+ "ui.workspace",
6040+ "editor.workspace",
6041+ ] {
6042+ assert!(
6043+ !manifest.contains(reaching_into_the_app),
6044+ "OMEGA-DELTA-0041: crates/omega_acp_server depends on {reaching_into_the_app}. The crate that opens the unauthenticated loopback socket must not be reachable from the UI layer — that is exactly what omega#82's falsifier names."
6045+ );
6046+ }
6047+
6048+ let crates = repository_path("crates");
6049+ let mut binders: Vec<String> = Vec::new();
6050+ let mut starters: Vec<String> = Vec::new();
6051+ for_each_source_file(&crates, &["rs"], |path, source| {
6052+ let display = path
6053+ .display()
6054+ .to_string()
6055+ .rsplit("crates/")
6056+ .next()
6057+ .unwrap_or_default()
6058+ .to_owned();
6059+ if display.starts_with("omega_deltas/") {
6060+ return;
6061+ }
6062+ for line in source.lines() {
6063+ let trimmed = line.trim();
6064+ if trimmed.starts_with("//") || trimmed.starts_with("///") {
6065+ continue;
6066+ }
6067+ if trimmed.contains("LoopbackAcpServer::bind(") {
6068+ binders.push(display.clone());
6069+ }
6070+ if trimmed.contains("start_if_enabled()") && !trimmed.starts_with("pub fn ") {
6071+ starters.push(display.clone());
6072+ }
6073+ }
6074+ });
6075+
6076+ assert!(
6077+ !binders.is_empty() && !starters.is_empty(),
6078+ "OMEGA-DELTA-0041: the scan found no bind and no start, so this check is vacuous. Either the served surface is gone or the needles stopped matching."
6079+ );
6080+ for file in &binders {
6081+ assert!(
6082+ file.starts_with("omega_acp_server/"),
6083+ "OMEGA-DELTA-0041: {file} binds the served ACP listener. Only crates/omega_acp_server may."
6084+ );
6085+ }
6086+ for file in &starters {
6087+ assert!(
6088+ file.starts_with("omega_effectd/") || file.starts_with("omega_acp_server/"),
6089+ "OMEGA-DELTA-0041: {file} starts the served ACP listener. The supervisor layer owns the lifecycle; GPUI never opens its own socket."
6090+ );
6091+ }
6092+ }
6093+
6094+ /// OMEGA-DELTA-0041. Nothing reachable over the socket can take an
6095+ /// executor pin.
6096+ ///
6097+ /// Owner gate 8 at the socket. An engine lane *is* Full Auto authority, a
6098+ /// pin is the only door to one, and `OMEGA-DELTA-0035` already requires
6099+ /// every pin-setting call to name a literal `PinGesture`. This closes the
6100+ /// other end: the crate serving an unauthenticated surface never names a
6101+ /// pin at all, so there is nothing there for a later edit to reach for.
6102+ #[test]
6103+ fn nothing_over_the_served_acp_surface_can_take_a_pin() {
6104+ let source = std::fs::read_to_string(repository_path(ACP_SERVER_PATH))
6105+ .expect("the served ACP surface is readable");
6106+ // The shipped half only. The test module below it reads the pin ledger
6107+ // on purpose, to prove every pin gesture is classified as unexposed.
6108+ let shipped = source
6109+ .split_once("#[cfg(test)]")
6110+ .map(|(shipped, _)| shipped)
6111+ .expect("the served ACP surface has a test module");
6112+ for line in shipped.lines() {
6113+ let trimmed = line.trim();
6114+ // The doc comments explain *why* there is no pin here, and must be
6115+ // allowed to say the word.
6116+ if trimmed.starts_with("//") {
6117+ continue;
6118+ }
6119+ for pin_reaching in ["PinGesture", "pin_session(", "pin_next_session(", "ExecutorPin::"]
6120+ {
6121+ assert!(
6122+ !trimmed.contains(pin_reaching),
6123+ "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."
6124+ );
6125+ }
6126+ }
6127+ assert!(
6128+ source.contains("pin: None,"),
6129+ "OMEGA-DELTA-0041: the served route inputs no longer pin nothing. If this moved, the check above is watching the wrong thing."
6130+ );
6131+ }
6132+
6133+ /// OMEGA-DELTA-0041. The served surface presents the first-party agent's
6134+ /// own identity, not a second one.
6135+ ///
6136+ /// An attached host is told it is talking to Omega Agent. If the served
6137+ /// identity drifted from the one `crates/agent` declares, the served
6138+ /// surface would be disclosing an agent that does not exist — which is the
6139+ /// same defect class as a rendered label in the record.
6140+ #[test]
6141+ fn the_served_surface_presents_the_first_party_agent_id() {
6142+ let identity = std::fs::read_to_string(repository_path(NATIVE_AGENT_IDENTITY_PATH))
6143+ .expect("the native agent is readable");
6144+ let served = std::fs::read_to_string(repository_path(ACP_SERVER_PATH))
6145+ .expect("the served ACP surface is readable");
6146+
6147+ let declared = identity
6148+ .lines()
6149+ .find_map(|line| {
6150+ let line = line.trim();
6151+ let rest = line.strip_prefix("pub static OMEGA_AGENT_ID")?;
6152+ let start = rest.find("AgentId::new(\"")? + "AgentId::new(\"".len();
6153+ let tail = &rest[start..];
6154+ let end = tail.find('"')?;
6155+ Some(tail[..end].to_owned())
6156+ })
6157+ .expect("crates/agent declares OMEGA_AGENT_ID as a literal");
6158+
6159+ assert!(
6160+ served.contains(&format!("pub const SERVED_AGENT_ID: &str = \"{declared}\";")),
6161+ "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."
6162+ );
6163+ }
6164+
6165+ /// OMEGA-DELTA-0041. The supervisor's start is not conditional on anything
6166+ /// but the flag.
6167+ ///
6168+ /// A start hidden behind "and the engine is available" would make the
6169+ /// surface's default depend on packaging rather than on the flag, so the
6170+ /// default nobody could state would be back.
6171+ #[test]
6172+ fn the_supervisor_starts_the_served_surface_before_it_resolves_the_engine() {
6173+ let source = std::fs::read_to_string(repository_path(EFFECTD_PATH))
6174+ .expect("the supervisor is readable");
6175+ let start = source
6176+ .find("start_served_acp_surface();")
6177+ .expect("OMEGA-DELTA-0041: the supervisor no longer starts the served surface");
6178+ let resolve = source
6179+ .find("resolve_effectd_command(")
6180+ .expect("the supervisor resolves the packaged component");
6181+ assert!(
6182+ start < resolve,
6183+ "OMEGA-DELTA-0041: the served surface is started after the packaged component is resolved, so whether it listens depends on packaging as well as on the flag."
6184+ );
6185+ }
59696186 }
diff --git a/crates/omega_effectd/Cargo.toml b/crates/omega_effectd/Cargo.toml
index 1fae0e0f71..e6af30d431 100644
--- a/crates/omega_effectd/Cargo.toml
+++ b/crates/omega_effectd/Cargo.toml
@@ -21,7 +21,9 @@ credentials_provider.workspace = true
2121 futures.workspace = true
2222 gpui.workspace = true
2323 http_client.workspace = true
24+log.workspace = true
2425 nostr = { workspace = true, features = ["nip59"] }
26+omega_acp_server.workspace = true
2527 omega_identity.workspace = true
2628 paths.workspace = true
2729 rand.workspace = true
diff --git a/crates/omega_effectd/src/omega_effectd.rs b/crates/omega_effectd/src/omega_effectd.rs
index e51f0102a2..85c8ddb4cf 100644
--- a/crates/omega_effectd/src/omega_effectd.rs
+++ b/crates/omega_effectd/src/omega_effectd.rs
@@ -75,6 +75,8 @@ pub fn init_with_host_handler(handler: Option<OmegaEffectdHostHandler>, cx: &mut
7575 return;
7676 }
7777
78+ start_served_acp_surface();
79+
7880 let runtime = match std::env::current_exe()
7981 .map_err(anyhow::Error::from)
8082 .and_then(|executable| {
@@ -98,6 +100,39 @@ pub fn init_with_host_handler(handler: Option<OmegaEffectdHostHandler>, cx: &mut
98100 cx.set_global(runtime);
99101 }
100102
103+/// `OMEGA-DELTA-0041`, omega#82. Serve Omega Agent over ACP, if the flag says
104+/// so.
105+///
106+/// The supervisor layer owns this, not GPUI. The socket itself lives in
107+/// `crates/omega_acp_server`, which depends on no part of GPUI, and this is its
108+/// only production call site — `crates/omega_deltas` fails if a second one
109+/// appears in a UI crate. Omega's own windows never open a listener.
110+///
111+/// Off unless `OMEGA_ACP_SERVER` is exactly `1`, so the shipped default binds
112+/// nothing at all and this is a no-op in every normal launch.
113+fn start_served_acp_surface() {
114+ match omega_acp_server::start_if_enabled() {
115+ omega_acp_server::StartOutcome::NotStarted(reason) => {
116+ log::debug!(
117+ "OMEGA-DELTA-0041: Omega Agent is not served over ACP ({})",
118+ reason.token()
119+ );
120+ }
121+ omega_acp_server::StartOutcome::Listening(address) => {
122+ log::info!(
123+ "OMEGA-DELTA-0041: Omega Agent is served over ACP on {address} \
124+ (loopback, unauthenticated, read-only)"
125+ );
126+ }
127+ omega_acp_server::StartOutcome::Failed(error) => {
128+ log::error!(
129+ "OMEGA-DELTA-0041: the served ACP surface was asked for and \
130+ could not listen: {error}"
131+ );
132+ }
133+ }
134+}
135+
101136 pub fn shared_supervisor(cx: &App) -> Result<SharedOmegaEffectdSupervisor> {
102137 match cx.try_global::<OmegaEffectdRuntime>() {
103138 Some(OmegaEffectdRuntime::Available(supervisor)) => Ok(supervisor.clone()),
diff --git a/crates/omega_front_door/src/omega_front_door.rs b/crates/omega_front_door/src/omega_front_door.rs
index bf9aa46583..5641483601 100644
--- a/crates/omega_front_door/src/omega_front_door.rs
+++ b/crates/omega_front_door/src/omega_front_door.rs
@@ -127,6 +127,13 @@ impl ExecutorClass {
127127 /// So [`label`](Self::label) is a function of the fields, and there is no
128128 /// field to put a rendered label in. omega#76 fixed this shape; omega#77
129129 /// populates it from live threads and renders the line.
130+///
131+/// omega#82 tightened how that is checked. The old check dumped the struct and
132+/// failed if the word `label` appeared in it — a **denylist**, which passes for
133+/// a field called `line`, `text`, `summary`, or `rendered`. The check now
134+/// asserts the declared fields **exactly** against
135+/// [`EXECUTOR_DISCLOSURE_FIELDS`], so any new field at all is a deliberate edit
136+/// to a list that says why the shape is closed.
130137 #[derive(Debug, Clone, PartialEq, Eq)]
131138 pub struct ExecutorDisclosure {
132139 /// Which of the three admitted classes ran the work.
@@ -232,6 +239,204 @@ impl ExecutorDisclosure {
232239 }
233240 }
234241
242+/// Every field [`ExecutorDisclosure`] is allowed to have, in declaration order.
243+///
244+/// Asserted **exactly** against the struct's own source by
245+/// `the_disclosure_record_holds_no_rendered_label`. The point of an exact list
246+/// rather than a denylist is that a denylist only catches the names its author
247+/// thought of: a `label` field fails a `contains("label")` check, and `line`,
248+/// `text`, `summary`, `rendered`, and `caption` all sail through it. The
249+/// binding condition of the owner's 2026-07-25 identity decision is that
250+/// disclosure is a *record a label renders*, and a stored rendering under any
251+/// name breaks it, not just one spelled `label`.
252+pub const EXECUTOR_DISCLOSURE_FIELDS: &[&str] = &[
253+ "class", "agent_id", "provider", "model", "run_ref", "route",
254+];
255+
256+// -------------------------------------------------------------------------
257+// Where a session was reached from
258+// -------------------------------------------------------------------------
259+
260+/// How a session reached Omega Agent. omega#82.
261+///
262+/// **This is not an [`ExecutorClass`].** `ExecutorClass` answers *who ran the
263+/// work*; this answers *who asked*. Serving Omega Agent over ACP to an external
264+/// host changes the second and nothing about the first: the turn is still
265+/// executed by the native loop, an external ACP agent, or an engine lane, and
266+/// which one is a fact about the route decision rather than about the socket
267+/// the request arrived on.
268+///
269+/// Reusing [`ExecutorClass::ExternalAcp`] for a served session would be
270+/// actively wrong rather than merely imprecise. It would make a served
271+/// session's disclosure say *an external ACP agent did this work* when Omega
272+/// did, and it would make one token mean two opposite things depending on which
273+/// side of the socket the reader is standing on — Omega-as-client reaching out
274+/// to a foreign agent, and Omega-as-agent being reached by a foreign host. A
275+/// disclosure record whose meaning depends on the reader's vantage point is not
276+/// a disclosure record. A fourth `ExecutorClass` variant is wrong for the same
277+/// reason: there is no fourth *executor*, and `ServedOverAcp` would be an
278+/// ingress fact wearing an execution field's clothes.
279+///
280+/// So `OMEGA-AGENT-AC-04` stands unrevised at exactly three executor classes,
281+/// and ingress is modelled here instead. That also keeps the identity decision
282+/// cheap to reverse in the direction that matters: an origin record can be
283+/// added and removed without rewriting a single stored thread record, whereas
284+/// re-pointing `ExternalAcp` would retroactively change what every record
285+/// already written meant.
286+#[derive(Debug, Clone, Copy, PartialEq, Eq)]
287+pub enum Ingress {
288+ /// The session was started inside Omega, by a person at the front door.
289+ InApp,
290+ /// The session was reached over the loopback ACP server by an external
291+ /// host. `OMEGA-DELTA-0041`.
292+ LoopbackAcp,
293+}
294+
295+impl Ingress {
296+ /// Every admitted ingress, in declaration order.
297+ #[must_use]
298+ pub const fn all() -> &'static [Self] {
299+ &[Self::InApp, Self::LoopbackAcp]
300+ }
301+
302+ /// The stable wire token for this ingress.
303+ #[must_use]
304+ pub const fn token(self) -> &'static str {
305+ match self {
306+ Self::InApp => "in_app",
307+ Self::LoopbackAcp => "loopback_acp",
308+ }
309+ }
310+}
311+
312+/// What a session says about where it was reached from.
313+///
314+/// A record, on the same terms as [`ExecutorDisclosure`]: [`label`](Self::label)
315+/// is derived on every call and nothing stores the output, so there is no field
316+/// here to put a rendered sentence in either.
317+#[derive(Debug, Clone, PartialEq, Eq)]
318+pub struct SessionOrigin {
319+ /// How the session was reached.
320+ pub ingress: Ingress,
321+ /// The name the external host gave for itself, where one did.
322+ ///
323+ /// `None` is *not disclosed*. An in-app session has no host to name, and a
324+ /// served session whose client sent no `clientInfo` genuinely did not say.
325+ pub host_name: Option<String>,
326+ /// The version the external host gave for itself, where one did.
327+ pub host_version: Option<String>,
328+ /// Whether the host authenticated.
329+ ///
330+ /// Always `false` for [`Ingress::LoopbackAcp`], and
331+ /// [`is_coherent`](Self::is_coherent) enforces it: the served surface
332+ /// declares an empty `authMethods` list, so a served session claiming an
333+ /// authenticated host is claiming a credential no one could have presented.
334+ pub authenticated: bool,
335+}
336+
337+impl SessionOrigin {
338+ /// The origin of a session a person started inside Omega.
339+ #[must_use]
340+ pub const fn in_app() -> Self {
341+ Self {
342+ ingress: Ingress::InApp,
343+ host_name: None,
344+ host_version: None,
345+ authenticated: false,
346+ }
347+ }
348+
349+ /// The origin of a session an external host reached over the loopback ACP
350+ /// server.
351+ #[must_use]
352+ pub const fn loopback_acp(host_name: Option<String>, host_version: Option<String>) -> Self {
353+ Self {
354+ ingress: Ingress::LoopbackAcp,
355+ host_name,
356+ host_version,
357+ authenticated: false,
358+ }
359+ }
360+
361+ /// Render the origin line for a session.
362+ ///
363+ /// Derived on every call. Nothing stores the output. An undisclosed host is
364+ /// *said*, for the same reason [`ExecutorDisclosure::label`] says an
365+ /// undisclosed model: a line that quietly dropped the host would read as a
366+ /// complete origin.
367+ #[must_use]
368+ pub fn label(&self) -> String {
369+ let host = match (&self.host_name, &self.host_version) {
370+ (Some(name), Some(version)) => format!("{name} {version}"),
371+ (Some(name), None) => format!("{name}, version not disclosed"),
372+ (None, _) => "host not disclosed".to_owned(),
373+ };
374+ match self.ingress {
375+ Ingress::InApp => "in_app".to_owned(),
376+ Ingress::LoopbackAcp => {
377+ format!("loopback_acp · {host} · unauthenticated")
378+ }
379+ }
380+ }
381+
382+ /// Whether this record is internally consistent.
383+ #[must_use]
384+ pub fn is_coherent(&self) -> bool {
385+ let present_and_named =
386+ |value: &Option<String>| value.as_ref().is_none_or(|value| !value.is_empty());
387+ let named_host_matches_the_ingress = match self.ingress {
388+ // An in-app session has no external host, so naming one means the
389+ // record was built from the wrong session.
390+ Ingress::InApp => self.host_name.is_none() && self.host_version.is_none(),
391+ Ingress::LoopbackAcp => true,
392+ };
393+ // The served surface offers no authentication method at all, so an
394+ // authenticated served session is a credential nobody could present.
395+ let unauthenticated_where_it_must_be =
396+ self.ingress != Ingress::LoopbackAcp || !self.authenticated;
397+ present_and_named(&self.host_name)
398+ && present_and_named(&self.host_version)
399+ && named_host_matches_the_ingress
400+ && unauthenticated_where_it_must_be
401+ }
402+}
403+
404+/// Every field [`SessionOrigin`] is allowed to have, in declaration order.
405+///
406+/// Asserted exactly, for the reason [`EXECUTOR_DISCLOSURE_FIELDS`] gives.
407+pub const SESSION_ORIGIN_FIELDS: &[&str] =
408+ &["ingress", "host_name", "host_version", "authenticated"];
409+
410+/// The `pub` field names a struct declares, read from source text.
411+///
412+/// A lexical scan rather than reflection, because Rust has none: the check that
413+/// matters is "what does this struct declare", and the source is where that is
414+/// written. Starts at `pub struct {name} {` and stops at the first line that is
415+/// exactly `}`, which is where rustfmt puts a struct's closing brace.
416+#[must_use]
417+pub fn declared_struct_fields(source: &str, struct_name: &str) -> Vec<String> {
418+ let opening = format!("pub struct {struct_name} {{");
419+ let mut fields = Vec::new();
420+ let mut inside = false;
421+ for line in source.lines() {
422+ if !inside {
423+ inside = line.trim_start().starts_with(&opening);
424+ continue;
425+ }
426+ if line == "}" {
427+ break;
428+ }
429+ let trimmed = line.trim_start();
430+ let Some(rest) = trimmed.strip_prefix("pub ") else {
431+ continue;
432+ };
433+ if let Some((name, _)) = rest.split_once(':') {
434+ fields.push(name.trim().to_owned());
435+ }
436+ }
437+ fields
438+}
439+
235440 // -------------------------------------------------------------------------
236441 // Who may start Full Auto
237442 // -------------------------------------------------------------------------
@@ -629,8 +834,34 @@ mod tests {
629834
630835 /// A struct with no field to hold a rendered label cannot accidentally
631836 /// grow one without this test's author noticing.
837+ ///
838+ /// omega#82 replaced a denylist with an **exact** field list. The old check
839+ /// dumped the struct and failed if the word `label` appeared; a field named
840+ /// `line`, `text`, `summary`, `rendered`, or `caption` passed it, and every
841+ /// one of those is the same defect. The list is the whole check now: a new
842+ /// field of any name fails until someone edits `EXECUTOR_DISCLOSURE_FIELDS`
843+ /// on purpose.
632844 #[test]
633845 fn the_disclosure_record_holds_no_rendered_label() {
846+ let source = std::fs::read_to_string(repository_path(
847+ "crates/omega_front_door/src/omega_front_door.rs",
848+ ))
849+ .expect("this crate's source is readable");
850+ let declared = declared_struct_fields(&source, "ExecutorDisclosure");
851+ assert_eq!(
852+ declared, EXECUTOR_DISCLOSURE_FIELDS,
853+ "ExecutorDisclosure's declared fields moved. The owner admitted \
854+ the non-signing identity choice on the condition that disclosure \
855+ stays a typed record a label renders, so a field holding a \
856+ rendered line — under any name, not only `label` — breaks it. If \
857+ this is a deliberate shape change, edit \
858+ EXECUTOR_DISCLOSURE_FIELDS and say why."
859+ );
860+
861+ // The scan reaching nothing would make the assertion above vacuous in
862+ // one direction, so the list is also checked to be non-empty and to
863+ // name a field the struct genuinely round-trips.
864+ assert!(!declared.is_empty(), "the field scan reached no fields");
634865 let disclosure = ExecutorDisclosure {
635866 class: ExecutorClass::NativeLoop,
636867 agent_id: "a".into(),
@@ -639,14 +870,113 @@ mod tests {
639870 run_ref: None,
640871 route: None,
641872 };
642- // Debug is the closest thing to reflection available here: if a
643- // `label` field is ever added, it shows up in the struct dump.
644873 let dumped = format!("{disclosure:?}");
874+ for field in EXECUTOR_DISCLOSURE_FIELDS {
875+ assert!(
876+ dumped.contains(field),
877+ "EXECUTOR_DISCLOSURE_FIELDS names {field}, which the struct \
878+ does not carry: {dumped}"
879+ );
880+ }
881+ }
882+
883+ /// The field scan finds real fields and stops at the struct it was asked
884+ /// about.
885+ ///
886+ /// A scan that silently matched nothing would make the exactness check
887+ /// above pass for a struct that had grown a rendered line.
888+ #[test]
889+ fn the_field_scan_reads_a_struct_and_stops_at_its_brace() {
890+ let source = "\
891+pub struct Wanted {
892+ /// doc
893+ pub first: String,
894+ pub second: Option<u8>,
895+ private: bool,
896+}
897+
898+pub struct Other {
899+ pub third: String,
900+}
901+";
902+ assert_eq!(
903+ declared_struct_fields(source, "Wanted"),
904+ ["first".to_owned(), "second".to_owned()],
905+ "the scan must read public fields and must not walk into the \
906+ next struct"
907+ );
908+ assert!(declared_struct_fields(source, "Missing").is_empty());
909+ }
910+
911+ /// omega#82. Ingress is a separate record from the executor class, and the
912+ /// executor set stays closed at three.
913+ #[test]
914+ fn reaching_over_acp_is_an_origin_and_not_an_executor() {
915+ // The whole design call, as an assertion: no executor class names a
916+ // socket direction, so a served session cannot claim an external agent
917+ // did work Omega did.
918+ for class in ExecutorClass::all() {
919+ assert!(
920+ !class.token().contains("served") && !class.token().contains("ingress"),
921+ "{} names an ingress fact in an execution field",
922+ class.token()
923+ );
924+ }
925+ assert_eq!(ExecutorClass::all().len(), 3);
926+
927+ let origin = SessionOrigin::loopback_acp(Some("Zed".into()), Some("1.12.0".into()));
928+ assert!(origin.is_coherent());
929+ assert_eq!(origin.label(), "loopback_acp · Zed 1.12.0 · unauthenticated");
930+ assert_eq!(origin.ingress.token(), "loopback_acp");
931+
932+ let quiet = SessionOrigin::loopback_acp(None, None);
933+ assert!(quiet.is_coherent());
934+ assert_eq!(
935+ quiet.label(),
936+ "loopback_acp · host not disclosed · unauthenticated"
937+ );
938+ }
939+
940+ /// omega#82. A served session cannot claim it authenticated, and an in-app
941+ /// session cannot claim an external host.
942+ #[test]
943+ fn a_served_origin_cannot_claim_a_credential_nobody_could_present() {
944+ let claiming = SessionOrigin {
945+ authenticated: true,
946+ ..SessionOrigin::loopback_acp(Some("Zed".into()), None)
947+ };
645948 assert!(
646- !dumped.contains("label"),
647- "ExecutorDisclosure grew a stored label field: {dumped}. \
648- The owner admitted the non-signing identity choice on the \
649- condition that disclosure stays a typed record a label renders."
949+ !claiming.is_coherent(),
950+ "the served surface offers no auth method, so an authenticated \
951+ served session is a credential that could not have been presented"
952+ );
953+
954+ let borrowed_host = SessionOrigin {
955+ host_name: Some("Zed".into()),
956+ ..SessionOrigin::in_app()
957+ };
958+ assert!(!borrowed_host.is_coherent());
959+
960+ let blank_host = SessionOrigin::loopback_acp(Some(String::new()), None);
961+ assert!(
962+ !blank_host.is_coherent(),
963+ "absent means not disclosed; empty means something built the \
964+ record out of a missing value and lost the distinction"
965+ );
966+ }
967+
968+ /// The origin record holds no rendered line either, on the same terms.
969+ #[test]
970+ fn the_origin_record_holds_no_rendered_label() {
971+ let source = std::fs::read_to_string(repository_path(
972+ "crates/omega_front_door/src/omega_front_door.rs",
973+ ))
974+ .expect("this crate's source is readable");
975+ assert_eq!(
976+ declared_struct_fields(&source, "SessionOrigin"),
977+ SESSION_ORIGIN_FIELDS,
978+ "SessionOrigin's declared fields moved. Ingress is disclosed the \
979+ same way execution is: a typed record a label renders."
650980 );
651981 }