fix(policy): standardize side-effect tool autonomy gates

This commit is contained in:
fettpl 2026-02-17 22:56:07 +01:00 committed by Chummy
parent 89d0fb9a1e
commit 4f9c87ff74
6 changed files with 369 additions and 38 deletions

View file

@ -24,6 +24,13 @@ pub enum CommandRiskLevel {
High,
}
/// Classifies whether a tool operation is read-only or side-effecting.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ToolOperation {
Read,
Act,
}
/// Sliding-window action tracker for rate limiting.
#[derive(Debug)]
pub struct ActionTracker {
@ -530,6 +537,33 @@ impl SecurityPolicy {
self.autonomy != AutonomyLevel::ReadOnly
}
/// Enforce policy for a tool operation.
///
/// Read operations are always allowed by autonomy/rate gates.
/// Act operations require non-readonly autonomy and available action budget.
pub fn enforce_tool_operation(
&self,
operation: ToolOperation,
operation_name: &str,
) -> Result<(), String> {
match operation {
ToolOperation::Read => Ok(()),
ToolOperation::Act => {
if !self.can_act() {
return Err(format!(
"Security policy: read-only mode, cannot perform '{operation_name}'"
));
}
if !self.record_action() {
return Err("Rate limit exceeded: action budget exhausted".to_string());
}
Ok(())
}
}
}
/// Record an action and check if the rate limit has been exceeded.
/// Returns `true` if the action is allowed, `false` if rate-limited.
pub fn record_action(&self) -> bool {
@ -616,6 +650,35 @@ mod tests {
assert!(full_policy().can_act());
}
#[test]
fn enforce_tool_operation_read_allowed_in_readonly_mode() {
let p = readonly_policy();
assert!(p
.enforce_tool_operation(ToolOperation::Read, "memory_recall")
.is_ok());
}
#[test]
fn enforce_tool_operation_act_blocked_in_readonly_mode() {
let p = readonly_policy();
let err = p
.enforce_tool_operation(ToolOperation::Act, "memory_store")
.unwrap_err();
assert!(err.contains("read-only mode"));
}
#[test]
fn enforce_tool_operation_act_uses_rate_budget() {
let p = SecurityPolicy {
max_actions_per_hour: 0,
..default_policy()
};
let err = p
.enforce_tool_operation(ToolOperation::Act, "memory_store")
.unwrap_err();
assert!(err.contains("Rate limit exceeded"));
}
// ── is_command_allowed ───────────────────────────────────
#[test]