use serde::{Deserialize, Serialize}; use std::fmt; use std::time::{Duration, SystemTime, UNIX_EPOCH}; use uuid::Uuid; pub const POLICY_SCHEMA_VERSION: u16 = 1; #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] #[serde(rename_all = "snake_case")] pub enum CommitmentSource { Manual, Planner, Recurring, Recovery, Template, } #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] #[serde(rename_all = "snake_case")] pub enum CommitmentState { Draft, Active, Paused, Completed, Abandoned, Violated, } #[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)] #[serde(rename_all = "snake_case")] pub enum RuntimeState { Locked, Planning, Active, Transition, Review, AdminOverride, } #[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)] #[serde(rename_all = "snake_case")] pub enum EnforcementLevel { Observe, Warn, Block, Locked, } #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] #[serde(rename_all = "snake_case")] pub enum EvidenceHealth { Available, Degraded(String), Unavailable(String), } #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, Default)] pub struct AllowedContext { pub window_classes: Vec, pub window_title_substrings: Vec, pub domains: Vec, pub repos: Vec, pub commands: Vec, } #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] pub struct Commitment { pub id: String, pub created_at_unix_secs: u64, pub source: CommitmentSource, pub project_id: Option, pub template_id: Option, pub next_action: String, pub success_condition: String, pub timebox_secs: u64, pub transition_policy_id: Option, pub state: CommitmentState, } #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] pub struct PolicySnapshot { pub id: String, pub commitment_id: String, pub schema_version: u16, pub created_at_unix_secs: u64, pub runtime_state: RuntimeState, pub enforcement_level: EnforcementLevel, pub allowed_context: AllowedContext, pub required_monitors: Vec, pub violation_actions: Vec, pub expires_at_unix_secs: Option, pub generated_by_agent_version: String, } #[derive(Clone, Debug, PartialEq, Eq)] pub enum CommitmentValidationError { MissingId, MissingNextAction, MissingSuccessCondition, MissingTimebox, } impl fmt::Display for CommitmentValidationError { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { CommitmentValidationError::MissingId => write!(f, "commitment id is required"), CommitmentValidationError::MissingNextAction => write!(f, "next action is required"), CommitmentValidationError::MissingSuccessCondition => { write!(f, "success condition is required") } CommitmentValidationError::MissingTimebox => write!(f, "timebox must be nonzero"), } } } impl std::error::Error for CommitmentValidationError {} #[derive(Clone, Debug, PartialEq, Eq)] pub enum PolicySnapshotValidationError { WrongSchemaVersion, MissingId, MissingCommitmentId, MissingGeneratedByAgentVersion, } impl fmt::Display for PolicySnapshotValidationError { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { PolicySnapshotValidationError::WrongSchemaVersion => { write!(f, "policy schema version is unsupported") } PolicySnapshotValidationError::MissingId => write!(f, "policy id is required"), PolicySnapshotValidationError::MissingCommitmentId => { write!(f, "commitment id is required") } PolicySnapshotValidationError::MissingGeneratedByAgentVersion => { write!(f, "generated agent version is required") } } } } impl std::error::Error for PolicySnapshotValidationError {} impl Commitment { pub fn new_manual( next_action: impl Into, success_condition: impl Into, timebox: Duration, ) -> Result { let next_action = next_action.into().trim().to_string(); let success_condition = success_condition.into().trim().to_string(); if timebox.as_secs() == 0 { return Err(CommitmentValidationError::MissingTimebox); } let created_at_unix_secs = unix_secs_now(); let commitment = Self { id: unique_id("commitment"), created_at_unix_secs, source: CommitmentSource::Manual, project_id: None, template_id: None, next_action, success_condition, timebox_secs: timebox.as_secs(), transition_policy_id: None, state: CommitmentState::Draft, }; commitment.validate()?; Ok(commitment) } pub fn validate(&self) -> Result<(), CommitmentValidationError> { if self.id.trim().is_empty() { return Err(CommitmentValidationError::MissingId); } if self.next_action.trim().is_empty() { return Err(CommitmentValidationError::MissingNextAction); } if self.success_condition.trim().is_empty() { return Err(CommitmentValidationError::MissingSuccessCondition); } if self.timebox_secs == 0 { return Err(CommitmentValidationError::MissingTimebox); } Ok(()) } } impl PolicySnapshot { pub fn for_runtime( commitment_id: String, runtime_state: RuntimeState, enforcement_level: EnforcementLevel, allowed_context: AllowedContext, ) -> Self { Self::try_for_runtime( commitment_id, runtime_state, enforcement_level, allowed_context, ) .expect("runtime policy snapshot requires a non-empty commitment id") } pub fn try_for_runtime( commitment_id: String, runtime_state: RuntimeState, enforcement_level: EnforcementLevel, allowed_context: AllowedContext, ) -> Result { let commitment_id = commitment_id.trim().to_string(); if commitment_id.is_empty() { return Err(PolicySnapshotValidationError::MissingCommitmentId); } let created_at_unix_secs = unix_secs_now(); let policy = Self { id: unique_id("policy"), commitment_id, schema_version: POLICY_SCHEMA_VERSION, created_at_unix_secs, runtime_state, enforcement_level, allowed_context, required_monitors: Vec::new(), violation_actions: Vec::new(), expires_at_unix_secs: None, generated_by_agent_version: env!("CARGO_PKG_VERSION").to_string(), }; policy.validate()?; Ok(policy) } pub fn validate(&self) -> Result<(), PolicySnapshotValidationError> { if self.schema_version != POLICY_SCHEMA_VERSION { return Err(PolicySnapshotValidationError::WrongSchemaVersion); } if self.id.trim().is_empty() { return Err(PolicySnapshotValidationError::MissingId); } if self.commitment_id.trim().is_empty() { return Err(PolicySnapshotValidationError::MissingCommitmentId); } if self.generated_by_agent_version.trim().is_empty() { return Err(PolicySnapshotValidationError::MissingGeneratedByAgentVersion); } Ok(()) } } pub fn unix_secs_now() -> u64 { SystemTime::now() .duration_since(UNIX_EPOCH) .unwrap_or_default() .as_secs() } fn unique_id(prefix: &str) -> String { format!("{prefix}-{}", Uuid::now_v7()) } #[cfg(test)] mod tests { use super::*; use std::time::Duration; #[test] fn commitment_requires_concrete_action_and_success_condition() { let err = Commitment::new_manual("", "tests pass", Duration::from_secs(1500)) .expect_err("empty next action must be rejected"); assert_eq!(err, CommitmentValidationError::MissingNextAction); let err = Commitment::new_manual("Refactor state", "", Duration::from_secs(1500)) .expect_err("empty success condition must be rejected"); assert_eq!(err, CommitmentValidationError::MissingSuccessCondition); } #[test] fn commitment_requires_nonzero_timebox() { let err = Commitment::new_manual("Refactor state", "tests pass", Duration::ZERO) .expect_err("zero timebox must be rejected"); assert_eq!(err, CommitmentValidationError::MissingTimebox); } #[test] fn policy_snapshot_carries_runtime_contract() { let commitment = Commitment::new_manual( "Implement domain types", "domain tests pass", Duration::from_secs(1500), ) .unwrap(); let policy = PolicySnapshot::for_runtime( commitment.id.clone(), RuntimeState::Active, EnforcementLevel::Warn, AllowedContext::default(), ); assert_eq!(policy.commitment_id, commitment.id); assert_eq!(policy.runtime_state, RuntimeState::Active); assert_eq!(policy.enforcement_level, EnforcementLevel::Warn); assert_eq!(policy.schema_version, 1); } #[test] fn commitment_ids_are_unique_for_rapid_consecutive_construction() { let first = Commitment::new_manual( "Implement first action", "first tests pass", Duration::from_secs(1500), ) .unwrap(); let second = Commitment::new_manual( "Implement second action", "second tests pass", Duration::from_secs(1500), ) .unwrap(); assert_ne!(first.id, second.id); } #[test] fn policy_snapshot_ids_are_unique_for_rapid_consecutive_construction() { let first = PolicySnapshot::for_runtime( "commitment-1".to_string(), RuntimeState::Active, EnforcementLevel::Warn, AllowedContext::default(), ); let second = PolicySnapshot::for_runtime( "commitment-1".to_string(), RuntimeState::Active, EnforcementLevel::Warn, AllowedContext::default(), ); assert_ne!(first.id, second.id); } #[test] fn commitment_serializes_enum_contract_as_snake_case() { let commitment = Commitment::new_manual( "Implement domain contracts", "domain tests pass", Duration::from_secs(1500), ) .unwrap(); let json = serde_json::to_string(&commitment).unwrap(); assert!(json.contains(r#""source":"manual""#)); assert!(json.contains(r#""state":"draft""#)); } #[test] fn policy_snapshot_serializes_enum_contract_as_snake_case() { let policy = PolicySnapshot::for_runtime( "commitment-1".to_string(), RuntimeState::Active, EnforcementLevel::Warn, AllowedContext::default(), ); let json = serde_json::to_string(&policy).unwrap(); assert!(json.contains(r#""runtime_state":"active""#)); assert!(json.contains(r#""enforcement_level":"warn""#)); } #[test] fn commitment_validate_rejects_invalid_public_field_values() { let mut commitment = Commitment::new_manual("Refactor state", "tests pass", Duration::from_secs(1500)) .unwrap(); commitment.id = " ".to_string(); assert_eq!( commitment.validate(), Err(CommitmentValidationError::MissingId) ); commitment.id = "commitment-1".to_string(); commitment.next_action = " ".to_string(); assert_eq!( commitment.validate(), Err(CommitmentValidationError::MissingNextAction) ); commitment.next_action = "Refactor state".to_string(); commitment.success_condition = " ".to_string(); assert_eq!( commitment.validate(), Err(CommitmentValidationError::MissingSuccessCondition) ); commitment.success_condition = "tests pass".to_string(); commitment.timebox_secs = 0; assert_eq!( commitment.validate(), Err(CommitmentValidationError::MissingTimebox) ); } #[test] fn commitment_validate_rejects_invalid_deserialized_values() { let json = r#"{ "id": "", "created_at_unix_secs": 1, "source": "manual", "project_id": null, "template_id": null, "next_action": "Refactor state", "success_condition": "tests pass", "timebox_secs": 1500, "transition_policy_id": null, "state": "draft" }"#; let commitment: Commitment = serde_json::from_str(json).unwrap(); assert_eq!( commitment.validate(), Err(CommitmentValidationError::MissingId) ); } #[test] fn validation_errors_implement_std_error() { fn assert_std_error() {} assert_std_error::(); assert_std_error::(); } #[test] fn policy_snapshot_try_for_runtime_rejects_empty_commitment_id() { let err = PolicySnapshot::try_for_runtime( " ".to_string(), RuntimeState::Active, EnforcementLevel::Warn, AllowedContext::default(), ) .expect_err("empty commitment id must be rejected"); assert_eq!(err, PolicySnapshotValidationError::MissingCommitmentId); } #[test] fn policy_snapshot_validate_rejects_invalid_public_field_values() { let mut policy = PolicySnapshot::for_runtime( "commitment-1".to_string(), RuntimeState::Active, EnforcementLevel::Warn, AllowedContext::default(), ); policy.schema_version = POLICY_SCHEMA_VERSION + 1; assert_eq!( policy.validate(), Err(PolicySnapshotValidationError::WrongSchemaVersion) ); policy.schema_version = POLICY_SCHEMA_VERSION; policy.id = " ".to_string(); assert_eq!( policy.validate(), Err(PolicySnapshotValidationError::MissingId) ); policy.id = "policy-1".to_string(); policy.commitment_id = " ".to_string(); assert_eq!( policy.validate(), Err(PolicySnapshotValidationError::MissingCommitmentId) ); policy.commitment_id = "commitment-1".to_string(); policy.generated_by_agent_version = " ".to_string(); assert_eq!( policy.validate(), Err(PolicySnapshotValidationError::MissingGeneratedByAgentVersion) ); } #[test] fn policy_snapshot_validate_rejects_invalid_deserialized_values() { let json = r#"{ "id": "", "commitment_id": "commitment-1", "schema_version": 1, "created_at_unix_secs": 1, "runtime_state": "active", "enforcement_level": "warn", "allowed_context": { "window_classes": [], "window_title_substrings": [], "domains": [], "repos": [], "commands": [] }, "required_monitors": [], "violation_actions": [], "expires_at_unix_secs": null, "generated_by_agent_version": "0.1.0" }"#; let policy: PolicySnapshot = serde_json::from_str(json).unwrap(); assert_eq!( policy.validate(), Err(PolicySnapshotValidationError::MissingId) ); } }