Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
68 changes: 68 additions & 0 deletions crates/aios-protocol/src/event.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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,
},
Expand Down Expand Up @@ -1531,6 +1576,29 @@ impl From<EventKindKnown> 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 },
}
Expand Down
2 changes: 1 addition & 1 deletion crates/aios-protocol/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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::{
Expand Down
115 changes: 115 additions & 0 deletions crates/aios-protocol/src/policy.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
}
Comment on lines +63 to +65
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Verify whether tier limit fields are consumed beyond struct initialization.
# Expected: usages in enforcement/runtime code paths, not only in constructors/tests.

rg -n -C3 '\bmax_tool_runtime_secs\b|\bmax_events_per_turn\b' --type=rust
rg -n -C3 'from_policy_set\s*\(' --type=rust

Repository: broomva/aiOS

Length of output: 12726


🏁 Script executed:

# Search for any enforcement logic that might use these tier limits
rg -n 'max_tool_runtime_secs|max_events_per_turn' --type=rust -A 2 -B 2 | head -100

# Also search for where runtime constraints are actually enforced
rg -n 'timeout|duration|runtime.*limit|event.*count' --type=rust -i | grep -i 'enforce\|check\|validate' | head -20

# Check if kernel or runtime code enforces these limits
rg -n 'StaticPolicyEngine|SessionPolicyEngine' --type=rust -A 5 | head -50

Repository: broomva/aiOS

Length of output: 9393


🏁 Script executed:

# Search kernel for timeout/duration enforcement
rg -n 'timeout|duration|max_tool|max_events' crates/aios-kernel/src/ --type=rust

# Search sandbox for runtime constraint enforcement
rg -n 'timeout|duration|max_tool|max_events' crates/aios-sandbox/src/ --type=rust 2>/dev/null || echo "No sandbox enforcement found"

# Check if there's a separate timeout/constraint mechanism in runtime
fd -e rs . crates/ -x grep -l 'timeout\|duration' {} \; | head -10

Repository: broomva/aiOS

Length of output: 1301


🏁 Script executed:

# Check sandbox module to see what timeout limit is used
sed -n '100,110p' crates/aios-sandbox/src/lib.rs

# Search for where execute() is called and what limit is passed
rg -n '\.execute\(' --type=rust -B 3 -A 3 | head -50

# Check if sandbox takes policy as parameter
rg -n 'impl.*Sandbox|pub.*execute|fn execute' crates/aios-sandbox/src/lib.rs -A 5 | head -40

Repository: broomva/aiOS

Length of output: 3023


🏁 Script executed:

# Find ToolExecutionRequest struct definition
rg -n 'struct ToolExecutionRequest' --type=rust -A 10

# Find where ToolExecutionRequest is created in runtime
rg -n 'ToolExecutionRequest {' crates/aios-runtime/src/lib.rs -A 10 | head -30

# Check if limits field references PolicySet anywhere
rg -n 'limits.*policy|policy.*limits' --type=rust

Repository: broomva/aiOS

Length of output: 876


🏁 Script executed:

# Final check: see if SandboxLimits is ever set from policy
rg -n 'SandboxLimits' crates/ --type=rust -B 2 -A 2 | grep -E 'policy|PolicySet|max_tool|max_events' || echo "No connection found"

# Verify ToolExecutionRequest definitely doesn't have limits
cat crates/aios-protocol/src/ports.rs | sed -n '83,88p'

Repository: broomva/aiOS

Length of output: 273


Tier limit fields are not enforced anywhere downstream.

PolicySet configures max_tool_runtime_secs and max_events_per_turn, but StaticPolicyEngine::from_policy_set() (crates/aios-policy/src/lib.rs:61–66) discards them, storing only allow/gate capabilities. ToolExecutionRequest has no limits field, and SandboxRunner uses hardcoded defaults (30 seconds), completely bypassing policy-driven limits.

Either remove these unused fields or implement the full enforcement path with tests proving the limits are applied. Per coding guidelines, "Add or extend tests with every behavioral change; do not silently widen capabilities or safety boundaries."

Also applies to: 81-83, 92-94, 103-105

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@crates/aios-protocol/src/policy.rs` around lines 63 - 65, PolicySet defines
tier limits (max_tool_runtime_secs, max_events_per_turn) but they are dropped in
StaticPolicyEngine::from_policy_set and never propagated to ToolExecutionRequest
or enforced by SandboxRunner which uses hardcoded defaults; update
StaticPolicyEngine::from_policy_set to store these limit fields, extend the
policy-to-execution path to populate ToolExecutionRequest (or an
execution-limits struct) with max_tool_runtime_secs and max_events_per_turn,
modify SandboxRunner to read and enforce those limits instead of hardcoded
values, and add unit/integration tests that assert the runner respects
policy-specified time and event limits (and/or remove the fields if you decide
not to support them).

}

/// 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 {
Expand Down Expand Up @@ -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("*")));
}
}
Loading