use super::{empty, Verdict};
use serde_json::{json, Value};
const DEFAULT_DENY_REASON: &str = "Blocked by OpenLatch policy.";
pub fn translate(event: &str, verdict: &Verdict<'_>) -> Value {
match event {
"pre_tool_use" => pre_tool_use(verdict),
_ => empty(),
}
}
fn pre_tool_use(verdict: &Verdict<'_>) -> Value {
match (verdict.decision, verdict.context) {
("block" | "deny", Some(ctx)) => skip(&format!("{}: {}", ctx.headline, ctx.body)),
("block" | "deny", None) => skip(verdict.reason.unwrap_or(DEFAULT_DENY_REASON)),
("allow", _) => empty(),
("ask", _) => empty(),
_ => empty(),
}
}
pub fn translate_delivery(event: &str, verdict: &Verdict<'_>) -> Value {
translate(event, verdict)
}
fn skip(reason: &str) -> Value {
let reason = if reason.trim().is_empty() {
DEFAULT_DENY_REASON
} else {
reason
};
json!({ "skip": true, "reason": reason })
}
#[cfg(test)]
mod tests {
use super::super::VerdictContext;
use super::*;
const EVENTS: [&str; 11] = [
"task_start",
"task_resume",
"task_cancel",
"task_complete",
"task_error",
"pre_tool_use",
"post_tool_use",
"user_prompt_submit",
"pre_compact",
"session_shutdown",
"future_event_from_next_release",
];
const DECISIONS: [&str; 7] = [
"allow", "approve", "block", "deny", "ask", "optimize", "nonsense",
];
fn alert() -> VerdictContext<'static> {
VerdictContext {
headline: "Configuration alert pending",
body: "MCP server 'evil' was added; review before running.",
}
}
#[test]
fn a_block_verdict_renders_a_skip() {
for decision in ["block", "deny"] {
let out = translate(
"pre_tool_use",
&Verdict {
decision,
reason: Some("rm -rf / is denied here"),
context: None,
},
);
assert_eq!(
out,
json!({ "skip": true, "reason": "rm -rf / is denied here" }),
"decision={decision}"
);
assert!(out.get("cancel").is_none(), "decision={decision}");
}
}
#[test]
fn a_deny_always_carries_a_reason() {
let ctx = alert();
let cases: [(&str, Verdict<'_>); 4] = [
(
"reason",
Verdict {
decision: "deny",
reason: Some("credentials detected"),
context: None,
},
),
(
"context",
Verdict {
decision: "block",
reason: None,
context: Some(&ctx),
},
),
(
"neither",
Verdict {
decision: "block",
reason: None,
context: None,
},
),
(
"empty reason",
Verdict {
decision: "deny",
reason: Some(" "),
context: None,
},
),
];
for (label, verdict) in cases {
let out = translate("pre_tool_use", &verdict);
assert_eq!(out["skip"], json!(true), "{label}");
let rendered = out["reason"]
.as_str()
.unwrap_or_else(|| panic!("{label}: a skip must carry a reason"));
assert!(
!rendered.trim().is_empty(),
"{label}: a skip with no reason is a silent cancellation"
);
}
let with_context = translate(
"pre_tool_use",
&Verdict {
decision: "block",
reason: None,
context: Some(&ctx),
},
);
let rendered = with_context["reason"].as_str().expect("context deny");
assert!(rendered.contains("Configuration alert pending"));
assert!(rendered.contains("MCP server 'evil'"));
}
#[test]
fn plain_allow_stays_empty() {
let ctx = alert();
for decision in ["allow", "approve", "ask", "optimize", "nonsense"] {
for reason in [None, Some("a config file changed")] {
for context in [None, Some(&ctx)] {
let verdict = Verdict {
decision,
reason,
context,
};
assert_eq!(
translate("pre_tool_use", &verdict),
empty(),
"decision={decision} reason={reason:?} context={}",
context.is_some()
);
}
}
}
}
#[test]
fn a_non_tool_event_never_skips() {
let ctx = alert();
for event in EVENTS.into_iter().filter(|e| *e != "pre_tool_use") {
for decision in DECISIONS {
for reason in [None, Some("test reason")] {
for context in [None, Some(&ctx)] {
let verdict = Verdict {
decision,
reason,
context,
};
assert_eq!(
translate(event, &verdict),
empty(),
"event={event} decision={decision} reason={reason:?} context={}",
context.is_some()
);
}
}
}
}
assert_eq!(
translate(
"session_shutdown",
&Verdict {
decision: "deny",
reason: Some("credentials detected"),
context: None,
},
),
empty(),
);
}
#[test]
fn delivery_renders_the_verdict_alone() {
assert_eq!(
translate_delivery(
"pre_tool_use",
&Verdict {
decision: "block",
reason: Some("denied"),
context: None,
},
),
json!({ "skip": true, "reason": "denied" }),
);
assert_eq!(
translate_delivery("pre_tool_use", &Verdict::allow()),
empty()
);
}
}