use serde_json::Value;
use crate::core::envelope::HookEventType;
use crate::hooks::DetectedAgent;
use crate::zone_eval::join::{AskDelivery, AskFallback};
use crate::zone_eval::{Decision, LoadedArtifact};
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct AskOutcome {
pub mode: String,
pub delivery: AskDelivery,
pub notice: bool,
pub tag: String,
}
impl AskOutcome {
pub fn resolve(
decision: &Decision,
artifact: Option<&LoadedArtifact>,
can_ask: bool,
notice: bool,
) -> Option<Self> {
if decision.verdict != crate::generated::types::Verdict::Ask
|| decision.mode.as_ref().map(|mode| mode.0.as_str()) != Some("enforce")
{
return None;
}
let policy = artifact?.ask_policy()?;
Some(Self {
delivery: policy.delivery(can_ask),
mode: policy.mode,
notice,
tag: format!(
"[OL-A{}]",
decision.artifact_id.as_deref().unwrap_or_default()
),
})
}
pub fn delivery_token(&self) -> &'static str {
match self.delivery {
AskDelivery::Agent => "agent",
AskDelivery::Fallback(_) => "fallback",
}
}
pub fn refuses(&self) -> bool {
self.delivery == AskDelivery::Fallback(AskFallback::Block)
}
pub fn refusal_reason(&self) -> String {
format!("{} Awaiting review", self.tag)
}
pub fn notice_text(&self) -> Option<String> {
(self.delivery == AskDelivery::Fallback(AskFallback::Allow) && self.notice)
.then(|| format!("Flagged for review {}", self.tag))
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub struct EventCaps {
pub ask: bool,
pub notice: bool,
}
impl EventCaps {
pub fn expressible(&self) -> Vec<String> {
let mut set = vec!["allow".to_string()];
if self.ask {
set.push("ask".to_string());
}
set.push("deny".to_string());
set
}
}
pub fn event_caps(
bindings: &[DetectedAgent],
source: &str,
event_type: &HookEventType,
data: Option<&Value>,
) -> EventCaps {
let Some(binding) = bindings
.iter()
.map(|agent| &agent.binding)
.find(|binding| binding.agent_type() == source)
else {
return EventCaps::default();
};
let caps = binding.capabilities();
EventCaps {
ask: caps.ask()
&& *event_type == HookEventType::PreToolUse
&& !data.is_some_and(super::handlers::cursor_event_cannot_ask),
notice: caps.notice,
}
}
pub fn capability_tokens(bindings: &[DetectedAgent]) -> Vec<String> {
let mut tokens = Vec::new();
for agent in bindings {
let binding = &agent.binding;
let caps = binding.capabilities();
let id = binding.binding_id(binding.plugin_surface().is_some());
let prefix = format!("binding.{id}");
if caps.ask() {
tokens.push(format!("{prefix}.ask"));
}
if caps.notice {
tokens.push(format!("{prefix}.notice"));
}
if caps.observes_execution {
tokens.push(format!("{prefix}.observes_execution"));
}
tokens.push(format!("{prefix}.enforces.{}", caps.enforces.token()));
}
tokens
}
#[cfg(test)]
mod tests {
use super::*;
use crate::generated::types::{PolicyMode, Verdict};
fn artifact(mode: &str, fallback: &str) -> LoadedArtifact {
let bundle = crate::zone_eval::load(serde_json::json!({
"schema_version": 2,
"contract_version": 3,
"organization_id": "org",
"revision": 1,
"built_at": "2026-09-01T00:00:00Z",
"enforcement_enabled": true,
"signature": null,
"artifacts": [{
"artifact_id": "a1", "atom_id": "x", "kind": "t1_predicate_tree",
"verdict": "ask", "mode": "enforce", "tier": 1,
"ask_mode": mode, "ask_fallback": fallback,
"body": {
"node": {"op": "leaf", "leaf": {"pred": "equals", "field": "tool.name", "value": "Bash"}},
"verdict": "ask", "reason": "confirm",
},
}],
}))
.expect("the bundle loads");
bundle.artifacts.into_iter().next().expect("one artifact")
}
fn asked(mode: &str) -> Decision {
Decision {
verdict: Verdict::Ask,
artifact_id: Some("a1".into()),
mode: Some(PolicyMode(mode.into())),
..Decision::default()
}
}
fn outcome(mode: &str, fallback: &str, can_ask: bool) -> AskOutcome {
AskOutcome::resolve(
&asked("enforce"),
Some(&artifact(mode, fallback)),
can_ask,
true,
)
.expect("an enforced ASK with an ask policy resolves")
}
#[test]
fn delivery_follows_mode_capability_and_fallback() {
for fallback in ["allow", "block"] {
let fb = if fallback == "block" {
AskDelivery::Fallback(AskFallback::Block)
} else {
AskDelivery::Fallback(AskFallback::Allow)
};
assert_eq!(outcome("platform", fallback, true).delivery, fb);
assert_eq!(outcome("platform", fallback, false).delivery, fb);
for mode in ["agent", "both"] {
assert_eq!(outcome(mode, fallback, true).delivery, AskDelivery::Agent);
assert_eq!(outcome(mode, fallback, false).delivery, fb);
}
}
}
#[test]
fn the_fallback_block_refuses_with_its_tag_and_the_allow_fallback_only_notifies() {
let block = outcome("platform", "block", true);
assert!(block.refuses());
assert_eq!(block.refusal_reason(), "[OL-Aa1] Awaiting review");
assert_eq!(block.notice_text(), None);
assert_eq!(block.delivery_token(), "fallback");
let allow = outcome("platform", "allow", true);
assert!(!allow.refuses());
assert_eq!(
allow.notice_text().as_deref(),
Some("Flagged for review [OL-Aa1]")
);
let mut silent = outcome("platform", "allow", true);
silent.notice = false;
assert_eq!(
silent.notice_text(),
None,
"no notice where the agent has none"
);
let prompt = outcome("agent", "block", true);
assert_eq!(prompt.delivery_token(), "agent");
assert!(!prompt.refuses());
assert_eq!(prompt.notice_text(), None);
}
#[test]
fn nothing_resolves_without_an_ask_policy_or_outside_enforce() {
let v2 = crate::zone_eval::load(serde_json::json!({
"schema_version": 2, "organization_id": "org", "revision": 1,
"built_at": "2026-09-01T00:00:00Z", "enforcement_enabled": true,
"signature": null,
"artifacts": [{
"artifact_id": "a1", "atom_id": "x", "kind": "t1_predicate_tree",
"verdict": "ask", "mode": "enforce", "tier": 1,
"body": {
"node": {"op": "leaf", "leaf": {"pred": "equals", "field": "tool.name", "value": "Bash"}},
"verdict": "ask", "reason": "confirm",
},
}],
}))
.expect("loads");
let legacy = v2.artifacts.first();
assert_eq!(
AskOutcome::resolve(&asked("enforce"), legacy, true, true),
None
);
assert_eq!(
AskOutcome::resolve(
&asked("monitor"),
Some(&artifact("platform", "block")),
true,
true
),
None,
"a monitor ASK delivers nothing"
);
let mut allowed = asked("enforce");
allowed.verdict = Verdict::Allow;
assert_eq!(
AskOutcome::resolve(&allowed, Some(&artifact("platform", "block")), true, true),
None
);
assert_eq!(
AskOutcome::resolve(&asked("enforce"), None, true, true),
None
);
}
#[test]
fn expressible_holds_ask_only_where_the_agent_can_prompt() {
assert_eq!(
EventCaps {
ask: true,
notice: true
}
.expressible(),
["allow", "ask", "deny"]
);
assert_eq!(
EventCaps {
ask: false,
notice: true
}
.expressible(),
["allow", "deny"]
);
}
#[test]
fn an_undetected_source_cannot_prompt_and_shows_no_notice() {
assert_eq!(
event_caps(&[], "claude-code", &HookEventType::PreToolUse, None),
EventCaps::default()
);
}
}