use std::path::PathBuf;
use std::sync::Arc;
use vigil_audit::Ledger;
use vigil_firewall::scorer::DescriptorOracle;
use vigil_mcp::RegistryDescriptorOracle;
use vigil_policy::{defaults::default_ruleset, PolicyEngine};
use vigil_types::ToolInvocation;
use crate::{Firewall, FirewallConfig, FirewallError, FirewallOutcome, OAuthScopeContext};
#[derive(Debug, Clone, Default)]
pub struct FirewallBuilder {
project_roots: Vec<String>,
allowed_hosts: Vec<String>,
ledger_path: Option<PathBuf>,
}
impl FirewallBuilder {
pub fn new() -> Self {
Self::default()
}
pub fn project_roots<I, S>(mut self, roots: I) -> Self
where
I: IntoIterator<Item = S>,
S: Into<String>,
{
self.project_roots = roots.into_iter().map(Into::into).collect();
self
}
pub fn allowed_hosts<I, S>(mut self, hosts: I) -> Self
where
I: IntoIterator<Item = S>,
S: Into<String>,
{
self.allowed_hosts = hosts.into_iter().map(Into::into).collect();
self
}
pub fn ledger_path(mut self, path: impl Into<PathBuf>) -> Self {
self.ledger_path = Some(path.into());
self
}
pub fn build(self) -> Result<SdkFirewall, FirewallBuildError> {
let ledger = match self.ledger_path {
Some(path) => Ledger::open(path),
None => Ledger::open_in_memory(),
}
.map_err(|e| FirewallBuildError::LedgerOpen {
reason: e.to_string(),
})?;
let ledger = Arc::new(ledger);
let policy = PolicyEngine::new(default_ruleset());
let config = FirewallConfig {
project_roots: self.project_roots,
allowed_hosts: self.allowed_hosts,
..Default::default()
};
let fw = Firewall::new(Arc::clone(&ledger), policy, config);
let oracle = RegistryDescriptorOracle::new(ledger);
Ok(SdkFirewall { fw, oracle })
}
}
#[derive(Debug)]
pub struct SdkFirewall {
fw: Firewall,
oracle: RegistryDescriptorOracle,
}
impl SdkFirewall {
pub fn decide(&self, call: &ToolInvocation) -> Result<FirewallOutcome, FirewallError> {
self.fw.evaluate(
call,
&self.oracle as &dyn DescriptorOracle,
OAuthScopeContext::NonOauth,
)
}
}
#[derive(Debug)]
#[non_exhaustive]
pub enum FirewallBuildError {
LedgerOpen {
reason: String,
},
}
impl std::fmt::Display for FirewallBuildError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::LedgerOpen { reason } => {
write!(f, "firewall build: ledger open failed: {reason}")
}
}
}
}
impl std::error::Error for FirewallBuildError {}
#[cfg(test)]
#[allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)]
mod tests {
use super::*;
use vigil_types::DecisionKind;
fn mk_call(tool: &str, args: serde_json::Value) -> ToolInvocation {
ToolInvocation {
invocation_id: "test-invocation-id".into(),
session_id: "test-session".into(),
server_id: "test-srv".into(),
tool_name: tool.into(),
args,
descriptor_hash: "test-hash".into(),
requested_at: 0,
}
}
#[test]
fn build_assembles_usable_firewall() {
let fw = FirewallBuilder::new().project_roots(["/proj"]).build();
assert!(
fw.is_ok(),
"default build should succeed, got {:?}",
fw.err()
);
}
#[test]
fn decide_fresh_repo_read_is_approve_not_blanket_allow() {
let fw = FirewallBuilder::new()
.project_roots(["/proj"])
.build()
.unwrap();
let call = mk_call(
"fs_read_file",
serde_json::json!({"path": "/proj/src/main.rs"}),
);
let outcome = fw.decide(&call).expect("decide should succeed");
assert_eq!(
outcome.decision_kind(),
DecisionKind::Approve,
"fresh ledger 上 in-repo 读应因 FirstSeen 走 Approve(证明默认 oracle = RegistryDescriptorOracle,非 ApprovedStable blanket-allow)"
);
}
#[test]
fn decide_denies_write_outside_project() {
let fw = FirewallBuilder::new()
.project_roots(["/proj"])
.build()
.unwrap();
let call = mk_call("fs_write_file", serde_json::json!({"path": "/etc/hosts"}));
let outcome = fw.decide(&call).expect("decide should succeed");
assert_eq!(
outcome.decision_kind(),
DecisionKind::Deny,
"项目外写必须 Deny(fail-closed)"
);
}
#[test]
fn decide_denies_destructive_shell() {
let fw = FirewallBuilder::new()
.project_roots(["/proj"])
.build()
.unwrap();
let call = mk_call(
"shell_run",
serde_json::json!({"argv": ["rm", "-rf", "/home/user/Downloads"]}),
);
let outcome = fw.decide(&call).expect("decide should succeed");
assert_eq!(
outcome.decision_kind(),
DecisionKind::Deny,
"rm -rf 必须 Deny"
);
}
#[test]
fn build_with_ledger_path() {
let tmp = std::env::temp_dir().join("vigil_sdk_fwbuilder_test.sqlite3");
let _ = std::fs::remove_file(&tmp);
let fw = FirewallBuilder::new().ledger_path(&tmp).build();
assert!(
fw.is_ok(),
"file-backed ledger build should succeed, got {:?}",
fw.err()
);
let _ = std::fs::remove_file(&tmp);
}
}