Skip to repository content

tenant.openagents/omega

No repository description is available.

OpenAgents Git authority 2026-07-28T03:09:19.428Z 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

endpoint.rs

274 lines · 9.6 KB · rust
1//! Exo stays on the machine it runs on. `OMEGA-DELTA-0042`, omega#87.
2//!
3//! `exo serve` is a single unary HTTP endpoint with **no authentication**. Its
4//! own documentation says so: a client may send a bearer token and the server
5//! never checks it. It exposes the full 53-variant request protocol, which
6//! includes reading secrets. Loopback is the entire boundary, and Exo knows it.
7//!
8//! Omega therefore treats "where Exo listens" and "where Omega talks to Exo" as
9//! typed values that cannot be built out of a non-loopback address, rather than
10//! as strings a caller is trusted to have checked. The refuse list in the
11//! teardown is stated as a law here: *do not expose Exo's unauthenticated HTTP
12//! endpoint or agent-cli socket beyond loopback through any Omega surface.*
13//!
14//! # Why a parser rather than a check
15//!
16//! A check is something a call site can forget. [`LoopbackEndpoint`] has one
17//! constructor, it takes a string, and it fails. Every value of the type is a
18//! loopback address because there is no other way to make one — the same
19//! discipline `MeasuredDigest` uses for bytes.
20//!
21//! # What "loopback" admits
22//!
23//! `127.0.0.0/8`, `::1`, and the literal name `localhost`. Nothing else. In
24//! particular `0.0.0.0` and `::` are refused, and those are the two that matter:
25//! they are the *plausible* mistakes. They read as "local" and they mean "every
26//! interface on this machine", which on a laptop that joins a Tailnet or a café
27//! network publishes an unauthenticated agent with a shell to that network.
28
29/// An address Omega will let Exo be reached at.
30///
31/// Loopback by construction. See the module documentation.
32#[derive(Clone, Debug, PartialEq, Eq)]
33pub struct LoopbackEndpoint {
34    host: String,
35    port: Option<u16>,
36}
37
38/// Why an address was refused.
39#[derive(Clone, Copy, Debug, PartialEq, Eq)]
40pub enum OffLoopback {
41    /// The address names every interface — `0.0.0.0` or `::`.
42    EveryInterface,
43    /// The address names a specific host that is not this machine.
44    RemoteHost,
45    /// The string is not an address this build can read. Unreadable is refused
46    /// rather than assumed local, which is the fail-closed direction.
47    Unreadable,
48}
49
50impl std::fmt::Display for OffLoopback {
51    fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
52        formatter.write_str(match self {
53            Self::EveryInterface => {
54                "that address publishes Exo on every interface, and Exo has no authentication"
55            }
56            Self::RemoteHost => "Omega only reaches Exo on this machine",
57            Self::Unreadable => "that is not an address Omega can read as loopback",
58        })
59    }
60}
61
62impl std::error::Error for OffLoopback {}
63
64/// Where `exo serve` listens when nobody says otherwise.
65///
66/// Exo's own default, restated here so the lane's default is a value in Omega's
67/// source rather than a behaviour inherited from an unstable upstream.
68pub const EXO_SERVE_DEFAULT_BIND: &str = "127.0.0.1:4766";
69
70impl LoopbackEndpoint {
71    /// Read an address, admitting only loopback.
72    ///
73    /// # Errors
74    ///
75    /// [`OffLoopback`] for anything that is not this machine.
76    pub fn parse(address: &str) -> Result<Self, OffLoopback> {
77        let address = address.trim();
78        if address.is_empty() {
79            return Err(OffLoopback::Unreadable);
80        }
81
82        // A URL is reduced to its authority before anything else, so
83        // `http://127.0.0.1:4766/request` and `127.0.0.1:4766` are the same
84        // decision. A scheme this build does not know is unreadable, not
85        // assumed harmless.
86        let authority = match address.split_once("://") {
87            Some((scheme, rest)) => {
88                if !matches!(scheme, "http" | "https") {
89                    return Err(OffLoopback::Unreadable);
90                }
91                rest.split(['/', '?', '#']).next().unwrap_or(rest)
92            }
93            None => address,
94        };
95        if authority.contains('@') {
96            // Userinfo can hide the real host behind an `@`. Refuse rather than
97            // pick a side of it.
98            return Err(OffLoopback::Unreadable);
99        }
100
101        let (host, port) = split_host_port(authority)?;
102        match classify_host(&host) {
103            HostKind::Loopback => Ok(Self { host, port }),
104            HostKind::EveryInterface => Err(OffLoopback::EveryInterface),
105            HostKind::Remote => Err(OffLoopback::RemoteHost),
106            HostKind::Unreadable => Err(OffLoopback::Unreadable),
107        }
108    }
109
110    /// The host, as read.
111    #[must_use]
112    pub fn host(&self) -> &str {
113        &self.host
114    }
115
116    /// The port, when the address carried one.
117    #[must_use]
118    pub const fn port(&self) -> Option<u16> {
119        self.port
120    }
121}
122
123impl std::fmt::Display for LoopbackEndpoint {
124    fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
125        match self.port {
126            Some(port) if self.host.contains(':') => write!(formatter, "[{}]:{port}", self.host),
127            Some(port) => write!(formatter, "{}:{port}", self.host),
128            None => formatter.write_str(&self.host),
129        }
130    }
131}
132
133enum HostKind {
134    Loopback,
135    EveryInterface,
136    Remote,
137    Unreadable,
138}
139
140fn split_host_port(authority: &str) -> Result<(String, Option<u16>), OffLoopback> {
141    if let Some(rest) = authority.strip_prefix('[') {
142        let (host, tail) = rest.split_once(']').ok_or(OffLoopback::Unreadable)?;
143        let port = match tail.strip_prefix(':') {
144            Some(port) => Some(port.parse().map_err(|_| OffLoopback::Unreadable)?),
145            None if tail.is_empty() => None,
146            None => return Err(OffLoopback::Unreadable),
147        };
148        return Ok((host.to_ascii_lowercase(), port));
149    }
150    // A bare IPv6 literal carries colons and no port.
151    if authority.matches(':').count() > 1 {
152        return Ok((authority.to_ascii_lowercase(), None));
153    }
154    match authority.split_once(':') {
155        Some((host, port)) => Ok((
156            host.to_ascii_lowercase(),
157            Some(port.parse().map_err(|_| OffLoopback::Unreadable)?),
158        )),
159        None => Ok((authority.to_ascii_lowercase(), None)),
160    }
161}
162
163fn classify_host(host: &str) -> HostKind {
164    if host == "localhost" {
165        return HostKind::Loopback;
166    }
167    if host == "0.0.0.0" || host == "::" || host == "[::]" {
168        return HostKind::EveryInterface;
169    }
170    if host == "::1" {
171        return HostKind::Loopback;
172    }
173    let octets: Vec<&str> = host.split('.').collect();
174    if octets.len() == 4 && octets.iter().all(|part| part.parse::<u8>().is_ok()) {
175        return if octets[0] == "127" {
176            HostKind::Loopback
177        } else {
178            HostKind::Remote
179        };
180    }
181    if host.contains(':') {
182        // Any other IPv6 literal. `::ffff:0.0.0.0` and friends are deliberately
183        // not decoded into their embedded IPv4 form: a lane that tried would be
184        // making a routing judgement, and refusing is the fail-closed answer.
185        return HostKind::Remote;
186    }
187    if host.is_empty() {
188        return HostKind::Unreadable;
189    }
190    HostKind::Remote
191}
192
193#[cfg(test)]
194mod tests {
195    use super::*;
196
197    #[test]
198    fn exos_own_default_bind_is_loopback_and_is_admitted() {
199        let endpoint = LoopbackEndpoint::parse(EXO_SERVE_DEFAULT_BIND).expect("loopback");
200        assert_eq!(endpoint.host(), "127.0.0.1");
201        assert_eq!(endpoint.port(), Some(4766));
202        assert_eq!(endpoint.to_string(), EXO_SERVE_DEFAULT_BIND);
203    }
204
205    #[test]
206    fn every_loopback_spelling_is_admitted() {
207        for spelling in [
208            "127.0.0.1",
209            "127.0.0.1:4766",
210            "127.1.2.3:4766",
211            "localhost:4766",
212            "LOCALHOST",
213            "::1",
214            "[::1]:4766",
215            "http://127.0.0.1:4766/request",
216            "http://localhost:4766",
217        ] {
218            assert!(
219                LoopbackEndpoint::parse(spelling).is_ok(),
220                "{spelling} is loopback"
221            );
222        }
223    }
224
225    /// The two plausible mistakes. Both read as "local" and neither is.
226    #[test]
227    fn binding_every_interface_is_refused() {
228        for spelling in ["0.0.0.0:4766", "0.0.0.0", "::", "[::]:4766"] {
229            assert_eq!(
230                LoopbackEndpoint::parse(spelling),
231                Err(OffLoopback::EveryInterface),
232                "{spelling}"
233            );
234        }
235    }
236
237    #[test]
238    fn a_remote_host_is_refused() {
239        for spelling in [
240            "10.0.0.4:4766",
241            "100.64.7.9:4766",
242            "192.168.1.20",
243            "exo.example.com:4766",
244            "http://exo.openagents.com/request",
245            "[fd7a:115c::1]:4766",
246        ] {
247            assert_eq!(
248                LoopbackEndpoint::parse(spelling),
249                Err(OffLoopback::RemoteHost),
250                "{spelling}"
251            );
252        }
253    }
254
255    /// A host hidden behind userinfo, an unknown scheme, and an empty string
256    /// are refused rather than guessed at. Unreadable fails closed.
257    #[test]
258    fn an_address_this_build_cannot_read_is_refused_rather_than_assumed_local() {
259        for spelling in [
260            "http://127.0.0.1@evil.example.com/request",
261            "ssh://127.0.0.1:4766",
262            "",
263            "   ",
264            "127.0.0.1:notaport",
265        ] {
266            assert_eq!(
267                LoopbackEndpoint::parse(spelling),
268                Err(OffLoopback::Unreadable),
269                "{spelling:?}"
270            );
271        }
272    }
273}
274
Served at tenant.openagents/omega Member data and write actions are omitted.