use serde::{Deserialize, Serialize};
use crate::generated::types::{Dimension, EffectVerb, Lever, PolicyMode, TargetClass, Verdict};
pub const MODE_ENFORCE: &str = "enforce";
pub const MODE_MONITOR: &str = "monitor";
#[derive(Debug, Clone, PartialEq, Default, Serialize, Deserialize)]
pub struct Event {
#[serde(default)]
pub event_type: String,
#[serde(default)]
pub session_id: String,
#[serde(default)]
pub tool_use_id: String,
#[serde(default)]
pub tool_name: String,
#[serde(default)]
pub tool_input: serde_json::Value,
#[serde(default)]
pub tool_result: Option<serde_json::Value>,
#[serde(default)]
pub agent: Option<AgentContext>,
#[serde(default)]
pub binding: serde_json::Value,
#[serde(default)]
pub env: Option<EventEnv>,
#[serde(default)]
pub session: Option<SessionFacts>,
#[serde(default)]
pub spend_delta: Option<SpendDelta>,
}
#[derive(Debug, Clone, PartialEq, Default, Serialize, Deserialize)]
pub struct AgentContext {
#[serde(default)]
pub agent_id: Option<String>,
#[serde(default)]
pub agent_row_id: Option<String>,
#[serde(default)]
pub agent_type: Option<String>,
#[serde(default)]
pub environment: Option<String>,
#[serde(default)]
pub function: Option<String>,
#[serde(default)]
pub principal: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Default, Serialize, Deserialize)]
pub struct EventEnv {
#[serde(default)]
pub home: Option<String>,
#[serde(default)]
pub cwd: Option<String>,
#[serde(default)]
pub path_dirs: Vec<String>,
#[serde(default)]
pub additional_dirs: Vec<String>,
}
#[derive(Debug, Clone, PartialEq, Default, Serialize, Deserialize)]
pub struct SessionFacts {
#[serde(default)]
pub elapsed_ms: Option<i64>,
#[serde(default)]
pub tool_calls: Option<i64>,
#[serde(default)]
pub spend_micro_usd: Option<i64>,
#[serde(default)]
pub tokens: Option<i64>,
}
#[derive(Debug, Clone, PartialEq, Default, Serialize, Deserialize)]
pub struct SpendDelta {
#[serde(default)]
pub tokens: Option<i64>,
#[serde(default)]
pub micro_usd: Option<i64>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct Decision {
pub verdict: Verdict,
pub artifact_id: Option<String>,
pub atom_id: Option<String>,
pub policy_public_id: Option<String>,
pub dimension: Option<Dimension>,
pub mode: Option<PolicyMode>,
pub tier: Option<i64>,
pub reason: String,
pub would_have_verdict: Option<Verdict>,
pub inconclusive_facts: Vec<String>,
pub rewrite: Option<Rewrite>,
pub optimize: Option<OptimizeContext>,
pub hold: Option<HoldRequest>,
pub effects: Vec<Effect>,
pub undecided: bool,
pub unknown: Vec<UnknownCommand>,
pub anomalies: Vec<Anomaly>,
pub ground_key: Option<String>,
pub warnings: Vec<String>,
}
impl Default for Decision {
fn default() -> Self {
Self {
verdict: Verdict::Allow,
artifact_id: None,
atom_id: None,
policy_public_id: None,
dimension: None,
mode: None,
tier: None,
reason: String::new(),
would_have_verdict: None,
inconclusive_facts: Vec::new(),
rewrite: None,
optimize: None,
hold: None,
effects: Vec::new(),
undecided: true,
unknown: Vec::new(),
anomalies: Vec::new(),
ground_key: None,
warnings: Vec::new(),
}
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct Effect {
pub verb: EffectVerb,
pub target_class: TargetClass,
#[serde(default)]
pub attrs: serde_json::Map<String, serde_json::Value>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct UnknownCommand {
pub shape: i64,
pub reason: String,
pub command: String,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct Anomaly {
pub code: String,
pub artifact_id: Option<String>,
pub atom_id: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct Rewrite {
pub lever: Lever,
pub artifact_id: Option<String>,
pub steer_instruction: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct OptimizeContext {
pub actual: Option<OptimizeCandidate>,
pub monitor: Option<OptimizeCandidate>,
pub shadowed: Vec<OptimizeCandidate>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct OptimizeCandidate {
pub artifact_id: Option<String>,
pub atom_id: Option<String>,
pub policy_id: Option<String>,
pub layer_unit_id: Option<String>,
pub lever: Lever,
pub reason: String,
}
#[derive(Debug, Clone, PartialEq)]
pub struct OptimizeDirective {
pub lever: Lever,
pub params: OptimizeParams,
}
#[derive(Debug, Clone, PartialEq)]
pub enum OptimizeParams {
EffortClamp(serde_json::Map<String, serde_json::Value>),
ContextEdit(serde_json::Map<String, serde_json::Value>),
PrefixGuard(serde_json::Map<String, serde_json::Value>),
LoopStop(serde_json::Map<String, serde_json::Value>),
NarrowOutput(serde_json::Map<String, serde_json::Value>),
Steer(serde_json::Map<String, serde_json::Value>),
Substitute(serde_json::Map<String, serde_json::Value>),
}
impl OptimizeParams {
pub(crate) fn parse(
lever: &str,
params: &serde_json::Map<String, serde_json::Value>,
) -> Option<Self> {
match lever {
"effort_clamp" => {
validate_effort_clamp(params).then(|| Self::EffortClamp(params.clone()))
}
"context_edit" => {
validate_context_edit(params).then(|| Self::ContextEdit(params.clone()))
}
"prefix_guard" => {
validate_prefix_guard(params).then(|| Self::PrefixGuard(params.clone()))
}
"loop_stop" => validate_loop_stop(params).then(|| Self::LoopStop(params.clone())),
"narrow_output" => {
validate_narrow_output(params).then(|| Self::NarrowOutput(params.clone()))
}
"steer" => validate_steer(params).then(|| Self::Steer(params.clone())),
"substitute" => validate_substitute(params).then(|| Self::Substitute(params.clone())),
_ => None,
}
}
pub(crate) fn canonical_value(&self) -> serde_json::Value {
let params = match self {
Self::EffortClamp(v)
| Self::ContextEdit(v)
| Self::PrefixGuard(v)
| Self::LoopStop(v)
| Self::NarrowOutput(v)
| Self::Steer(v)
| Self::Substitute(v) => v,
};
serde_json::Value::Object(params.clone())
}
}
fn exact_keys(params: &serde_json::Map<String, serde_json::Value>, keys: &[&str]) -> bool {
params.len() == keys.len() && keys.iter().all(|key| params.contains_key(*key))
}
fn string_array(value: Option<&serde_json::Value>) -> bool {
value
.and_then(serde_json::Value::as_array)
.is_some_and(|items| items.iter().all(|item| item.as_str().is_some()))
}
fn non_negative_integer(value: Option<&serde_json::Value>) -> bool {
value
.and_then(serde_json::Value::as_i64)
.is_some_and(|n| (0..=9_007_199_254_740_991).contains(&n))
}
fn validate_effort_clamp(params: &serde_json::Map<String, serde_json::Value>) -> bool {
exact_keys(params, &["effort", "quality_floor"])
&& params
.get("effort")
.and_then(serde_json::Value::as_str)
.is_some()
&& params
.get("quality_floor")
.and_then(serde_json::Value::as_str)
.is_some()
}
fn validate_context_edit(params: &serde_json::Map<String, serde_json::Value>) -> bool {
exact_keys(
params,
&["trigger", "keep", "clear_at_least", "exclude_tools"],
) && non_negative_integer(params.get("trigger"))
&& non_negative_integer(params.get("keep"))
&& non_negative_integer(params.get("clear_at_least"))
&& string_array(params.get("exclude_tools"))
}
fn validate_prefix_guard(params: &serde_json::Map<String, serde_json::Value>) -> bool {
exact_keys(params, &["pin_ttl"])
&& params
.get("pin_ttl")
.is_some_and(|value| value.is_null() || value.as_str().is_some())
}
fn validate_loop_stop(params: &serde_json::Map<String, serde_json::Value>) -> bool {
exact_keys(params, &["n_errors", "n_identical", "exempt_patterns"])
&& non_negative_integer(params.get("n_errors"))
&& non_negative_integer(params.get("n_identical"))
&& string_array(params.get("exempt_patterns"))
}
fn validate_narrow_output(params: &serde_json::Map<String, serde_json::Value>) -> bool {
exact_keys(params, &["allowlist"])
&& params
.get("allowlist")
.and_then(serde_json::Value::as_array)
.is_some_and(|items| {
items.iter().all(|item| {
item.as_object().is_some_and(|entry| {
exact_keys(entry, &["command_prefix", "filter"])
&& entry
.get("command_prefix")
.and_then(serde_json::Value::as_str)
.is_some()
&& entry
.get("filter")
.and_then(serde_json::Value::as_str)
.is_some()
})
})
})
}
fn validate_steer(params: &serde_json::Map<String, serde_json::Value>) -> bool {
exact_keys(params, &["steer_instruction", "dedupe_window_s"])
&& params
.get("steer_instruction")
.and_then(serde_json::Value::as_str)
.is_some()
&& non_negative_integer(params.get("dedupe_window_s"))
}
fn validate_substitute(params: &serde_json::Map<String, serde_json::Value>) -> bool {
exact_keys(params, &["pairs"])
&& params
.get("pairs")
.and_then(serde_json::Value::as_array)
.is_some_and(|items| {
items.iter().all(|item| {
item.as_object().is_some_and(|pair| {
exact_keys(pair, &["from", "to"])
&& pair
.get("from")
.and_then(serde_json::Value::as_str)
.is_some()
&& pair.get("to").and_then(serde_json::Value::as_str).is_some()
})
})
})
}
#[derive(Debug, Clone, PartialEq)]
pub struct OptimizeCandidateInternal {
pub evidence: OptimizeCandidate,
pub params: OptimizeParams,
pub layer_path: Vec<String>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct HoldRequest {
pub resolve: Option<String>,
pub directive_template_id: Option<String>,
pub max_attempts: Option<i64>,
pub timeout_s: Option<i64>,
pub on_timeout: Option<Verdict>,
pub verdict_on_approve: Option<Verdict>,
pub verdict_on_reject: Option<Verdict>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub struct StateLayout {
pub c: usize,
pub f: usize,
pub t: usize,
pub a: usize,
pub run: bool,
}
#[derive(Debug, Clone, PartialEq, Default, Serialize, Deserialize)]
pub struct SessionState {
#[serde(default)]
pub c: Vec<i64>,
#[serde(default)]
pub f: Vec<bool>,
#[serde(default)]
pub t: Vec<i64>,
#[serde(default)]
pub a: Vec<i64>,
#[serde(default)]
pub run: Option<RunState>,
}
impl SessionState {
pub fn blank(layout: &StateLayout) -> Self {
Self {
c: vec![0; layout.c],
f: vec![false; layout.f],
t: vec![0; layout.t],
a: vec![0; layout.a],
run: layout.run.then(RunState::default),
}
}
pub fn matches(&self, layout: &StateLayout) -> bool {
self.c.len() == layout.c
&& self.f.len() == layout.f
&& self.t.len() == layout.t
&& self.a.len() == layout.a
&& self.run.is_some() == layout.run
}
}
#[derive(Debug, Clone, PartialEq, Default, Serialize, Deserialize)]
pub struct RunState {
pub shape: Option<String>,
pub len: i64,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct SkippedItem {
pub kind: String,
pub id: String,
pub reason: String,
}
pub const SKIP_UNKNOWN_KIND: &str = "unknown_kind";
pub const SKIP_INVALID_OPTIMIZE_DIRECTIVE: &str = "invalid_optimize_directive";
pub const SKIP_SAME_LAYER_CONFLICT: &str = "same_layer_conflict";
pub const SKIP_BODY_PARSE_ERROR: &str = "body_parse_error";
pub const SKIP_BAD_PATTERN: &str = "bad_pattern";
pub const SKIP_BELOW_FLOOR: &str = "below_floor";
pub const SKIP_FLOAT_PRESENT: &str = "float_present";
pub const SKIP_UNKNOWN_FIELD: &str = "unknown_field";
pub const SKIP_UNKNOWN_PREDICATE: &str = "unknown_predicate";
pub const SKIP_KIND_EFFECT_CLASS_ENTRY: &str = "effect_class_entry";
#[derive(Debug, Clone, PartialEq, Default)]
pub struct Classification {
pub effects: Vec<Effect>,
pub unknown: Vec<UnknownCommand>,
pub simple: Vec<SimpleCommand>,
pub paths: Vec<ClassifiedPath>,
pub urls: Vec<ClassifiedUrl>,
}
#[derive(Debug, Clone, PartialEq, Default, Serialize, Deserialize)]
pub struct SimpleCommand {
pub program: String,
pub argv: Vec<String>,
pub raw: String,
pub redirects: Vec<(String, String)>,
pub raw_argv: Vec<String>,
}
impl SimpleCommand {
pub fn as_value(&self) -> serde_json::Value {
serde_json::json!({ "program": self.program, "argv": self.argv })
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct ClassifiedPath {
pub class: TargetClass,
pub value: String,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct ClassifiedUrl {
pub value: String,
pub host: String,
pub tld: String,
pub scheme: String,
pub boundary: UrlBoundary,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum UrlBoundary {
Internal,
External,
}
impl UrlBoundary {
pub fn as_str(self) -> &'static str {
match self {
UrlBoundary::Internal => "internal",
UrlBoundary::External => "external",
}
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct Contribution {
pub artifact_id: Option<String>,
pub atom_id: Option<String>,
pub policy_public_id: Option<String>,
pub dimension: Option<Dimension>,
pub mode: PolicyMode,
pub tier: Option<i64>,
pub verdict: Verdict,
pub reason: String,
pub inconclusive: Vec<String>,
pub anomalies: Vec<String>,
pub hold: Option<HoldRequest>,
pub exception_ground_key: Option<String>,
pub lever: Option<Lever>,
pub steer_instruction: Option<String>,
pub optimize: Option<OptimizeCandidateInternal>,
}
impl Contribution {
pub fn is_enforcing(&self) -> bool {
self.mode.as_str() == MODE_ENFORCE
}
}
#[derive(Debug)]
pub struct EvalContext<'a> {
pub event: &'a Event,
pub classification: &'a Classification,
pub facts: &'a super::facts::FactSet,
pub now_ms: i64,
pub inconclusive: Vec<String>,
pub warnings: Vec<String>,
}
impl<'a> EvalContext<'a> {
pub fn new(
event: &'a Event,
classification: &'a Classification,
facts: &'a super::facts::FactSet,
now_ms: i64,
) -> Self {
Self {
event,
classification,
facts,
now_ms,
inconclusive: Vec::new(),
warnings: Vec::new(),
}
}
pub fn fork(&self) -> EvalContext<'a> {
EvalContext::new(self.event, self.classification, self.facts, self.now_ms)
}
pub fn note_inconclusive(&mut self, fact_id: &str) {
if !self.inconclusive.iter().any(|f| f == fact_id) {
self.inconclusive.push(fact_id.to_string());
}
}
pub fn warn(&mut self, message: impl Into<String>) {
let message = message.into();
if !self.warnings.contains(&message) {
self.warnings.push(message);
}
}
pub fn merge_warnings(&mut self, child: &EvalContext<'_>) {
for warning in &child.warnings {
self.warn(warning.clone());
}
}
}