1414from pathlib import Path
1515from typing import Any
1616
17- from agent_machine .contracts import load_json , schema_by_kind , validate_instance
17+ from agent_machine .contracts import load_json , schema_by_kind
1818from agent_machine .governance import (
1919 activation_ready ,
2020 grant_allows_activation ,
2626DEFAULT_DECIDED_AT = "1970-01-01T00:00:00Z"
2727
2828
29- def validate_activation_decision_payload (decision : dict [str , Any ], root : Path | None = None ) -> None :
30- schema = schema_by_kind (root )["ActivationDecision" ]
31- # Validate in-memory payload through a temporary jsonschema path without writing a file.
32- schema_payload = load_json (schema )
29+ def validate_payload_against_kind (value : dict [str , Any ], kind : str , root : Path | None = None ) -> None :
30+ schema_path = schema_by_kind (root )[kind ]
31+ schema_payload = load_json (schema_path )
3332 try :
3433 from jsonschema .validators import validator_for
3534 except ImportError as exc : # pragma: no cover
@@ -39,13 +38,75 @@ def validate_activation_decision_payload(decision: dict[str, Any], root: Path |
3938 validator_cls = validator_for (schema_payload )
4039 validator_cls .check_schema (schema_payload )
4140 validator = validator_cls (schema_payload )
42- errors = sorted (validator .iter_errors (decision ), key = lambda err : list (err .path ))
41+ errors = sorted (validator .iter_errors (value ), key = lambda err : list (err .path ))
4342 if errors :
4443 rendered = []
4544 for err in errors :
4645 location = "/" .join (str (part ) for part in err .path ) or "<root>"
4746 rendered .append (f" - { location } : { err .message } " )
48- raise AssertionError ("ActivationDecision failed schema validation:\n " + "\n " .join (rendered ))
47+ raise AssertionError (f"{ kind } failed schema validation:\n " + "\n " .join (rendered ))
48+
49+
50+ def validate_activation_decision_payload (decision : dict [str , Any ], root : Path | None = None ) -> None :
51+ validate_payload_against_kind (decision , "ActivationDecision" , root )
52+
53+
54+ def validate_storage_receipt_payload (receipt : dict [str , Any ], root : Path | None = None ) -> None :
55+ validate_payload_against_kind (receipt , "StorageReceipt" , root )
56+ safety = receipt .get ("receiptSafety" , {})
57+ for key in ["includeRawContent" , "rawPromptContentIncluded" , "rawKvCacheContentIncluded" , "secretValuesIncluded" ]:
58+ if safety .get (key ) is not False :
59+ raise AssertionError (f"StorageReceipt { receipt .get ('id' )} : receiptSafety.{ key } must be false" )
60+ filesystem = receipt .get ("filesystem" , {})
61+ if filesystem .get ("worldWritable" ) is not False :
62+ raise AssertionError (f"StorageReceipt { receipt .get ('id' )} : worldWritable must be false" )
63+ if filesystem .get ("symlinkTraversalObserved" ) is not False :
64+ raise AssertionError (f"StorageReceipt { receipt .get ('id' )} : symlinkTraversalObserved must be false" )
65+
66+
67+ def validate_storage_receipts (
68+ * ,
69+ storage_receipt_refs : list [str ],
70+ storage_receipts : list [dict [str , Any ]] | None ,
71+ root : Path | None = None ,
72+ ) -> tuple [list [str ], list [str ]]:
73+ """Validate storage receipt files and return (valid_refs, failure_reasons)."""
74+ requested_refs = sorted (set (storage_receipt_refs ))
75+ if not requested_refs :
76+ return [], ["storage_receipts_missing" ]
77+ if storage_receipts is None :
78+ return requested_refs , ["storage_receipt_files_missing" ]
79+
80+ seen : set [str ] = set ()
81+ failures : list [str ] = []
82+ for receipt in storage_receipts :
83+ try :
84+ validate_storage_receipt_payload (receipt , root )
85+ except AssertionError as exc :
86+ failures .append (f"storage_receipt_invalid:{ receipt .get ('id' , 'unknown' )} :{ exc } " )
87+ continue
88+ receipt_id = receipt .get ("id" )
89+ if not isinstance (receipt_id , str ):
90+ failures .append ("storage_receipt_id_missing" )
91+ continue
92+ seen .add (receipt_id )
93+ encryption = receipt .get ("encryption" , {})
94+ if encryption .get ("required" ) is True and encryption .get ("observed" ) is not True :
95+ failures .append (f"storage_receipt_encryption_required_not_observed:{ receipt_id } " )
96+ quota = receipt .get ("quota" , {})
97+ if quota .get ("required" ) is True and quota .get ("observed" ) is not True :
98+ failures .append (f"storage_receipt_quota_required_not_observed:{ receipt_id } " )
99+
100+ missing = sorted (set (requested_refs ) - seen )
101+ for missing_ref in missing :
102+ failures .append (f"storage_receipt_ref_unresolved:{ missing_ref } " )
103+ return requested_refs , sorted (set (failures ))
104+
105+
106+ def sorted_list (value : Any ) -> list [Any ]:
107+ if not isinstance (value , list ):
108+ return []
109+ return sorted (value , key = lambda item : json .dumps (item , sort_keys = True ))
49110
50111
51112def evaluate_activation (
@@ -57,6 +118,8 @@ def evaluate_activation(
57118 storage_receipt_refs : list [str ],
58119 decided_at : str ,
59120 decision_id : str | None = None ,
121+ storage_receipts : list [dict [str , Any ]] | None = None ,
122+ root : Path | None = None ,
60123) -> dict [str , Any ]:
61124 validate_policy_admission_semantics (policy , source = "activation:policy" )
62125 validate_agent_registry_grant_semantics (grant , source = "activation:grant" )
@@ -81,9 +144,16 @@ def evaluate_activation(
81144 if not grant_allows_activation (grant , provider_id = provider_id ):
82145 failure_reasons .append ("agent_registry_grant_does_not_allow_activation_scope" )
83146 required_before_activation .append ("agent_registry_grant_active_activation" )
84- if not storage_receipt_refs :
85- failure_reasons .append ("storage_receipts_missing" )
86- required_before_activation .append ("storage_receipts" )
147+
148+ resolved_storage_refs , storage_failures = validate_storage_receipts (
149+ storage_receipt_refs = storage_receipt_refs ,
150+ storage_receipts = storage_receipts ,
151+ root = root ,
152+ )
153+ failure_reasons .extend (storage_failures )
154+ if storage_failures :
155+ required_before_activation .append ("valid_storage_receipts" )
156+
87157 if not deployment_receipt_id :
88158 failure_reasons .append ("deployment_receipt_missing" )
89159 required_before_activation .append ("deployment_receipt" )
@@ -116,24 +186,22 @@ def evaluate_activation(
116186 "policyAdmissionId" : policy_id ,
117187 "agentRegistryGrantId" : grant_id ,
118188 "deploymentReceiptId" : deployment_receipt_id ,
119- "storageReceiptRefs" : storage_receipt_refs ,
189+ "storageReceiptRefs" : resolved_storage_refs ,
120190 },
121191 "scope" : {
122192 "runtimeMode" : runtime_mode ,
123- "networkExposure" : policy_allowed_scope .get ("networkExposure" ) or [] ,
124- "sideEffects" : policy_allowed_scope .get ("sideEffects" ) or [] ,
125- "toolRefs" : grant_allowed_scope .get ("toolRefs" ) or [] ,
126- "storageScopeRefs" : grant_allowed_scope .get ("storageScopeRefs" ) or [] ,
193+ "networkExposure" : sorted_list ( policy_allowed_scope .get ("networkExposure" )) ,
194+ "sideEffects" : sorted_list ( policy_allowed_scope .get ("sideEffects" )) ,
195+ "toolRefs" : sorted_list ( grant_allowed_scope .get ("toolRefs" )) ,
196+ "storageScopeRefs" : sorted_list ( grant_allowed_scope .get ("storageScopeRefs" )) ,
127197 "cacheReuseAllowed" : bool (policy_allowed_scope .get ("cacheReuse" )) and bool (grant_allowed_scope .get ("cacheScopeRefs" )),
128198 },
129199 "obligations" : {
130- "requiredReceipts" : obligations .get ("requiredReceipts" ) or [] ,
200+ "requiredReceipts" : sorted_list ( obligations .get ("requiredReceipts" )) ,
131201 "policyDecisionRef" : policy_decision .get ("decisionRef" ),
132202 "agentRegistryGrantRef" : grant_payload .get ("grantRef" ),
133203 "expiresAt" : obligations .get ("expiresAt" ) or grant_payload .get ("expiresAt" ),
134- "revocationRefs" : [
135- ref for ref in [obligations .get ("revocationRef" ), grant_payload .get ("revocationRef" )] if ref
136- ],
204+ "revocationRefs" : sorted (ref for ref in [obligations .get ("revocationRef" ), grant_payload .get ("revocationRef" )] if ref ),
137205 },
138206 "receiptSafety" : {
139207 "includeRawContent" : False ,
@@ -146,6 +214,7 @@ def evaluate_activation(
146214 "labels" : {
147215 "sourceos.activation.prototype" : "true" ,
148216 "sourceos.activation.allowed" : str (allowed ).lower (),
217+ "sourceos.activation.fail-closed" : str (not allowed ).lower (),
149218 },
150219 }
151220
@@ -157,20 +226,27 @@ def parse_args() -> argparse.Namespace:
157226 parser .add_argument ("grant_json" , type = Path )
158227 parser .add_argument ("--deployment-receipt-id" , required = True )
159228 parser .add_argument ("--storage-receipt-ref" , action = "append" , default = [])
229+ parser .add_argument ("--storage-receipt-file" , action = "append" , type = Path , default = [])
160230 parser .add_argument ("--decided-at" , default = DEFAULT_DECIDED_AT )
231+ parser .add_argument ("--decision-id" )
161232 parser .add_argument ("--pretty" , action = "store_true" )
162233 return parser .parse_args ()
163234
164235
165236def main () -> int :
166237 args = parse_args ()
238+ storage_receipts = [load_json (path ) for path in args .storage_receipt_file ]
239+ if not args .storage_receipt_ref and storage_receipts :
240+ args .storage_receipt_ref = [str (receipt .get ("id" )) for receipt in storage_receipts ]
167241 decision = evaluate_activation (
168242 agentpod = load_json (args .agentpod_json ),
169243 policy = load_json (args .policy_json ),
170244 grant = load_json (args .grant_json ),
171245 deployment_receipt_id = args .deployment_receipt_id ,
172246 storage_receipt_refs = args .storage_receipt_ref ,
247+ storage_receipts = storage_receipts if storage_receipts else None ,
173248 decided_at = args .decided_at ,
249+ decision_id = args .decision_id ,
174250 )
175251 validate_activation_decision_payload (decision )
176252 if args .pretty :
0 commit comments