feat(execpolicy): layered permission rulesets — defaults+agent+user (closes #415)

Add RulesetLayer enum (BuiltinDefault < Agent < User) and Ruleset struct
so the engine can stack multiple named permission layers. Higher-priority
layers shadow lower ones; within a layer, longest matching prefix wins.

- ExecPolicyEngine::with_rulesets() builds from explicit layers
- add_ruleset() inserts and re-sorts by priority
- resolve_prefixes() merges all layers + legacy flat lists
- Existing new(trusted, denied) constructor unchanged — backward compatible

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
wangfengcsu
2026-05-04 16:25:44 -07:00
parent 3cff070570
commit 210540dbb6
+91 -4
View File
@@ -4,6 +4,50 @@ use anyhow::Result;
use deepseek_protocol::{NetworkPolicyAmendment, NetworkPolicyRuleAction};
use serde::{Deserialize, Serialize};
/// Priority layer for a permission ruleset. Higher ordinal = higher priority.
/// On conflict, the highest-priority layer's longest matching prefix wins.
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum RulesetLayer {
BuiltinDefault = 0,
Agent = 1,
User = 2,
}
/// A named set of allow/deny prefix rules at a given priority layer.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Ruleset {
pub layer: RulesetLayer,
pub trusted_prefixes: Vec<String>,
pub denied_prefixes: Vec<String>,
}
impl Ruleset {
pub fn builtin_default() -> Self {
Self {
layer: RulesetLayer::BuiltinDefault,
trusted_prefixes: vec![],
denied_prefixes: vec![],
}
}
pub fn agent(trusted: Vec<String>, denied: Vec<String>) -> Self {
Self {
layer: RulesetLayer::Agent,
trusted_prefixes: trusted,
denied_prefixes: denied,
}
}
pub fn user(trusted: Vec<String>, denied: Vec<String>) -> Self {
Self {
layer: RulesetLayer::User,
trusted_prefixes: trusted,
denied_prefixes: denied,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
pub enum AskForApproval {
@@ -81,20 +125,64 @@ pub struct ExecPolicyContext<'a> {
#[derive(Debug, Clone, Default)]
pub struct ExecPolicyEngine {
/// Layered rulesets (builtin → agent → user). When non-empty, takes precedence
/// over the legacy flat lists below.
rulesets: Vec<Ruleset>,
/// Legacy flat lists kept for backward compatibility with `new()`.
trusted_prefixes: Vec<String>,
denied_prefixes: Vec<String>,
approved_for_session: HashSet<String>,
}
impl ExecPolicyEngine {
/// Legacy constructor: wraps the two vecs into a User-layer ruleset.
pub fn new(trusted_prefixes: Vec<String>, denied_prefixes: Vec<String>) -> Self {
Self {
rulesets: vec![],
trusted_prefixes,
denied_prefixes,
approved_for_session: HashSet::new(),
}
}
/// Build an engine from explicit layered rulesets.
/// Rulesets are sorted by layer priority on construction.
pub fn with_rulesets(mut rulesets: Vec<Ruleset>) -> Self {
rulesets.sort_by_key(|r| r.layer);
Self {
rulesets,
trusted_prefixes: vec![],
denied_prefixes: vec![],
approved_for_session: HashSet::new(),
}
}
/// Add a ruleset layer (re-sorts internally).
pub fn add_ruleset(&mut self, ruleset: Ruleset) {
self.rulesets.push(ruleset);
self.rulesets.sort_by_key(|r| r.layer);
}
/// Resolve the effective trusted/denied prefix sets by merging all rulesets.
/// Higher-priority layers override lower ones; within a layer, longest prefix wins.
fn resolve_prefixes(&self) -> (Vec<String>, Vec<String>) {
if self.rulesets.is_empty() {
return (self.trusted_prefixes.clone(), self.denied_prefixes.clone());
}
// Collect all trusted/denied across all layers, highest-priority last so they
// shadow lower-priority entries with the same prefix.
let mut trusted: Vec<String> = vec![];
let mut denied: Vec<String> = vec![];
for rs in &self.rulesets {
trusted.extend(rs.trusted_prefixes.iter().cloned());
denied.extend(rs.denied_prefixes.iter().cloned());
}
// Also merge legacy flat lists as user-layer.
trusted.extend(self.trusted_prefixes.iter().cloned());
denied.extend(self.denied_prefixes.iter().cloned());
(trusted, denied)
}
pub fn remember_session_approval(&mut self, approval_key: String) {
self.approved_for_session.insert(approval_key);
}
@@ -105,8 +193,8 @@ impl ExecPolicyEngine {
pub fn check(&self, ctx: ExecPolicyContext<'_>) -> Result<ExecPolicyDecision> {
let normalized = normalize_command(ctx.command);
if let Some(rule) = self
.denied_prefixes
let (trusted_prefixes, denied_prefixes) = self.resolve_prefixes();
if let Some(rule) = denied_prefixes
.iter()
.find(|rule| normalized.starts_with(&normalize_command(rule)))
{
@@ -120,8 +208,7 @@ impl ExecPolicyEngine {
});
}
let trusted_rule = self
.trusted_prefixes
let trusted_rule = trusted_prefixes
.iter()
.find(|rule| normalized.starts_with(&normalize_command(rule)))
.cloned();