Skip to content

Commit d988b25

Browse files
broomvaclaude
andcommitted
feat: add sandbox port and expand tool protocol with registry + execution traits
- Add sandbox module (SandboxTier, SandboxLimits, NetworkPolicy) to canonical protocol - Expand tool module with Tool trait, ToolRegistry, ToolDefinition, ToolAnnotations - Add ToolContext, ToolError with structured variants for policy/execution failures - Include Praxis in project description Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 0fd0aa3 commit d988b25

3 files changed

Lines changed: 837 additions & 12 deletions

File tree

crates/aios-protocol/src/lib.rs

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
//! # aios-protocol — Canonical Agent OS Protocol
22
//!
33
//! This crate defines the shared types, event taxonomy, and trait interfaces
4-
//! that all Agent OS projects (Arcan, Lago, Autonomic) depend on.
4+
//! that all Agent OS projects (Arcan, Lago, Praxis, Autonomic) depend on.
55
//!
66
//! It is intentionally dependency-light (no runtime deps like tokio, axum, or redb)
77
//! so it can be used as a pure contract crate.
@@ -13,7 +13,8 @@
1313
//! - [`state`] — AgentStateVector, BudgetState (homeostasis vitals)
1414
//! - [`mode`] — OperatingMode, GatingProfile (operating constraints)
1515
//! - [`policy`] — Capability, PolicySet, PolicyEvaluation
16-
//! - [`tool`] — ToolCall, ToolOutcome
16+
//! - [`tool`] — ToolCall, ToolOutcome, ToolDefinition, ToolResult, Tool trait, ToolRegistry
17+
//! - [`sandbox`] — SandboxTier, SandboxLimits, NetworkPolicy
1718
//! - [`memory`] — SoulProfile, Observation, Provenance, MemoryScope
1819
//! - [`session`] — SessionManifest, BranchInfo, CheckpointManifest
1920
//! - [`ports`] — Runtime boundary ports (event store, provider, tools, policy, approvals, memory)
@@ -26,6 +27,7 @@ pub mod memory;
2627
pub mod mode;
2728
pub mod policy;
2829
pub mod ports;
30+
pub mod sandbox;
2931
pub mod session;
3032
pub mod state;
3133
pub mod tool;
@@ -49,11 +51,15 @@ pub use ports::{
4951
ModelStopReason, PolicyGateDecision, PolicyGatePort, ToolExecutionReport, ToolExecutionRequest,
5052
ToolHarnessPort,
5153
};
54+
pub use sandbox::{NetworkPolicy, SandboxLimits, SandboxTier};
5255
pub use session::{
5356
BranchInfo, BranchMergeResult, CheckpointManifest, ModelRouting, SessionManifest,
5457
};
5558
pub use state::{
5659
AgentStateVector, BlobRef, BudgetState, CanonicalState, MemoryNamespace, PatchApplyError,
5760
PatchOp, ProvenanceRef, StatePatch, VersionedCanonicalState,
5861
};
59-
pub use tool::{ToolCall, ToolOutcome};
62+
pub use tool::{
63+
Tool, ToolAnnotations, ToolCall, ToolContent, ToolContext, ToolDefinition, ToolError,
64+
ToolOutcome, ToolRegistry, ToolResult,
65+
};
Lines changed: 189 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,189 @@
1+
//! Canonical sandbox types for the Agent OS.
2+
//!
3+
//! These types define the shared vocabulary for sandbox isolation across
4+
//! all projects (Arcan, Lago, Praxis). Implementations live in their
5+
//! respective crates; this module provides only the contract.
6+
7+
use serde::{Deserialize, Serialize};
8+
9+
/// Sandbox isolation tiers, ordered from least to most isolated.
10+
///
11+
/// Derives `PartialOrd`/`Ord` so comparisons like `tier >= SandboxTier::Process`
12+
/// work naturally for policy enforcement.
13+
#[derive(
14+
Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize, Default,
15+
)]
16+
#[serde(rename_all = "snake_case")]
17+
pub enum SandboxTier {
18+
/// No isolation — direct host access.
19+
#[default]
20+
None,
21+
/// Basic restrictions (e.g. seccomp, pledge).
22+
Basic,
23+
/// Process-level isolation (e.g. bubblewrap, firejail).
24+
Process,
25+
/// Full container isolation (e.g. Apple Containers, Docker).
26+
Container,
27+
}
28+
29+
/// Resource limits for sandboxed command execution.
30+
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
31+
pub struct SandboxLimits {
32+
/// Maximum wall-clock execution time in seconds.
33+
pub max_runtime_secs: u64,
34+
/// Maximum bytes for stdout/stderr output.
35+
pub max_output_bytes: usize,
36+
/// Maximum memory in megabytes (optional, not always enforced).
37+
#[serde(default, skip_serializing_if = "Option::is_none")]
38+
pub max_memory_mb: Option<u64>,
39+
}
40+
41+
impl Default for SandboxLimits {
42+
fn default() -> Self {
43+
Self {
44+
max_runtime_secs: 30,
45+
max_output_bytes: 64 * 1024,
46+
max_memory_mb: None,
47+
}
48+
}
49+
}
50+
51+
/// Network access policy for sandboxed execution.
52+
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Default)]
53+
#[serde(tag = "policy", rename_all = "snake_case")]
54+
pub enum NetworkPolicy {
55+
/// No network access allowed.
56+
#[default]
57+
Disabled,
58+
/// Unrestricted network access.
59+
AllowAll,
60+
/// Network access limited to specific hosts.
61+
AllowList {
62+
#[serde(default)]
63+
hosts: Vec<String>,
64+
},
65+
}
66+
67+
#[cfg(test)]
68+
mod tests {
69+
use super::*;
70+
71+
// ── SandboxTier tests ──
72+
73+
#[test]
74+
fn tier_ordering() {
75+
assert!(SandboxTier::None < SandboxTier::Basic);
76+
assert!(SandboxTier::Basic < SandboxTier::Process);
77+
assert!(SandboxTier::Process < SandboxTier::Container);
78+
}
79+
80+
#[test]
81+
fn tier_default_is_none() {
82+
assert_eq!(SandboxTier::default(), SandboxTier::None);
83+
}
84+
85+
#[test]
86+
fn tier_serde_roundtrip() {
87+
for tier in [
88+
SandboxTier::None,
89+
SandboxTier::Basic,
90+
SandboxTier::Process,
91+
SandboxTier::Container,
92+
] {
93+
let json = serde_json::to_string(&tier).unwrap();
94+
let back: SandboxTier = serde_json::from_str(&json).unwrap();
95+
assert_eq!(back, tier);
96+
}
97+
assert_eq!(
98+
serde_json::to_string(&SandboxTier::None).unwrap(),
99+
"\"none\""
100+
);
101+
assert_eq!(
102+
serde_json::to_string(&SandboxTier::Container).unwrap(),
103+
"\"container\""
104+
);
105+
}
106+
107+
#[test]
108+
fn tier_ge_comparison_for_policy() {
109+
let required = SandboxTier::Process;
110+
assert!(SandboxTier::Process >= required);
111+
assert!(SandboxTier::Container >= required);
112+
assert!(SandboxTier::Basic < required);
113+
assert!(SandboxTier::None < required);
114+
}
115+
116+
// ── SandboxLimits tests ──
117+
118+
#[test]
119+
fn limits_default() {
120+
let limits = SandboxLimits::default();
121+
assert_eq!(limits.max_runtime_secs, 30);
122+
assert_eq!(limits.max_output_bytes, 64 * 1024);
123+
assert!(limits.max_memory_mb.is_none());
124+
}
125+
126+
#[test]
127+
fn limits_serde_roundtrip() {
128+
let limits = SandboxLimits {
129+
max_runtime_secs: 60,
130+
max_output_bytes: 128 * 1024,
131+
max_memory_mb: Some(512),
132+
};
133+
let json = serde_json::to_string(&limits).unwrap();
134+
let back: SandboxLimits = serde_json::from_str(&json).unwrap();
135+
assert_eq!(limits, back);
136+
}
137+
138+
#[test]
139+
fn limits_omits_none_memory() {
140+
let limits = SandboxLimits::default();
141+
let json = serde_json::to_string(&limits).unwrap();
142+
assert!(!json.contains("max_memory_mb"));
143+
}
144+
145+
// ── NetworkPolicy tests ──
146+
147+
#[test]
148+
fn network_policy_default_is_disabled() {
149+
assert_eq!(NetworkPolicy::default(), NetworkPolicy::Disabled);
150+
}
151+
152+
#[test]
153+
fn network_policy_disabled_serde() {
154+
let policy = NetworkPolicy::Disabled;
155+
let json = serde_json::to_string(&policy).unwrap();
156+
assert!(json.contains("\"policy\":\"disabled\""));
157+
let back: NetworkPolicy = serde_json::from_str(&json).unwrap();
158+
assert_eq!(policy, back);
159+
}
160+
161+
#[test]
162+
fn network_policy_allow_all_serde() {
163+
let policy = NetworkPolicy::AllowAll;
164+
let json = serde_json::to_string(&policy).unwrap();
165+
let back: NetworkPolicy = serde_json::from_str(&json).unwrap();
166+
assert_eq!(policy, back);
167+
}
168+
169+
#[test]
170+
fn network_policy_allow_list_serde() {
171+
let policy = NetworkPolicy::AllowList {
172+
hosts: vec!["api.anthropic.com".into(), "api.openai.com".into()],
173+
};
174+
let json = serde_json::to_string(&policy).unwrap();
175+
assert!(json.contains("api.anthropic.com"));
176+
let back: NetworkPolicy = serde_json::from_str(&json).unwrap();
177+
assert_eq!(policy, back);
178+
}
179+
180+
#[test]
181+
fn network_policy_allow_list_empty_hosts() {
182+
let json = r#"{"policy":"allow_list"}"#;
183+
let policy: NetworkPolicy = serde_json::from_str(json).unwrap();
184+
match policy {
185+
NetworkPolicy::AllowList { hosts } => assert!(hosts.is_empty()),
186+
_ => panic!("expected AllowList"),
187+
}
188+
}
189+
}

0 commit comments

Comments
 (0)