use std::path::Path;
use std::time::{Duration, Instant};
use serde_json::{Map, Value};
use sha2::{Digest, Sha256};
use crate::daemon::session_state::DispatchStateManager;
use crate::generated::types::Verdict;
use crate::zone_eval::types::OptimizeParams;
use crate::zone_eval::{Bundle, Decision, Event, SessionState};
#[derive(Clone, Debug, Default)]
pub struct Delivery {
pub updated_input: Option<Map<String, Value>>,
pub additional_context: Option<String>,
pub system_message: Option<String>,
pub enforced: Option<bool>,
pub result: Option<&'static str>,
pub second_optimize_degraded: bool,
}
#[derive(Clone, Debug)]
pub struct Outcome {
pub decision: Decision,
pub state_to_commit: SessionState,
pub delivery: Delivery,
}
#[allow(clippy::too_many_arguments)]
pub fn evaluate_and_dispatch(
bundle: &Bundle,
event: &Event,
source: &str,
dispatch_sessions: &DispatchStateManager,
output_dir: &Path,
prior: Option<&SessionState>,
now: Instant,
now_ms: i64,
) -> Outcome {
let (mut initial, initial_state) = crate::zone_eval::evaluate(bundle, event, prior, now_ms);
let mut delivery = Delivery::default();
observe_completion(dispatch_sessions, event, now);
if event.event_type == "post_tool_use" {
delivery.additional_context = take_output_context(dispatch_sessions, event, now, source);
}
if matches!(event.event_type.as_str(), "stop" | "subagent_stop") {
delivery.system_message = take_stop_reason(dispatch_sessions, event, now, source);
}
if matches!(initial.verdict, Verdict::Block | Verdict::Ask) {
return Outcome {
decision: initial,
state_to_commit: initial_state,
delivery,
};
}
let selected = initial
.optimize
.as_ref()
.and_then(|context| context.actual.as_ref().or(context.monitor.as_ref()));
let Some(selected) = selected else {
return Outcome {
decision: initial,
state_to_commit: initial_state,
delivery,
};
};
let directive = selected
.artifact_id
.as_deref()
.and_then(|id| {
bundle
.artifacts
.iter()
.find(|artifact| artifact.artifact_id() == Some(id))
})
.and_then(|artifact| artifact.optimize.as_ref());
let Some(directive) = directive else {
mark_selected_reason(&mut initial, "incapable");
initial.verdict = Verdict::Allow;
delivery.enforced = Some(false);
delivery.result = Some("flagged");
return Outcome {
decision: initial,
state_to_commit: initial_state,
delivery,
};
};
let monitor_only = initial
.optimize
.as_ref()
.is_some_and(|context| context.actual.is_none() && context.monitor.is_some());
if monitor_only {
monitor(
directive.params.clone(),
event,
dispatch_sessions,
now,
&mut initial,
&mut delivery,
);
return Outcome {
decision: initial,
state_to_commit: initial_state,
delivery,
};
}
match &directive.params {
OptimizeParams::LoopStop(params) => {
apply_loop_stop(
params,
event,
source,
dispatch_sessions,
now,
&mut initial,
&mut delivery,
);
Outcome {
decision: initial,
state_to_commit: initial_state,
delivery,
}
}
OptimizeParams::Steer(params) => {
apply_steer(
params,
event,
source,
dispatch_sessions,
now,
&mut initial,
&mut delivery,
);
Outcome {
decision: initial,
state_to_commit: initial_state,
delivery,
}
}
OptimizeParams::NarrowOutput(params) => apply_narrow_output(
params,
bundle,
event,
source,
dispatch_sessions,
output_dir,
prior,
now,
now_ms,
initial,
initial_state,
delivery,
),
OptimizeParams::Substitute(_) => {
mark_selected_reason(&mut initial, "incapable");
initial.verdict = Verdict::Allow;
delivery.enforced = Some(false);
delivery.result = Some("skipped_invalid");
Outcome {
decision: initial,
state_to_commit: initial_state,
delivery,
}
}
_ => {
mark_selected_reason(&mut initial, "incapable");
initial.verdict = Verdict::Allow;
delivery.enforced = Some(false);
delivery.result = Some("flagged");
Outcome {
decision: initial,
state_to_commit: initial_state,
delivery,
}
}
}
}
fn monitor(
params: OptimizeParams,
event: &Event,
sessions: &DispatchStateManager,
now: Instant,
decision: &mut Decision,
delivery: &mut Delivery,
) {
delivery.enforced = Some(false);
match params {
OptimizeParams::LoopStop(params) => {
if loop_stop_trigger(¶ms, event, sessions, now) {
decision.would_have_verdict = Some(Verdict::Ask);
delivery.result = Some("flagged");
} else {
decision.would_have_verdict = None;
delivery.result = Some("allowed");
}
}
OptimizeParams::Substitute(params) => {
delivery.result = Some(if substitute_pair(¶ms, event).is_some() {
"flagged"
} else {
"skipped_invalid"
});
}
_ => delivery.result = Some("flagged"),
}
decision.verdict = Verdict::Allow;
}
fn apply_loop_stop(
params: &Map<String, Value>,
event: &Event,
source: &str,
sessions: &DispatchStateManager,
now: Instant,
decision: &mut Decision,
delivery: &mut Delivery,
) {
if event.event_type != "pre_tool_use" || !matches!(source, "claude-code" | "codex-cli") {
mark_selected_reason(decision, "incapable");
decision.verdict = Verdict::Allow;
delivery.enforced = Some(false);
delivery.result = Some("flagged");
return;
}
if loop_stop_trigger(params, event, sessions, now) {
decision.verdict = Verdict::Block;
decision.reason = "OpenLatch stopped a repeated tool loop.".into();
delivery.enforced = Some(true);
delivery.result = Some("blocked");
} else {
decision.verdict = Verdict::Allow;
delivery.enforced = Some(false);
delivery.result = Some("allowed");
}
}
fn loop_stop_trigger(
params: &Map<String, Value>,
event: &Event,
sessions: &DispatchStateManager,
now: Instant,
) -> bool {
if event.event_type != "pre_tool_use" || exempt_loop(params, event) {
return false;
}
let action_key = action_key(event);
let n_errors = params.get("n_errors").and_then(Value::as_u64).unwrap_or(3);
let n_identical = params
.get("n_identical")
.and_then(Value::as_u64)
.unwrap_or(4);
sessions.with_session(&event.session_id, now, |state| {
state.last_action_key.as_deref() == Some(action_key.as_str())
&& ((state.last_result_was_error
&& state.identical_error_completions.saturating_add(1) >= n_errors)
|| state.identical_completions.saturating_add(1) >= n_identical)
})
}
fn apply_steer(
params: &Map<String, Value>,
event: &Event,
source: &str,
sessions: &DispatchStateManager,
now: Instant,
decision: &mut Decision,
delivery: &mut Delivery,
) {
if event.event_type != "pre_tool_use" || !matches!(source, "claude-code" | "codex-cli") {
mark_selected_reason(decision, "incapable");
decision.verdict = Verdict::Allow;
delivery.enforced = Some(false);
delivery.result = Some("flagged");
return;
}
let instruction = params
.get("steer_instruction")
.and_then(Value::as_str)
.unwrap_or_default();
let window = Duration::from_secs(
params
.get("dedupe_window_s")
.and_then(Value::as_u64)
.unwrap_or(0),
);
let key = action_key(event);
let duplicate = sessions.with_session(&event.session_id, now, |state| {
let duplicate = state.last_steer_key.as_deref() == Some(key.as_str())
&& state
.last_steer_at
.is_some_and(|at| now.saturating_duration_since(at) <= window);
if !duplicate {
state.last_steer_key = Some(key);
state.last_steer_at = Some(now);
if source == "claude-code" {
state.pending_stop_reason = Some(instruction.to_string());
state.remaining_stop_blocks = 1;
}
}
duplicate
});
if duplicate || instruction.trim().is_empty() {
mark_selected_reason(decision, "guarded");
decision.verdict = Verdict::Allow;
delivery.enforced = Some(false);
delivery.result = Some("flagged");
return;
}
decision.verdict = Verdict::Block;
decision.reason = instruction.to_string();
delivery.enforced = Some(true);
delivery.result = Some("steered");
}
#[allow(clippy::too_many_arguments)]
fn apply_narrow_output(
params: &Map<String, Value>,
bundle: &Bundle,
event: &Event,
source: &str,
dispatch_sessions: &DispatchStateManager,
output_dir: &Path,
prior: Option<&SessionState>,
now: Instant,
now_ms: i64,
mut initial: Decision,
initial_state: SessionState,
mut delivery: Delivery,
) -> Outcome {
let Some((command, filter)) = narrow_match(params, event) else {
mark_selected_reason(&mut initial, "guarded");
initial.verdict = Verdict::Allow;
delivery.enforced = Some(false);
delivery.result = Some("flagged");
return Outcome {
decision: initial,
state_to_commit: initial_state,
delivery,
};
};
if source != "claude-code" || event.event_type != "pre_tool_use" {
mark_selected_reason(&mut initial, "incapable");
initial.verdict = Verdict::Allow;
delivery.enforced = Some(false);
delivery.result = Some("flagged");
return Outcome {
decision: initial,
state_to_commit: initial_state,
delivery,
};
}
let path = output_dir.join(format!(
"tool-output-{}.log",
short_hash(&event.tool_use_id)
));
let Some(wrapped) = wrap_command(command, filter, &path) else {
mark_selected_reason(&mut initial, "guarded");
initial.verdict = Verdict::Allow;
delivery.enforced = Some(false);
delivery.result = Some("flagged");
return Outcome {
decision: initial,
state_to_commit: initial_state,
delivery,
};
};
let mut updated = match event.tool_input.as_object() {
Some(input) => input.clone(),
None => {
mark_selected_reason(&mut initial, "guarded");
initial.verdict = Verdict::Allow;
delivery.enforced = Some(false);
delivery.result = Some("flagged");
return Outcome {
decision: initial,
state_to_commit: initial_state,
delivery,
};
}
};
updated.insert("command".into(), Value::String(wrapped));
let mut final_event = event.clone();
final_event.tool_input = Value::Object(updated.clone());
let (mut final_decision, final_state) =
crate::zone_eval::evaluate(bundle, &final_event, prior, now_ms);
let initial_optimize = initial.optimize.take();
let selected_id = initial_optimize
.as_ref()
.and_then(|ctx| ctx.actual.as_ref())
.and_then(|candidate| candidate.artifact_id.as_deref());
let final_id = final_decision
.optimize
.as_ref()
.and_then(|ctx| ctx.actual.as_ref())
.and_then(|candidate| candidate.artifact_id.as_deref());
if final_decision.verdict == Verdict::Optimize && final_id != selected_id {
final_decision.verdict = Verdict::Ask;
delivery.second_optimize_degraded = true;
}
final_decision.optimize = initial_optimize;
if matches!(final_decision.verdict, Verdict::Block | Verdict::Ask) {
if final_decision.verdict == Verdict::Block {
delivery.enforced = Some(true);
delivery.result = Some("blocked");
} else {
delivery.enforced = Some(false);
delivery.result = Some("flagged");
}
return Outcome {
decision: final_decision,
state_to_commit: final_state,
delivery,
};
}
retain_record_identity(&initial, &mut final_decision);
final_decision.verdict = Verdict::Optimize;
let reserved = dispatch_sessions.with_session(&event.session_id, now, |state| {
if state.pending_output_paths.len() >= 32
&& !state.pending_output_paths.contains_key(&event.tool_use_id)
{
return false;
}
state.pending_output_paths.insert(
event.tool_use_id.clone(),
path.to_string_lossy().into_owned(),
);
true
});
if !reserved {
mark_selected_reason(&mut final_decision, "guarded");
final_decision.verdict = Verdict::Allow;
delivery.enforced = Some(false);
delivery.result = Some("flagged");
return Outcome {
decision: final_decision,
state_to_commit: initial_state,
delivery,
};
}
delivery.updated_input = Some(updated);
delivery.enforced = Some(true);
delivery.result = Some("rewritten");
Outcome {
decision: final_decision,
state_to_commit: final_state,
delivery,
}
}
fn narrow_match<'a>(
params: &'a Map<String, Value>,
event: &'a Event,
) -> Option<(&'a str, &'a str)> {
if event.tool_name != "Bash" {
return None;
}
let command = event.tool_input.get("command")?.as_str()?;
let entries = params.get("allowlist")?.as_array()?;
entries.iter().find_map(|entry| {
let prefix = entry.get("command_prefix")?.as_str()?;
let filter = entry.get("filter")?.as_str()?;
if prefix.trim().is_empty()
|| filter.trim().is_empty()
|| !simple_diagnostic_shape(command)
|| !simple_diagnostic_shape(filter)
{
return None;
}
(command == prefix
|| command
.strip_prefix(prefix)
.is_some_and(|tail| tail.starts_with(char::is_whitespace)))
.then_some((command, filter))
})
}
fn substitute_pair<'a>(
params: &'a Map<String, Value>,
event: &Event,
) -> Option<(&'a str, &'a str)> {
let command = event.tool_input.get("command")?.as_str()?;
params.get("pairs")?.as_array()?.iter().find_map(|pair| {
let from = pair.get("from")?.as_str()?;
let to = pair.get("to")?.as_str()?;
if from.trim().is_empty() || to.trim().is_empty() {
return None;
}
command
.split_whitespace()
.next()
.is_some_and(|tool| tool == from)
.then_some((from, to))
})
}
fn exempt_loop(params: &Map<String, Value>, event: &Event) -> bool {
let Some(command) = event.tool_input.get("command").and_then(Value::as_str) else {
return false;
};
let normalized = crate::core::policy::normalize(command);
let program = normalized.split_whitespace().next().unwrap_or_default();
if matches!(program, "sleep" | "wait" | "poll") {
return true;
}
params
.get("exempt_patterns")
.and_then(Value::as_array)
.is_some_and(|patterns| {
patterns
.iter()
.filter_map(Value::as_str)
.any(|pattern| crate::core::policy::matches(pattern, &normalized))
})
}
fn observe_completion(sessions: &DispatchStateManager, event: &Event, now: Instant) {
if !matches!(
event.event_type.as_str(),
"post_tool_use" | "post_tool_use_failure"
) {
return;
}
let Some(result) = event.tool_result.as_ref() else {
return;
};
let action = action_key(event);
let result_hash = hash_value(result);
let completed = format!("{action}:{result_hash}");
let error = result_is_error(result);
sessions.with_session(&event.session_id, now, |state| {
if !event.tool_use_id.is_empty() {
if state
.seen_completion_ids
.iter()
.any(|id| id == &event.tool_use_id)
{
return;
}
state
.seen_completion_ids
.push_back(event.tool_use_id.clone());
while state.seen_completion_ids.len() > 64 {
state.seen_completion_ids.pop_front();
}
}
if state.last_completed_key.as_deref() == Some(completed.as_str()) {
state.identical_completions = state.identical_completions.saturating_add(1);
} else {
state.identical_completions = 1;
}
if error
&& state.last_action_key.as_deref() == Some(action.as_str())
&& state.last_result_hash.as_deref() == Some(result_hash.as_str())
&& state.last_result_was_error
{
state.identical_error_completions = state.identical_error_completions.saturating_add(1);
} else {
state.identical_error_completions = u64::from(error);
}
state.last_completed_key = Some(completed);
state.last_action_key = Some(action);
state.last_result_hash = Some(result_hash);
state.last_result_was_error = error;
});
}
fn result_is_error(result: &Value) -> bool {
result.get("is_error").and_then(Value::as_bool) == Some(true)
|| result
.get("exit_code")
.and_then(Value::as_i64)
.is_some_and(|code| code != 0)
|| result.get("error").is_some_and(|error| !error.is_null())
}
fn action_key(event: &Event) -> String {
let mut input = event.tool_input.clone();
if event.tool_name == "Bash" {
if let Some(command) = input.get_mut("command") {
if let Some(raw) = command.as_str() {
*command = Value::String(crate::core::policy::normalize(raw));
}
}
}
hash_value(&serde_json::json!({"tool": event.tool_name, "input": input}))
}
fn hash_value(value: &Value) -> String {
let canonical = serde_json_canonicalizer::to_string(value).unwrap_or_default();
let digest = Sha256::digest(canonical.as_bytes());
hex::encode(digest)
}
fn short_hash(value: &str) -> String {
hash_value(&Value::String(value.to_string()))[..16].to_string()
}
fn wrap_command(command: &str, filter: &str, path: &Path) -> Option<String> {
#[cfg(windows)]
{
let _ = (command, filter, path);
None
}
#[cfg(not(windows))]
{
let escaped = path.to_string_lossy().replace('\'', "'\\''");
Some(format!(
": > '{escaped}'; {{ {command}; }} > >(tee -a '{escaped}' | {{ {filter}; }}) 2> >(tee -a '{escaped}' >&2); __ol_status=$?; wait; exit \"$__ol_status\""
))
}
}
fn take_output_context(
sessions: &DispatchStateManager,
event: &Event,
now: Instant,
source: &str,
) -> Option<String> {
if source != "claude-code" || event.tool_use_id.is_empty() {
return None;
}
sessions.with_session(&event.session_id, now, |state| {
state
.pending_output_paths
.remove(&event.tool_use_id)
.map(|path| format!("Full command output is available at {path}"))
})
}
fn take_stop_reason(
sessions: &DispatchStateManager,
event: &Event,
now: Instant,
source: &str,
) -> Option<String> {
(source == "claude-code").then(|| {
sessions.with_session(&event.session_id, now, |state| {
if state.remaining_stop_blocks == 0 {
state.pending_stop_reason = None;
return None;
}
state.remaining_stop_blocks -= 1;
let reason = state.pending_stop_reason.clone();
if state.remaining_stop_blocks == 0 {
state.pending_stop_reason = None;
}
reason
})
})?
}
fn simple_diagnostic_shape(command: &str) -> bool {
let trimmed = command.trim();
!trimmed.is_empty()
&& !trimmed.contains(['\n', '\r', ';', '|', '&', '>', '<', '`', '#'])
&& !trimmed.contains("$(")
&& !trimmed.contains("${")
}
fn retain_record_identity(initial: &Decision, final_decision: &mut Decision) {
final_decision.artifact_id = initial.artifact_id.clone();
final_decision.atom_id = initial.atom_id.clone();
final_decision.policy_public_id = initial.policy_public_id.clone();
final_decision.dimension = initial.dimension.clone();
final_decision.mode = initial.mode.clone();
final_decision.tier = initial.tier;
final_decision.reason.clone_from(&initial.reason);
final_decision.undecided = false;
}
fn mark_selected_reason(decision: &mut Decision, reason: &str) {
let Some(context) = decision.optimize.as_mut() else {
return;
};
if let Some(candidate) = context.actual.as_mut().or(context.monitor.as_mut()) {
candidate.reason = reason.to_string();
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn result_error_detection_is_explicit() {
assert!(result_is_error(&serde_json::json!({"exit_code": 1})));
assert!(result_is_error(&serde_json::json!({"is_error": true})));
assert!(!result_is_error(&serde_json::json!({"exit_code": 0})));
}
#[test]
fn narrow_prefix_requires_a_token_boundary() {
let params =
serde_json::json!({"allowlist":[{"command_prefix":"cargo test","filter":"tail -100"}]});
let mut event = Event {
tool_name: "Bash".into(),
tool_input: serde_json::json!({"command":"cargo tester"}),
..Event::default()
};
assert!(narrow_match(params.as_object().unwrap(), &event).is_none());
event.tool_input = serde_json::json!({"command":"cargo test --lib"});
assert!(narrow_match(params.as_object().unwrap(), &event).is_some());
event.tool_input = serde_json::json!({"command":"cargo test ; echo unsafe"});
assert!(narrow_match(params.as_object().unwrap(), &event).is_none());
}
#[cfg(unix)]
#[test]
fn narrow_wrapper_preserves_output_and_the_original_exit_status() {
use std::process::Command;
let dir = tempfile::tempdir().expect("temporary output directory");
let path = dir.path().join("full.log");
let wrapped = wrap_command(
"printf 'full-output\\n'; printf 'full-error\\n' >&2; false",
"tail -1",
&path,
)
.expect("Unix Bash wrapper");
let output = Command::new("bash")
.args(["-c", &wrapped])
.output()
.expect("run Bash wrapper");
assert_eq!(output.status.code(), Some(1));
assert_eq!(String::from_utf8_lossy(&output.stdout), "full-output\n");
assert_eq!(String::from_utf8_lossy(&output.stderr), "full-error\n");
let full = std::fs::read_to_string(path).unwrap();
assert!(full.contains("full-output\n"), "{full:?}");
assert!(full.contains("full-error\n"), "{full:?}");
}
}