Skip to repository content

tenant.openagents/omega

No repository description is available.

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

capability.rs

495 lines · 18.5 KB · rust
1//! What the Exo agent behind this lane is allowed to be. `OMEGA-DELTA-0042`,
2//! omega#87.
3//!
4//! Exo's differentiator is that its agent can rewrite itself. The flagship Exo
5//! agent runs unrestricted networked `shell` in a sandbox with Exo's own source
6//! tree mounted read-write, plus a `guardian_action` tool that builds and
7//! restarts Exo. There is no approval prompt anywhere in Exo — the security
8//! model is sandbox isolation, and Exo's threat model assumes you *want* the
9//! agent to modify itself.
10//!
11//! Omega supplies the authority gate Exo lacks. The ordinary lane refuses
12//! self-modification. A separate, typed, one-use grant can authorize one exact
13//! turn after a visible human confirmation. The lane never silently enables
14//! Exo's self-modification tools.
15//!
16//! # Read the agent, do not assume it
17//!
18//! "Never enables" is easy and nearly worthless on its own: the lane emits no
19//! configuring command (see [`crate::command`]), so of course it enables
20//! nothing. The capability that matters is the one the agent *already has*,
21//! configured by whoever set Exo up — and an Omega lane pointed at the flagship
22//! self-improving agent would be surfacing exactly the capability this packet
23//! excludes, without Omega having enabled anything.
24//!
25//! So the lane reads `exo agent show` before it sends a turn and refuses when
26//! the agent carries self-modification capability. That is a live observation of
27//! the agent that is about to run, not an assertion about Omega's own argv.
28//!
29//! Three capabilities are refused, and each is one of the three things
30//! `docs/RSI.md` and the flagship agent's own configuration use to rewrite Exo:
31//!
32//! * **agent-authored tools** — `tool_creation: enabled`, the agent writing
33//!   TypeScript into `.exo/agent-tools/` at runtime;
34//! * **a tool module** — `typescript_tool_modules: N` where `N > 0`, which is
35//!   how `guardian_action` is installed (`examples/exo/guardian-tools.ts`);
36//! * **a read-write mount** — a `sandbox_mounts` entry in `rw` mode, which is
37//!   how Exo's source tree gets into the sandbox to be edited.
38//!
39//! Networking is deliberately **not** in that list. An agent with a shell and a
40//! network is a high-capability agent, and it is also what any useful coding
41//! agent is; refusing it would make the lane refuse everything and would say
42//! nothing about self-modification. It is reported, so a reader sees it, and it
43//! does not refuse.
44
45/// What `exo agent show` said about the agent this lane points at.
46///
47/// Every field is read out of Exo's own output. Nothing here is a default this
48/// file invented: a record Omega could not parse becomes
49/// [`ExoAgentReadError`], not a permissive record.
50#[derive(Clone, Debug, PartialEq, Eq)]
51pub struct ExoAgent {
52    /// The agent slug, as Exo reports it.
53    pub slug: String,
54    /// The executor Exo will run the turn with: `basic`, `rlm`, `typescript`,
55    /// `codex`, `claude-code`, `cursor`, or a module path.
56    pub harness: String,
57    /// The model binding the agent resolves to.
58    pub model: String,
59    /// Whether the agent may author its own tools at runtime.
60    pub agent_authored_tools: bool,
61    /// How many TypeScript tool modules are loaded into it.
62    pub tool_modules: u32,
63    /// Exact TypeScript tool-module paths reported by Exo.
64    pub tool_module_paths: Vec<String>,
65    /// Whether any sandbox mount is read-write.
66    pub read_write_mount: bool,
67    /// Exact agent-level sandbox mounts.
68    pub mounts: Vec<ExoMount>,
69    /// Whether the agent's sandbox has a network. Reported, never refused.
70    pub networking: bool,
71}
72
73/// A mount observed in an Exo agent or conversation record.
74#[derive(Clone, Debug, PartialEq, Eq)]
75pub struct ExoMount {
76    pub host_path: String,
77    pub mount_path: String,
78    pub read_write: bool,
79}
80
81/// Capability-bearing fields from `exo conversation show`.
82#[derive(Clone, Debug, PartialEq, Eq)]
83pub struct ExoConversation {
84    pub slug: String,
85    pub mounts: Vec<ExoMount>,
86}
87
88/// Why the lane will not run a turn on this agent.
89#[derive(Clone, Copy, Debug, PartialEq, Eq)]
90pub enum SelfModification {
91    /// `tool_creation: enabled`.
92    AgentAuthoredTools,
93    /// One or more `--tool-module` modules, the `guardian_action` shape.
94    ToolModule,
95    /// A read-write sandbox mount, the source-tree shape.
96    ReadWriteMount,
97}
98
99impl SelfModification {
100    /// The stable token this refusal is recorded under.
101    #[must_use]
102    pub const fn token(self) -> &'static str {
103        match self {
104            Self::AgentAuthoredTools => "agent_authored_tools",
105            Self::ToolModule => "tool_module",
106            Self::ReadWriteMount => "read_write_mount",
107        }
108    }
109
110    /// Every capability the lane refuses, in declaration order.
111    #[must_use]
112    pub const fn all() -> &'static [Self] {
113        &[
114            Self::AgentAuthoredTools,
115            Self::ToolModule,
116            Self::ReadWriteMount,
117        ]
118    }
119}
120
121impl std::fmt::Display for SelfModification {
122    fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
123        formatter.write_str(match self {
124            Self::AgentAuthoredTools => {
125                "this Exo agent may write its own tools at runtime; the Omega lane does not \
126                 surface Exo's self-modification"
127            }
128            Self::ToolModule => {
129                "this Exo agent has a tool module loaded, which is how guardian_action is \
130                 installed; the Omega lane does not surface Exo's self-modification"
131            }
132            Self::ReadWriteMount => {
133                "this Exo agent has a read-write mount, which is how Exo's source tree is \
134                 edited from inside the sandbox; the Omega lane does not surface Exo's \
135                 self-modification"
136            }
137        })
138    }
139}
140
141impl std::error::Error for SelfModification {}
142
143/// Why `exo agent show` could not be read.
144#[derive(Clone, Copy, Debug, PartialEq, Eq)]
145pub enum ExoAgentReadError {
146    /// A field the decision depends on was not in the output.
147    MissingField(&'static str),
148    /// A field was present and this build could not read its value.
149    UnreadableField(&'static str),
150}
151
152impl std::fmt::Display for ExoAgentReadError {
153    fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
154        match self {
155            Self::MissingField(field) => {
156                write!(formatter, "exo agent show did not report {field}")
157            }
158            Self::UnreadableField(field) => {
159                write!(formatter, "exo agent show reported an unreadable {field}")
160            }
161        }
162    }
163}
164
165impl std::error::Error for ExoAgentReadError {}
166
167impl ExoAgent {
168    /// Read `exo agent show` output.
169    ///
170    /// Fails closed on every shape it does not recognise. Exo's house rule is
171    /// that it does not keep backwards compatibility, so the realistic failure
172    /// is not a malicious record — it is upstream renaming `tool_creation` in a
173    /// commit nobody read, and a parser that defaulted the missing field to
174    /// `false` would answer "no self-modification" about an agent it could no
175    /// longer see. That is the one wrong answer this whole file exists to
176    /// prevent, so an absent field is an error and never a default.
177    ///
178    /// # Errors
179    ///
180    /// [`ExoAgentReadError`] when a load-bearing field is missing or unreadable.
181    pub fn parse(output: &str) -> Result<Self, ExoAgentReadError> {
182        let field = |name: &'static str| -> Result<&str, ExoAgentReadError> {
183            output
184                .lines()
185                .find_map(|line| line.strip_prefix(name)?.strip_prefix(':'))
186                .map(str::trim)
187                .ok_or(ExoAgentReadError::MissingField(name))
188        };
189
190        let tool_creation = match field("tool_creation")? {
191            "enabled" => true,
192            "disabled" => false,
193            _ => return Err(ExoAgentReadError::UnreadableField("tool_creation")),
194        };
195        let tool_modules: u32 = field("typescript_tool_modules")?
196            .parse()
197            .map_err(|_| ExoAgentReadError::UnreadableField("typescript_tool_modules"))?;
198        let networking = match field("enable_networking")? {
199            "true" => true,
200            "false" => false,
201            _ => return Err(ExoAgentReadError::UnreadableField("enable_networking")),
202        };
203
204        let mounts = mounts(output, "sandbox_mounts")?;
205        Ok(Self {
206            slug: field("slug")?.to_owned(),
207            harness: field("harness")?.to_owned(),
208            model: field("model")?.to_owned(),
209            agent_authored_tools: tool_creation,
210            tool_modules,
211            tool_module_paths: tool_module_paths(output, tool_modules)?,
212            read_write_mount: mounts.iter().any(|mount| mount.read_write),
213            mounts,
214            networking,
215        })
216    }
217
218    /// Whether the lane may run a turn on this agent.
219    ///
220    /// # Errors
221    ///
222    /// The first self-modification capability found, in
223    /// [`SelfModification::all`] order.
224    pub fn admits_lane_turn(&self) -> Result<(), SelfModification> {
225        if self.agent_authored_tools {
226            return Err(SelfModification::AgentAuthoredTools);
227        }
228        if self.tool_modules > 0 {
229            return Err(SelfModification::ToolModule);
230        }
231        if self.read_write_mount {
232            return Err(SelfModification::ReadWriteMount);
233        }
234        Ok(())
235    }
236}
237
238impl ExoConversation {
239    /// Parse the conversation record and retain exact mount paths.
240    pub fn parse(output: &str) -> Result<Self, ExoAgentReadError> {
241        let slug = output
242            .lines()
243            .find_map(|line| line.strip_prefix("slug:"))
244            .map(str::trim)
245            .filter(|value| !value.is_empty())
246            .ok_or(ExoAgentReadError::MissingField("slug"))?
247            .to_owned();
248        Ok(Self {
249            slug,
250            mounts: mounts(output, "mounts")?,
251        })
252    }
253
254    /// Whether the conversation itself can modify a mounted host path.
255    pub fn admits_lane_turn(&self) -> Result<(), SelfModification> {
256        if self.mounts.iter().any(|mount| mount.read_write) {
257            Err(SelfModification::ReadWriteMount)
258        } else {
259            Ok(())
260        }
261    }
262}
263
264fn tool_module_paths(output: &str, expected: u32) -> Result<Vec<String>, ExoAgentReadError> {
265    let mut lines = output.lines();
266    lines
267        .find(|line| line.starts_with("typescript_tool_modules:"))
268        .ok_or(ExoAgentReadError::MissingField("typescript_tool_modules"))?;
269    let paths = lines
270        .take_while(|line| line.starts_with("  - "))
271        .map(|line| line.trim_start_matches("  - ").to_owned())
272        .collect::<Vec<_>>();
273    if paths.len() != expected as usize {
274        return Err(ExoAgentReadError::UnreadableField(
275            "typescript_tool_modules",
276        ));
277    }
278    Ok(paths)
279}
280
281/// Read the `sandbox_mounts:` block.
282///
283/// Exo prints `  none` for an empty set and `  <host> -> <path> (ro)` or
284/// `(rw[, internal])` per mount. A block this build cannot read is an error for
285/// the same reason a missing field is.
286fn mounts(output: &str, field: &'static str) -> Result<Vec<ExoMount>, ExoAgentReadError> {
287    let mut lines = output.lines();
288    lines
289        .find(|line| line.trim_end() == format!("{field}:"))
290        .ok_or(ExoAgentReadError::MissingField(field))?;
291
292    let mut mounts = Vec::new();
293    for line in lines {
294        let Some(entry) = line.strip_prefix("  ") else {
295            break;
296        };
297        let entry = entry.trim();
298        if entry == "none" {
299            return Ok(Vec::new());
300        }
301        let Some((paths, mode)) = entry.rsplit_once(" (") else {
302            return Err(ExoAgentReadError::UnreadableField(field));
303        };
304        let mode = mode.trim_end_matches(')');
305        let read_write = match mode.split(',').next().map(str::trim) {
306            Some("rw") => true,
307            Some("ro") => false,
308            _ => return Err(ExoAgentReadError::UnreadableField(field)),
309        };
310        let Some((host_path, mount_path)) = paths.split_once(" -> ") else {
311            return Err(ExoAgentReadError::UnreadableField(field));
312        };
313        mounts.push(ExoMount {
314            host_path: host_path.to_owned(),
315            mount_path: mount_path.to_owned(),
316            read_write,
317        });
318    }
319    Ok(mounts)
320}
321
322#[cfg(test)]
323mod tests {
324    use super::*;
325
326    /// Real `exo agent show` output, captured from the pinned Exo at
327    /// `baa07f67` on 2026-07-25. Not a hand-written fixture: this is what the
328    /// binary printed for the agent the lane was driven against.
329    const DRIVEN_AGENT: &str = "\
330id: 019f9cc4-0bf8-7fa2-8a48-eb84460661c1
331slug: omega-lane
332name: OmegaLane
333harness: basic
334typescript_module: none
335typescript_tool_modules: 0
336tool_creation: disabled
337sandbox_image: default
338sandbox_provider: apple-container
339sandbox_scope: conversation
340enable_networking: false
341sandbox_mounts:
342  none
343model: gpt5mini
344max_output_tokens: none
345max_tool_round_trips: none
346braintrust: none
347";
348
349    #[test]
350    fn the_agent_the_lane_was_driven_against_is_admitted() {
351        let agent = ExoAgent::parse(DRIVEN_AGENT).expect("real output parses");
352        assert_eq!(agent.slug, "omega-lane");
353        assert_eq!(agent.harness, "basic");
354        assert_eq!(agent.model, "gpt5mini");
355        assert!(!agent.agent_authored_tools);
356        assert_eq!(agent.tool_modules, 0);
357        assert!(!agent.read_write_mount);
358        assert!(!agent.networking);
359        assert_eq!(agent.admits_lane_turn(), Ok(()));
360    }
361
362    /// The flagship Exo agent, which is the thing this refusal exists for: the
363    /// source tree mounted read-write, `guardian-tools.ts` loaded, and runtime
364    /// tool authoring on.
365    #[test]
366    fn the_self_improving_exo_agent_is_refused() {
367        let self_improving = DRIVEN_AGENT
368            .replace("tool_creation: disabled", "tool_creation: enabled")
369            .replace(
370                "typescript_tool_modules: 0",
371                "typescript_tool_modules: 1\n  - /workspace/exo/examples/exo/guardian-tools.ts",
372            )
373            .replace(
374                "sandbox_mounts:\n  none",
375                "sandbox_mounts:\n  /Users/x/exo -> /workspace/exo (rw)",
376            );
377        let agent = ExoAgent::parse(&self_improving).expect("parses");
378        assert_eq!(
379            agent.admits_lane_turn(),
380            Err(SelfModification::AgentAuthoredTools)
381        );
382    }
383
384    /// Each capability refuses on its own, so removing any one of the three
385    /// checks fails a test rather than being masked by the other two.
386    #[test]
387    fn each_self_modification_capability_refuses_on_its_own() {
388        let cases = [
389            (
390                "tool_creation: disabled",
391                "tool_creation: enabled",
392                SelfModification::AgentAuthoredTools,
393            ),
394            (
395                "typescript_tool_modules: 0",
396                "typescript_tool_modules: 2\n  - /tools/one.ts\n  - /tools/two.ts",
397                SelfModification::ToolModule,
398            ),
399            (
400                "sandbox_mounts:\n  none",
401                "sandbox_mounts:\n  /Users/x/exo -> /workspace/exo (rw, internal)",
402                SelfModification::ReadWriteMount,
403            ),
404        ];
405        assert_eq!(cases.len(), SelfModification::all().len());
406        for (from, to, expected) in cases {
407            let agent = ExoAgent::parse(&DRIVEN_AGENT.replace(from, to)).expect("parses");
408            assert_eq!(agent.admits_lane_turn(), Err(expected), "{to}");
409        }
410    }
411
412    /// A read-only mount is not self-modification. Refusing it would make the
413    /// gate refuse an ordinary working directory and say nothing about Exo
414    /// rewriting itself.
415    #[test]
416    fn a_read_only_mount_is_not_self_modification() {
417        let agent = ExoAgent::parse(&DRIVEN_AGENT.replace(
418            "sandbox_mounts:\n  none",
419            "sandbox_mounts:\n  /Users/x/project -> /workspace/project (ro)",
420        ))
421        .expect("parses");
422        assert!(!agent.read_write_mount);
423        assert_eq!(agent.admits_lane_turn(), Ok(()));
424    }
425
426    /// Networking is reported and does not refuse. See the module docs.
427    #[test]
428    fn a_networked_agent_is_reported_and_not_refused() {
429        let agent = ExoAgent::parse(
430            &DRIVEN_AGENT.replace("enable_networking: false", "enable_networking: true"),
431        )
432        .expect("parses");
433        assert!(agent.networking);
434        assert_eq!(agent.admits_lane_turn(), Ok(()));
435    }
436
437    #[test]
438    fn a_conversation_only_read_write_mount_is_refused() {
439        let shown = "\
440slug: omega-lane
441mounts:
442  /Users/x/exo -> /workspace/exo (rw)
443";
444        let conversation = ExoConversation::parse(shown).expect("conversation parses");
445        assert_eq!(
446            conversation.admits_lane_turn(),
447            Err(SelfModification::ReadWriteMount)
448        );
449        assert_eq!(conversation.mounts[0].host_path, "/Users/x/exo");
450    }
451
452    /// The upstream-rename failure, which is the realistic one. A field the
453    /// decision depends on going missing must not read as "no capability".
454    #[test]
455    fn a_renamed_field_refuses_to_parse_rather_than_defaulting_to_permissive() {
456        for field in [
457            "tool_creation",
458            "typescript_tool_modules",
459            "sandbox_mounts",
460            "harness",
461            "model",
462            "slug",
463            "enable_networking",
464        ] {
465            let renamed = DRIVEN_AGENT
466                .lines()
467                .filter(|line| !line.starts_with(&format!("{field}:")))
468                .collect::<Vec<_>>()
469                .join("\n");
470            let read = ExoAgent::parse(&renamed);
471            assert!(
472                read.is_err(),
473                "dropping {field} still parsed, and the record would claim no capability: {read:?}"
474            );
475        }
476    }
477
478    #[test]
479    fn an_unreadable_value_is_refused_rather_than_read_as_false() {
480        for (from, to) in [
481            ("tool_creation: disabled", "tool_creation: maybe"),
482            (
483                "typescript_tool_modules: 0",
484                "typescript_tool_modules: some",
485            ),
486            ("enable_networking: false", "enable_networking: yes"),
487        ] {
488            assert!(
489                ExoAgent::parse(&DRIVEN_AGENT.replace(from, to)).is_err(),
490                "{to}"
491            );
492        }
493    }
494}
495
Served at tenant.openagents/omega Member data and write actions are omitted.