pub const EXIT_SUCCESS: i32 = 0;
pub const EXIT_AGENT_ERROR: i32 = 1;
pub const EXIT_TIMEOUT: i32 = 124;
pub const EXIT_FAIL_CLOSED: i32 = 125;
pub const EXIT_POLICY_BLOCKED: i32 = 126;
pub const EXIT_COMMAND_NOT_FOUND: i32 = 127;
pub const EXIT_SIGNAL_BASE: i32 = 128;
pub fn map_session_exit_code(
raw_exit_code: i32,
timed_out: bool,
fail_on_block_triggered: bool,
) -> i32 {
if timed_out {
return EXIT_TIMEOUT;
}
if fail_on_block_triggered {
return EXIT_POLICY_BLOCKED;
}
if raw_exit_code < 0 {
let sig = raw_exit_code.saturating_neg();
return EXIT_SIGNAL_BASE.saturating_add(sig);
}
raw_exit_code
}
pub fn recap_hint(final_code: i32, blocked_total: u64, timed_out: bool) -> Option<String> {
if timed_out || final_code == EXIT_TIMEOUT {
return Some(
"session hit the deadline — re-run with a larger --timeout or split the task"
.to_string(),
);
}
if final_code == EXIT_POLICY_BLOCKED {
return Some(format!(
"{blocked_total} boundary denials tripped fail-on-block — 'vetto audit --latest' shows what the kernel denied; adjust grants with 'vetto allow'/'vetto deny'"
));
}
if final_code == EXIT_FAIL_CLOSED {
return Some(
"sandbox refused fail-closed — run 'vetto doctor' for the capability picture, then 'vetto pack --bug -o bug.vetto-pack' to bundle a report"
.to_string(),
);
}
if final_code == EXIT_COMMAND_NOT_FOUND {
return Some(
"agent binary not found in PATH — 'vetto enable <agent>' wires the shim, or check PATH"
.to_string(),
);
}
if final_code != EXIT_SUCCESS {
return Some(format!(
"agent exited {final_code} — 'vetto pack --bug -o bug.vetto-pack' bundles a redacted report for your issue"
));
}
if blocked_total > 0 {
return Some(format!(
"{blocked_total} denials contained, exit 0 — 'vetto audit --latest' shows what was denied"
));
}
None
}
pub fn map_error_to_exit_code(err: &anyhow::Error) -> i32 {
if let Some(typed) = err.downcast_ref::<crate::error::VettoError>() {
return typed.exit_code();
}
let msg = err.to_string().to_lowercase();
if msg.contains("not found in path") || msg.contains("no such file or directory") {
EXIT_COMMAND_NOT_FOUND
} else if msg.contains("lockdown violation") || msg.contains("fail-on-block") {
EXIT_POLICY_BLOCKED
} else if msg.contains("fail-closed")
|| msg.contains("boundary verification failed")
|| msg.contains("refusing to run")
|| msg.contains("not supported")
|| msg.contains("sandbox setup failed")
|| msg.contains("landlock")
|| msg.contains("namespace")
|| msg.contains("mount")
{
EXIT_FAIL_CLOSED
} else {
EXIT_AGENT_ERROR
}
}
#[cfg(test)]
mod tests {
use super::*;
use anyhow::anyhow;
#[test]
fn maps_successful_exit() {
assert_eq!(map_session_exit_code(0, false, false), EXIT_SUCCESS);
}
#[test]
fn maps_agent_error_code() {
assert_eq!(map_session_exit_code(1, false, false), 1);
assert_eq!(map_session_exit_code(42, false, false), 42);
}
#[test]
fn maps_timeout_exit() {
assert_eq!(map_session_exit_code(0, true, false), EXIT_TIMEOUT);
assert_eq!(map_session_exit_code(1, true, false), EXIT_TIMEOUT);
assert_eq!(map_session_exit_code(-9, true, false), EXIT_TIMEOUT);
}
#[test]
fn maps_fail_on_block_exit() {
assert_eq!(map_session_exit_code(0, false, true), EXIT_POLICY_BLOCKED);
assert_eq!(map_session_exit_code(1, false, true), EXIT_POLICY_BLOCKED);
}
#[test]
fn maps_signal_terminations() {
assert_eq!(map_session_exit_code(-9, false, false), 137); assert_eq!(map_session_exit_code(-15, false, false), 143); assert_eq!(map_session_exit_code(-2, false, false), 130); }
#[test]
fn maps_errors_to_appropriate_codes() {
assert_eq!(
map_error_to_exit_code(&anyhow!("agent command 'missing' not found in PATH")),
EXIT_COMMAND_NOT_FOUND
);
assert_eq!(
map_error_to_exit_code(&anyhow!(
"--verify: boundary verification failed; refusing to start the agent (fail-closed)"
)),
EXIT_FAIL_CLOSED
);
assert_eq!(
map_error_to_exit_code(&anyhow!(
"policy lockdown violation: cannot override immutable root"
)),
EXIT_POLICY_BLOCKED
);
assert_eq!(
map_error_to_exit_code(&anyhow!("invalid CLI argument provided")),
EXIT_AGENT_ERROR
);
}
#[test]
fn recap_stays_quiet_on_clean_exit() {
assert_eq!(recap_hint(EXIT_SUCCESS, 0, false), None);
}
#[test]
fn recap_points_at_one_action_per_outcome() {
let timeout = recap_hint(EXIT_TIMEOUT, 0, true).expect("timeout recap");
assert!(timeout.contains("--timeout"));
let blocked = recap_hint(EXIT_POLICY_BLOCKED, 7, false).expect("blocked recap");
assert!(blocked.contains('7'));
assert!(blocked.contains("vetto audit --latest"));
let fail_closed = recap_hint(EXIT_FAIL_CLOSED, 0, false).expect("fail-closed recap");
assert!(fail_closed.contains("vetto doctor"));
assert!(fail_closed.contains("vetto pack --bug"));
let missing = recap_hint(EXIT_COMMAND_NOT_FOUND, 0, false).expect("missing recap");
assert!(missing.contains("vetto enable"));
let agent_err = recap_hint(3, 0, false).expect("agent error recap");
assert!(agent_err.contains('3'));
assert!(agent_err.contains("vetto pack --bug"));
let signal = recap_hint(137, 0, false).expect("signal recap");
assert!(signal.contains("137"));
let contained = recap_hint(EXIT_SUCCESS, 4, false).expect("contained recap");
assert!(contained.contains('4'));
assert!(contained.contains("vetto audit --latest"));
}
struct Lcg(u64);
impl Lcg {
fn next(&mut self) -> u64 {
self.0 = self
.0
.wrapping_mul(6364136223846793005)
.wrapping_add(1442695040888963407);
self.0
}
fn below(&mut self, bound: u64) -> u64 {
if bound == 0 {
0
} else {
self.next() % bound
}
}
}
fn arb_raw(rng: &mut Lcg) -> i32 {
match rng.below(8) {
0 => i32::MIN,
1 => i32::MAX,
2 => -1,
3 => 0,
4 => rng.below(256) as i32,
5 => -(rng.below(64) as i32 + 1),
6 => 124 + rng.below(10) as i32,
_ => rng.next() as i32,
}
}
#[test]
fn prop_timeout_dominates_everything() {
let mut rng = Lcg(0x1234_5678_9abc_def0);
for _ in 0..512 {
let raw = arb_raw(&mut rng);
let fail = rng.below(2) == 1;
assert_eq!(map_session_exit_code(raw, true, fail), EXIT_TIMEOUT);
}
assert_eq!(map_session_exit_code(i32::MIN, true, true), EXIT_TIMEOUT);
assert_eq!(map_session_exit_code(i32::MAX, true, true), EXIT_TIMEOUT);
}
#[test]
fn prop_signals_map_to_128_plus_n() {
assert_eq!(map_session_exit_code(-9, false, false), 137);
assert_eq!(map_session_exit_code(-15, false, false), 143);
assert_eq!(map_session_exit_code(-2, false, false), 130);
assert_eq!(map_session_exit_code(-1, false, false), 129);
for sig in 1..=64 {
assert_eq!(map_session_exit_code(-sig, false, false), 128 + sig);
}
let mut rng = Lcg(0xabcdef01_23456789);
for _ in 0..256 {
let sig = rng.below(64) as i32 + 1;
assert_eq!(map_session_exit_code(-sig, false, false), 128 + sig);
}
}
#[test]
fn prop_min_signal_never_panics() {
let got = map_session_exit_code(i32::MIN, false, false);
assert_eq!(got, i32::MAX);
assert!(got >= EXIT_SIGNAL_BASE);
let got_fail = map_session_exit_code(i32::MIN, false, true);
assert_eq!(got_fail, EXIT_POLICY_BLOCKED);
}
#[test]
fn prop_nonnegative_passthrough() {
let mut rng = Lcg(0x0bad_f00d_dead_beef);
for _ in 0..512 {
let raw = arb_raw(&mut rng);
if raw >= 0 {
assert_eq!(map_session_exit_code(raw, false, false), raw);
}
}
for raw in [0, 1, 42, 124, 125, 126, 127, 137, 255, 1000, i32::MAX] {
assert_eq!(map_session_exit_code(raw, false, false), raw);
}
}
#[test]
fn prop_fail_block_dominates_signal_but_not_timeout() {
assert_eq!(map_session_exit_code(-9, false, true), EXIT_POLICY_BLOCKED);
assert_eq!(map_session_exit_code(0, false, true), EXIT_POLICY_BLOCKED);
assert_eq!(map_session_exit_code(0, true, true), EXIT_TIMEOUT);
assert_eq!(map_session_exit_code(-9, true, true), EXIT_TIMEOUT);
}
#[test]
fn prop_recap_none_only_on_clean() {
let mut rng = Lcg(0x51ab_3c5d_7e9f_1a2b);
for _ in 0..1024 {
let code = arb_raw(&mut rng);
let blocked = rng.below(5);
let timed_out = rng.below(2) == 1;
let got = recap_hint(code, blocked, timed_out);
let clean = code == 0 && blocked == 0 && !timed_out;
assert_eq!(got.is_none(), clean, "code={code} blocked={blocked}");
}
assert_eq!(recap_hint(0, 0, false), None);
assert!(recap_hint(0, 1, false).is_some());
assert!(recap_hint(1, 0, false).is_some());
assert!(recap_hint(0, 0, true).is_some());
}
}