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}