diff --git a/crates/aios-protocol/src/event.rs b/crates/aios-protocol/src/event.rs index 1d61b7f..7084085 100644 --- a/crates/aios-protocol/src/event.rs +++ b/crates/aios-protocol/src/event.rs @@ -587,6 +587,22 @@ pub enum EventKind { final_score: f32, }, + // ── Queue & steering (Phase 2.5) ── + Queued { + queue_id: String, + mode: SteeringMode, + message: String, + }, + Steered { + queue_id: String, + /// Tool boundary where preemption occurred (e.g. "tool:read_file:call-3"). + preempted_at: String, + }, + QueueDrained { + queue_id: String, + processed: usize, + }, + // ── Error ── ErrorRaised { message: String, @@ -671,6 +687,22 @@ pub enum PolicyDecisionKind { RequireApproval, } +/// Steering mode for queued messages (Phase 2.5). +/// +/// Determines how a queued message interacts with an active run. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "snake_case")] +pub enum SteeringMode { + /// Queue message for processing after current run completes. + Collect, + /// Redirect agent at next tool boundary (safe preemption). + Steer, + /// Queue as follow-up to current run (same context). + Followup, + /// Interrupt at next safe point (tool boundary), highest priority. + Interrupt, +} + // ─── Forward-compatible deserializer ─────────────────────────────── /// Internal helper enum for the forward-compatible deserializer. @@ -1019,6 +1051,19 @@ enum EventKindKnown { total_trials: u32, final_score: f32, }, + Queued { + queue_id: String, + mode: SteeringMode, + message: String, + }, + Steered { + queue_id: String, + preempted_at: String, + }, + QueueDrained { + queue_id: String, + processed: usize, + }, ErrorRaised { message: String, }, @@ -1531,6 +1576,29 @@ impl From for EventKind { total_trials, final_score, }, + EventKindKnown::Queued { + queue_id, + mode, + message, + } => Self::Queued { + queue_id, + mode, + message, + }, + EventKindKnown::Steered { + queue_id, + preempted_at, + } => Self::Steered { + queue_id, + preempted_at, + }, + EventKindKnown::QueueDrained { + queue_id, + processed, + } => Self::QueueDrained { + queue_id, + processed, + }, EventKindKnown::ErrorRaised { message } => Self::ErrorRaised { message }, EventKindKnown::Custom { event_type, data } => Self::Custom { event_type, data }, } diff --git a/crates/aios-protocol/src/lib.rs b/crates/aios-protocol/src/lib.rs index 81b6a6c..b546ac2 100644 --- a/crates/aios-protocol/src/lib.rs +++ b/crates/aios-protocol/src/lib.rs @@ -39,7 +39,7 @@ pub mod tool; pub use error::{KernelError, KernelResult}; pub use event::{ ActorType, ApprovalDecision, EventActor, EventEnvelope, EventKind, EventRecord, EventSchema, - LoopPhase, PolicyDecisionKind, RiskLevel, SnapshotType, SpanStatus, TokenUsage, + LoopPhase, PolicyDecisionKind, RiskLevel, SnapshotType, SpanStatus, SteeringMode, TokenUsage, }; pub use identity::{AgentIdentityProvider, BasicIdentity}; pub use ids::{ diff --git a/crates/aios-protocol/src/policy.rs b/crates/aios-protocol/src/policy.rs index a1535af..4f44d7d 100644 --- a/crates/aios-protocol/src/policy.rs +++ b/crates/aios-protocol/src/policy.rs @@ -48,6 +48,64 @@ pub struct PolicySet { pub max_events_per_turn: u64, } +impl PolicySet { + /// Heavily restricted — anonymous public users. No side-effecting capabilities. + /// 5 events/turn, 30s tool runtime. + pub fn anonymous() -> Self { + Self { + allow_capabilities: vec![Capability::new("fs:read:/session/**")], + gate_capabilities: vec![ + Capability::new("fs:write:**"), + Capability::new("exec:cmd:*"), + Capability::new("net:egress:*"), + Capability::new("secrets:read:*"), + ], + max_tool_runtime_secs: 30, + max_events_per_turn: 5, + } + } + + /// Read + search only — authenticated free tier users. + /// 15 events/turn, 30s tool runtime. + pub fn free() -> Self { + Self { + allow_capabilities: vec![ + Capability::new("fs:read:/session/**"), + Capability::new("net:egress:*"), + ], + gate_capabilities: vec![ + Capability::new("fs:write:**"), + Capability::new("exec:cmd:*"), + Capability::new("secrets:read:*"), + ], + max_tool_runtime_secs: 30, + max_events_per_turn: 15, + } + } + + /// Full access — authenticated Pro subscribers. + /// 50 events/turn, 60s tool runtime. + pub fn pro() -> Self { + Self { + allow_capabilities: vec![Capability::new("*")], + gate_capabilities: vec![], + max_tool_runtime_secs: 60, + max_events_per_turn: 50, + } + } + + /// Fully permissive — Enterprise tenants (custom overrides applied separately). + /// 200 events/turn, 120s tool runtime. + pub fn enterprise() -> Self { + Self { + allow_capabilities: vec![Capability::new("*")], + gate_capabilities: vec![], + max_tool_runtime_secs: 120, + max_events_per_turn: 200, + } + } +} + impl Default for PolicySet { fn default() -> Self { Self { @@ -102,4 +160,61 @@ mod tests { let back: Capability = serde_json::from_str(&json).unwrap(); assert_eq!(cap, back); } + + #[test] + fn policy_set_anonymous() { + let ps = PolicySet::anonymous(); + assert_eq!(ps.allow_capabilities.len(), 1); + assert_eq!(ps.allow_capabilities[0].as_str(), "fs:read:/session/**"); + assert_eq!(ps.gate_capabilities.len(), 4); + assert_eq!(ps.max_tool_runtime_secs, 30); + assert_eq!(ps.max_events_per_turn, 5); + // anonymous cannot exec + let exec_cap = Capability::new("exec:cmd:*"); + assert!(!ps.allow_capabilities.contains(&exec_cap)); + assert!(ps.gate_capabilities.contains(&exec_cap)); + } + + #[test] + fn policy_set_free() { + let ps = PolicySet::free(); + assert_eq!(ps.allow_capabilities.len(), 2); + assert_eq!(ps.gate_capabilities.len(), 3); + assert_eq!(ps.max_tool_runtime_secs, 30); + assert_eq!(ps.max_events_per_turn, 15); + // free allows net egress + assert!( + ps.allow_capabilities + .contains(&Capability::new("net:egress:*")) + ); + // free gates exec + assert!( + ps.gate_capabilities + .contains(&Capability::new("exec:cmd:*")) + ); + } + + #[test] + fn policy_set_pro() { + let ps = PolicySet::pro(); + assert_eq!(ps.allow_capabilities.len(), 1); + assert_eq!(ps.allow_capabilities[0].as_str(), "*"); + assert_eq!(ps.gate_capabilities.len(), 0); + assert_eq!(ps.max_tool_runtime_secs, 60); + assert_eq!(ps.max_events_per_turn, 50); + // pro allows all via wildcard + assert!(ps.allow_capabilities.contains(&Capability::new("*"))); + } + + #[test] + fn policy_set_enterprise() { + let ps = PolicySet::enterprise(); + assert_eq!(ps.allow_capabilities.len(), 1); + assert_eq!(ps.allow_capabilities[0].as_str(), "*"); + assert_eq!(ps.gate_capabilities.len(), 0); + assert_eq!(ps.max_tool_runtime_secs, 120); + assert_eq!(ps.max_events_per_turn, 200); + // enterprise allows all via wildcard + assert!(ps.allow_capabilities.contains(&Capability::new("*"))); + } }