Skip to content
Merged
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
54 changes: 44 additions & 10 deletions crates/aios-protocol/src/policy.rs
Original file line number Diff line number Diff line change
Expand Up @@ -50,13 +50,19 @@ pub struct PolicySet {

impl PolicySet {
/// Heavily restricted — anonymous public users. No side-effecting capabilities.
///
/// Shell execution (`exec:cmd:*`) is NOT gated (approval queue) — it is
/// absent from both `allow_capabilities` and `gate_capabilities`, so the
/// policy engine immediately **denies** any bash/shell tool call without
/// creating an approval ticket. BRO-216.
///
/// 5 events/turn, 30s tool runtime.
pub fn anonymous() -> Self {
Self {
allow_capabilities: vec![Capability::new("fs:read:/session/**")],
// exec:cmd:* removed — falls through to denied by StaticPolicyEngine.
gate_capabilities: vec![
Capability::new("fs:write:**"),
Capability::new("exec:cmd:*"),
Capability::new("net:egress:*"),
Capability::new("secrets:read:*"),
],
Expand All @@ -65,17 +71,33 @@ impl PolicySet {
}
}

/// Read + search only — authenticated free tier users.
/// Read + network + limited shell — authenticated free tier users.
///
/// Shell execution is restricted to a safe read-only whitelist; unlisted
/// commands are denied immediately (not gated). BRO-216.
///
/// 15 events/turn, 30s tool runtime.
pub fn free() -> Self {
Self {
allow_capabilities: vec![
Capability::new("fs:read:/session/**"),
Capability::new("net:egress:*"),
// Shell whitelist — safe read-only commands only.
Capability::new("exec:cmd:cat"),
Capability::new("exec:cmd:ls"),
Capability::new("exec:cmd:echo"),
Capability::new("exec:cmd:grep"),
Capability::new("exec:cmd:jq"),
Capability::new("exec:cmd:python3"),
Capability::new("exec:cmd:find"),
Capability::new("exec:cmd:head"),
Capability::new("exec:cmd:tail"),
Capability::new("exec:cmd:sort"),
Capability::new("exec:cmd:wc"),
],
// exec:cmd:* removed — unlisted exec commands fall through to denied.
gate_capabilities: vec![
Capability::new("fs:write:**"),
Capability::new("exec:cmd:*"),
Capability::new("secrets:read:*"),
],
max_tool_runtime_secs: 30,
Expand Down Expand Up @@ -166,30 +188,42 @@ mod tests {
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);
// exec:cmd:* must NOT be in gate_capabilities — it must be denied outright (BRO-216).
assert_eq!(ps.gate_capabilities.len(), 3);
assert_eq!(ps.max_tool_runtime_secs, 30);
assert_eq!(ps.max_events_per_turn, 5);
// anonymous cannot exec
// anonymous: exec is in neither allow nor gate → immediately denied
let exec_cap = Capability::new("exec:cmd:*");
assert!(!ps.allow_capabilities.contains(&exec_cap));
assert!(ps.gate_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);
// allow: session read + net egress + 11 whitelisted exec commands
assert_eq!(ps.allow_capabilities.len(), 13);
// gate: fs:write + secrets (exec removed — unlisted exec → denied)
assert_eq!(ps.gate_capabilities.len(), 2);
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
// free has whitelisted exec commands
assert!(
ps.allow_capabilities
.contains(&Capability::new("exec:cmd:cat"))
);
assert!(
ps.allow_capabilities
.contains(&Capability::new("exec:cmd:grep"))
);
// exec:cmd:* wildcard is NOT in gate (unlisted commands → denied immediately)
assert!(
ps.gate_capabilities
!ps.gate_capabilities
.contains(&Capability::new("exec:cmd:*"))
);
}
Expand Down
Loading