Harden commitment domain contracts

This commit is contained in:
2026-05-25 12:44:01 -04:00
parent 54e9433835
commit 27fabab2b9
3 changed files with 587 additions and 13 deletions
Generated
+336 -2
View File
@@ -29,6 +29,7 @@ dependencies = [
"serde_json",
"sha2",
"shellexpand",
"uuid",
"winapi",
]
@@ -53,6 +54,12 @@ dependencies = [
"generic-array",
]
[[package]]
name = "bumpalo"
version = "3.20.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649"
[[package]]
name = "cassowary"
version = "0.3.0"
@@ -180,6 +187,30 @@ version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2"
[[package]]
name = "futures-core"
version = "0.3.32"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d"
[[package]]
name = "futures-task"
version = "0.3.32"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393"
[[package]]
name = "futures-util"
version = "0.3.32"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6"
dependencies = [
"futures-core",
"futures-task",
"pin-project-lite",
"slab",
]
[[package]]
name = "generic-array"
version = "0.14.7"
@@ -201,6 +232,19 @@ dependencies = [
"wasi",
]
[[package]]
name = "getrandom"
version = "0.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0de51e6874e94e7bf76d726fc5d13ba782deca734ff60d5bb2fb2607c7406555"
dependencies = [
"cfg-if",
"libc",
"r-efi",
"wasip2",
"wasip3",
]
[[package]]
name = "hashbrown"
version = "0.15.5"
@@ -212,6 +256,12 @@ dependencies = [
"foldhash",
]
[[package]]
name = "hashbrown"
version = "0.17.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a"
[[package]]
name = "heck"
version = "0.5.0"
@@ -224,6 +274,24 @@ version = "0.4.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70"
[[package]]
name = "id-arena"
version = "2.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954"
[[package]]
name = "indexmap"
version = "2.14.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9"
dependencies = [
"equivalent",
"hashbrown 0.17.1",
"serde",
"serde_core",
]
[[package]]
name = "itertools"
version = "0.13.0"
@@ -239,6 +307,24 @@ version = "1.0.16"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7ee5b5339afb4c41626dde77b7a611bd4f2c202b897852b4bcf5d03eddc61010"
[[package]]
name = "js-sys"
version = "0.3.99"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "142bc4740e452c1e57ade0cbc129f139c9093e354346f0872ef985f4f5cf5f11"
dependencies = [
"cfg-if",
"futures-util",
"once_cell",
"wasm-bindgen",
]
[[package]]
name = "leb128fmt"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2"
[[package]]
name = "libc"
version = "0.2.178"
@@ -276,7 +362,7 @@ version = "0.12.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "234cf4f4a04dc1f57e24b96cc0cd600cf2af460d4161ac5ecdd0af8e1f3b2a38"
dependencies = [
"hashbrown",
"hashbrown 0.15.5",
]
[[package]]
@@ -297,6 +383,12 @@ dependencies = [
"windows-sys 0.48.0",
]
[[package]]
name = "once_cell"
version = "1.21.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50"
[[package]]
name = "option-ext"
version = "0.2.0"
@@ -332,6 +424,22 @@ version = "1.0.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a"
[[package]]
name = "pin-project-lite"
version = "0.2.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd"
[[package]]
name = "prettyplease"
version = "0.2.37"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b"
dependencies = [
"proc-macro2",
"syn",
]
[[package]]
name = "proc-macro2"
version = "1.0.103"
@@ -350,6 +458,12 @@ dependencies = [
"proc-macro2",
]
[[package]]
name = "r-efi"
version = "6.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf"
[[package]]
name = "ratatui"
version = "0.27.0"
@@ -386,7 +500,7 @@ version = "0.5.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a4e608c6638b9c18977b00b475ac1f28d14e84b27d8d42f70e0bf1e3dec127ac"
dependencies = [
"getrandom",
"getrandom 0.2.16",
"libredox",
"thiserror",
]
@@ -438,6 +552,12 @@ version = "1.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
[[package]]
name = "semver"
version = "1.0.28"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd"
[[package]]
name = "serde"
version = "1.0.228"
@@ -531,6 +651,12 @@ dependencies = [
"libc",
]
[[package]]
name = "slab"
version = "0.4.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5"
[[package]]
name = "smallvec"
version = "1.15.1"
@@ -641,6 +767,23 @@ version = "0.1.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7dd6e30e90baa6f72411720665d41d89b9a3d039dc45b8faea1ddd07f617f6af"
[[package]]
name = "unicode-xid"
version = "0.2.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853"
[[package]]
name = "uuid"
version = "1.23.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ddd74a9687298c6858e9b88ec8935ec45d22e8fd5e6394fa1bd4e99a87789c76"
dependencies = [
"getrandom 0.4.2",
"js-sys",
"wasm-bindgen",
]
[[package]]
name = "version_check"
version = "0.9.5"
@@ -653,6 +796,103 @@ version = "0.11.1+wasi-snapshot-preview1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b"
[[package]]
name = "wasip2"
version = "1.0.3+wasi-0.2.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "20064672db26d7cdc89c7798c48a0fdfac8213434a1186e5ef29fd560ae223d6"
dependencies = [
"wit-bindgen 0.57.1",
]
[[package]]
name = "wasip3"
version = "0.4.0+wasi-0.3.0-rc-2026-01-06"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5428f8bf88ea5ddc08faddef2ac4a67e390b88186c703ce6dbd955e1c145aca5"
dependencies = [
"wit-bindgen 0.51.0",
]
[[package]]
name = "wasm-bindgen"
version = "0.2.122"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3ed04576f974d2b2fba0f38c51dbc5518011e38c36bf1143164be765528fd409"
dependencies = [
"cfg-if",
"once_cell",
"rustversion",
"wasm-bindgen-macro",
"wasm-bindgen-shared",
]
[[package]]
name = "wasm-bindgen-macro"
version = "0.2.122"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "916151b09da36bd82f6615cbf3a419e2f0ba23a03c6160e8e92eb6bd4aa1dec6"
dependencies = [
"quote",
"wasm-bindgen-macro-support",
]
[[package]]
name = "wasm-bindgen-macro-support"
version = "0.2.122"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "299047362ccbfce148b67ab7e73349f77748e00c8296f9542adfad2ad82c5c5e"
dependencies = [
"bumpalo",
"proc-macro2",
"quote",
"syn",
"wasm-bindgen-shared",
]
[[package]]
name = "wasm-bindgen-shared"
version = "0.2.122"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9a929b2c61f11ba3e9bc35b50c1f25cb38e0e892c0c231ae2b8cf78d5dad4437"
dependencies = [
"unicode-ident",
]
[[package]]
name = "wasm-encoder"
version = "0.244.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "990065f2fe63003fe337b932cfb5e3b80e0b4d0f5ff650e6985b1048f62c8319"
dependencies = [
"leb128fmt",
"wasmparser",
]
[[package]]
name = "wasm-metadata"
version = "0.244.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bb0e353e6a2fbdc176932bbaab493762eb1255a7900fe0fea1a2f96c296cc909"
dependencies = [
"anyhow",
"indexmap",
"wasm-encoder",
"wasmparser",
]
[[package]]
name = "wasmparser"
version = "0.244.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe"
dependencies = [
"bitflags",
"hashbrown 0.15.5",
"indexmap",
"semver",
]
[[package]]
name = "winapi"
version = "0.3.9"
@@ -755,3 +995,97 @@ name = "windows_x86_64_msvc"
version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538"
[[package]]
name = "wit-bindgen"
version = "0.51.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5"
dependencies = [
"wit-bindgen-rust-macro",
]
[[package]]
name = "wit-bindgen"
version = "0.57.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e"
[[package]]
name = "wit-bindgen-core"
version = "0.51.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ea61de684c3ea68cb082b7a88508a8b27fcc8b797d738bfc99a82facf1d752dc"
dependencies = [
"anyhow",
"heck",
"wit-parser",
]
[[package]]
name = "wit-bindgen-rust"
version = "0.51.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b7c566e0f4b284dd6561c786d9cb0142da491f46a9fbed79ea69cdad5db17f21"
dependencies = [
"anyhow",
"heck",
"indexmap",
"prettyplease",
"syn",
"wasm-metadata",
"wit-bindgen-core",
"wit-component",
]
[[package]]
name = "wit-bindgen-rust-macro"
version = "0.51.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0c0f9bfd77e6a48eccf51359e3ae77140a7f50b1e2ebfe62422d8afdaffab17a"
dependencies = [
"anyhow",
"prettyplease",
"proc-macro2",
"quote",
"syn",
"wit-bindgen-core",
"wit-bindgen-rust",
]
[[package]]
name = "wit-component"
version = "0.244.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2"
dependencies = [
"anyhow",
"bitflags",
"indexmap",
"log",
"serde",
"serde_derive",
"serde_json",
"wasm-encoder",
"wasm-metadata",
"wasmparser",
"wit-parser",
]
[[package]]
name = "wit-parser"
version = "0.244.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ecc8ac4bc1dc3381b7f59c34f00b67e18f910c2c0f50015669dde7def656a736"
dependencies = [
"anyhow",
"id-arena",
"indexmap",
"log",
"semver",
"serde",
"serde_derive",
"serde_json",
"unicode-xid",
"wasmparser",
]
+1
View File
@@ -12,6 +12,7 @@ shellexpand = "3.1.0"
serde = { version = "1.0", features = ["derive", "rc"] }
serde_json = "1.0"
sha2 = "0.10.8"
uuid = { version = "1", features = ["v7"] }
[target.'cfg(windows)'.dependencies]
winapi = { version = "0.3", features = ["winuser", "processthreadsapi"] }
+250 -11
View File
@@ -1,10 +1,12 @@
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,
@@ -14,6 +16,7 @@ pub enum CommitmentSource {
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum CommitmentState {
Draft,
Active,
@@ -24,6 +27,7 @@ pub enum CommitmentState {
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum RuntimeState {
Locked,
Planning,
@@ -34,6 +38,7 @@ pub enum RuntimeState {
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum EnforcementLevel {
Observe,
Warn,
@@ -42,6 +47,7 @@ pub enum EnforcementLevel {
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum EvidenceHealth {
Available,
Degraded(String),
@@ -105,6 +111,31 @@ impl fmt::Display 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 Commitment {
pub fn new_manual(
next_action: impl Into<String>,
@@ -114,19 +145,13 @@ impl Commitment {
let next_action = next_action.into().trim().to_string();
let success_condition = success_condition.into().trim().to_string();
if next_action.is_empty() {
return Err(CommitmentValidationError::MissingNextAction);
}
if success_condition.is_empty() {
return Err(CommitmentValidationError::MissingSuccessCondition);
}
if timebox.is_zero() {
if timebox.as_secs() == 0 {
return Err(CommitmentValidationError::MissingTimebox);
}
let created_at_unix_secs = unix_secs_now();
Ok(Self {
id: format!("commitment-{created_at_unix_secs}"),
let commitment = Self {
id: unique_id("commitment"),
created_at_unix_secs,
source: CommitmentSource::Manual,
project_id: None,
@@ -136,7 +161,22 @@ impl Commitment {
timebox_secs: timebox.as_secs(),
transition_policy_id: None,
state: CommitmentState::Draft,
})
};
commitment.validate()?;
Ok(commitment)
}
pub fn validate(&self) -> Result<(), CommitmentValidationError> {
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(())
}
}
@@ -149,7 +189,7 @@ impl PolicySnapshot {
) -> Self {
let created_at_unix_secs = unix_secs_now();
Self {
id: format!("policy-{created_at_unix_secs}"),
id: unique_id("policy"),
commitment_id,
schema_version: POLICY_SCHEMA_VERSION,
created_at_unix_secs,
@@ -162,6 +202,22 @@ impl PolicySnapshot {
generated_by_agent_version: env!("CARGO_PKG_VERSION").to_string(),
}
}
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 {
@@ -171,6 +227,10 @@ pub fn unix_secs_now() -> u64 {
.as_secs()
}
fn unique_id(prefix: &str) -> String {
format!("{prefix}-{}", Uuid::now_v7())
}
#[cfg(test)]
mod tests {
use super::*;
@@ -214,4 +274,183 @@ mod tests {
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.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": "commitment-1",
"created_at_unix_secs": 1,
"source": "manual",
"project_id": null,
"template_id": null,
"next_action": "",
"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::MissingNextAction)
);
}
#[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)
);
}
}