use serde_json::Value;
use std::fmt;
const REGISTRY: &[(&str, &str)] = &[
(
"memory.write.v1",
include_str!("schemas/memory.write.v1.json"),
),
(
"memory.read.v1",
include_str!("schemas/memory.read.v1.json"),
),
("boundary.v1", include_str!("schemas/boundary.v1.json")),
(
"agent_card.v1",
include_str!("schemas/agent_card.v1.json"),
),
(
"agent_card_revocation.v1",
include_str!("schemas/agent_card_revocation.v1.json"),
),
];
pub fn schema_json(suffix: &str) -> Option<&'static str> {
REGISTRY.iter().find(|(k, _)| *k == suffix).map(|(_, s)| *s)
}
pub fn registered_suffixes() -> Vec<&'static str> {
REGISTRY.iter().map(|(k, _)| *k).collect()
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum PredicateError {
MissingField { suffix: String, field: String },
TypeMismatch {
suffix: String,
field: String,
expected: String,
},
NotAnObject { suffix: String },
SchemaParse { suffix: String, detail: String },
}
impl fmt::Display for PredicateError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
PredicateError::MissingField { suffix, field } => {
write!(f, "{suffix}: missing required field `{field}`")
}
PredicateError::TypeMismatch {
suffix,
field,
expected,
} => write!(
f,
"{suffix}: field `{field}` has the wrong type (expected {expected})"
),
PredicateError::NotAnObject { suffix } => {
write!(f, "{suffix}: payload must be a JSON object")
}
PredicateError::SchemaParse { suffix, detail } => {
write!(f, "{suffix}: registered schema is invalid JSON: {detail}")
}
}
}
}
impl std::error::Error for PredicateError {}
pub fn validate(suffix: &str, payload: Option<&Value>) -> Result<(), PredicateError> {
let Some(schema_str) = schema_json(suffix) else {
return Ok(());
};
let schema: Value =
serde_json::from_str(schema_str).map_err(|e| PredicateError::SchemaParse {
suffix: suffix.to_string(),
detail: e.to_string(),
})?;
let empty = Value::Object(serde_json::Map::new());
let value = payload.unwrap_or(&empty);
let map = value
.as_object()
.ok_or_else(|| PredicateError::NotAnObject {
suffix: suffix.to_string(),
})?;
if let Some(required) = schema.get("required").and_then(Value::as_array) {
for entry in required {
if let Some(name) = entry.as_str() {
if !map.contains_key(name) {
return Err(PredicateError::MissingField {
suffix: suffix.to_string(),
field: name.to_string(),
});
}
}
}
}
if let Some(props) = schema.get("properties").and_then(Value::as_object) {
for (field, subschema) in props {
let Some(actual) = map.get(field) else {
continue; };
let Some(type_decl) = subschema.get("type") else {
continue; };
if !type_matches(actual, type_decl) {
return Err(PredicateError::TypeMismatch {
suffix: suffix.to_string(),
field: field.to_string(),
expected: type_decl.to_string(),
});
}
}
}
Ok(())
}
fn type_matches(value: &Value, type_decl: &Value) -> bool {
match type_decl {
Value::String(t) => json_is(value, t),
Value::Array(types) => types
.iter()
.any(|t| t.as_str().is_some_and(|t| json_is(value, t))),
_ => true,
}
}
fn json_is(value: &Value, ty: &str) -> bool {
match ty {
"string" => value.is_string(),
"integer" => value.is_i64() || value.is_u64(),
"number" => value.is_number(),
"boolean" => value.is_boolean(),
"object" => value.is_object(),
"array" => value.is_array(),
"null" => value.is_null(),
_ => true,
}
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
#[test]
fn registry_lists_the_three_seed_predicates() {
let suffixes = registered_suffixes();
assert!(suffixes.contains(&"memory.write.v1"));
assert!(suffixes.contains(&"memory.read.v1"));
assert!(suffixes.contains(&"boundary.v1"));
assert!(suffixes.contains(&"agent_card.v1"));
assert!(schema_json("memory.write.v1").is_some());
assert!(schema_json("nope.v1").is_none());
}
#[test]
fn embedded_schemas_parse() {
for s in registered_suffixes() {
let raw = schema_json(s).unwrap();
serde_json::from_str::<Value>(raw).expect("embedded schema must be valid JSON");
}
}
#[test]
fn unregistered_suffix_is_backward_compatible() {
assert!(validate("custom.kind.v1", None).is_ok());
assert!(validate("custom.kind.v1", Some(&json!({"anything": 1}))).is_ok());
}
#[test]
fn memory_write_valid_passes() {
let payload = json!({
"memory_id": "mem_abc",
"content_hash": "sha256:deadbeef",
"memory_type": "episodic",
"scope": "tenant://acme",
"activegraph_run_id": "run_1",
"supersedes": null
});
assert!(validate("memory.write.v1", Some(&payload)).is_ok());
}
#[test]
fn memory_write_missing_required_fails_closed() {
let payload = json!({
"memory_id": "mem_abc",
"memory_type": "episodic",
"scope": "tenant://acme"
}); let err = validate("memory.write.v1", Some(&payload)).unwrap_err();
assert_eq!(
err,
PredicateError::MissingField {
suffix: "memory.write.v1".into(),
field: "content_hash".into()
}
);
}
#[test]
fn memory_write_wrong_type_fails() {
let payload = json!({
"memory_id": "mem_abc",
"content_hash": 12345, "memory_type": "episodic",
"scope": "tenant://acme"
});
let err = validate("memory.write.v1", Some(&payload)).unwrap_err();
assert!(
matches!(err, PredicateError::TypeMismatch { field, .. } if field == "content_hash")
);
}
#[test]
fn memory_write_nullable_supersedes_accepts_string_and_null() {
let base = |sup: Value| {
json!({
"memory_id": "m", "content_hash": "h", "memory_type": "t", "scope": "s",
"supersedes": sup
})
};
assert!(validate("memory.write.v1", Some(&base(json!("mem_old")))).is_ok());
assert!(validate("memory.write.v1", Some(&base(Value::Null))).is_ok());
assert!(validate("memory.write.v1", Some(&base(json!(7)))).is_err());
}
#[test]
fn registered_predicate_requires_a_payload() {
let err = validate("memory.write.v1", None).unwrap_err();
assert!(matches!(err, PredicateError::MissingField { .. }));
}
#[test]
fn memory_read_valid_and_integer_enforced() {
let ok = json!({
"zmem_receipt_id": "act_1",
"trace_sha256": "abcd",
"query_hash": "qh",
"retrieval_mode": "semantic",
"memories_returned": 3
});
assert!(validate("memory.read.v1", Some(&ok)).is_ok());
let bad = json!({
"zmem_receipt_id": "act_1",
"trace_sha256": "abcd",
"query_hash": "qh",
"retrieval_mode": "semantic",
"memories_returned": "three" });
assert!(matches!(
validate("memory.read.v1", Some(&bad)).unwrap_err(),
PredicateError::TypeMismatch { field, .. } if field == "memories_returned"
));
}
#[test]
fn memory_read_missing_required_fails() {
let payload = json!({
"zmem_receipt_id": "act_1",
"trace_sha256": "abcd",
"retrieval_mode": "semantic",
"memories_returned": 3
}); assert!(matches!(
validate("memory.read.v1", Some(&payload)).unwrap_err(),
PredicateError::MissingField { field, .. } if field == "query_hash"
));
}
#[test]
fn boundary_structural_required_fields_enforced() {
let valid = json!({
"schema": "treeship.boundary.v1",
"subject_ref": "art_aabbccdd11223344",
"actor": {"uri": "agent://codex", "keyid": "key_aaaa1111"},
"checker": {"uri": "human://alice", "keyid": "key_bbbb2222"},
"decision": "allow",
"policy": {"digest": "sha256:p"},
"diet_root": "sha256:r",
"diet": [{"type": "memory_bundle", "digest": "sha256:d"}],
"committed_at": {"anchor": "merkle://zmem/checkpoint#4821", "ts": "2026-06-06T00:00:00Z"}
});
assert!(validate("boundary.v1", Some(&valid)).is_ok());
let mut wrong = valid.clone();
wrong.as_object_mut().unwrap()["committed_at"] = json!("not-an-object");
assert!(matches!(
validate("boundary.v1", Some(&wrong)).unwrap_err(),
PredicateError::TypeMismatch { field, .. } if field == "committed_at"
));
let mut missing = valid.clone();
missing.as_object_mut().unwrap().remove("decision");
assert!(matches!(
validate("boundary.v1", Some(&missing)).unwrap_err(),
PredicateError::MissingField { field, .. } if field == "decision"
));
}
#[test]
fn agent_card_valid_passes() {
let card = json!({
"schema": "agent_card.v1",
"agent": "agent://deployer",
"keyid": "key_9f8e7d6c",
"owner": "human://alice",
"version": "1.2.0",
"capabilities": {
"tools": ["file.read", "file.write", "db.*"],
"models": ["claude-sonnet-4"],
"can_delegate": true
},
"evidence_anchor": { "receipt_count": 1247, "merkle_root": "mroot_a0be" },
"supersedes": null
});
assert!(validate("agent_card.v1", Some(&card)).is_ok());
}
#[test]
fn agent_card_missing_keyid_fails_closed() {
let card = json!({
"schema": "agent_card.v1",
"agent": "agent://deployer",
"version": "1.0.0",
"capabilities": { "tools": ["file.read"] }
});
assert!(matches!(
validate("agent_card.v1", Some(&card)).unwrap_err(),
PredicateError::MissingField { field, .. } if field == "keyid"
));
}
#[test]
fn agent_card_capabilities_must_be_an_object() {
let card = json!({
"schema": "agent_card.v1",
"agent": "agent://deployer",
"keyid": "key_1",
"version": "1.0.0",
"capabilities": ["file.read"] });
assert!(matches!(
validate("agent_card.v1", Some(&card)).unwrap_err(),
PredicateError::TypeMismatch { field, .. } if field == "capabilities"
));
}
#[test]
fn agent_card_revocation_valid_passes() {
let rev = json!({
"schema": "agent_card_revocation.v1",
"card": "art_deadbeefdeadbeef",
"keyid": "key_1",
"reason": "key-rotation",
"revoked_at": "2026-06-23T00:00:00Z"
});
assert!(validate("agent_card_revocation.v1", Some(&rev)).is_ok());
}
#[test]
fn agent_card_revocation_requires_card_id() {
let rev = json!({
"schema": "agent_card_revocation.v1",
"revoked_at": "2026-06-23T00:00:00Z"
});
assert!(matches!(
validate("agent_card_revocation.v1", Some(&rev)).unwrap_err(),
PredicateError::MissingField { field, .. } if field == "card"
));
}
}