use std::path::{Path, PathBuf};
use crate::permissions::{ApprovalOutcome, ApprovalRequest, PermissionsApprovalHandler};
use crate::plugins::TrustDecision;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum TrustSurface {
Plugins,
Hooks,
Instructions,
}
impl TrustSurface {
fn label(self) -> &'static str {
match self {
TrustSurface::Plugins => "config-declared plugin code",
TrustSurface::Hooks => "config-declared lifecycle hooks",
TrustSurface::Instructions => "project instruction files (CLAUDE.md / AGENTS.md)",
}
}
fn undecided(self) -> bool {
match self {
TrustSurface::Plugins => false,
TrustSurface::Hooks | TrustSurface::Instructions => true,
}
}
}
#[derive(Debug, serde::Serialize, serde::Deserialize)]
struct TrustRecord {
trusted: bool,
}
pub fn default_trust_store(cwd: &Path) -> PathBuf {
crate::agent::global_instructions_dir()
.join("trust")
.join(format!("{}.json", crate::checkpoint::project_tag(cwd)))
}
fn store_path(config: &crate::Config) -> PathBuf {
config
.trust_store
.clone()
.unwrap_or_else(|| default_trust_store(&config.cwd))
}
fn recorded(config: &crate::Config) -> Option<bool> {
let text = std::fs::read_to_string(store_path(config)).ok()?;
serde_json::from_str::<TrustRecord>(&text)
.ok()
.map(|r| r.trusted)
}
fn record(config: &crate::Config, trusted: bool) {
let path = store_path(config);
if let Some(parent) = path.parent() {
let _ = std::fs::create_dir_all(parent);
}
if let Ok(text) = serde_json::to_string(&TrustRecord { trusted }) {
let _ = std::fs::write(&path, text);
}
}
pub fn revoke(config: &crate::Config) {
let _ = std::fs::remove_file(store_path(config));
}
pub fn is_trusted(config: &crate::Config, surface: TrustSurface) -> bool {
if !config.trust_enabled {
return surface.undecided();
}
match config.trust_default {
TrustDecision::Always => return true,
TrustDecision::Never => return false,
TrustDecision::Ask => {}
}
if let Some(answer) = recorded(config) {
return answer;
}
let Some(handler) = config.trust_handler.as_deref() else {
return surface.undecided();
};
ask(config, handler, surface)
}
fn ask(
config: &crate::Config,
handler: &dyn PermissionsApprovalHandler,
surface: TrustSurface,
) -> bool {
let project = config.cwd.display().to_string();
let raw_args = serde_json::json!({
"project": project,
"loading": surface.label(),
});
let req = ApprovalRequest {
tool: "trust",
subject: Some(&project),
raw_args: &raw_args,
};
match handler.ask(&req) {
ApprovalOutcome::Deny => false,
ApprovalOutcome::Allow => true,
ApprovalOutcome::AllowForSession => {
record(config, true);
true
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::Config;
struct Answer(ApprovalOutcome);
impl PermissionsApprovalHandler for Answer {
fn ask(&self, _req: &ApprovalRequest) -> ApprovalOutcome {
self.0
}
}
fn tmp(tag: &str) -> PathBuf {
let dir = std::env::temp_dir().join(format!(
"supercode-trust-test-{tag}-{}-{:?}",
std::process::id(),
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_nanos()
));
std::fs::create_dir_all(&dir).unwrap();
dir
}
fn config(tag: &str) -> (Config, PathBuf) {
let dir = tmp(tag);
let store = dir.join("trust.json");
let mut c = Config::builder().cwd(dir.clone()).build();
c.trust_enabled = true;
c.trust_default = TrustDecision::Ask;
c.trust_store = Some(store.clone());
(c, store)
}
#[test]
fn the_module_being_off_means_no_gate_not_a_blanket_refusal() {
let (mut c, _) = config("master");
c.trust_enabled = false;
c.trust_default = TrustDecision::Always;
assert!(!is_trusted(&c, TrustSurface::Plugins));
assert!(is_trusted(&c, TrustSurface::Instructions));
}
#[test]
fn ask_without_a_door_is_the_pre_bp10_outcome_on_every_surface() {
let (c, _) = config("no-door");
assert!(!is_trusted(&c, TrustSurface::Plugins));
assert!(is_trusted(&c, TrustSurface::Hooks));
assert!(is_trusted(&c, TrustSurface::Instructions));
}
#[test]
fn never_refuses_every_surface() {
let (mut c, _) = config("never");
c.trust_default = TrustDecision::Never;
assert!(!is_trusted(&c, TrustSurface::Instructions));
assert!(!is_trusted(&c, TrustSurface::Hooks));
}
#[test]
fn a_door_that_refuses_blocks_code_and_records_nothing() {
let (mut c, store) = config("refuse");
c.trust_handler = Some(std::sync::Arc::new(Answer(ApprovalOutcome::Deny)));
assert!(!is_trusted(&c, TrustSurface::Plugins));
assert!(!store.exists(), "a refusal must not be remembered as a no");
}
#[test]
fn dont_ask_again_persists_and_a_revoke_re_asks() {
let (mut c, store) = config("persist");
c.trust_handler = Some(std::sync::Arc::new(Answer(
ApprovalOutcome::AllowForSession,
)));
assert!(is_trusted(&c, TrustSurface::Plugins));
assert!(store.exists(), "the grant is on disk");
let mut c2 = Config::builder().cwd(c.cwd.clone()).build();
c2.trust_enabled = true;
c2.trust_default = TrustDecision::Ask;
c2.trust_store = Some(store.clone());
assert!(is_trusted(&c2, TrustSurface::Plugins));
revoke(&c2);
assert!(!store.exists());
assert!(
!is_trusted(&c2, TrustSurface::Plugins),
"after a revoke, a doorless run is back to refusing code"
);
}
#[test]
fn a_one_shot_allow_is_not_remembered() {
let (mut c, store) = config("one-shot");
c.trust_handler = Some(std::sync::Arc::new(Answer(ApprovalOutcome::Allow)));
assert!(is_trusted(&c, TrustSurface::Plugins));
assert!(!store.exists());
}
}