use std::fmt::Write as _;
use chrono::DateTime;
use serde_json::{Map, Value};
use sha2::{Digest, Sha256};
use crate::generated::types::{EffectiveZone, Exception, Verdict};
use super::bundle::LoadedArtifact;
use super::types::{Contribution, EvalContext};
pub fn validate_exception(_body: &Exception) -> Result<(), String> {
Ok(())
}
pub fn ground_key(atom_id: &str, selector: &Map<String, Value>) -> String {
sha256_hex(&[atom_id, &jcs(selector)])
}
pub fn action_hash(tool_name: &str, tool_input: &Value) -> String {
sha256_hex(&[tool_name, &jcs(tool_input)])
}
pub fn apply(
contributions: &mut [Contribution],
exceptions: &[LoadedArtifact],
ctx: &EvalContext<'_>,
zone: Option<&EffectiveZone>,
) {
if exceptions.is_empty() {
return;
}
let action = action_hash(&ctx.event.tool_name, &ctx.event.tool_input);
for contribution in contributions.iter_mut() {
let atom_id = contribution.atom_id.as_deref().unwrap_or("");
let mut matched: Option<&Exception> = None;
for artifact in exceptions {
let Some(body) = artifact.as_exception() else {
continue;
};
if exception_atom_id(artifact, body) != atom_id {
continue;
}
if !ground_key_is_declared_correctly(atom_id, body) {
continue;
}
if is_expired(body, ctx.now_ms) {
continue;
}
if !matches!(body.verdict, Some(Verdict::Allow) | Some(Verdict::Block)) {
continue;
}
if !selector_matches(body, atom_id, ctx, &action, zone) {
continue;
}
let wins = match matched {
None => true,
Some(current) => {
current.verdict != Some(Verdict::Block) && body.verdict == Some(Verdict::Block)
}
};
if wins {
matched = Some(body);
}
}
if let Some(body) = matched {
let verdict = body.verdict.unwrap_or(Verdict::Allow);
contribution.verdict = verdict;
contribution.exception_ground_key = Some(ground_key(atom_id, &body.selector));
contribution.reason = format!("exception {} -> {verdict}", scope_of(body));
}
}
}
fn exception_atom_id<'a>(artifact: &'a LoadedArtifact, body: &'a Exception) -> &'a str {
artifact.atom_id().unwrap_or_else(|| {
body.selector
.get("atom_id")
.and_then(Value::as_str)
.unwrap_or("")
})
}
fn ground_key_is_declared_correctly(atom_id: &str, body: &Exception) -> bool {
match body.ground_key.as_deref() {
Some(declared) => declared == ground_key(atom_id, &body.selector),
None => true,
}
}
fn is_expired(body: &Exception, now_ms: i64) -> bool {
body.expires_at
.as_deref()
.and_then(|text| DateTime::parse_from_rfc3339(text).ok())
.is_some_and(|at| now_ms > at.timestamp_millis())
}
fn selector_matches(
body: &Exception,
atom_id: &str,
ctx: &EvalContext<'_>,
action: &str,
zone: Option<&EffectiveZone>,
) -> bool {
let selector = &body.selector;
let field = |name: &str| selector.get(name).and_then(Value::as_str);
match scope_of(body) {
"exact_action" => field("action_hash") == Some(action),
"resource_class" => {
let Some(wanted) = selector.get("effect").and_then(Value::as_object) else {
return false;
};
let verb = wanted.get("verb").and_then(Value::as_str);
let target_class = wanted.get("target_class").and_then(Value::as_str);
ctx.classification.effects.iter().any(|effect| {
Some(effect.verb.as_str()) == verb
&& Some(effect.target_class.as_str()) == target_class
})
}
"rule_agent" => {
field("atom_id") == Some(atom_id)
&& field("agent_row_id")
== ctx
.event
.agent
.as_ref()
.and_then(|agent| agent.agent_row_id.as_deref())
}
"rule_zone" => {
if field("atom_id") != Some(atom_id) {
return false;
}
let Some(zone_id) = field("zone_id") else {
return false;
};
zone.is_some_and(|zone| {
zone.node_id.as_deref() == Some(zone_id)
|| zone.path.iter().any(|node| node == zone_id)
})
}
_ => false,
}
}
fn scope_of(body: &Exception) -> &str {
body.scope
.as_ref()
.map(|scope| scope.0.as_str())
.unwrap_or("")
}
fn jcs<T: serde::Serialize>(value: &T) -> String {
serde_json_canonicalizer::to_string(value).unwrap_or_default()
}
fn sha256_hex(parts: &[&str]) -> String {
let mut hasher = Sha256::new();
for part in parts {
hasher.update(part.as_bytes());
}
let mut hex = String::with_capacity(64);
for byte in hasher.finalize() {
let _ = write!(hex, "{byte:02x}");
}
hex
}
#[cfg(test)]
mod tests {
use super::*;
use crate::generated::types::{AskScope, PolicyArtifact};
use crate::generated::types::{EffectVerb, PolicyMode, TargetClass};
use crate::zone_eval::facts::FactSet;
use crate::zone_eval::types::{AgentContext, Classification, Contribution, Effect, Event};
const NOW: i64 = 1_756_742_400_000;
fn selector(json: serde_json::Value) -> Map<String, Value> {
json.as_object().expect("an object").clone()
}
fn exception_artifact(atom_id: &str, body: serde_json::Value) -> LoadedArtifact {
let parsed: Exception = serde_json::from_value(body.clone()).expect("the body parses");
LoadedArtifact {
envelope: PolicyArtifact {
artifact_id: Some("exc".to_string()),
atom_id: Some(atom_id.to_string()),
body: body.as_object().expect("an object").clone(),
kind: Some("exception".to_string().into()),
..Default::default()
},
body: super::super::bundle::ArtifactBody::Exception(Box::new(parsed)),
}
}
fn contribution(atom_id: &str) -> Contribution {
Contribution {
artifact_id: Some("sa1".to_string()),
atom_id: Some(atom_id.to_string()),
policy_public_id: None,
dimension: None,
mode: PolicyMode("enforce".to_string()),
tier: Some(1),
verdict: Verdict::Block,
reason: "the atom's own reason".to_string(),
inconclusive: Vec::new(),
anomalies: Vec::new(),
hold: None,
exception_ground_key: None,
lever: None,
steer_instruction: None,
}
}
fn classification() -> Classification {
Classification {
effects: vec![Effect {
verb: EffectVerb("delete".to_string()),
target_class: TargetClass("data_store".to_string()),
attrs: Map::new(),
}],
..Default::default()
}
}
#[test]
fn the_ground_key_is_stable_under_key_reordering() {
let one = ground_key(
"atom-sa1",
&selector(serde_json::json!({"atom_id": "atom-sa1", "zone_id": "zone-eng"})),
);
let other = ground_key(
"atom-sa1",
&selector(serde_json::json!({"zone_id": "zone-eng", "atom_id": "atom-sa1"})),
);
assert_eq!(one, other);
assert_eq!(one.len(), 64, "hex-encoded sha256, lowercase");
assert!(one
.chars()
.all(|c| c.is_ascii_hexdigit() && !c.is_uppercase()));
}
#[test]
fn the_ground_key_matches_the_platforms_for_the_same_answer() {
assert_eq!(
ground_key(
"atom-sa1",
&selector(serde_json::json!({
"action_hash": "3093cf4eea6b308644691caf2c508f24014873d17220767c7b48ca291f53e0b1"
})),
),
"328e118913c002066a27efbf1f32c192a12c361fc6361776a27f9168d3b724e3"
);
}
#[test]
fn the_action_hash_is_stable_under_key_reordering() {
let one = action_hash(
"Bash",
&serde_json::json!({"command": "rm -rf /data", "timeout": 1}),
);
let other = action_hash(
"Bash",
&serde_json::json!({"timeout": 1, "command": "rm -rf /data"}),
);
assert_eq!(one, other);
assert_eq!(
action_hash(
"Bash",
&serde_json::json!({"command": "rm -rf /data/warehouse/orders.db"})
),
"3093cf4eea6b308644691caf2c508f24014873d17220767c7b48ca291f53e0b1",
"the platform computes this for the same action"
);
}
#[test]
fn the_concatenation_carries_no_separator() {
assert_eq!(
ground_key("ab", &selector(serde_json::json!({}))),
sha256_hex(&["ab{}"])
);
}
#[test]
fn block_wins_over_allow_whatever_order_they_sort_in() {
let event = Event {
tool_name: "Bash".to_string(),
tool_input: serde_json::json!({"command": "rm -rf /data"}),
agent: Some(AgentContext {
agent_row_id: Some("agent-1".to_string()),
..Default::default()
}),
..Default::default()
};
let classification = classification();
let facts = FactSet::default();
let ctx = EvalContext::new(&event, &classification, &facts, NOW);
let allow = exception_artifact(
"atom-sa1",
serde_json::json!({
"scope": "rule_agent",
"selector": {"atom_id": "atom-sa1", "agent_row_id": "agent-1"},
"verdict": "allow",
}),
);
let block = exception_artifact(
"atom-sa1",
serde_json::json!({
"scope": "resource_class",
"selector": {"effect": {"verb": "delete", "target_class": "data_store"}},
"verdict": "block",
}),
);
for order in [vec![allow.clone(), block.clone()], vec![block, allow]] {
let mut contributions = vec![contribution("atom-sa1")];
apply(&mut contributions, &order, &ctx, None);
assert_eq!(
contributions[0].verdict,
Verdict::Block,
"the same two exceptions must not decide opposite ways on their names"
);
}
}
#[test]
fn rule_zone_matches_at_the_node_or_below_it_and_nowhere_else() {
let event = Event::default();
let classification = Classification::default();
let facts = FactSet::default();
let ctx = EvalContext::new(&event, &classification, &facts, NOW);
let zone = EffectiveZone {
node_id: Some("zone-eng".to_string()),
path: vec!["zone-root".to_string(), "zone-eng".to_string()],
resolved_from: None,
};
for (zone_id, expected) in [
("zone-eng", Verdict::Allow),
("zone-root", Verdict::Allow),
("zone-finance", Verdict::Block),
] {
let exception = exception_artifact(
"atom-sa1",
serde_json::json!({
"scope": "rule_zone",
"selector": {"atom_id": "atom-sa1", "zone_id": zone_id},
"verdict": "allow",
}),
);
let mut contributions = vec![contribution("atom-sa1")];
apply(&mut contributions, &[exception], &ctx, Some(&zone));
assert_eq!(contributions[0].verdict, expected, "zone_id {zone_id}");
}
}
#[test]
fn a_declared_ground_key_that_does_not_match_its_selector_never_applies() {
let event = Event {
tool_name: "Bash".to_string(),
tool_input: serde_json::json!({"command": "rm -rf /data"}),
..Default::default()
};
let classification = Classification::default();
let facts = FactSet::default();
let ctx = EvalContext::new(&event, &classification, &facts, NOW);
let exception = exception_artifact(
"atom-sa1",
serde_json::json!({
"scope": "exact_action",
"selector": {"action_hash": action_hash("Bash", &serde_json::json!({"command": "rm -rf /data"}))},
"verdict": "allow",
"ground_key": "0000000000000000000000000000000000000000000000000000000000000000",
}),
);
let mut contributions = vec![contribution("atom-sa1")];
apply(&mut contributions, &[exception], &ctx, None);
assert_eq!(
contributions[0].verdict,
Verdict::Block,
"a key the client cannot trust grants nothing"
);
}
#[test]
fn an_expired_exception_does_not_apply_and_an_unparseable_expiry_never_expires() {
let event = Event::default();
let classification = Classification::default();
let facts = FactSet::default();
let ctx = EvalContext::new(&event, &classification, &facts, NOW);
for (expires_at, expected) in [
("2025-08-31T16:00:00+00:00", Verdict::Block),
("2999-01-01T00:00:00Z", Verdict::Allow),
("not a timestamp", Verdict::Allow),
] {
let exception = exception_artifact(
"atom-sa1",
serde_json::json!({
"scope": "rule_agent",
"selector": {"atom_id": "atom-sa1"},
"verdict": "allow",
"expires_at": expires_at,
}),
);
let mut contributions = vec![contribution("atom-sa1")];
apply(&mut contributions, &[exception], &ctx, None);
assert_eq!(
contributions[0].verdict, expected,
"expires_at {expires_at}"
);
}
}
#[test]
fn a_scope_this_client_cannot_read_matches_nothing() {
let event = Event::default();
let classification = classification();
let facts = FactSet::default();
let ctx = EvalContext::new(&event, &classification, &facts, NOW);
let exception = exception_artifact(
"atom-sa1",
serde_json::json!({
"scope": "whole_organization",
"selector": {},
"verdict": "allow",
}),
);
let mut contributions = vec![contribution("atom-sa1")];
apply(&mut contributions, &[exception], &ctx, None);
assert_eq!(
contributions[0].verdict,
Verdict::Block,
"widening an answer the console never gave is the unsafe direction"
);
let _ = AskScope::from("exact_action".to_string());
}
}