From 10b9442aa915f05b2c16f1285a7fc9ba2bb88194 Mon Sep 17 00:00:00 2001 From: "Abdulaziz M. Shehri" Date: Sat, 29 Aug 2026 16:52:34 +0300 Subject: [PATCH 01/10] evidence(af02): add empty canonical T018 corpus manifest --- specs/016-af-02-adversarial-test-strength/corpus-manifest.json | 1 + 1 file changed, 1 insertion(+) create mode 100644 specs/016-af-02-adversarial-test-strength/corpus-manifest.json diff --git a/specs/016-af-02-adversarial-test-strength/corpus-manifest.json b/specs/016-af-02-adversarial-test-strength/corpus-manifest.json new file mode 100644 index 00000000..e7c9f8cc --- /dev/null +++ b/specs/016-af-02-adversarial-test-strength/corpus-manifest.json @@ -0,0 +1 @@ +{"entries":[],"max_fixture_bytes":262144,"max_total_bytes":8388608,"schema":"commandf.af02-corpus/v1","source_sha":"94bf4f1a9987f474613e67ddbc182ece8dff5a8d"} \ No newline at end of file From abeeb7fa8ad6781da78b5e721c41050c6d81d664 Mon Sep 17 00:00:00 2001 From: "Abdulaziz M. Shehri" Date: Sat, 29 Aug 2026 16:52:46 +0300 Subject: [PATCH 02/10] evidence(af02): add empty T018 assertion registry --- .../016-af-02-adversarial-test-strength/assertion-registry.json | 1 + 1 file changed, 1 insertion(+) create mode 100644 specs/016-af-02-adversarial-test-strength/assertion-registry.json diff --git a/specs/016-af-02-adversarial-test-strength/assertion-registry.json b/specs/016-af-02-adversarial-test-strength/assertion-registry.json new file mode 100644 index 00000000..adb21e0b --- /dev/null +++ b/specs/016-af-02-adversarial-test-strength/assertion-registry.json @@ -0,0 +1 @@ +{"corpus_manifest_sha256":"31b60a670f66d37dbbe039456b9f1a0fab95a75e880f3026f88e871b8a737a10","entries":[],"schema":"commandf.af02-assertion-registry/v1","source_sha":"94bf4f1a9987f474613e67ddbc182ece8dff5a8d","surface_policy_sha256":"dbd4a455d03e0bc4068155c1927bd827d3d1204fb26a0b7cb55e38c1b4abcf26"} \ No newline at end of file From 9fe96daadb34899c6d4f2bfd06b2b7438187b051 Mon Sep 17 00:00:00 2001 From: "Abdulaziz M. Shehri" Date: Sat, 29 Aug 2026 16:53:41 +0300 Subject: [PATCH 03/10] feat(af02): add T018 corpus and assertion contract validator --- tools/af02-verifier/src/corpus.rs | 615 ++++++++++++++++++++++++++++++ 1 file changed, 615 insertions(+) create mode 100644 tools/af02-verifier/src/corpus.rs diff --git a/tools/af02-verifier/src/corpus.rs b/tools/af02-verifier/src/corpus.rs new file mode 100644 index 00000000..d3bc6d78 --- /dev/null +++ b/tools/af02-verifier/src/corpus.rs @@ -0,0 +1,615 @@ +use std::collections::{BTreeMap, BTreeSet}; + +use serde::{Deserialize, Serialize}; +use serde_json::Value; +use thiserror::Error; + +use crate::canonical::{git_blob_sha1_hex, parse_json_no_duplicates, sha256_hex}; +use crate::surface::parse_surface_policy; + +const CORPUS_SCHEMA_ID: &str = "commandf.af02-corpus/v1"; +const CORPUS_SCHEMA_URL: &str = "https://commandf.dev/schemas/af02-corpus-v1.schema.json"; +const CORPUS_SCHEMA_GIT_BLOB_SHA: &str = "7ef4591d96adaa507014e4ad2f137cba6462fde2"; +const ASSERTION_SCHEMA_ID: &str = "commandf.af02-assertion-registry/v1"; +const MAX_FIXTURE_BYTES: u64 = 262_144; +const MAX_TOTAL_BYTES: u64 = 8_388_608; + +#[derive(Debug, Error)] +pub enum CorpusError { + #[error("corpus JSON error: {0}")] + Json(#[from] serde_json::Error), + #[error("corpus schema violation: {0}")] + Schema(String), + #[error("corpus contract violation: {0}")] + Contract(String), + #[error("surface policy validation failed: {0}")] + Surface(String), +} + +#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)] +#[serde(deny_unknown_fields)] +pub struct CorpusManifest { + pub schema: String, + pub source_sha: String, + pub max_fixture_bytes: u64, + pub max_total_bytes: u64, + pub entries: Vec, +} + +#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)] +#[serde(deny_unknown_fields)] +pub struct CorpusEntry { + pub scenario_id: String, + pub fixture_path: String, + pub fixture_sha256: String, + pub byte_length: u64, + pub provenance_class: ProvenanceClass, + pub expected_outcome: ExpectedOutcome, + pub assertion_id: String, + pub replay_id: String, + pub discovery_origin: DiscoveryOrigin, + pub parent_scenario_id_or_null: Option, + pub minimization_tool_or_null: Option, + pub contains_phi: bool, +} + +#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "SCREAMING_SNAKE_CASE")] +pub enum ProvenanceClass { + Synthetic, + PublicRedistributable, + GeneratedFromSynthetic, + OpaqueFuzzArtifactSafe, +} + +#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "SCREAMING_SNAKE_CASE")] +pub enum ExpectedOutcome { + AcceptCanonical, + RejectInvalid, + FailClosedLimit, +} + +#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "SCREAMING_SNAKE_CASE")] +pub enum DiscoveryOrigin { + HandAuthored, + FuzzDiscovery, + PropertyCounterexample, + MutationRegression, +} + +#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)] +#[serde(deny_unknown_fields)] +pub struct AssertionRegistry { + pub schema: String, + pub source_sha: String, + pub corpus_manifest_sha256: String, + pub surface_policy_sha256: String, + pub entries: Vec, +} + +#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)] +#[serde(deny_unknown_fields)] +pub struct AssertionEntry { + pub assertion_id: String, + pub scenario_id: String, + pub surface_id: String, + pub runner_kind: RunnerKind, + pub manifest_path: String, + pub package_or_binary: String, + pub cargo_target_or_null: Option, + pub test_name_or_null: Option, + pub argv: Vec, + pub cwd_repo_relative: String, + pub environment_allowlist: BTreeMap, + pub expected_outcome: ExpectedOutcome, + pub result_parser_id: String, + pub source_paths: Vec, + pub config_sha256s: Vec, +} + +#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "SCREAMING_SNAKE_CASE")] +pub enum RunnerKind { + CargoTest, + Af02ReplayBinary, +} + +pub fn parse_corpus_manifest( + instance_bytes: &[u8], + schema_bytes: &[u8], +) -> Result { + if git_blob_sha1_hex(schema_bytes) != CORPUS_SCHEMA_GIT_BLOB_SHA { + return Err(CorpusError::Schema( + "corpus schema bytes do not match the planning-frozen Git blob".to_owned(), + )); + } + let schema = parse_json_no_duplicates(schema_bytes)?; + if schema.get("$id").and_then(Value::as_str) != Some(CORPUS_SCHEMA_URL) { + return Err(CorpusError::Schema( + "unexpected planning-frozen corpus schema id".to_owned(), + )); + } + + let value = parse_json_no_duplicates(instance_bytes)?; + let manifest: CorpusManifest = serde_json::from_value(value)?; + validate_manifest(&manifest)?; + Ok(manifest) +} + +pub fn parse_assertion_registry(bytes: &[u8]) -> Result { + let value = parse_json_no_duplicates(bytes)?; + let registry: AssertionRegistry = serde_json::from_value(value)?; + validate_registry(®istry)?; + Ok(registry) +} + +pub fn validate_corpus_and_assertions( + corpus_bytes: &[u8], + corpus_schema_bytes: &[u8], + assertion_registry_bytes: &[u8], + surface_policy_bytes: &[u8], +) -> Result<(CorpusManifest, AssertionRegistry), CorpusError> { + let manifest = parse_corpus_manifest(corpus_bytes, corpus_schema_bytes)?; + let registry = parse_assertion_registry(assertion_registry_bytes)?; + let surface_policy = parse_surface_policy(surface_policy_bytes) + .map_err(|error| CorpusError::Surface(error.to_string()))?; + + if registry.source_sha != manifest.source_sha { + return Err(CorpusError::Contract( + "assertion registry source_sha does not match corpus source_sha".to_owned(), + )); + } + if registry.corpus_manifest_sha256 != sha256_hex(corpus_bytes) { + return Err(CorpusError::Contract( + "assertion registry corpus_manifest_sha256 does not match exact corpus bytes" + .to_owned(), + )); + } + if registry.surface_policy_sha256 != sha256_hex(surface_policy_bytes) { + return Err(CorpusError::Contract( + "assertion registry surface_policy_sha256 does not match exact surface policy bytes" + .to_owned(), + )); + } + + let allowed_surfaces = surface_policy + .critical_surfaces + .iter() + .map(|surface| surface.surface_id.as_str()) + .collect::>(); + for assertion in ®istry.entries { + if !allowed_surfaces.contains(assertion.surface_id.as_str()) { + return Err(CorpusError::Contract(format!( + "assertion {} references unknown critical surface {}", + assertion.assertion_id, assertion.surface_id + ))); + } + } + + let assertions_by_id = registry + .entries + .iter() + .map(|entry| (entry.assertion_id.as_str(), entry)) + .collect::>(); + if assertions_by_id.len() != registry.entries.len() { + return Err(CorpusError::Contract( + "assertion ids are not unique".to_owned(), + )); + } + + if manifest.entries.len() != registry.entries.len() { + return Err(CorpusError::Contract(format!( + "corpus/assertion cardinality mismatch: {} scenarios versus {} assertions", + manifest.entries.len(), + registry.entries.len() + ))); + } + for scenario in &manifest.entries { + let assertion = assertions_by_id + .get(scenario.assertion_id.as_str()) + .ok_or_else(|| { + CorpusError::Contract(format!( + "scenario {} has no assertion {}", + scenario.scenario_id, scenario.assertion_id + )) + })?; + if assertion.scenario_id != scenario.scenario_id { + return Err(CorpusError::Contract(format!( + "assertion {} binds scenario {}, expected {}", + assertion.assertion_id, assertion.scenario_id, scenario.scenario_id + ))); + } + if assertion.expected_outcome != scenario.expected_outcome { + return Err(CorpusError::Contract(format!( + "assertion {} expected outcome does not match scenario {}", + assertion.assertion_id, scenario.scenario_id + ))); + } + } + + let scenario_ids = manifest + .entries + .iter() + .map(|entry| entry.scenario_id.as_str()) + .collect::>(); + for assertion in ®istry.entries { + if !scenario_ids.contains(assertion.scenario_id.as_str()) { + return Err(CorpusError::Contract(format!( + "assertion {} is orphaned from corpus scenario {}", + assertion.assertion_id, assertion.scenario_id + ))); + } + } + + Ok((manifest, registry)) +} + +fn validate_manifest(manifest: &CorpusManifest) -> Result<(), CorpusError> { + if manifest.schema != CORPUS_SCHEMA_ID { + return Err(CorpusError::Contract(format!( + "unexpected corpus schema {}", + manifest.schema + ))); + } + validate_git_sha(&manifest.source_sha, "corpus source_sha")?; + if manifest.max_fixture_bytes != MAX_FIXTURE_BYTES + || manifest.max_total_bytes != MAX_TOTAL_BYTES + { + return Err(CorpusError::Contract( + "corpus limits do not match the planning-frozen schema".to_owned(), + )); + } + + let mut scenarios = BTreeSet::new(); + let mut assertions = BTreeSet::new(); + let mut replays = BTreeSet::new(); + let mut fixture_paths = BTreeSet::new(); + let mut total_bytes = 0_u64; + let mut previous_scenario: Option<&str> = None; + + for entry in &manifest.entries { + validate_id(&entry.scenario_id, "scenario_id")?; + validate_id(&entry.assertion_id, "assertion_id")?; + validate_id(&entry.replay_id, "replay_id")?; + validate_repo_path(&entry.fixture_path, "fixture_path")?; + validate_sha256(&entry.fixture_sha256, "fixture_sha256")?; + if entry.byte_length > MAX_FIXTURE_BYTES { + return Err(CorpusError::Contract(format!( + "scenario {} exceeds the per-fixture byte limit", + entry.scenario_id + ))); + } + total_bytes = total_bytes.checked_add(entry.byte_length).ok_or_else(|| { + CorpusError::Contract("corpus byte total overflowed".to_owned()) + })?; + if total_bytes > MAX_TOTAL_BYTES { + return Err(CorpusError::Contract( + "corpus exceeds the aggregate committed byte limit".to_owned(), + )); + } + if entry.contains_phi { + return Err(CorpusError::Contract(format!( + "scenario {} is marked as containing PHI", + entry.scenario_id + ))); + } + if let Some(parent) = &entry.parent_scenario_id_or_null { + validate_id(parent, "parent_scenario_id_or_null")?; + } + if let Some(tool) = &entry.minimization_tool_or_null { + if tool.is_empty() || tool.len() > 160 { + return Err(CorpusError::Contract(format!( + "scenario {} has invalid minimization tool identity", + entry.scenario_id + ))); + } + } + if !scenarios.insert(entry.scenario_id.as_str()) { + return Err(CorpusError::Contract(format!( + "duplicate scenario id {}", + entry.scenario_id + ))); + } + if !assertions.insert(entry.assertion_id.as_str()) { + return Err(CorpusError::Contract(format!( + "duplicate corpus assertion id {}", + entry.assertion_id + ))); + } + if !replays.insert(entry.replay_id.as_str()) { + return Err(CorpusError::Contract(format!( + "duplicate replay id {}", + entry.replay_id + ))); + } + if !fixture_paths.insert(entry.fixture_path.as_str()) { + return Err(CorpusError::Contract(format!( + "duplicate fixture path {}", + entry.fixture_path + ))); + } + if let Some(previous) = previous_scenario { + if previous >= entry.scenario_id.as_str() { + return Err(CorpusError::Contract( + "corpus entries must be strictly ordered by scenario_id".to_owned(), + )); + } + } + previous_scenario = Some(entry.scenario_id.as_str()); + } + Ok(()) +} + +fn validate_registry(registry: &AssertionRegistry) -> Result<(), CorpusError> { + if registry.schema != ASSERTION_SCHEMA_ID { + return Err(CorpusError::Contract(format!( + "unexpected assertion registry schema {}", + registry.schema + ))); + } + validate_git_sha(®istry.source_sha, "assertion registry source_sha")?; + validate_sha256( + ®istry.corpus_manifest_sha256, + "assertion registry corpus_manifest_sha256", + )?; + validate_sha256( + ®istry.surface_policy_sha256, + "assertion registry surface_policy_sha256", + )?; + + let mut assertion_ids = BTreeSet::new(); + let mut scenario_ids = BTreeSet::new(); + let mut previous_assertion: Option<&str> = None; + for entry in ®istry.entries { + validate_id(&entry.assertion_id, "assertion_id")?; + validate_id(&entry.scenario_id, "scenario_id")?; + validate_id(&entry.surface_id, "surface_id")?; + validate_repo_path(&entry.manifest_path, "manifest_path")?; + validate_repo_path(&entry.cwd_repo_relative, "cwd_repo_relative")?; + validate_id(&entry.result_parser_id, "result_parser_id")?; + if entry.package_or_binary.is_empty() { + return Err(CorpusError::Contract(format!( + "assertion {} has empty package_or_binary", + entry.assertion_id + ))); + } + if entry.argv.is_empty() || entry.argv.iter().any(|arg| arg.len() > 4096) { + return Err(CorpusError::Contract(format!( + "assertion {} has invalid argv", + entry.assertion_id + ))); + } + for (key, value) in &entry.environment_allowlist { + if !valid_environment_key(key) || value.len() > 4096 { + return Err(CorpusError::Contract(format!( + "assertion {} has invalid environment allowlist entry {}", + entry.assertion_id, key + ))); + } + } + if entry.source_paths.is_empty() { + return Err(CorpusError::Contract(format!( + "assertion {} must bind at least one source path", + entry.assertion_id + ))); + } + validate_sorted_unique_paths(&entry.source_paths, "source_paths")?; + validate_sorted_unique_sha256s(&entry.config_sha256s, "config_sha256s")?; + + match entry.runner_kind { + RunnerKind::CargoTest => { + if entry + .cargo_target_or_null + .as_deref() + .is_none_or(str::is_empty) + || entry.test_name_or_null.as_deref().is_none_or(str::is_empty) + { + return Err(CorpusError::Contract(format!( + "CARGO_TEST assertion {} requires cargo target and test name", + entry.assertion_id + ))); + } + } + RunnerKind::Af02ReplayBinary => { + if entry.cargo_target_or_null.is_some() || entry.test_name_or_null.is_some() { + return Err(CorpusError::Contract(format!( + "AF02_REPLAY_BINARY assertion {} requires null Cargo target/test fields", + entry.assertion_id + ))); + } + } + } + + if !assertion_ids.insert(entry.assertion_id.as_str()) { + return Err(CorpusError::Contract(format!( + "duplicate assertion id {}", + entry.assertion_id + ))); + } + if !scenario_ids.insert(entry.scenario_id.as_str()) { + return Err(CorpusError::Contract(format!( + "multiple assertions bind scenario {}", + entry.scenario_id + ))); + } + if let Some(previous) = previous_assertion { + if previous >= entry.assertion_id.as_str() { + return Err(CorpusError::Contract( + "assertion entries must be strictly ordered by assertion_id".to_owned(), + )); + } + } + previous_assertion = Some(entry.assertion_id.as_str()); + } + Ok(()) +} + +fn validate_git_sha(value: &str, label: &str) -> Result<(), CorpusError> { + if value.len() != 40 || !value.bytes().all(is_lower_hex) { + return Err(CorpusError::Contract(format!( + "{label} must be 40 lowercase hexadecimal characters" + ))); + } + Ok(()) +} + +fn validate_sha256(value: &str, label: &str) -> Result<(), CorpusError> { + if value.len() != 64 || !value.bytes().all(is_lower_hex) { + return Err(CorpusError::Contract(format!( + "{label} must be 64 lowercase hexadecimal characters" + ))); + } + Ok(()) +} + +fn is_lower_hex(byte: u8) -> bool { + byte.is_ascii_digit() || (b'a'..=b'f').contains(&byte) +} + +fn validate_id(value: &str, label: &str) -> Result<(), CorpusError> { + if value.is_empty() || value.len() > 160 { + return Err(CorpusError::Contract(format!("invalid {label}"))); + } + let mut bytes = value.bytes(); + let first = bytes.next().ok_or_else(|| CorpusError::Contract(format!("invalid {label}")))?; + if !first.is_ascii_alphanumeric() + || !bytes.all(|byte| byte.is_ascii_alphanumeric() || b"._:-".contains(&byte)) + { + return Err(CorpusError::Contract(format!("invalid {label}"))); + } + Ok(()) +} + +fn validate_repo_path(value: &str, label: &str) -> Result<(), CorpusError> { + if value.is_empty() + || value.starts_with('/') + || value.contains('\\') + || value.contains("//") + || value.contains('\0') + || value.split('/').any(|part| matches!(part, "." | "..")) + { + return Err(CorpusError::Contract(format!("invalid {label}: {value}"))); + } + Ok(()) +} + +fn valid_environment_key(value: &str) -> bool { + let mut bytes = value.bytes(); + let Some(first) = bytes.next() else { + return false; + }; + (first.is_ascii_uppercase() || first == b'_') + && bytes.all(|byte| byte.is_ascii_uppercase() || byte.is_ascii_digit() || byte == b'_') +} + +fn validate_sorted_unique_paths(values: &[String], label: &str) -> Result<(), CorpusError> { + let mut previous: Option<&str> = None; + for value in values { + validate_repo_path(value, label)?; + if let Some(prior) = previous { + if prior >= value.as_str() { + return Err(CorpusError::Contract(format!( + "{label} must be strictly sorted and unique" + ))); + } + } + previous = Some(value.as_str()); + } + Ok(()) +} + +fn validate_sorted_unique_sha256s(values: &[String], label: &str) -> Result<(), CorpusError> { + let mut previous: Option<&str> = None; + for value in values { + validate_sha256(value, label)?; + if let Some(prior) = previous { + if prior >= value.as_str() { + return Err(CorpusError::Contract(format!( + "{label} must be strictly sorted and unique" + ))); + } + } + previous = Some(value.as_str()); + } + Ok(()) +} + +#[cfg(test)] +mod tests { + use super::*; + + const CORPUS_SCHEMA: &[u8] = include_bytes!( + "../../../specs/016-af-02-adversarial-test-strength/schemas/af02-corpus-v1.schema.json" + ); + const SURFACE_POLICY: &[u8] = include_bytes!( + "../../../specs/016-af-02-adversarial-test-strength/surface-policy.json" + ); + + fn corpus(entry: &str) -> Vec { + format!( + "{{\"entries\":[{entry}],\"max_fixture_bytes\":262144,\"max_total_bytes\":8388608,\"schema\":\"commandf.af02-corpus/v1\",\"source_sha\":\"94bf4f1a9987f474613e67ddbc182ece8dff5a8d\"}}" + ) + .into_bytes() + } + + fn scenario(contains_phi: bool) -> String { + format!( + "{{\"assertion_id\":\"A001\",\"byte_length\":2,\"contains_phi\":{contains_phi},\"discovery_origin\":\"HAND_AUTHORED\",\"expected_outcome\":\"REJECT_INVALID\",\"fixture_path\":\"tests/assurance/corpus/f001.json\",\"fixture_sha256\":\"{}\",\"minimization_tool_or_null\":null,\"parent_scenario_id_or_null\":null,\"provenance_class\":\"SYNTHETIC\",\"replay_id\":\"R001\",\"scenario_id\":\"S001\"}}", + "0".repeat(64) + ) + } + + fn assertion(corpus_sha: &str, scenario_id: &str) -> Vec { + let surface_sha = sha256_hex(SURFACE_POLICY); + format!( + "{{\"corpus_manifest_sha256\":\"{corpus_sha}\",\"entries\":[{{\"argv\":[\"cargo\",\"test\"],\"assertion_id\":\"A001\",\"cargo_target_or_null\":\"af02_corpus\",\"config_sha256s\":[],\"cwd_repo_relative\":\".github\",\"environment_allowlist\":{{}},\"expected_outcome\":\"REJECT_INVALID\",\"manifest_path\":\"crates/commandf-pkg/Cargo.toml\",\"package_or_binary\":\"commandf-pkg\",\"result_parser_id\":\"cargo-test-v1\",\"runner_kind\":\"CARGO_TEST\",\"scenario_id\":\"{scenario_id}\",\"source_paths\":[\"crates/commandf-pkg/src/lock.rs\"],\"surface_id\":\"serde-json-from-slice\",\"test_name_or_null\":\"af02_corpus\"}}],\"schema\":\"commandf.af02-assertion-registry/v1\",\"source_sha\":\"94bf4f1a9987f474613e67ddbc182ece8dff5a8d\",\"surface_policy_sha256\":\"{surface_sha}\"}}" + ) + .into_bytes() + } + + #[test] + fn accepts_empty_design_freeze() { + let corpus = corpus(""); + let registry = format!( + "{{\"corpus_manifest_sha256\":\"{}\",\"entries\":[],\"schema\":\"commandf.af02-assertion-registry/v1\",\"source_sha\":\"94bf4f1a9987f474613e67ddbc182ece8dff5a8d\",\"surface_policy_sha256\":\"{}\"}}", + sha256_hex(&corpus), + sha256_hex(SURFACE_POLICY) + ); + validate_corpus_and_assertions( + &corpus, + CORPUS_SCHEMA, + registry.as_bytes(), + SURFACE_POLICY, + ) + .unwrap(); + } + + #[test] + fn rejects_phi_even_with_approved_provenance_class() { + let error = parse_corpus_manifest(&corpus(&scenario(true)), CORPUS_SCHEMA).unwrap_err(); + assert!(error.to_string().contains("containing PHI")); + } + + #[test] + fn rejects_orphan_assertion_scenario() { + let corpus = corpus(&scenario(false)); + let registry = assertion(&sha256_hex(&corpus), "S999"); + let error = validate_corpus_and_assertions( + &corpus, + CORPUS_SCHEMA, + ®istry, + SURFACE_POLICY, + ) + .unwrap_err(); + assert!(error.to_string().contains("binds scenario")); + } + + #[test] + fn rejects_replay_runner_with_cargo_target() { + let raw = br#"{"corpus_manifest_sha256":"0000000000000000000000000000000000000000000000000000000000000000","entries":[{"argv":["replay"],"assertion_id":"A001","cargo_target_or_null":"not-null","config_sha256s":[],"cwd_repo_relative":"tools","environment_allowlist":{},"expected_outcome":"REJECT_INVALID","manifest_path":"tools/af02-verifier/Cargo.toml","package_or_binary":"commandf-af02-verifier","result_parser_id":"replay-v1","runner_kind":"AF02_REPLAY_BINARY","scenario_id":"S001","source_paths":["tools/af02-verifier/src/main.rs"],"surface_id":"filesystem-read","test_name_or_null":null}],"schema":"commandf.af02-assertion-registry/v1","source_sha":"94bf4f1a9987f474613e67ddbc182ece8dff5a8d","surface_policy_sha256":"0000000000000000000000000000000000000000000000000000000000000000"}"#; + let error = parse_assertion_registry(raw).unwrap_err(); + assert!(error.to_string().contains("requires null Cargo target/test fields")); + } +} From 1cbeb4f2e25d8c84b9f78af97331e220bad59d68 Mon Sep 17 00:00:00 2001 From: "Abdulaziz M. Shehri" Date: Sat, 29 Aug 2026 16:55:24 +0300 Subject: [PATCH 04/10] feat(af02): expose T018 corpus contract validator --- tools/af02-verifier/src/lib.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/tools/af02-verifier/src/lib.rs b/tools/af02-verifier/src/lib.rs index 8a90dbc9..e469b406 100644 --- a/tools/af02-verifier/src/lib.rs +++ b/tools/af02-verifier/src/lib.rs @@ -1,5 +1,6 @@ pub mod authority; pub mod canonical; +pub mod corpus; pub mod resource; pub mod retained; pub mod surface; From f3c87df560c8fcd63159857f36a39b4e1325b986 Mon Sep 17 00:00:00 2001 From: "Abdulaziz M. Shehri" Date: Sat, 29 Aug 2026 16:55:53 +0300 Subject: [PATCH 05/10] feat(af02): wire T018 corpus contract commands --- tools/af02-verifier/src/main.rs | 47 +++++++++++++++++++++++++++++++++ 1 file changed, 47 insertions(+) diff --git a/tools/af02-verifier/src/main.rs b/tools/af02-verifier/src/main.rs index 6307f980..9ac0a478 100644 --- a/tools/af02-verifier/src/main.rs +++ b/tools/af02-verifier/src/main.rs @@ -3,6 +3,7 @@ use std::path::PathBuf; use commandf_af02_verifier::authority::{project_authority, Cf06Source}; use commandf_af02_verifier::canonical::{canonical_json_bytes, parse_json_no_duplicates}; +use commandf_af02_verifier::corpus::{parse_corpus_manifest, validate_corpus_and_assertions}; use commandf_af02_verifier::resource::{parse_resource_policy, run_bounded}; use commandf_af02_verifier::retained::{ locator_plan, project_retained, validate_and_parse, verify_artifacts, verify_workflow_run, @@ -210,6 +211,52 @@ fn run() -> Result<(), Box> { &canonical_json_bytes(&value)?, )?; } + "parse-corpus" => { + let corpus_path = PathBuf::from(args.next().ok_or("missing corpus manifest path")?); + let schema_path = PathBuf::from(args.next().ok_or("missing corpus schema path")?); + if args.next().is_some() { + return Err("parse-corpus accepts exactly a corpus path and schema path".into()); + } + let corpus = parse_corpus_manifest(&fs::read(corpus_path)?, &fs::read(schema_path)?)?; + let value = serde_json::to_value(corpus)?; + std::io::Write::write_all( + &mut std::io::stdout().lock(), + &canonical_json_bytes(&value)?, + )?; + } + "validate-corpus-assertions" => { + let corpus_path = PathBuf::from(args.next().ok_or("missing corpus manifest path")?); + let schema_path = PathBuf::from(args.next().ok_or("missing corpus schema path")?); + let assertion_path = + PathBuf::from(args.next().ok_or("missing assertion registry path")?); + let surface_policy_path = + PathBuf::from(args.next().ok_or("missing surface policy path")?); + if args.next().is_some() { + return Err( + "validate-corpus-assertions accepts exactly corpus, corpus schema, assertion registry, and surface policy paths" + .into(), + ); + } + let corpus_bytes = fs::read(corpus_path)?; + let schema_bytes = fs::read(schema_path)?; + let assertion_bytes = fs::read(assertion_path)?; + let surface_policy_bytes = fs::read(surface_policy_path)?; + let (corpus, assertions) = validate_corpus_and_assertions( + &corpus_bytes, + &schema_bytes, + &assertion_bytes, + &surface_policy_bytes, + )?; + let value = serde_json::json!({ + "assertion_count": assertions.entries.len(), + "scenario_count": corpus.entries.len(), + "schema": "commandf.af02-corpus-assertion-validation/v1" + }); + std::io::Write::write_all( + &mut std::io::stdout().lock(), + &canonical_json_bytes(&value)?, + )?; + } "verify-pr" => { return Err( "verify-pr is fail-closed until AF-02 T021-T025 semantic/input/base-gate enforcement is canonical" From 45ee728ca922d677d6d57c2350f7722aa23cdddb Mon Sep 17 00:00:00 2001 From: "Abdulaziz M. Shehri" Date: Sat, 29 Aug 2026 16:56:31 +0300 Subject: [PATCH 06/10] chore(af02): add temporary T018 static qualification --- .github/workflows/af02-t018-static.yml | 119 +++++++++++++++++++++++++ 1 file changed, 119 insertions(+) create mode 100644 .github/workflows/af02-t018-static.yml diff --git a/.github/workflows/af02-t018-static.yml b/.github/workflows/af02-t018-static.yml new file mode 100644 index 00000000..9583e06a --- /dev/null +++ b/.github/workflows/af02-t018-static.yml @@ -0,0 +1,119 @@ +name: af02-t018-static + +on: + push: + branches: + - feat/af02-a0-corpus-assertion-contract + paths: + - ".github/workflows/af02-t018-static.yml" + +permissions: + contents: read + +jobs: + validate: + runs-on: ubuntu-24.04 + timeout-minutes: 15 + steps: + - uses: actions/checkout@fbc6f3992d24b796d5a048ff273f7fcc4a7b6c09 + with: + fetch-depth: 0 + persist-credentials: false + - uses: dtolnay/rust-toolchain@032958afbdc797a9164d3bc0b56325c1308924a5 + with: + toolchain: 1.97.1 + components: rustfmt + - name: Compile and test T018 corpus contract + shell: bash + run: | + set -euo pipefail + cargo fmt --manifest-path tools/af02-verifier/Cargo.toml -- --check + cargo test --manifest-path tools/af02-verifier/Cargo.toml --locked + - name: Validate exact T018 design seed and parser outputs + shell: bash + run: | + set -euo pipefail + SPEC=specs/016-af-02-adversarial-test-strength + MANIFEST=tools/af02-verifier/Cargo.toml + cargo run --quiet --manifest-path "$MANIFEST" --locked -- parse-corpus \ + "$SPEC/corpus-manifest.json" \ + "$SPEC/schemas/af02-corpus-v1.schema.json" > /tmp/corpus.out + cmp "$SPEC/corpus-manifest.json" /tmp/corpus.out + cargo run --quiet --manifest-path "$MANIFEST" --locked -- validate-corpus-assertions \ + "$SPEC/corpus-manifest.json" \ + "$SPEC/schemas/af02-corpus-v1.schema.json" \ + "$SPEC/assertion-registry.json" \ + "$SPEC/surface-policy.json" > /tmp/validation.out + test "$(cat /tmp/validation.out)" = '{"assertion_count":0,"scenario_count":0,"schema":"commandf.af02-corpus-assertion-validation/v1"}' + python3 - <<'PY' + import hashlib, json, pathlib, subprocess + spec = pathlib.Path('specs/016-af-02-adversarial-test-strength') + base = '94bf4f1a9987f474613e67ddbc182ece8dff5a8d' + corpus_path = spec / 'corpus-manifest.json' + assertion_path = spec / 'assertion-registry.json' + surface_path = spec / 'surface-policy.json' + for path in (corpus_path, assertion_path): + raw = path.read_bytes() + if raw.endswith(b'\n'): + raise SystemExit(f'{path} must not have a trailing newline') + value = json.loads(raw) + canonical = json.dumps(value, sort_keys=True, separators=(',', ':')).encode() + if raw != canonical: + raise SystemExit(f'{path} is not canonical JSON') + corpus = json.loads(corpus_path.read_bytes()) + assertions = json.loads(assertion_path.read_bytes()) + if corpus != { + 'entries': [], + 'max_fixture_bytes': 262144, + 'max_total_bytes': 8388608, + 'schema': 'commandf.af02-corpus/v1', + 'source_sha': base, + }: + raise SystemExit('unexpected empty corpus design seed') + if assertions['entries'] != [] or assertions['source_sha'] != base: + raise SystemExit('unexpected empty assertion design seed') + corpus_sha = hashlib.sha256(corpus_path.read_bytes()).hexdigest() + surface_sha = hashlib.sha256(surface_path.read_bytes()).hexdigest() + if assertions['corpus_manifest_sha256'] != corpus_sha: + raise SystemExit(f'corpus digest mismatch: retained={assertions["corpus_manifest_sha256"]} actual={corpus_sha}') + if assertions['surface_policy_sha256'] != surface_sha: + raise SystemExit('surface policy digest mismatch') + changed = subprocess.check_output(['git','diff','--name-only',f'{base}...HEAD'], text=True).splitlines() + expected = sorted([ + '.github/workflows/af02-t018-static.yml', + 'specs/016-af-02-adversarial-test-strength/assertion-registry.json', + 'specs/016-af-02-adversarial-test-strength/corpus-manifest.json', + 'tools/af02-verifier/src/corpus.rs', + 'tools/af02-verifier/src/lib.rs', + 'tools/af02-verifier/src/main.rs', + ]) + if sorted(changed) != expected: + raise SystemExit(f'unexpected T018 paths: {changed!r}') + print(f'AF02_T018_CORPUS_SHA256={corpus_sha}') + print(f'AF02_T018_ASSERTION_REGISTRY_SHA256={hashlib.sha256(assertion_path.read_bytes()).hexdigest()}') + print('AF02_T018_SCENARIO_COUNT=0') + print('AF02_T018_ASSERTION_COUNT=0') + print('AF02_T018_STATIC_VALIDATION=PASS') + PY + - name: Prove new scanner findings remain classified + shell: bash + run: | + set -euo pipefail + SPEC=specs/016-af-02-adversarial-test-strength + MANIFEST=tools/af02-verifier/Cargo.toml + cargo run --quiet --manifest-path "$MANIFEST" --locked -- scan-surface "$SPEC/surface-policy.json" . > /tmp/findings.json + python3 - <<'PY' + import json + policy = json.load(open('specs/016-af-02-adversarial-test-strength/surface-policy.json')) + findings = json.load(open('/tmp/findings.json')) + surfaces = policy['critical_surfaces'] + errors = [] + for finding in findings: + matches = [s['surface_id'] for s in surfaces if finding['matcher_id'] in s['matcher_ids'] and finding['source_path'] in s['source_paths']] + if len(matches) != 1: + errors.append((finding['source_path'], finding['matcher_id'], matches)) + if errors: + raise SystemExit(f'unclassified or multiply classified findings: {errors!r}') + print(f'AF02_T018_SURFACE_FINDING_COUNT={len(findings)}') + print('AF02_T018_SURFACE_CLASSIFICATION=PASS') + PY From 6c3b593f318f547d86989f5702116a99b367a7c3 Mon Sep 17 00:00:00 2001 From: "Abdulaziz M. Shehri" Date: Sat, 29 Aug 2026 16:58:32 +0300 Subject: [PATCH 07/10] chore(af02): retry T018 static qualification on default runner --- .github/workflows/af02-t018-static.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/af02-t018-static.yml b/.github/workflows/af02-t018-static.yml index 9583e06a..021f3872 100644 --- a/.github/workflows/af02-t018-static.yml +++ b/.github/workflows/af02-t018-static.yml @@ -12,7 +12,7 @@ permissions: jobs: validate: - runs-on: ubuntu-24.04 + runs-on: ubuntu-latest timeout-minutes: 15 steps: - uses: actions/checkout@fbc6f3992d24b796d5a048ff273f7fcc4a7b6c09 From f02c23fc7375ae56bf3909da82007c2e065a95af Mon Sep 17 00:00:00 2001 From: "Abdulaziz M. Shehri" Date: Sat, 29 Aug 2026 16:59:29 +0300 Subject: [PATCH 08/10] chore(af02): remove unavailable temporary T018 qualification workflow --- .github/workflows/af02-t018-static.yml | 119 ------------------------- 1 file changed, 119 deletions(-) delete mode 100644 .github/workflows/af02-t018-static.yml diff --git a/.github/workflows/af02-t018-static.yml b/.github/workflows/af02-t018-static.yml deleted file mode 100644 index 021f3872..00000000 --- a/.github/workflows/af02-t018-static.yml +++ /dev/null @@ -1,119 +0,0 @@ -name: af02-t018-static - -on: - push: - branches: - - feat/af02-a0-corpus-assertion-contract - paths: - - ".github/workflows/af02-t018-static.yml" - -permissions: - contents: read - -jobs: - validate: - runs-on: ubuntu-latest - timeout-minutes: 15 - steps: - - uses: actions/checkout@fbc6f3992d24b796d5a048ff273f7fcc4a7b6c09 - with: - fetch-depth: 0 - persist-credentials: false - - uses: dtolnay/rust-toolchain@032958afbdc797a9164d3bc0b56325c1308924a5 - with: - toolchain: 1.97.1 - components: rustfmt - - name: Compile and test T018 corpus contract - shell: bash - run: | - set -euo pipefail - cargo fmt --manifest-path tools/af02-verifier/Cargo.toml -- --check - cargo test --manifest-path tools/af02-verifier/Cargo.toml --locked - - name: Validate exact T018 design seed and parser outputs - shell: bash - run: | - set -euo pipefail - SPEC=specs/016-af-02-adversarial-test-strength - MANIFEST=tools/af02-verifier/Cargo.toml - cargo run --quiet --manifest-path "$MANIFEST" --locked -- parse-corpus \ - "$SPEC/corpus-manifest.json" \ - "$SPEC/schemas/af02-corpus-v1.schema.json" > /tmp/corpus.out - cmp "$SPEC/corpus-manifest.json" /tmp/corpus.out - cargo run --quiet --manifest-path "$MANIFEST" --locked -- validate-corpus-assertions \ - "$SPEC/corpus-manifest.json" \ - "$SPEC/schemas/af02-corpus-v1.schema.json" \ - "$SPEC/assertion-registry.json" \ - "$SPEC/surface-policy.json" > /tmp/validation.out - test "$(cat /tmp/validation.out)" = '{"assertion_count":0,"scenario_count":0,"schema":"commandf.af02-corpus-assertion-validation/v1"}' - python3 - <<'PY' - import hashlib, json, pathlib, subprocess - spec = pathlib.Path('specs/016-af-02-adversarial-test-strength') - base = '94bf4f1a9987f474613e67ddbc182ece8dff5a8d' - corpus_path = spec / 'corpus-manifest.json' - assertion_path = spec / 'assertion-registry.json' - surface_path = spec / 'surface-policy.json' - for path in (corpus_path, assertion_path): - raw = path.read_bytes() - if raw.endswith(b'\n'): - raise SystemExit(f'{path} must not have a trailing newline') - value = json.loads(raw) - canonical = json.dumps(value, sort_keys=True, separators=(',', ':')).encode() - if raw != canonical: - raise SystemExit(f'{path} is not canonical JSON') - corpus = json.loads(corpus_path.read_bytes()) - assertions = json.loads(assertion_path.read_bytes()) - if corpus != { - 'entries': [], - 'max_fixture_bytes': 262144, - 'max_total_bytes': 8388608, - 'schema': 'commandf.af02-corpus/v1', - 'source_sha': base, - }: - raise SystemExit('unexpected empty corpus design seed') - if assertions['entries'] != [] or assertions['source_sha'] != base: - raise SystemExit('unexpected empty assertion design seed') - corpus_sha = hashlib.sha256(corpus_path.read_bytes()).hexdigest() - surface_sha = hashlib.sha256(surface_path.read_bytes()).hexdigest() - if assertions['corpus_manifest_sha256'] != corpus_sha: - raise SystemExit(f'corpus digest mismatch: retained={assertions["corpus_manifest_sha256"]} actual={corpus_sha}') - if assertions['surface_policy_sha256'] != surface_sha: - raise SystemExit('surface policy digest mismatch') - changed = subprocess.check_output(['git','diff','--name-only',f'{base}...HEAD'], text=True).splitlines() - expected = sorted([ - '.github/workflows/af02-t018-static.yml', - 'specs/016-af-02-adversarial-test-strength/assertion-registry.json', - 'specs/016-af-02-adversarial-test-strength/corpus-manifest.json', - 'tools/af02-verifier/src/corpus.rs', - 'tools/af02-verifier/src/lib.rs', - 'tools/af02-verifier/src/main.rs', - ]) - if sorted(changed) != expected: - raise SystemExit(f'unexpected T018 paths: {changed!r}') - print(f'AF02_T018_CORPUS_SHA256={corpus_sha}') - print(f'AF02_T018_ASSERTION_REGISTRY_SHA256={hashlib.sha256(assertion_path.read_bytes()).hexdigest()}') - print('AF02_T018_SCENARIO_COUNT=0') - print('AF02_T018_ASSERTION_COUNT=0') - print('AF02_T018_STATIC_VALIDATION=PASS') - PY - - name: Prove new scanner findings remain classified - shell: bash - run: | - set -euo pipefail - SPEC=specs/016-af-02-adversarial-test-strength - MANIFEST=tools/af02-verifier/Cargo.toml - cargo run --quiet --manifest-path "$MANIFEST" --locked -- scan-surface "$SPEC/surface-policy.json" . > /tmp/findings.json - python3 - <<'PY' - import json - policy = json.load(open('specs/016-af-02-adversarial-test-strength/surface-policy.json')) - findings = json.load(open('/tmp/findings.json')) - surfaces = policy['critical_surfaces'] - errors = [] - for finding in findings: - matches = [s['surface_id'] for s in surfaces if finding['matcher_id'] in s['matcher_ids'] and finding['source_path'] in s['source_paths']] - if len(matches) != 1: - errors.append((finding['source_path'], finding['matcher_id'], matches)) - if errors: - raise SystemExit(f'unclassified or multiply classified findings: {errors!r}') - print(f'AF02_T018_SURFACE_FINDING_COUNT={len(findings)}') - print('AF02_T018_SURFACE_CLASSIFICATION=PASS') - PY From d3bb49c801f5e05afb9062f3755093719edd834e Mon Sep 17 00:00:00 2001 From: "Abdulaziz M. Shehri" Date: Sat, 29 Aug 2026 17:05:07 +0300 Subject: [PATCH 09/10] fix(af02): verify retained fixture bytes and tighten T018 tests --- tools/af02-verifier/src/corpus.rs | 146 +++++++++++++++++++----------- 1 file changed, 95 insertions(+), 51 deletions(-) diff --git a/tools/af02-verifier/src/corpus.rs b/tools/af02-verifier/src/corpus.rs index d3bc6d78..02f0119e 100644 --- a/tools/af02-verifier/src/corpus.rs +++ b/tools/af02-verifier/src/corpus.rs @@ -131,7 +131,6 @@ pub fn parse_corpus_manifest( "unexpected planning-frozen corpus schema id".to_owned(), )); } - let value = parse_json_no_duplicates(instance_bytes)?; let manifest: CorpusManifest = serde_json::from_value(value)?; validate_manifest(&manifest)?; @@ -145,6 +144,35 @@ pub fn parse_assertion_registry(bytes: &[u8]) -> Result Result<(), CorpusError> { + let actual_bytes = u64::try_from(bytes.len()).map_err(|_| { + CorpusError::Contract(format!( + "fixture {} length cannot be represented as u64", + entry.fixture_path + )) + })?; + if actual_bytes > MAX_FIXTURE_BYTES { + return Err(CorpusError::Contract(format!( + "fixture {} exceeds the per-fixture byte limit", + entry.fixture_path + ))); + } + if actual_bytes != entry.byte_length { + return Err(CorpusError::Contract(format!( + "fixture {} byte length mismatch: declared {}, observed {}", + entry.fixture_path, entry.byte_length, actual_bytes + ))); + } + let actual_sha256 = sha256_hex(bytes); + if actual_sha256 != entry.fixture_sha256 { + return Err(CorpusError::Contract(format!( + "fixture {} SHA-256 mismatch: declared {}, observed {}", + entry.fixture_path, entry.fixture_sha256, actual_sha256 + ))); + } + Ok(()) +} + pub fn validate_corpus_and_assertions( corpus_bytes: &[u8], corpus_schema_bytes: &[u8], @@ -188,24 +216,22 @@ pub fn validate_corpus_and_assertions( } } + if manifest.entries.len() != registry.entries.len() { + return Err(CorpusError::Contract(format!( + "corpus/assertion cardinality mismatch: {} scenarios versus {} assertions", + manifest.entries.len(), + registry.entries.len() + ))); + } let assertions_by_id = registry .entries .iter() .map(|entry| (entry.assertion_id.as_str(), entry)) .collect::>(); if assertions_by_id.len() != registry.entries.len() { - return Err(CorpusError::Contract( - "assertion ids are not unique".to_owned(), - )); + return Err(CorpusError::Contract("assertion ids are not unique".to_owned())); } - if manifest.entries.len() != registry.entries.len() { - return Err(CorpusError::Contract(format!( - "corpus/assertion cardinality mismatch: {} scenarios versus {} assertions", - manifest.entries.len(), - registry.entries.len() - ))); - } for scenario in &manifest.entries { let assertion = assertions_by_id .get(scenario.assertion_id.as_str()) @@ -330,12 +356,10 @@ fn validate_manifest(manifest: &CorpusManifest) -> Result<(), CorpusError> { entry.fixture_path ))); } - if let Some(previous) = previous_scenario { - if previous >= entry.scenario_id.as_str() { - return Err(CorpusError::Contract( - "corpus entries must be strictly ordered by scenario_id".to_owned(), - )); - } + if previous_scenario.is_some_and(|previous| previous >= entry.scenario_id.as_str()) { + return Err(CorpusError::Contract( + "corpus entries must be strictly ordered by scenario_id".to_owned(), + )); } previous_scenario = Some(entry.scenario_id.as_str()); } @@ -400,12 +424,15 @@ fn validate_registry(registry: &AssertionRegistry) -> Result<(), CorpusError> { match entry.runner_kind { RunnerKind::CargoTest => { - if entry + let cargo_target_missing = entry .cargo_target_or_null .as_deref() - .is_none_or(str::is_empty) - || entry.test_name_or_null.as_deref().is_none_or(str::is_empty) - { + .map_or(true, str::is_empty); + let test_name_missing = entry + .test_name_or_null + .as_deref() + .map_or(true, str::is_empty); + if cargo_target_missing || test_name_missing { return Err(CorpusError::Contract(format!( "CARGO_TEST assertion {} requires cargo target and test name", entry.assertion_id @@ -434,12 +461,10 @@ fn validate_registry(registry: &AssertionRegistry) -> Result<(), CorpusError> { entry.scenario_id ))); } - if let Some(previous) = previous_assertion { - if previous >= entry.assertion_id.as_str() { - return Err(CorpusError::Contract( - "assertion entries must be strictly ordered by assertion_id".to_owned(), - )); - } + if previous_assertion.is_some_and(|previous| previous >= entry.assertion_id.as_str()) { + return Err(CorpusError::Contract( + "assertion entries must be strictly ordered by assertion_id".to_owned(), + )); } previous_assertion = Some(entry.assertion_id.as_str()); } @@ -473,7 +498,9 @@ fn validate_id(value: &str, label: &str) -> Result<(), CorpusError> { return Err(CorpusError::Contract(format!("invalid {label}"))); } let mut bytes = value.bytes(); - let first = bytes.next().ok_or_else(|| CorpusError::Contract(format!("invalid {label}")))?; + let first = bytes + .next() + .ok_or_else(|| CorpusError::Contract(format!("invalid {label}")))?; if !first.is_ascii_alphanumeric() || !bytes.all(|byte| byte.is_ascii_alphanumeric() || b"._:-".contains(&byte)) { @@ -508,12 +535,10 @@ fn validate_sorted_unique_paths(values: &[String], label: &str) -> Result<(), Co let mut previous: Option<&str> = None; for value in values { validate_repo_path(value, label)?; - if let Some(prior) = previous { - if prior >= value.as_str() { - return Err(CorpusError::Contract(format!( - "{label} must be strictly sorted and unique" - ))); - } + if previous.is_some_and(|prior| prior >= value.as_str()) { + return Err(CorpusError::Contract(format!( + "{label} must be strictly sorted and unique" + ))); } previous = Some(value.as_str()); } @@ -524,12 +549,10 @@ fn validate_sorted_unique_sha256s(values: &[String], label: &str) -> Result<(), let mut previous: Option<&str> = None; for value in values { validate_sha256(value, label)?; - if let Some(prior) = previous { - if prior >= value.as_str() { - return Err(CorpusError::Contract(format!( - "{label} must be strictly sorted and unique" - ))); - } + if previous.is_some_and(|prior| prior >= value.as_str()) { + return Err(CorpusError::Contract(format!( + "{label} must be strictly sorted and unique" + ))); } previous = Some(value.as_str()); } @@ -546,25 +569,25 @@ mod tests { const SURFACE_POLICY: &[u8] = include_bytes!( "../../../specs/016-af-02-adversarial-test-strength/surface-policy.json" ); + const SOURCE_SHA: &str = "94bf4f1a9987f474613e67ddbc182ece8dff5a8d"; fn corpus(entry: &str) -> Vec { format!( - "{{\"entries\":[{entry}],\"max_fixture_bytes\":262144,\"max_total_bytes\":8388608,\"schema\":\"commandf.af02-corpus/v1\",\"source_sha\":\"94bf4f1a9987f474613e67ddbc182ece8dff5a8d\"}}" + "{{\"entries\":[{entry}],\"max_fixture_bytes\":262144,\"max_total_bytes\":8388608,\"schema\":\"commandf.af02-corpus/v1\",\"source_sha\":\"{SOURCE_SHA}\"}}" ) .into_bytes() } - fn scenario(contains_phi: bool) -> String { + fn scenario(contains_phi: bool, fixture_sha256: &str, byte_length: u64) -> String { format!( - "{{\"assertion_id\":\"A001\",\"byte_length\":2,\"contains_phi\":{contains_phi},\"discovery_origin\":\"HAND_AUTHORED\",\"expected_outcome\":\"REJECT_INVALID\",\"fixture_path\":\"tests/assurance/corpus/f001.json\",\"fixture_sha256\":\"{}\",\"minimization_tool_or_null\":null,\"parent_scenario_id_or_null\":null,\"provenance_class\":\"SYNTHETIC\",\"replay_id\":\"R001\",\"scenario_id\":\"S001\"}}", - "0".repeat(64) + "{{\"assertion_id\":\"A001\",\"byte_length\":{byte_length},\"contains_phi\":{contains_phi},\"discovery_origin\":\"HAND_AUTHORED\",\"expected_outcome\":\"REJECT_INVALID\",\"fixture_path\":\"tests/assurance/corpus/f001.json\",\"fixture_sha256\":\"{fixture_sha256}\",\"minimization_tool_or_null\":null,\"parent_scenario_id_or_null\":null,\"provenance_class\":\"SYNTHETIC\",\"replay_id\":\"R001\",\"scenario_id\":\"S001\"}}" ) } fn assertion(corpus_sha: &str, scenario_id: &str) -> Vec { let surface_sha = sha256_hex(SURFACE_POLICY); format!( - "{{\"corpus_manifest_sha256\":\"{corpus_sha}\",\"entries\":[{{\"argv\":[\"cargo\",\"test\"],\"assertion_id\":\"A001\",\"cargo_target_or_null\":\"af02_corpus\",\"config_sha256s\":[],\"cwd_repo_relative\":\".github\",\"environment_allowlist\":{{}},\"expected_outcome\":\"REJECT_INVALID\",\"manifest_path\":\"crates/commandf-pkg/Cargo.toml\",\"package_or_binary\":\"commandf-pkg\",\"result_parser_id\":\"cargo-test-v1\",\"runner_kind\":\"CARGO_TEST\",\"scenario_id\":\"{scenario_id}\",\"source_paths\":[\"crates/commandf-pkg/src/lock.rs\"],\"surface_id\":\"serde-json-from-slice\",\"test_name_or_null\":\"af02_corpus\"}}],\"schema\":\"commandf.af02-assertion-registry/v1\",\"source_sha\":\"94bf4f1a9987f474613e67ddbc182ece8dff5a8d\",\"surface_policy_sha256\":\"{surface_sha}\"}}" + "{{\"corpus_manifest_sha256\":\"{corpus_sha}\",\"entries\":[{{\"argv\":[\"cargo\",\"test\"],\"assertion_id\":\"A001\",\"cargo_target_or_null\":\"af02_corpus\",\"config_sha256s\":[],\"cwd_repo_relative\":\"tools\",\"environment_allowlist\":{{}},\"expected_outcome\":\"REJECT_INVALID\",\"manifest_path\":\"crates/commandf-pkg/Cargo.toml\",\"package_or_binary\":\"commandf-pkg\",\"result_parser_id\":\"cargo-test-v1\",\"runner_kind\":\"CARGO_TEST\",\"scenario_id\":\"{scenario_id}\",\"source_paths\":[\"crates/commandf-pkg/src/lock.rs\"],\"surface_id\":\"serde-json-from-slice\",\"test_name_or_null\":\"af02_corpus\"}}],\"schema\":\"commandf.af02-assertion-registry/v1\",\"source_sha\":\"{SOURCE_SHA}\",\"surface_policy_sha256\":\"{surface_sha}\"}}" ) .into_bytes() } @@ -573,7 +596,7 @@ mod tests { fn accepts_empty_design_freeze() { let corpus = corpus(""); let registry = format!( - "{{\"corpus_manifest_sha256\":\"{}\",\"entries\":[],\"schema\":\"commandf.af02-assertion-registry/v1\",\"source_sha\":\"94bf4f1a9987f474613e67ddbc182ece8dff5a8d\",\"surface_policy_sha256\":\"{}\"}}", + "{{\"corpus_manifest_sha256\":\"{}\",\"entries\":[],\"schema\":\"commandf.af02-assertion-registry/v1\",\"source_sha\":\"{SOURCE_SHA}\",\"surface_policy_sha256\":\"{}\"}}", sha256_hex(&corpus), sha256_hex(SURFACE_POLICY) ); @@ -588,16 +611,18 @@ mod tests { #[test] fn rejects_phi_even_with_approved_provenance_class() { - let error = parse_corpus_manifest(&corpus(&scenario(true)), CORPUS_SCHEMA).unwrap_err(); + let raw = corpus(&scenario(true, &"0".repeat(64), 2)); + let error = parse_corpus_manifest(&raw, CORPUS_SCHEMA).unwrap_err(); assert!(error.to_string().contains("containing PHI")); } #[test] fn rejects_orphan_assertion_scenario() { - let corpus = corpus(&scenario(false)); - let registry = assertion(&sha256_hex(&corpus), "S999"); + let fixture = b"{}"; + let raw = corpus(&scenario(false, &sha256_hex(fixture), 2)); + let registry = assertion(&sha256_hex(&raw), "S999"); let error = validate_corpus_and_assertions( - &corpus, + &raw, CORPUS_SCHEMA, ®istry, SURFACE_POLICY, @@ -606,10 +631,29 @@ mod tests { assert!(error.to_string().contains("binds scenario")); } + #[test] + fn rejects_fixture_digest_mismatch() { + let fixture = b"{}"; + let raw = corpus(&scenario(false, &"0".repeat(64), 2)); + let manifest = parse_corpus_manifest(&raw, CORPUS_SCHEMA).unwrap(); + let error = verify_fixture_bytes(&manifest.entries[0], fixture).unwrap_err(); + assert!(error.to_string().contains("SHA-256 mismatch")); + } + + #[test] + fn accepts_exact_fixture_bytes() { + let fixture = b"{}"; + let raw = corpus(&scenario(false, &sha256_hex(fixture), 2)); + let manifest = parse_corpus_manifest(&raw, CORPUS_SCHEMA).unwrap(); + verify_fixture_bytes(&manifest.entries[0], fixture).unwrap(); + } + #[test] fn rejects_replay_runner_with_cargo_target() { let raw = br#"{"corpus_manifest_sha256":"0000000000000000000000000000000000000000000000000000000000000000","entries":[{"argv":["replay"],"assertion_id":"A001","cargo_target_or_null":"not-null","config_sha256s":[],"cwd_repo_relative":"tools","environment_allowlist":{},"expected_outcome":"REJECT_INVALID","manifest_path":"tools/af02-verifier/Cargo.toml","package_or_binary":"commandf-af02-verifier","result_parser_id":"replay-v1","runner_kind":"AF02_REPLAY_BINARY","scenario_id":"S001","source_paths":["tools/af02-verifier/src/main.rs"],"surface_id":"filesystem-read","test_name_or_null":null}],"schema":"commandf.af02-assertion-registry/v1","source_sha":"94bf4f1a9987f474613e67ddbc182ece8dff5a8d","surface_policy_sha256":"0000000000000000000000000000000000000000000000000000000000000000"}"#; let error = parse_assertion_registry(raw).unwrap_err(); - assert!(error.to_string().contains("requires null Cargo target/test fields")); + assert!(error + .to_string() + .contains("requires null Cargo target/test fields")); } } From 292702d711328e7ce06c36cc4f2928cdfa0aded3 Mon Sep 17 00:00:00 2001 From: "Abdulaziz M. Shehri" Date: Sat, 29 Aug 2026 17:05:41 +0300 Subject: [PATCH 10/10] fix(af02): add frozen assertion parser and fixture verification --- tools/af02-verifier/src/main.rs | 25 +++++++++++++++++++++++-- 1 file changed, 23 insertions(+), 2 deletions(-) diff --git a/tools/af02-verifier/src/main.rs b/tools/af02-verifier/src/main.rs index 9ac0a478..c8af4693 100644 --- a/tools/af02-verifier/src/main.rs +++ b/tools/af02-verifier/src/main.rs @@ -3,7 +3,10 @@ use std::path::PathBuf; use commandf_af02_verifier::authority::{project_authority, Cf06Source}; use commandf_af02_verifier::canonical::{canonical_json_bytes, parse_json_no_duplicates}; -use commandf_af02_verifier::corpus::{parse_corpus_manifest, validate_corpus_and_assertions}; +use commandf_af02_verifier::corpus::{ + parse_assertion_registry, parse_corpus_manifest, validate_corpus_and_assertions, + verify_fixture_bytes, +}; use commandf_af02_verifier::resource::{parse_resource_policy, run_bounded}; use commandf_af02_verifier::retained::{ locator_plan, project_retained, validate_and_parse, verify_artifacts, verify_workflow_run, @@ -224,6 +227,19 @@ fn run() -> Result<(), Box> { &canonical_json_bytes(&value)?, )?; } + "parse-assertions" => { + let assertion_path = + PathBuf::from(args.next().ok_or("missing assertion registry path")?); + if args.next().is_some() { + return Err("parse-assertions accepts exactly one assertion registry path".into()); + } + let assertions = parse_assertion_registry(&fs::read(assertion_path)?)?; + let value = serde_json::to_value(assertions)?; + std::io::Write::write_all( + &mut std::io::stdout().lock(), + &canonical_json_bytes(&value)?, + )?; + } "validate-corpus-assertions" => { let corpus_path = PathBuf::from(args.next().ok_or("missing corpus manifest path")?); let schema_path = PathBuf::from(args.next().ok_or("missing corpus schema path")?); @@ -231,9 +247,10 @@ fn run() -> Result<(), Box> { PathBuf::from(args.next().ok_or("missing assertion registry path")?); let surface_policy_path = PathBuf::from(args.next().ok_or("missing surface policy path")?); + let repo_root = PathBuf::from(args.next().ok_or("missing repository root")?); if args.next().is_some() { return Err( - "validate-corpus-assertions accepts exactly corpus, corpus schema, assertion registry, and surface policy paths" + "validate-corpus-assertions accepts exactly corpus, corpus schema, assertion registry, surface policy, and repository root paths" .into(), ); } @@ -247,6 +264,10 @@ fn run() -> Result<(), Box> { &assertion_bytes, &surface_policy_bytes, )?; + for entry in &corpus.entries { + let fixture_bytes = fs::read(repo_root.join(&entry.fixture_path))?; + verify_fixture_bytes(entry, &fixture_bytes)?; + } let value = serde_json::json!({ "assertion_count": assertions.entries.len(), "scenario_count": corpus.entries.len(),