agentguard/commands/
doctor.rs1use agentguard_core::decision::DecisionLog;
7use agentguard_core::decode_chain_secret;
8use agentguard_core::policy::PolicyStore;
9use anyhow::Result;
10use std::path::Path;
11
12#[derive(Debug, Clone)]
14pub enum CheckStatus {
15 Ok,
16 Warn(String),
17 Fail(String),
18}
19
20impl CheckStatus {
21 pub fn symbol(&self) -> &'static str {
23 match self {
24 CheckStatus::Ok => "✓",
25 CheckStatus::Warn(_) => "!",
26 CheckStatus::Fail(_) => "✗",
27 }
28 }
29}
30
31#[derive(Debug, Clone)]
33pub struct DoctorReport {
34 pub checks: Vec<(&'static str, CheckStatus)>,
35}
36
37impl DoctorReport {
38 pub fn has_failures(&self) -> bool {
39 self.checks
40 .iter()
41 .any(|(_, s)| matches!(s, CheckStatus::Fail(_)))
42 }
43 pub fn has_warnings(&self) -> bool {
44 self.checks
45 .iter()
46 .any(|(_, s)| matches!(s, CheckStatus::Warn(_)))
47 }
48 pub fn print(&self) {
49 for (name, status) in &self.checks {
50 match status {
51 CheckStatus::Ok => println!(" \x1b[32m{}\x1b[0m {}", status.symbol(), name),
52 CheckStatus::Warn(msg) => {
53 println!(" \x1b[33m{}\x1b[0m {}: {}", status.symbol(), name, msg)
54 }
55 CheckStatus::Fail(msg) => {
56 println!(" \x1b[31m{}\x1b[0m {}: {}", status.symbol(), name, msg)
57 }
58 }
59 }
60 }
61}
62
63pub fn run(
64 store_root: &Path,
65 audit_log: &Path,
66 chain_secret: Option<&Path>,
67) -> Result<DoctorReport> {
68 let mut report = DoctorReport { checks: vec![] };
69
70 let store = match PolicyStore::open(store_root) {
72 Ok(s) => s,
73 Err(e) => {
74 report
75 .checks
76 .push(("schema", CheckStatus::Fail(e.to_string())));
77 return Ok(report);
78 }
79 };
80 let schema = store.load_schema().ok().flatten();
81 match &schema {
82 Some(_) => report.checks.push(("schema", CheckStatus::Ok)),
83 None => report.checks.push((
84 "schema",
85 CheckStatus::Warn("no schema.cedarschema at store root".into()),
86 )),
87 }
88
89 let validation = match store.validate() {
91 Ok(v) => v,
92 Err(e) => {
93 report
94 .checks
95 .push(("policies", CheckStatus::Fail(e.to_string())));
96 return Ok(report);
97 }
98 };
99 if validation.is_ok() {
100 report.checks.push(("policies", CheckStatus::Ok));
101 } else {
102 let errs: Vec<String> = validation
103 .errors
104 .iter()
105 .map(|e| e.message.clone())
106 .collect();
107 report
108 .checks
109 .push(("policies", CheckStatus::Fail(errs.join("; "))));
110 }
111
112 if audit_log.exists() {
114 match std::fs::OpenOptions::new().append(true).open(audit_log) {
115 Ok(_) => report.checks.push(("audit log", CheckStatus::Ok)),
116 Err(e) => report
117 .checks
118 .push(("audit log", CheckStatus::Fail(e.to_string()))),
119 }
120 } else if let Some(parent) = audit_log.parent() {
121 match std::fs::create_dir_all(parent) {
122 Ok(_) => report.checks.push(("audit log", CheckStatus::Ok)),
123 Err(e) => report
124 .checks
125 .push(("audit log", CheckStatus::Fail(e.to_string()))),
126 }
127 } else {
128 report.checks.push(("audit log", CheckStatus::Ok));
129 }
130
131 if let Some(secret_path) = chain_secret {
133 if let Ok(key) = std::fs::read(secret_path) {
134 if let Some(key_bytes) = decode_chain_secret(&key) {
135 match DecisionLog::verify_chain(audit_log, &key_bytes) {
136 Ok(_) => report.checks.push(("hash chain", CheckStatus::Ok)),
137 Err(e) => report
138 .checks
139 .push(("hash chain", CheckStatus::Fail(e.to_string()))),
140 }
141 } else {
142 report.checks.push((
143 "hash chain",
144 CheckStatus::Warn(format!("secret file {} is empty", secret_path.display())),
145 ));
146 }
147 } else {
148 report.checks.push((
149 "hash chain",
150 CheckStatus::Warn(format!("cannot read secret file {}", secret_path.display())),
151 ));
152 }
153 } else {
154 report.checks.push((
155 "hash chain",
156 CheckStatus::Warn("no chain secret configured (AGENTGUARD_CHAIN_SECRET unset)".into()),
157 ));
158 }
159
160 match agentguard_core::Authorizer::new(store) {
162 Ok(_) => report.checks.push(("authorizer", CheckStatus::Ok)),
163 Err(e) => report
164 .checks
165 .push(("authorizer", CheckStatus::Fail(e.to_string()))),
166 }
167
168 Ok(report)
169}