#![expect(
clippy::redundant_pub_crate,
reason = "explicit pub(crate) documents the crate-wide visibility intent at each item"
)]
use core::fmt::Write as _;
use std::collections::BTreeSet;
use std::io::Write;
use std::path::Path;
use airsl::Policy;
use airsl::extension::{
ApprovalRequest, Approver as _, Capability, CapabilityRequest, Ceiling, Decision, Manifest,
ManifestApprover, Negotiation, Variables, negotiate,
};
use crate::cli::{ExtFlags, resolve_ceiling};
use crate::doctor::describe;
const KIND_WIDTH: usize = 12;
const VALUE_WIDTH: usize = 24;
const LABEL_WIDTH: usize = 14;
#[must_use]
pub(crate) fn render(
manifest: &Manifest,
ceiling: &Ceiling,
negotiation: &Negotiation,
decision: &Decision,
events: &[String],
) -> String {
let mut out = String::new();
let _ = writeln!(
out,
"{:<LABEL_WIDTH$}{} {} (api {}, entry {})",
"extension:",
manifest.name(),
manifest.version(),
manifest.api().get(),
manifest.entry().display(),
);
let _ = writeln!(out, "{:<LABEL_WIDTH$}{}", "events:", events_summary(events));
policy_block(&mut out, "ceiling:", ceiling.policy());
policy_block(&mut out, "negotiated:", negotiation.policy());
let _ = writeln!(out, "requested:");
for (kind, value, capability) in requested_lines(manifest) {
let tag = tag_for(&capability, negotiation);
let _ = writeln!(out, " {kind:<KIND_WIDTH$} {value:<VALUE_WIDTH$} {tag}");
}
match decision {
Decision::Approve => {
let _ = writeln!(out, "{:<LABEL_WIDTH$}approve", "decision:");
}
Decision::Deny(reason) => {
let _ = writeln!(out, "{:<LABEL_WIDTH$}deny — {reason}", "decision:");
}
}
out
}
fn policy_block(out: &mut String, label: &str, policy: &Policy) {
let _ = writeln!(out, "{label}");
let _ = writeln!(out, " {:<LABEL_WIDTH$}{}", "language:", policy.language());
let _ = writeln!(out, " {:<LABEL_WIDTH$}{}", "grants:", policy.grants());
let _ = writeln!(
out,
" {:<LABEL_WIDTH$}{}",
"memory:",
describe(policy.limits().memory())
);
let _ = writeln!(
out,
" {:<LABEL_WIDTH$}{}",
"instructions:",
describe(policy.limits().instructions())
);
}
fn events_summary(events: &[String]) -> String {
let unique: BTreeSet<&str> = events.iter().map(String::as_str).collect();
if unique.is_empty() {
"none".to_owned()
} else {
unique.into_iter().collect::<Vec<_>>().join(", ")
}
}
fn requested_lines(manifest: &Manifest) -> Vec<(&'static str, String, Capability)> {
let mut lines = Vec::new();
push_block(&mut lines, manifest.required());
push_block(&mut lines, manifest.optional());
lines
}
fn push_block(lines: &mut Vec<(&'static str, String, Capability)>, block: &CapabilityRequest) {
for root in block.fs_read() {
let resolved = airsl::FsGrant::resolve_root(root);
lines.push((
"fs.read",
resolved.display().to_string(),
Capability::FsRead(resolved),
));
}
for root in block.fs_write() {
let resolved = airsl::FsGrant::resolve_root(root);
lines.push((
"fs.write",
resolved.display().to_string(),
Capability::FsWrite(resolved),
));
}
for program in block.proc_run() {
lines.push((
"proc.run",
program.to_owned(),
Capability::ProcRun(program.to_owned()),
));
}
for name in block.env_read() {
lines.push((
"env.read",
name.to_owned(),
Capability::EnvRead(name.to_owned()),
));
}
for module in block.modules() {
lines.push((
"module",
module.to_string(),
Capability::Module(module.clone()),
));
}
}
fn tag_for(capability: &Capability, negotiation: &Negotiation) -> String {
negotiation
.denied()
.iter()
.find(|denial| denial.capability() == capability)
.map(|denial| format!("denied ({})", denial.detail()))
.or_else(|| {
negotiation
.reduced()
.iter()
.find(|reduction| reduction.capability() == capability)
.map(|reduction| format!("reduced ({})", reduction.detail()))
})
.unwrap_or_else(|| "granted".to_owned())
}
pub(crate) fn run(dir: &Path, mut flags: ExtFlags, stdout: &mut impl Write) -> i32 {
let variables = Variables::from_pairs(std::mem::take(&mut flags.vars));
let events = std::mem::take(&mut flags.events);
let ceiling = match resolve_ceiling(flags) {
Ok(ceiling) => ceiling,
Err(error) => {
eprintln!("airsl ext doctor: {error}");
return 1;
}
};
let manifest = match Manifest::from_dir(dir, &variables) {
Ok(manifest) => manifest,
Err(error) => {
eprintln!("airsl ext doctor: {error}");
return 1;
}
};
let modules = match airsl::modules::stdlib() {
Ok(modules) => modules,
Err(error) => {
eprintln!("airsl ext doctor: {error}");
return 1;
}
};
let negotiation = negotiate(&manifest, &ceiling, &modules);
let decision = ManifestApprover.decide(&ApprovalRequest::new(dir, &manifest, &negotiation));
let _ = write!(
stdout,
"{}",
render(&manifest, &ceiling, &negotiation, &decision, &events)
);
0
}
#[cfg(test)]
mod tests {
#![expect(
clippy::unwrap_used,
reason = "tests unwrap known-valid fixtures; a panic is the intended failure signal"
)]
use airsl::extension::{Approver as _, Ceiling, MANIFEST_FILE};
use airsl::modules::stdlib;
use airsl::{GrantSet, MemoryLimit, Policy};
use tempfile::TempDir;
use super::{ApprovalRequest, Manifest, ManifestApprover, Variables, negotiate, render, run};
use crate::cli::ExtFlags;
fn fixture(name: &str, required: &str, optional: &str, limits: &str) -> TempDir {
let dir = TempDir::new().unwrap();
std::fs::write(
dir.path().join(MANIFEST_FILE),
format!(
"[extension]\nname='{name}'\nversion='0.2.0'\nentry='main.lua'\napi=1\n\
[capabilities]\n{required}\n[capabilities.optional]\n{optional}\n\
[limits]\n{limits}\n"
),
)
.unwrap();
std::fs::write(
dir.path().join("main.lua"),
"airsstack.fs.write('marker', 'ran')\n",
)
.unwrap();
dir
}
fn manifest(dir: &TempDir) -> Manifest {
Manifest::from_dir(dir.path(), &Variables::none()).unwrap()
}
fn doctor(dir: &std::path::Path, flags: ExtFlags) -> (i32, String) {
let mut out = Vec::new();
let code = run(dir, flags, &mut out);
(code, String::from_utf8(out).unwrap())
}
#[test]
fn render_tags_each_request_and_ends_with_the_decision() {
let dir = fixture(
"journal-indexer",
"fs.read=['/data/in']\nproc.run=['git']\nregex=true",
"proc.run=['tar']",
"memory='8MB'",
);
let m = manifest(&dir);
let ceiling = Ceiling::new(
Policy::confined().with_grants(
GrantSet::declared()
.with_fs(|fs| fs.read("/data"))
.with_proc(|p| p.allow(["git"])),
),
)
.unwrap();
let negotiation = negotiate(&m, &ceiling, &stdlib().unwrap());
assert!(negotiation.is_satisfied(), "{:?}", negotiation.denied());
let decision = ManifestApprover.decide(&ApprovalRequest::new(dir.path(), &m, &negotiation));
let out = render(&m, &ceiling, &negotiation, &decision, &[]);
assert_eq!(
out,
concat!(
"extension: journal-indexer 0.2.0 (api 1, entry main.lua)\n",
"events: none\n",
"ceiling:\n",
" language: restricted\n",
" grants: read /data; exec git\n",
" memory: 67108864 bytes\n",
" instructions: 100000000 instructions\n",
"negotiated:\n",
" language: restricted\n",
" grants: read /data/in; exec git\n",
" memory: 8388608 bytes\n",
" instructions: 100000000 instructions\n",
"requested:\n",
" fs.read /data/in granted\n",
" proc.run git granted\n",
" module regex granted\n",
" proc.run tar reduced (not among the granted executables: git)\n",
"decision: approve\n",
)
);
}
#[test]
fn render_lists_the_declared_events_sorted_and_deduplicated() {
let dir = fixture("t", "", "", "");
let m = manifest(&dir);
let ceiling = Ceiling::new(Policy::confined()).unwrap();
let negotiation = negotiate(&m, &ceiling, &stdlib().unwrap());
let decision = ManifestApprover.decide(&ApprovalRequest::new(dir.path(), &m, &negotiation));
let events = ["stop".to_owned(), "count".to_owned(), "count".to_owned()];
let out = render(&m, &ceiling, &negotiation, &decision, &events);
assert!(out.contains("events: count, stop\n"), "{out}");
}
#[test]
fn a_required_denial_is_listed_and_the_decision_is_deny() {
let dir = fixture("t", "fs.read=['/']", "", "");
let m = manifest(&dir);
let ceiling = Ceiling::new(Policy::confined()).unwrap();
let negotiation = negotiate(&m, &ceiling, &stdlib().unwrap());
assert!(!negotiation.is_satisfied());
let decision = ManifestApprover.decide(&ApprovalRequest::new(dir.path(), &m, &negotiation));
let out = render(&m, &ceiling, &negotiation, &decision, &[]);
assert!(
out.contains(" fs.read / denied ("),
"{out}"
);
assert!(out.contains("decision: deny — fs.read `/`:"), "{out}");
}
#[test]
fn a_denial_is_tagged_correctly_when_the_requested_root_resolves_to_a_different_path() {
let dir = TempDir::new().unwrap();
let requested = dir.path().join("data");
std::fs::create_dir(&requested).unwrap();
std::fs::write(
dir.path().join(MANIFEST_FILE),
format!(
"[extension]\nname='t'\nversion='0.2.0'\nentry='main.lua'\napi=1\n\
[capabilities]\nfs.read=['{}']\n[capabilities.optional]\n[limits]\n",
requested.display()
),
)
.unwrap();
std::fs::write(dir.path().join("main.lua"), "return 1").unwrap();
let m = Manifest::from_dir(dir.path(), &Variables::none()).unwrap();
let ceiling = Ceiling::new(Policy::confined()).unwrap();
let negotiation = negotiate(&m, &ceiling, &stdlib().unwrap());
assert!(!negotiation.is_satisfied(), "{:?}", negotiation.denied());
let decision = ManifestApprover.decide(&ApprovalRequest::new(dir.path(), &m, &negotiation));
let out = render(&m, &ceiling, &negotiation, &decision, &[]);
let resolved = airsl::FsGrant::resolve_root(&requested);
assert!(
out.contains(&format!("fs.read {}", resolved.display())),
"{out}"
);
assert!(out.contains(" denied ("), "{out}");
}
#[test]
fn a_doctor_run_does_not_execute_the_entry_and_writes_the_report_to_stdout() {
let dir = fixture("t", "", "", "");
let (code, out) = doctor(dir.path(), ExtFlags::default());
assert_eq!(code, 0);
assert!(!dir.path().join("marker").exists());
assert!(out.starts_with("extension:"), "{out}");
}
#[test]
fn run_exits_zero_on_a_denial_and_one_on_a_parse_error() {
let denied = fixture("t", "fs.read=['/']", "", "");
assert_eq!(doctor(denied.path(), ExtFlags::default()).0, 0);
let unparsable = TempDir::new().unwrap();
std::fs::write(unparsable.path().join(MANIFEST_FILE), "not = = toml").unwrap();
assert_eq!(doctor(unparsable.path(), ExtFlags::default()).0, 1);
}
#[test]
fn limits_are_reported_as_the_minimum_in_the_negotiated_block_and_the_ceiling_is_unchanged() {
let dir = fixture("t", "", "", "memory='8MB'");
let m = manifest(&dir);
let ceiling = Ceiling::new(Policy::confined()).unwrap();
assert_eq!(
ceiling.policy().limits().memory(),
Some(MemoryLimit::mebibytes(64))
);
let negotiation = negotiate(&m, &ceiling, &stdlib().unwrap());
let decision = ManifestApprover.decide(&ApprovalRequest::new(dir.path(), &m, &negotiation));
let out = render(&m, &ceiling, &negotiation, &decision, &[]);
assert_eq!(
out,
concat!(
"extension: t 0.2.0 (api 1, entry main.lua)\n",
"events: none\n",
"ceiling:\n",
" language: restricted\n",
" grants: none\n",
" memory: 67108864 bytes\n",
" instructions: 100000000 instructions\n",
"negotiated:\n",
" language: restricted\n",
" grants: none\n",
" memory: 8388608 bytes\n",
" instructions: 100000000 instructions\n",
"requested:\n",
"decision: approve\n",
)
);
}
#[test]
fn a_missing_manifest_exits_one() {
let dir = TempDir::new().unwrap();
assert_eq!(doctor(dir.path(), ExtFlags::default()).0, 1);
}
#[test]
fn a_var_flag_resolves_a_manifest_variable() {
let dir = fixture("t", "fs.read=['$APP_HOME/data']", "", "");
let unresolved = Manifest::from_dir(dir.path(), &Variables::none());
assert!(
matches!(unresolved, Err(ref error) if error.to_string().contains("$APP_HOME")),
"{unresolved:?}"
);
assert_eq!(doctor(dir.path(), ExtFlags::default()).0, 1);
let flags = ExtFlags {
vars: vec![("APP_HOME".to_owned(), dir.path().display().to_string())],
..ExtFlags::default()
};
assert_eq!(doctor(dir.path(), flags).0, 0);
}
}