use std::borrow::Cow;
use crate::core::events::{ToolGate, ToolGateVerdict, bounded_gate_reason};
use crate::localization::{Locale, MessageId, tr};
const MAX_TOOL_NAME_CHARS: usize = 64;
#[must_use]
pub fn tool_gate_receipt(
locale: Locale,
tool_name: &str,
gate: ToolGate,
decision: ToolGateVerdict,
risk: Option<&str>,
reason: &str,
) -> String {
let id = match (gate, decision) {
(ToolGate::AutoReviewGuardian, ToolGateVerdict::Allowed) => {
MessageId::AutoReviewReceiptGuardianAllowed
}
(ToolGate::AutoReviewGuardian, ToolGateVerdict::Denied) => {
MessageId::AutoReviewReceiptGuardianDenied
}
(ToolGate::AutoReviewGuardian, ToolGateVerdict::Unavailable) => {
MessageId::AutoReviewReceiptGuardianUnavailable
}
(
ToolGate::AutoReviewDeterministic,
ToolGateVerdict::Denied | ToolGateVerdict::Allowed | ToolGateVerdict::Unavailable,
) => MessageId::AutoReviewReceiptDeterministicBlocked,
};
fill(
tr(locale, id),
tool_name,
risk.unwrap_or("unknown"),
&bounded_gate_reason(reason),
)
}
#[must_use]
pub fn auto_review_held_receipt(locale: Locale, tool_name: &str) -> String {
fill(
tr(locale, MessageId::AutoReviewReceiptHeld),
tool_name,
"",
"",
)
}
fn fill(template: Cow<'static, str>, tool_name: &str, risk: &str, reason: &str) -> String {
template
.replace("{tool}", &bounded_tool_name(tool_name))
.replace("{risk}", risk)
.replace("{reason}", reason)
}
fn bounded_tool_name(tool_name: &str) -> String {
let cleaned = bounded_gate_reason(tool_name);
if cleaned.chars().count() <= MAX_TOOL_NAME_CHARS {
return cleaned;
}
let mut out: String = cleaned.chars().take(MAX_TOOL_NAME_CHARS - 1).collect();
out.push('…');
out
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn guardian_allow_names_tool_risk_and_reason() {
let line = tool_gate_receipt(
Locale::En,
"bash",
ToolGate::AutoReviewGuardian,
ToolGateVerdict::Allowed,
Some("low"),
"reads a log file inside the workspace",
);
assert_eq!(
line,
"Auto-Review allowed 'bash' (low risk, model guardian): reads a log file inside the workspace"
);
}
#[test]
fn guardian_deny_and_unavailable_are_distinct_receipts() {
let denied = tool_gate_receipt(
Locale::En,
"bash",
ToolGate::AutoReviewGuardian,
ToolGateVerdict::Denied,
Some("high"),
"would push to a remote",
);
assert!(denied.starts_with("Auto-Review denied 'bash' (high risk, model guardian): "));
let unavailable = tool_gate_receipt(
Locale::En,
"File",
ToolGate::AutoReviewGuardian,
ToolGateVerdict::Unavailable,
None,
"guardian request timed out",
);
assert!(unavailable.contains("could not review 'File' (guardian request timed out)"));
assert!(
unavailable.ends_with("denied, fail closed"),
"{unavailable}"
);
assert!(!unavailable.contains("unknown risk"), "{unavailable}");
}
#[test]
fn deterministic_block_receipt_names_the_policy() {
let line = tool_gate_receipt(
Locale::En,
"bash",
ToolGate::AutoReviewDeterministic,
ToolGateVerdict::Denied,
None,
"publish-like command",
);
assert_eq!(
line,
"Auto-Review blocked 'bash' (deterministic policy): publish-like command"
);
}
#[test]
fn receipts_bound_and_defang_untrusted_reason_and_tool_text() {
let long = "x".repeat(600);
let line = tool_gate_receipt(
Locale::En,
"ba\u{1b}[31msh\n\u{202E}",
ToolGate::AutoReviewGuardian,
ToolGateVerdict::Denied,
Some("medium"),
&format!("evil\u{1b}]0;title\u{7} {long}"),
);
assert!(!line.contains('\u{1b}'));
assert!(!line.contains('\n'));
assert!(!line.contains('\u{202E}'));
assert!(!line.contains('\u{7}'));
assert!(line.chars().count() < 340, "{}", line.chars().count());
}
#[test]
fn held_receipt_is_localized_and_names_the_tool() {
let en = auto_review_held_receipt(Locale::En, "write");
assert!(en.starts_with("Auto-Review held 'write' without pausing"));
let ja = auto_review_held_receipt(Locale::Ja, "write");
assert!(ja.contains("'write'"));
assert_ne!(ja, en);
}
#[test]
fn every_shipped_pack_keeps_receipt_placeholders() {
for locale in Locale::shipped_complete() {
let line = tool_gate_receipt(
*locale,
"bash",
ToolGate::AutoReviewGuardian,
ToolGateVerdict::Allowed,
Some("low"),
"REASON-SENTINEL",
);
assert!(line.contains("'bash'"), "{locale:?}: {line}");
assert!(line.contains("REASON-SENTINEL"), "{locale:?}: {line}");
assert!(!line.contains('{'), "{locale:?}: {line}");
}
}
}