M0: scaffold Cargo workspace, core types, event bus, permission engine, storage actor
Seven-crate workspace per docs/01-architecture.md. harness-core gets the domain types (Session/Message/Part/ToolState), a broadcast EventBus, the last-match-wins wildcard permission evaluator, and a SQLite storage actor (dedicated thread + mpsc, JSON-blob rows) with roundtrip tests. All other crates are compiling stubs. CI runs fmt/clippy -D warnings/test.
This commit is contained in:
@@ -0,0 +1,3 @@
|
||||
pub mod rule;
|
||||
|
||||
pub use rule::{evaluate, Action, Rule, Ruleset};
|
||||
@@ -0,0 +1,115 @@
|
||||
use globset::GlobBuilder;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum Action {
|
||||
Allow,
|
||||
Deny,
|
||||
Ask,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct Rule {
|
||||
pub permission: String,
|
||||
pub pattern: String,
|
||||
pub action: Action,
|
||||
}
|
||||
|
||||
pub type Ruleset = Vec<Rule>;
|
||||
|
||||
/// Wildcard match, `*` crosses separators (opencode/globset `literal_separator(false)`).
|
||||
fn glob_match(pattern: &str, value: &str) -> bool {
|
||||
if pattern == "*" {
|
||||
return true;
|
||||
}
|
||||
GlobBuilder::new(pattern)
|
||||
.literal_separator(false)
|
||||
.build()
|
||||
.map(|g| g.compile_matcher().is_match(value))
|
||||
.unwrap_or(false)
|
||||
}
|
||||
|
||||
/// Flatten `stack` (lowest to highest precedence) and return the action of the *last*
|
||||
/// rule whose `permission` and `pattern` both wildcard-match. Defaults to `Ask`.
|
||||
pub fn evaluate(stack: &[&Ruleset], permission: &str, pattern: &str) -> Action {
|
||||
let mut result = Action::Ask;
|
||||
for ruleset in stack {
|
||||
for rule in ruleset.iter() {
|
||||
if glob_match(&rule.permission, permission) && glob_match(&rule.pattern, pattern) {
|
||||
result = rule.action;
|
||||
}
|
||||
}
|
||||
}
|
||||
result
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
fn rule(permission: &str, pattern: &str, action: Action) -> Rule {
|
||||
Rule {
|
||||
permission: permission.to_string(),
|
||||
pattern: pattern.to_string(),
|
||||
action,
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn defaults_to_ask_when_nothing_matches() {
|
||||
let rules: Ruleset = vec![rule("bash", "ls*", Action::Allow)];
|
||||
assert_eq!(evaluate(&[&rules], "bash", "rm -rf /"), Action::Ask);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn exact_match_wins() {
|
||||
let rules: Ruleset = vec![rule("edit", "*.lock", Action::Deny)];
|
||||
assert_eq!(evaluate(&[&rules], "edit", "Cargo.lock"), Action::Deny);
|
||||
assert_eq!(evaluate(&[&rules], "edit", "main.rs"), Action::Ask);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn wildcard_crosses_path_separators() {
|
||||
let rules: Ruleset = vec![rule("read", "src/*", Action::Allow)];
|
||||
assert_eq!(evaluate(&[&rules], "read", "src/a/b/c.rs"), Action::Allow);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn last_match_within_a_ruleset_wins() {
|
||||
let rules: Ruleset = vec![
|
||||
rule("bash", "git *", Action::Allow),
|
||||
rule("bash", "git push*", Action::Deny),
|
||||
];
|
||||
assert_eq!(evaluate(&[&rules], "bash", "git status"), Action::Allow);
|
||||
assert_eq!(evaluate(&[&rules], "bash", "git push origin"), Action::Deny);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn later_ruleset_in_the_stack_overrides_earlier_ones() {
|
||||
let config: Ruleset = vec![rule("edit", "*", Action::Ask)];
|
||||
let agent: Ruleset = vec![rule("edit", "*", Action::Deny)];
|
||||
let session: Ruleset = vec![rule("edit", "main.rs", Action::Allow)];
|
||||
|
||||
// config < agent < session precedence, matching engine's [config, agent, session] stack.
|
||||
assert_eq!(
|
||||
evaluate(&[&config, &agent, &session], "edit", "main.rs"),
|
||||
Action::Allow
|
||||
);
|
||||
assert_eq!(
|
||||
evaluate(&[&config, &agent, &session], "edit", "other.rs"),
|
||||
Action::Deny
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn permission_field_itself_can_wildcard_match() {
|
||||
let rules: Ruleset = vec![rule("mcp", "*", Action::Ask)];
|
||||
assert_eq!(evaluate(&[&rules], "mcp", "context7_search"), Action::Ask);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn empty_stack_defaults_to_ask() {
|
||||
assert_eq!(evaluate(&[], "bash", "ls"), Action::Ask);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user