use std::process::ExitCode;
use crate::{json, ui};
const LIMIT: usize = 10_000;
pub enum Decision {
Silent,
Advise(String),
Deny(String),
Context(String),
}
impl Decision {
pub fn emit(&self) -> ExitCode {
match self {
Decision::Silent => {}
Decision::Advise(text) => {
println!(
"{}",
json::object(&[format!(
"\"hookSpecificOutput\":{}",
json::object(&[
json::string_field("hookEventName", "PreToolUse"),
json::string_field("additionalContext", &clamp(text)),
])
)])
);
}
Decision::Deny(text) => {
println!(
"{}",
json::object(&[format!(
"\"hookSpecificOutput\":{}",
json::object(&[
json::string_field("hookEventName", "PreToolUse"),
json::string_field("permissionDecision", "deny"),
json::string_field("permissionDecisionReason", &clamp(text)),
])
)])
);
}
Decision::Context(text) => {
println!(
"{}",
json::object(&[format!(
"\"hookSpecificOutput\":{}",
json::object(&[
json::string_field("hookEventName", "SessionStart"),
json::string_field("additionalContext", &clamp(text)),
])
)])
);
}
}
ExitCode::SUCCESS
}
}
fn clamp(text: &str) -> String {
let safe = text
.split('\n')
.map(ui::sanitize)
.collect::<Vec<_>>()
.join("\n");
if safe.chars().count() <= LIMIT {
return safe;
}
let kept: String = safe.chars().take(LIMIT - 1).collect();
format!("{kept}…")
}
pub fn phrase(rule_id: &str, reason: &str, remedy: &str) -> String {
format!("amont-agent/{rule_id}: {reason} {remedy}")
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn a_long_reason_is_capped_on_a_character_boundary() {
let long = "é".repeat(LIMIT * 2);
let out = clamp(&long);
assert!(out.chars().count() <= LIMIT);
assert!(out.ends_with('…'));
}
#[test]
fn control_bytes_are_expanded_before_the_cap_is_applied() {
let nasty = "\u{1b}".repeat(LIMIT);
let out = clamp(&nasty);
assert!(out.chars().count() <= LIMIT);
assert!(!out.contains('\u{1b}'), "no raw escape reaches the output");
}
#[test]
fn newlines_between_findings_survive_the_emitter() {
let out = clamp("first\n\nsecond\u{1b}[0m");
assert_eq!(out, "first\n\nsecond\\x1b[0m");
}
#[test]
fn silence_writes_nothing_at_all() {
assert!(matches!(Decision::Silent, Decision::Silent));
}
}