Skip to main content

oxios_kernel/
subagent_runner.rs

1//! Oxios sub-agent runner — in-process delegation via oxi-sdk 0.54.0+.
2//!
3//! Wraps [`oxi_sdk::SdkSubagentRunner`] (which wraps an `Oxi` instance)
4//! and exposes it as an [`oxi_agent::SubagentRunner`] for the `subagent`
5//! tool's in-process path. Each `run_isolated` call builds a fresh `Agent`
6//! with an empty context (full isolation from the parent), runs it, and
7//! returns only the final text + usage.
8//!
9//! # Security model
10//!
11//! The sub-agent built by `SdkSubagentRunner` has **zero tools** — it
12//! calls `self.oxi.agent(config).build()` with no `.tool()` / `.coding_tools()`
13//! / `.kernel_tools()` registration. A tool-less agent can only do pure
14//! text generation: no file access, no bash, no network, no side effects.
15//! This makes the sandbox-escape vector from RFC-035 §4.3 currently moot
16//! for this runner.
17//!
18//! **Defense-in-depth upgrade path:** if a future `SdkSubagentRunner`
19//! version starts honoring the `_tools` parameter or auto-registers
20//! built-in tools, switch this module to delegate through
21//! `AgentLifecycleManager::execute_directive` instead (RFC-035 Q2-B),
22//! which inherits `allowed_tools`/`network_access`/`max_execution_time_secs`/
23//! `access_manager` by construction. The "wrinkle" (ExecutionResult has
24//! no token usage) is resolvable by sourcing usage from `AgentEvent::Usage`.
25//!
26//! # Depth safety
27//!
28//! The runner sets the forked agent's `subagent_depth` to `depth + 1`.
29//! The SDK hardcodes the in-process max to 3 (`subagent.rs:649`), so
30//! recursion is bounded without env vars (concurrent `set_var` is UB).
31
32use std::sync::Arc;
33
34use oxi_sdk::SdkSubagentRunner;
35
36/// Oxios's in-process sub-agent runner.
37///
38/// Constructed once at boot from the [`OxiosEngine`]'s `Oxi` instance
39/// and shared via `Arc` into every agent build path. When wired into
40/// `AgentConfig.subagent_runner`, the `subagent` tool prefers this
41/// in-process path over shelling out to the CLI binary.
42#[derive(Clone)]
43pub struct OxiosSubagentRunner {
44    inner: SdkSubagentRunner,
45}
46
47impl std::fmt::Debug for OxiosSubagentRunner {
48    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
49        f.debug_struct("OxiosSubagentRunner")
50            .field("inner", &"<SdkSubagentRunner>")
51            .finish()
52    }
53}
54
55impl OxiosSubagentRunner {
56    /// Create from an [`oxi_sdk::Oxi`] engine instance.
57    ///
58    /// The `Oxi` instance is `Arc`-backed, so this is clone-cheap and
59    /// safe to share across concurrent tasks.
60    pub fn new(oxi: oxi_sdk::Oxi) -> Self {
61        Self {
62            inner: SdkSubagentRunner::new(oxi),
63        }
64    }
65
66    /// Return an `Arc<dyn SubagentRunner>` suitable for wiring into
67    /// `AgentConfig::subagent_runner`.
68    pub fn into_trait_object(self) -> Arc<dyn oxi_agent::SubagentRunner> {
69        Arc::new(self)
70    }
71}
72
73#[async_trait::async_trait]
74impl oxi_agent::SubagentRunner for OxiosSubagentRunner {
75    async fn run_isolated(
76        &self,
77        agent_name: &str,
78        task: &str,
79        system_prompt: Option<&str>,
80        model: Option<&str>,
81        tools: &[String],
82        cwd: &std::path::Path,
83        depth: u8,
84    ) -> anyhow::Result<oxi_agent::ForkResult> {
85        // Delegate to the SDK's reference implementation. The sub-agent
86        // is tool-less (see Security model above), so no sandbox escape.
87        self.inner
88            .run_isolated(agent_name, task, system_prompt, model, tools, cwd, depth)
89            .await
90    }
91}