Skip to repository content

tenant.openagents/omega

No repository description is available.

OpenAgents Git authority 2026-07-28T03:04:29.640Z Public web read
NIP-34 coordinate30617:7649603503856e5148d571eac2766b288a8ff1e9e35d380337a1d2b0015b4f92:omega
MaintainersHidden in public view
References2 branches · 1 tag
Read-only clonegit clone https://openagents.com/git/tenant.openagents/omega.git
Browse files

omega_acp_server.rs

1645 lines · 65.3 KB · rust
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
85use std::io::{BufRead, BufReader, Write};
86use std::net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr, TcpListener, TcpStream};
87
88use agent_client_protocol::schema::v1::{AGENT_METHOD_NAMES, CLIENT_METHOD_NAMES};
89use omega_front_door::{
90    EngineReadiness, EngineUnreachable, ExecutorDisclosure, RouteDecision, RouteInputs,
91    SessionOrigin, route,
92};
93use serde_json::{Value, json};
94
95// -------------------------------------------------------------------------
96// Default off
97// -------------------------------------------------------------------------
98
99/// The environment variable that turns the loopback ACP server on.
100pub const ENABLE_FLAG: &str = "OMEGA_ACP_SERVER";
101
102/// The environment variable that pins the port. Optional; `0` means ephemeral.
103pub 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.
109pub const ENABLE_VALUE: &str = "1";
110
111/// Why the server is off.
112#[derive(Debug, Clone, Copy, PartialEq, Eq)]
113pub 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
120impl 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)]
133pub enum Enablement {
134    /// Off, with the reason.
135    Off(OffReason),
136    /// On, because the flag said exactly [`ENABLE_VALUE`].
137    On,
138}
139
140impl 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]
153pub 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)]
172pub struct LoopbackHost(IpAddr);
173
174/// Why an address was refused as a bind target.
175#[derive(Debug, Clone, PartialEq, Eq)]
176pub 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
185impl 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
196impl 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
214impl 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)]
252pub 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
269impl 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)]
334pub 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)]
348pub 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)]
361pub 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`].
378pub 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]
403pub 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)]
410pub 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.
428pub 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.
517pub 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.
525pub 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]
537pub 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]
552pub 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]
562pub 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)]
575pub 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.
588pub 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]
598pub 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]
612pub 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]
633pub 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]
649pub 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]
669pub 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.
686const METHOD_NOT_FOUND: i64 = -32601;
687
688/// The JSON-RPC error code for a request this surface cannot read.
689const INVALID_REQUEST: i64 = -32600;
690
691/// The ACP protocol version this surface speaks.
692const 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)]
699pub 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
711impl 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(&params);
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
894fn result_line(id: &Value, result: Value) -> String {
895    json!({ "jsonrpc": "2.0", "id": id, "result": result }).to_string()
896}
897
898fn notification_line(method: &str, params: Value) -> String {
899    json!({ "jsonrpc": "2.0", "method": method, "params": params }).to_string()
900}
901
902fn 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)]
921pub struct LoopbackAcpServer {
922    listener: TcpListener,
923}
924
925impl 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.
973pub 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)]
995pub 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.
1009pub 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)]
1037mod 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}
1645
Served at tenant.openagents/omega Member data and write actions are omitted.