use std::sync::atomic::{AtomicU32, Ordering};
use std::sync::Arc;
use std::time::{Duration, SystemTime, UNIX_EPOCH};
use axum::{
body::Bytes,
extract::State,
http::{header::CONTENT_TYPE, HeaderMap, StatusCode},
Json,
};
use crate::core::policy::{PolicyMatch, ResidentBundle};
use crate::daemon::identity::{self, IdentitySignals};
use crate::daemon::AppState;
use crate::envelope::{
new_event_id, AgentType, EventEnvelope, HookEventType, Verdict, VerdictResponse,
VerdictResponseContext, VerdictResponseContextBody, VerdictResponseContextHeadline,
};
use crate::privacy;
const CT_CLOUDEVENTS_SINGLE: &str = "application/cloudevents+json";
const CT_CLOUDEVENTS_BATCH: &str = "application/cloudevents-batch+json";
const CT_JSON: &str = "application/json";
struct HookActivityGuard<'a> {
in_flight: &'a AtomicU32,
}
impl<'a> HookActivityGuard<'a> {
fn new(state: &'a AppState) -> Self {
let now = SystemTime::now()
.duration_since(UNIX_EPOCH)
.map(|d| d.as_secs())
.unwrap_or(0);
state.last_hook_at_unix_secs.store(now, Ordering::Relaxed);
state.hooks_in_flight.fetch_add(1, Ordering::AcqRel);
Self {
in_flight: &state.hooks_in_flight,
}
}
}
impl Drop for HookActivityGuard<'_> {
fn drop(&mut self) {
self.in_flight.fetch_sub(1, Ordering::AcqRel);
}
}
pub async fn ingest_cloudevent(
State(state): State<Arc<AppState>>,
headers: HeaderMap,
body: Bytes,
) -> (StatusCode, HeaderMap, Json<VerdictResponse>) {
let start = std::time::Instant::now();
let _activity_guard = HookActivityGuard::new(&state);
let ct = headers
.get(CONTENT_TYPE)
.and_then(|v| v.to_str().ok())
.unwrap_or("");
let is_batch = ct.starts_with(CT_CLOUDEVENTS_BATCH);
let is_single = ct.starts_with(CT_CLOUDEVENTS_SINGLE) || ct.starts_with(CT_JSON);
if !is_batch && !is_single {
return reject(
StatusCode::UNSUPPORTED_MEDIA_TYPE,
"unsupported Content-Type; expected application/cloudevents+json or application/cloudevents-batch+json",
start,
);
}
let envelopes: Vec<EventEnvelope> = if is_batch {
match serde_json::from_slice::<Vec<EventEnvelope>>(&body) {
Ok(v) if !v.is_empty() => v,
Ok(_) => {
return reject(
StatusCode::BAD_REQUEST,
"cloudevents-batch body must not be empty",
start,
);
}
Err(e) => {
return reject(
StatusCode::BAD_REQUEST,
&format!("invalid cloudevents batch body: {e}"),
start,
);
}
}
} else {
match serde_json::from_slice::<EventEnvelope>(&body) {
Ok(env) => vec![env],
Err(e) => {
return reject(
StatusCode::BAD_REQUEST,
&format!("invalid cloudevent body: {e}"),
start,
);
}
}
};
let mut worst: Option<BatchOutcome> = None;
let mut session_ref: Option<String> = None;
let mut response_headers = HeaderMap::new();
for envelope in envelopes {
let (ev_type, ev_id, ev_subject, dedup_hit, policy_match) =
process_envelope(state.clone(), envelope, start, ProcessMode::Live).await;
if dedup_hit {
response_headers.insert(
"x-openlatch-dedup",
"true".parse().expect("static header value is valid"),
);
}
if session_ref.is_none() {
session_ref = ev_subject.filter(|s| !s.is_empty());
}
join_most_restrictive(
&mut worst,
BatchOutcome {
verdict: verdict_for(&ev_type, policy_match.as_ref()),
event_type: ev_type,
event_id: ev_id,
policy_match,
},
);
}
let outcome = worst.unwrap_or_else(|| BatchOutcome {
verdict: Verdict::Allow,
event_type: HookEventType::Unknown(String::new()),
event_id: new_event_id(),
policy_match: None,
});
let latency_ms = start.elapsed().as_secs_f64() * 1000.0;
let mut response = match outcome.verdict {
Verdict::Deny => {
let m = outcome.policy_match.as_ref();
VerdictResponse::deny(
outcome.event_id,
latency_ms,
m.map(|m| m.reason.clone()),
m.map(|m| m.severity.to_string()),
m.map(|m| m.rule_id.clone()),
)
}
Verdict::Approve => VerdictResponse::approve(outcome.event_id, latency_ms),
Verdict::Allow => VerdictResponse::allow(outcome.event_id, latency_ms),
};
if !matches!(outcome.verdict, Verdict::Deny) && !state.pending_alerts.is_empty() {
if let Some(subject) = session_ref.as_deref() {
if let Some(alert) = state.pending_alerts.pop_for_session(subject) {
let surface = match outcome.event_type {
HookEventType::PreToolUse => "pretooluse_reason",
HookEventType::SessionStart => "session_start_context",
_ => "other",
};
crate::telemetry::capture_global(
crate::telemetry::Event::config_alert_surfaced_in_session(
&alert.severity,
surface,
),
);
attach_alert_context(&mut response, &alert);
}
}
}
(StatusCode::OK, response_headers, Json(response))
}
struct BatchOutcome {
verdict: Verdict,
event_type: HookEventType,
event_id: String,
policy_match: Option<PolicyMatch>,
}
fn verdict_rank(verdict: Verdict) -> u8 {
match verdict {
Verdict::Allow => 0,
Verdict::Approve => 1,
Verdict::Deny => 2,
}
}
fn join_most_restrictive(worst: &mut Option<BatchOutcome>, candidate: BatchOutcome) {
match worst {
Some(current) if verdict_rank(candidate.verdict) <= verdict_rank(current.verdict) => {}
_ => *worst = Some(candidate),
}
}
fn verdict_for(event_type: &HookEventType, policy_match: Option<&PolicyMatch>) -> Verdict {
if policy_match.is_some_and(|m| !m.shadow) {
return Verdict::Deny;
}
match event_type {
HookEventType::Stop => Verdict::Approve,
_ => Verdict::Allow,
}
}
fn attach_alert_context(
response: &mut VerdictResponse,
alert: &crate::daemon::config_monitor::PendingAlert,
) {
let headline = clamp_str(&alert.headline, 120);
let body = clamp_str(&alert.body, 500);
let Some(ctx) = build_context(&headline, &body) else {
return;
};
response.context = Some(ctx);
response.reason = Some(headline);
response.schema_version = "1.1".to_string();
}
fn build_context(headline: &str, body: &str) -> Option<VerdictResponseContext> {
let headline = VerdictResponseContextHeadline::try_from(headline.to_string()).ok()?;
let body = VerdictResponseContextBody::try_from(body.to_string()).ok()?;
Some(VerdictResponseContext {
body,
evidence: Vec::new(),
headline,
})
}
fn clamp_str(s: &str, max: usize) -> String {
if s.chars().count() <= max {
return s.to_string();
}
s.chars().take(max.saturating_sub(1)).chain(['…']).collect()
}
#[derive(Clone, Copy, PartialEq, Eq)]
pub(crate) enum ProcessMode {
Live,
Replay,
}
pub(crate) async fn process_envelope_for_replay(
state: Arc<AppState>,
envelope: EventEnvelope,
start: std::time::Instant,
) -> (HookEventType, String, Option<String>, bool) {
let (ev_type, ev_id, subject, dedup_hit, _policy_match) =
process_envelope(state, envelope, start, ProcessMode::Replay).await;
(ev_type, ev_id, subject, dedup_hit)
}
const REPLAY_YIELD_FILL_RATIO_INV: usize = 4;
const REPLAY_BACKPRESSURE_TICK: Duration = Duration::from_millis(50);
async fn forward_replay_event(
tx: &tokio::sync::mpsc::Sender<crate::cloud::CloudEvent>,
mut cloud_event: crate::cloud::CloudEvent,
) {
use tokio::sync::mpsc::error::TrySendError;
loop {
let max_cap = tx.max_capacity();
let free = tx.capacity();
if free.saturating_mul(REPLAY_YIELD_FILL_RATIO_INV) < max_cap {
if tx.is_closed() {
tracing::warn!("cloud channel closed during replay — worker has exited");
return;
}
tokio::time::sleep(REPLAY_BACKPRESSURE_TICK).await;
continue;
}
match tx.try_send(cloud_event) {
Ok(()) => return,
Err(TrySendError::Full(ev)) => {
cloud_event = ev;
tokio::time::sleep(REPLAY_BACKPRESSURE_TICK).await;
}
Err(TrySendError::Closed(_)) => {
tracing::warn!("cloud channel closed during replay — worker has exited");
return;
}
}
}
}
async fn process_envelope(
state: Arc<AppState>,
mut envelope: EventEnvelope,
start: std::time::Instant,
mode: ProcessMode,
) -> (
HookEventType,
String,
Option<String>,
bool,
Option<PolicyMatch>,
) {
let event_type = envelope.type_.clone();
let event_id = envelope.id.clone();
let policy_bundle: Option<Arc<Option<ResidentBundle>>> = match mode {
ProcessMode::Live => state.policy.as_ref().map(|p| p.handle.load_full()),
ProcessMode::Replay => None,
};
let resident = policy_bundle.as_deref().and_then(|b| b.as_ref());
let policy_match = resident.and_then(|bundle| {
crate::core::policy::evaluate(bundle, event_type.as_str(), envelope.data.as_ref())
});
if let (Some(m), Some(bundle)) = (policy_match.as_ref(), resident) {
tracing::info!(
target: "policy",
rule_id = %m.rule_id,
mode = %m.mode,
verdict = %verdict_for(&event_type, Some(m)),
shadow = m.shadow,
revision = bundle.revision,
"policy decision"
);
}
if let (Some(install_id), Some(session_id)) = (
state.config.agent_id.as_deref(),
envelope.subject.as_deref(),
) {
state
.registry
.upsert(install_id, install_id, envelope.source.as_str(), session_id);
}
let event_cwd = envelope
.data
.as_ref()
.and_then(|d| d.get("cwd"))
.and_then(|v| v.as_str());
let (osuser, identity) = identity_stamp(
mode,
resident.is_some_and(|b| b.capture_identity_signals),
envelope.subject.as_deref(),
event_cwd,
identity::os_user,
identity::observe_session,
);
envelope.osuser = osuser;
envelope.gitemail = identity.git_email;
envelope.provideracct = identity.provider_account;
if matches!(event_type, HookEventType::SessionStart) {
if let Err(e) = crate::daemon::config_monitor::enrich_session_start(
&mut envelope.data,
&state.content_hash_cache,
state.config_monitor_request_tx.as_ref(),
)
.await
{
tracing::warn!(
code = crate::error::ERR_INVENTORY_ENRICH_FAILED,
error = ?e,
"session_start enrichment failed; forwarding un-enriched"
);
}
} else if let Some(cwd) = event_cwd {
crate::daemon::config_monitor::try_register_project_from_cwd(
cwd,
state.config_monitor_request_tx.as_ref(),
)
.await;
}
let raw_subject = envelope.subject.clone();
let subject = raw_subject.clone().unwrap_or_else(|| "unknown".into());
let type_str = envelope.type_.as_str().to_string();
let data_for_dedup = envelope.data.clone().unwrap_or_default();
let is_duplicate = state
.dedup
.check_and_insert(&subject, &type_str, &data_for_dedup);
if is_duplicate {
tracing::debug!(
code = crate::error::ERR_EVENT_DEDUPED,
session_id = %subject,
event_type = %type_str,
"Event deduplicated within TTL window"
);
return (event_type, event_id, raw_subject, true, policy_match);
}
if matches!(envelope.source, AgentType::Unknown(_)) {
crate::telemetry::capture_hook_source_unknown(envelope.source.as_str());
}
if matches!(envelope.type_, HookEventType::Unknown(_)) {
crate::telemetry::capture_hook_type_unknown(envelope.type_.as_str());
}
if let Some(data) = envelope.data.as_mut() {
privacy::filter_event_with(data, &state.privacy_filter);
}
if envelope.os.is_none() {
envelope.os = Some(crate::envelope::os_string().to_string());
}
if envelope.arch.is_none() {
envelope.arch = Some(crate::envelope::arch_string().to_string());
}
envelope.clientversion = Some(env!("OPENLATCH_VERSION").to_string());
if envelope.datacontenttype.is_none() {
envelope.datacontenttype = Some("application/json".to_string());
}
if envelope.localipv4.is_none() {
envelope.localipv4 = state.local_ipv4;
}
if envelope.localipv6.is_none() {
envelope.localipv6 = state.local_ipv6;
}
if envelope.publicipv4.is_none() {
envelope.publicipv4 = state.public_ipv4;
}
if envelope.publicipv6.is_none() {
envelope.publicipv6 = state.public_ipv6;
}
let mut envelope_value = serde_json::to_value(&envelope).unwrap_or(serde_json::Value::Null);
let verdict_for_stored = verdict_for(&event_type, policy_match.as_ref());
let latency_snapshot_ms = start.elapsed().as_millis() as u64;
if let Some(obj) = envelope_value.as_object_mut() {
obj.insert(
"olverdict".to_string(),
serde_json::json!(verdict_for_stored.to_string()),
);
obj.insert(
"ollatencyms".to_string(),
serde_json::json!(latency_snapshot_ms),
);
if let Some(policy) = state.policy.as_ref() {
if mode == ProcessMode::Live {
stamp_policy_extensions(
obj,
policy_match.as_ref(),
resident,
!policy.last_fetch_ok.load(Ordering::Relaxed),
SystemTime::now(),
);
}
}
}
let envelope_json = serde_json::to_string(&envelope_value).unwrap_or_default();
state.event_logger.log(envelope_json);
state.event_counter.fetch_add(1, Ordering::Relaxed);
if let Some(tx) = &state.cloud_tx {
let agent_id = state.config.agent_id.as_deref().unwrap_or_default();
if agent_id.is_empty() {
tracing::warn!(
code = "OL-1200",
"cloud forward skipped: [daemon].agent_id is empty in config.toml — run 'openlatch init' to populate it"
);
} else {
let cloud_event = crate::cloud::CloudEvent {
envelope: match mode {
ProcessMode::Live => envelope_value,
ProcessMode::Replay => serde_json::to_value(&envelope).unwrap_or_default(),
},
agent_id: agent_id.to_string(),
};
match mode {
ProcessMode::Live => {
let max_cap = tx.max_capacity();
match tx.try_send(cloud_event) {
Ok(()) => {}
Err(tokio::sync::mpsc::error::TrySendError::Full(_)) => {
if let Some(cs) = state.cloud_state.as_ref() {
cs.record_live_drop();
}
tracing::warn!(code = "OL-1200", "cloud channel full - event dropped");
}
Err(tokio::sync::mpsc::error::TrySendError::Closed(_)) => {
tracing::warn!("cloud channel closed - worker has exited");
}
}
if let Some(cs) = state.cloud_state.as_ref() {
if max_cap > 0 {
let free = tx.capacity();
let depth = max_cap.saturating_sub(free);
let pct = (depth as u64).saturating_mul(100) / max_cap as u64;
cs.note_channel_depth_pct(pct);
}
}
}
ProcessMode::Replay => {
forward_replay_event(tx, cloud_event).await;
}
}
}
}
(event_type, event_id, raw_subject, false, policy_match)
}
fn stamp_policy_extensions(
obj: &mut serde_json::Map<String, serde_json::Value>,
policy_match: Option<&PolicyMatch>,
bundle: Option<&ResidentBundle>,
offline: bool,
now: SystemTime,
) {
if let Some(m) = policy_match {
if m.shadow {
obj.insert(
"olverdictshadow".to_string(),
serde_json::json!(Verdict::Deny.to_string()),
);
}
obj.insert(
"olpolicyruleid".to_string(),
serde_json::json!(m.rule_id.clone()),
);
}
if let Some(b) = bundle {
obj.insert(
"olpolicybundlerev".to_string(),
serde_json::json!(b.revision),
);
obj.insert(
"olpolicybundleage".to_string(),
serde_json::json!(b.age_seconds(now)),
);
}
obj.insert("olpolicyoffline".to_string(), serde_json::json!(offline));
}
fn identity_stamp(
mode: ProcessMode,
capture: bool,
subject: Option<&str>,
cwd: Option<&str>,
resolve_os_user: impl FnOnce() -> Option<String>,
observe_session: impl FnOnce(&str, Option<&str>) -> IdentitySignals,
) -> (Option<String>, IdentitySignals) {
if mode != ProcessMode::Live || !capture {
return (None, IdentitySignals::default());
}
let signals = match subject {
Some(session_id) => observe_session(session_id, cwd),
None => IdentitySignals::default(),
};
(resolve_os_user(), signals)
}
fn reject(
status: StatusCode,
message: &str,
start: std::time::Instant,
) -> (StatusCode, HeaderMap, Json<VerdictResponse>) {
tracing::warn!(%message, http_status = %status, "cloudevents ingest rejected");
let latency_ms = start.elapsed().as_secs_f64() * 1000.0;
let response = VerdictResponse::allow(new_event_id(), latency_ms);
(status, HeaderMap::new(), Json(response))
}
pub async fn health(State(state): State<Arc<AppState>>) -> Json<serde_json::Value> {
let uptime_secs = state.started_at.elapsed().as_secs();
let degraded = state.health.degraded_names();
if !degraded.is_empty() {
tracing::debug!(
code = crate::error::ERR_SUBSYSTEM_DEGRADED,
subsystems = %degraded.join(","),
"/health reporting degraded"
);
}
Json(serde_json::json!({
"status": if degraded.is_empty() { "ok" } else { "degraded" },
"version": env!("OPENLATCH_VERSION"),
"uptime_secs": uptime_secs,
"subsystems": state.health.subsystems_json(),
}))
}
pub async fn metrics(State(state): State<Arc<AppState>>) -> Json<serde_json::Value> {
let events = state.event_counter.load(Ordering::Relaxed);
let uptime_secs = state.started_at.elapsed().as_secs();
let update_available = state.get_available_update();
let (
cloud_status,
cloud_forwarded_count,
cloud_last_sync_secs,
cloud_drop_count,
cloud_api_url,
cloud_consecutive_live_drops,
cloud_emergency_mode,
cloud_channel_high_water_ms,
) = if let Some(ref cs) = state.cloud_state {
let forwarded = cs.forwarded_count();
let last_sync = cs.last_sync_secs();
let drops = cs.drop_count();
let live_drops = cs.consecutive_live_drops();
let emergency = cs.is_emergency_mode();
let high_water_ms = cs.channel_high_water_window_ms();
let status = if cs.is_auth_error() {
"auth_error"
} else if cs.is_no_credential() {
"no_credential"
} else if emergency {
"emergency"
} else if cs.consecutive_drops() > 0 {
"network_error"
} else {
"connected"
};
(
status,
forwarded,
last_sync,
drops,
state.config.cloud.api_url.as_str(),
live_drops,
emergency,
high_water_ms,
)
} else {
("not_configured", 0u64, 0u64, 0u64, "", 0u64, false, 0u64)
};
let (outbox_pending_bytes, outbox_pending_count) = match state.outbox.as_ref() {
Some(o) => (o.byte_size(), o.pending_count()),
None => (0u64, 0u64),
};
let fallback_pending_count = fallback_log_line_count();
let policy = policy_metrics(state.policy.as_ref(), SystemTime::now());
let (cloud_channel_depth, cloud_channel_size_max) = match state.cloud_tx.as_ref() {
Some(tx) => {
let max = tx.max_capacity();
let free = tx.capacity();
(max.saturating_sub(free) as u64, max as u64)
}
None => (0u64, 0u64),
};
Json(serde_json::json!({
"events_processed": events,
"uptime_secs": uptime_secs,
"update_available": update_available,
"cloud_status": cloud_status,
"cloud_forwarded_count": cloud_forwarded_count,
"cloud_last_sync_secs": cloud_last_sync_secs,
"cloud_drop_count": cloud_drop_count,
"cloud_api_url": cloud_api_url,
"cloud_channel_depth": cloud_channel_depth,
"cloud_channel_size_max": cloud_channel_size_max,
"cloud_consecutive_live_drops": cloud_consecutive_live_drops,
"cloud_emergency_mode": cloud_emergency_mode,
"cloud_channel_high_water_ms": cloud_channel_high_water_ms,
"cloud_channel_high_water_pct_trip": crate::cloud::worker::HIGH_WATER_TRIP_PCT,
"outbox_pending_bytes": outbox_pending_bytes,
"outbox_pending_count": outbox_pending_count,
"fallback_pending_count": fallback_pending_count,
"policy_enabled": policy.enabled,
"policy_has_bundle": policy.has_bundle,
"policy_agent_function": policy.agent_function,
"policy_revision": policy.revision,
"policy_bundle_age_seconds": policy.bundle_age_seconds,
"policy_last_poll_ok_secs": policy.last_poll_ok_secs,
"subsystem_restarts_total": state.health.total_restarts(),
"subsystem_degraded_count": state.health.degraded_count(),
}))
}
struct PolicyMetrics {
enabled: bool,
has_bundle: bool,
revision: serde_json::Value,
bundle_age_seconds: serde_json::Value,
last_poll_ok_secs: serde_json::Value,
agent_function: serde_json::Value,
}
fn policy_metrics(policy: Option<&crate::daemon::PolicyRuntime>, now: SystemTime) -> PolicyMetrics {
let Some(policy) = policy else {
return PolicyMetrics {
enabled: false,
has_bundle: false,
revision: serde_json::Value::Null,
bundle_age_seconds: serde_json::Value::Null,
last_poll_ok_secs: serde_json::Value::Null,
agent_function: serde_json::Value::Null,
};
};
let resident = policy.handle.load_full();
let (has_bundle, revision, bundle_age_seconds, agent_function) = match resident.as_ref() {
Some(b) => (
true,
serde_json::json!(b.revision),
serde_json::json!(b.age_seconds(now)),
serde_json::json!(b.agent_function),
),
None => (
false,
serde_json::Value::Null,
serde_json::Value::Null,
serde_json::Value::Null,
),
};
let last_poll_ok_at = policy.last_poll_ok_at.load(Ordering::Relaxed);
let last_poll_ok_secs = if last_poll_ok_at > 0 {
let now_unix = now
.duration_since(UNIX_EPOCH)
.map(|d| d.as_secs() as i64)
.unwrap_or(0);
serde_json::json!(now_unix.saturating_sub(last_poll_ok_at).max(0))
} else {
serde_json::Value::Null
};
PolicyMetrics {
enabled: true,
has_bundle,
revision,
bundle_age_seconds,
last_poll_ok_secs,
agent_function,
}
}
fn fallback_log_line_count() -> u64 {
use std::io::{BufRead, Seek, SeekFrom};
let log_dir = crate::config::openlatch_dir().join("logs");
let path = log_dir.join("fallback.jsonl");
let offset_path = log_dir.join("fallback.jsonl.offset");
let offset: u64 = std::fs::read_to_string(&offset_path)
.ok()
.and_then(|s| s.trim().parse().ok())
.unwrap_or(0);
let Ok(file) = std::fs::File::open(&path) else {
return 0;
};
let mut reader = std::io::BufReader::new(file);
if offset > 0 && reader.seek(SeekFrom::Start(offset)).is_err() {
return 0;
}
reader
.lines()
.map_while(Result::ok)
.filter(|l| !l.trim().is_empty())
.count() as u64
}
pub async fn shutdown_handler(State(state): State<Arc<AppState>>) -> StatusCode {
let mut tx = state.shutdown_tx.lock().await;
if let Some(sender) = tx.take() {
let _ = sender.send(());
StatusCode::OK
} else {
StatusCode::GONE
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::core::policy::test_support::{resident, rule};
use crate::generated::types::{PolicyRuleMode, PolicyRuleSeverity};
fn match_of(rule_id: &str, shadow: bool) -> PolicyMatch {
PolicyMatch {
rule_id: rule_id.to_string(),
reason: format!("{rule_id} says no"),
severity: PolicyRuleSeverity::High,
mode: if shadow {
PolicyRuleMode::Observe
} else {
PolicyRuleMode::Enforce
},
shadow,
}
}
fn outcome(verdict: Verdict, id: &str) -> BatchOutcome {
BatchOutcome {
verdict,
event_type: match verdict {
Verdict::Approve => HookEventType::Stop,
_ => HookEventType::PreToolUse,
},
event_id: id.to_string(),
policy_match: match verdict {
Verdict::Deny => Some(match_of("OL-CMD-ENF", false)),
_ => None,
},
}
}
#[test]
fn a_non_shadow_match_denies_and_a_shadow_match_does_not() {
assert_eq!(
verdict_for(
&HookEventType::PreToolUse,
Some(&match_of("OL-CMD-A", false))
),
Verdict::Deny
);
assert_eq!(
verdict_for(
&HookEventType::PreToolUse,
Some(&match_of("OL-CMD-A", true))
),
Verdict::Allow,
"observe never blocks"
);
assert_eq!(
verdict_for(&HookEventType::PreToolUse, None),
Verdict::Allow
);
assert_eq!(verdict_for(&HookEventType::Stop, None), Verdict::Approve);
}
#[test]
fn deny_outranks_the_stop_upgrade() {
assert_eq!(
verdict_for(&HookEventType::Stop, Some(&match_of("OL-CMD-A", false))),
Verdict::Deny
);
}
#[test]
fn the_fold_is_most_restrictive_wins() {
for (a, b) in [
(Verdict::Allow, Verdict::Approve),
(Verdict::Allow, Verdict::Deny),
(Verdict::Approve, Verdict::Deny),
] {
for (first, second) in [(a, b), (b, a)] {
let mut worst = None;
join_most_restrictive(&mut worst, outcome(first, "evt_1"));
join_most_restrictive(&mut worst, outcome(second, "evt_2"));
let got = worst.expect("folded");
assert_eq!(
verdict_rank(got.verdict),
verdict_rank(a).max(verdict_rank(b)),
"{first:?} then {second:?} must fold to the more restrictive"
);
}
}
}
#[test]
fn the_fold_is_order_independent() {
let orders = [
[Verdict::Deny, Verdict::Approve, Verdict::Allow],
[Verdict::Allow, Verdict::Deny, Verdict::Approve],
[Verdict::Approve, Verdict::Allow, Verdict::Deny],
];
for order in orders {
let mut worst = None;
for (i, v) in order.iter().enumerate() {
join_most_restrictive(&mut worst, outcome(*v, &format!("evt_{i}")));
}
assert_eq!(worst.expect("folded").verdict, Verdict::Deny, "{order:?}");
}
}
#[test]
fn equal_rank_keeps_the_first_envelope() {
let mut worst = None;
join_most_restrictive(&mut worst, outcome(Verdict::Allow, "evt_1"));
join_most_restrictive(&mut worst, outcome(Verdict::Allow, "evt_2"));
assert_eq!(worst.expect("folded").event_id, "evt_1");
}
#[test]
fn the_deciding_envelope_carries_its_own_match() {
let mut worst = None;
join_most_restrictive(&mut worst, outcome(Verdict::Allow, "evt_1"));
join_most_restrictive(&mut worst, outcome(Verdict::Deny, "evt_2"));
let got = worst.expect("folded");
assert_eq!(got.event_id, "evt_2");
assert_eq!(
got.policy_match.expect("deny carries its match").rule_id,
"OL-CMD-ENF"
);
}
fn stamped(
policy_match: Option<&PolicyMatch>,
bundle: Option<&ResidentBundle>,
offline: bool,
now: SystemTime,
) -> serde_json::Map<String, serde_json::Value> {
let mut obj = serde_json::Map::new();
stamp_policy_extensions(&mut obj, policy_match, bundle, offline, now);
obj
}
#[test]
fn a_deny_stamps_rule_id_revision_age_and_offline_but_no_shadow() {
let bundle = resident(
vec![rule("OL-CMD-ENF", "*rm*", PolicyRuleMode::Enforce)],
true,
);
let now = bundle.built_at + Duration::from_secs(90);
let obj = stamped(
Some(&match_of("OL-CMD-ENF", false)),
Some(&bundle),
false,
now,
);
assert_eq!(obj.get("olpolicyruleid").unwrap(), "OL-CMD-ENF");
assert_eq!(obj.get("olpolicybundlerev").unwrap(), 42);
assert_eq!(obj.get("olpolicybundleage").unwrap(), 90);
assert_eq!(obj.get("olpolicyoffline").unwrap(), false);
assert!(
obj.get("olverdictshadow").is_none(),
"an enforced deny is not a shadow verdict"
);
}
#[test]
fn an_observe_match_stamps_the_shadow_verdict() {
let bundle = resident(vec![], true);
let obj = stamped(
Some(&match_of("OL-CMD-OBS", true)),
Some(&bundle),
false,
bundle.built_at,
);
assert_eq!(obj.get("olverdictshadow").unwrap(), "deny");
assert_eq!(obj.get("olpolicyruleid").unwrap(), "OL-CMD-OBS");
}
#[test]
fn no_match_still_reports_the_bundle_revision() {
let bundle = resident(vec![], true);
let obj = stamped(None, Some(&bundle), false, bundle.built_at);
assert_eq!(obj.get("olpolicybundlerev").unwrap(), 42);
assert!(obj.get("olpolicyruleid").is_none());
assert!(obj.get("olverdictshadow").is_none());
}
#[test]
fn with_no_bundle_the_bundle_fields_are_omitted_and_offline_still_reported() {
let obj = stamped(None, None, true, SystemTime::now());
assert!(obj.get("olpolicybundlerev").is_none());
assert!(obj.get("olpolicybundleage").is_none());
assert_eq!(obj.get("olpolicyoffline").unwrap(), true);
}
#[test]
fn bundle_age_is_clamped_at_zero_when_the_local_clock_is_behind() {
let bundle = resident(vec![], true);
let behind = bundle.built_at - Duration::from_secs(3_600);
let obj = stamped(None, Some(&bundle), false, behind);
assert_eq!(obj.get("olpolicybundleage").unwrap(), 0);
}
fn runtime(
bundle: Option<ResidentBundle>,
last_poll_ok_at: i64,
) -> crate::daemon::PolicyRuntime {
crate::daemon::PolicyRuntime {
handle: crate::core::policy::new_handle(bundle),
last_fetch_ok: Arc::new(std::sync::atomic::AtomicBool::new(true)),
last_poll_ok_at: Arc::new(std::sync::atomic::AtomicI64::new(last_poll_ok_at)),
}
}
#[test]
fn metrics_report_policy_disabled_as_such() {
let m = policy_metrics(None, SystemTime::now());
assert!(!m.enabled);
assert!(!m.has_bundle);
assert!(m.revision.is_null());
assert!(m.bundle_age_seconds.is_null());
assert!(m.last_poll_ok_secs.is_null());
}
#[test]
fn metrics_distinguish_no_bundle_from_a_resident_one() {
let cold = policy_metrics(Some(&runtime(None, 0)), SystemTime::now());
assert!(cold.enabled);
assert!(!cold.has_bundle, "never fetched: allow, marked no-bundle");
assert!(cold.revision.is_null());
assert!(cold.last_poll_ok_secs.is_null(), "0 means never, not now");
let bundle = resident(vec![], true);
let now = bundle.built_at + Duration::from_secs(120);
let warm = policy_metrics(Some(&runtime(Some(bundle), 1_784_624_400)), now);
assert!(warm.has_bundle, "last-good resident: keep enforcing");
assert_eq!(warm.revision, 42);
assert_eq!(warm.bundle_age_seconds, 120);
}
#[test]
fn metrics_report_the_resident_agent_function() {
let mut bundle = resident(vec![], true);
bundle.agent_function = Some(crate::generated::types::AgentFunction::ItOps);
let m = policy_metrics(Some(&runtime(Some(bundle), 1_000)), SystemTime::now());
assert_eq!(
m.agent_function, "it_ops",
"the wire spelling, not the variant"
);
let contextless = policy_metrics(
Some(&runtime(Some(resident(vec![], true)), 1_000)),
SystemTime::now(),
);
assert!(contextless.agent_function.is_null());
assert!(policy_metrics(Some(&runtime(None, 0)), SystemTime::now())
.agent_function
.is_null());
}
#[test]
fn metrics_bundle_age_is_clamped_and_poll_age_is_seconds_since_the_last_ok() {
let bundle = resident(vec![], true);
let behind = bundle.built_at - Duration::from_secs(60);
let m = policy_metrics(Some(&runtime(Some(bundle), 1_000)), behind);
assert_eq!(m.bundle_age_seconds, 0, "clock skew never goes negative");
let expected = behind
.duration_since(UNIX_EPOCH)
.expect("after epoch")
.as_secs() as i64
- 1_000;
assert_eq!(m.last_poll_ok_secs, expected);
}
fn queued_alert() -> crate::daemon::config_monitor::PendingAlert {
crate::daemon::config_monitor::PendingAlert {
alert_id: "alert_1".to_string(),
session_ref_id: "sess_abc".to_string(),
severity: "high".to_string(),
headline: "Configuration alert pending".to_string(),
body: "MCP server 'evil' was added.".to_string(),
source_id: "claude-code:mcp:deadbeef".to_string(),
created_at: "2026-07-21T09:00:00Z".to_string(),
delivered: false,
}
}
#[test]
fn attaching_an_alert_to_a_deny_would_overwrite_the_rules_reason() {
let m = match_of("OL-CMD-ENF", false);
let mut response = VerdictResponse::deny(
"evt_1".to_string(),
1.0,
Some(m.reason.clone()),
Some(m.severity.to_string()),
Some(m.rule_id.clone()),
);
assert_eq!(response.reason.as_deref(), Some("OL-CMD-ENF says no"));
assert!(response.context.is_none());
attach_alert_context(&mut response, &queued_alert());
assert_eq!(
response.reason.as_deref(),
Some("Configuration alert pending"),
"attach_alert_context overwrites `reason` unconditionally"
);
assert!(
response.context.is_some(),
"and the translator prefers context over reason on a deny"
);
}
#[test]
fn a_guarded_deny_renders_the_rules_reason_to_the_agent() {
let m = match_of("OL-CMD-ENF", false);
let response = VerdictResponse::deny(
"evt_1".to_string(),
1.0,
Some(m.reason.clone()),
Some(m.severity.to_string()),
Some(m.rule_id.clone()),
);
let out = crate::hook_output::translate(
"claude-code",
"pre_tool_use",
&crate::hook_output::Verdict {
decision: &response.verdict.to_string(),
reason: response.reason.as_deref(),
context: None,
},
);
assert_eq!(
out["hookSpecificOutput"]["permissionDecision"], "deny",
"a policy deny must reach the agent as a native deny"
);
assert_eq!(
out["hookSpecificOutput"]["permissionDecisionReason"],
"OL-CMD-ENF says no"
);
}
#[test]
fn extension_names_are_lowercase_alphanumeric() {
let bundle = resident(vec![], true);
let obj = stamped(
Some(&match_of("OL-CMD-A", true)),
Some(&bundle),
true,
bundle.built_at,
);
assert_eq!(
obj.len(),
5,
"five of the six; olverdict is stamped by the caller"
);
for name in obj.keys() {
assert!(
name.chars()
.all(|c| c.is_ascii_lowercase() || c.is_ascii_digit()),
"illegal CloudEvents extension name: {name}"
);
}
}
fn fake_signals() -> IdentitySignals {
IdentitySignals {
git_email: Some("dev@example.com".to_string()),
provider_account: Some("dev@anthropic.example".to_string()),
}
}
fn stamp(
mode: ProcessMode,
capture: bool,
subject: Option<&str>,
) -> (Option<String>, IdentitySignals) {
identity_stamp(
mode,
capture,
subject,
Some("/repo"),
|| Some("injected-os-user".to_string()),
|_, _| fake_signals(),
)
}
#[test]
fn capture_on_with_a_session_stamps_all_three() {
let (osuser, signals) = stamp(ProcessMode::Live, true, Some("sess_a"));
assert_eq!(osuser.as_deref(), Some("injected-os-user"));
assert_eq!(signals, fake_signals());
}
#[test]
fn capture_off_or_no_bundle_stamps_nothing() {
let (osuser, signals) = stamp(ProcessMode::Live, false, Some("sess_a"));
assert_eq!(osuser, None);
assert_eq!(signals, IdentitySignals::default());
}
#[test]
fn replay_is_never_identity_stamped_even_with_capture_on() {
let (osuser, signals) = stamp(ProcessMode::Replay, true, Some("sess_a"));
assert_eq!(osuser, None);
assert_eq!(signals, IdentitySignals::default());
}
#[test]
fn a_sessionless_envelope_carries_osuser_only() {
let (osuser, signals) = identity_stamp(
ProcessMode::Live,
true,
None,
Some("/repo"),
|| Some("injected-os-user".to_string()),
|_, _| panic!("no subject means no session lookup"),
);
assert_eq!(osuser.as_deref(), Some("injected-os-user"));
assert_eq!(signals, IdentitySignals::default());
}
#[test]
fn the_events_cwd_reaches_the_resolver() {
let (_, signals) = identity_stamp(
ProcessMode::Live,
true,
Some("sess_a"),
Some("/repo/sub"),
|| None,
|session_id, cwd| {
assert_eq!(session_id, "sess_a");
IdentitySignals {
git_email: cwd.map(str::to_owned),
provider_account: None,
}
},
);
assert_eq!(signals.git_email.as_deref(), Some("/repo/sub"));
}
}