Skip to content

Commit 59003cd

Browse files
committed
Resolve storage receipts during activation evaluation
1 parent 4eab445 commit 59003cd

1 file changed

Lines changed: 95 additions & 19 deletions

File tree

‎src/agent_machine/activation.py‎

Lines changed: 95 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
from pathlib import Path
1515
from 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
1818
from agent_machine.governance import (
1919
activation_ready,
2020
grant_allows_activation,
@@ -26,10 +26,9 @@
2626
DEFAULT_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

51112
def 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

165236
def 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

Comments
 (0)