validate.py python
657 lines 25.9 KB
Raw
sha256:8461d44b77376fbf06fa7c3e085d309e3010fd8d5886d63c63e69ce118811ad4 docs: record AFF-b feature-tip SHAs after AFF-b-ISR commit. Human 4 days ago
1 """Ledger entry validation (§K9.7 / §K9.8 / §PE.3–§PE.4)."""
2
3 from __future__ import annotations
4
5 import re
6 from typing import Any
7
8 from tools.honesty.provenance import validate_provenance
9 from tools.honesty.types import (
10 ACTOR_ROLES,
11 AFF_POSTURES,
12 AFF_VERDICTS,
13 BV_VERDICTS,
14 ENTRY_KINDS,
15 FREEZE_VERDICTS,
16 ISR_VERDICTS,
17 VERIFICATION_ARTIFACT_TYPES,
18 EntryValidationError,
19 )
20
21 _SHA256_RE = re.compile(r"^[0-9a-f]{64}$")
22
23
24 def _require_mapping(value: Any, field: str) -> dict[str, Any]:
25 if not isinstance(value, dict):
26 raise EntryValidationError(2, f"{field} must be an object")
27 return value
28
29
30 def _validate_sha256(value: Any, field: str) -> str:
31 if not isinstance(value, str) or not _SHA256_RE.match(value):
32 raise EntryValidationError(2, f"{field} must be lowercase 64-char hex sha256")
33 return value
34
35
36 def validate_verification_artifacts(artifacts: Any) -> list[dict[str, Any]]:
37 """Validate ``verification_evidence.artifacts`` per §PE.4."""
38 if not isinstance(artifacts, list) or not artifacts:
39 raise EntryValidationError(24, "artifacts must be a non-empty list")
40 normalized: list[dict[str, Any]] = []
41 for index, item in enumerate(artifacts):
42 obj = _require_mapping(item, f"artifacts[{index}]")
43 art_type = obj.get("type")
44 if art_type not in VERIFICATION_ARTIFACT_TYPES:
45 raise EntryValidationError(
46 2,
47 f"artifacts[{index}].type must be test_output|deploy_health|screenshot",
48 )
49 sha256 = _validate_sha256(obj.get("sha256"), f"artifacts[{index}].sha256")
50 ref = obj.get("ref")
51 if art_type in {"deploy_health", "screenshot"}:
52 _require_non_empty_str(ref, f"artifacts[{index}].ref")
53 elif ref is not None and not isinstance(ref, str):
54 raise EntryValidationError(2, f"artifacts[{index}].ref must be a string when present")
55 notes = obj.get("notes")
56 if notes is not None and not isinstance(notes, str):
57 raise EntryValidationError(2, f"artifacts[{index}].notes must be a string when present")
58 entry: dict[str, Any] = {"type": art_type, "sha256": sha256}
59 if ref is not None:
60 entry["ref"] = ref
61 if notes is not None:
62 entry["notes"] = notes
63 normalized.append(entry)
64 return normalized
65
66
67 def find_matching_verification_evidence(
68 entries: list[dict[str, Any]],
69 *,
70 phase_id: str,
71 frozen_spec: str | None,
72 ) -> dict[str, Any] | None:
73 """Return the last matching pass entry for Mode B (§PE.6.1)."""
74 matches: list[dict[str, Any]] = []
75 for entry in entries:
76 if entry.get("kind") != "verification_evidence":
77 continue
78 if entry.get("actor_role") != "verifier":
79 continue
80 if entry.get("bv_verdict") != "pass":
81 continue
82 if entry.get("phase_id") != phase_id:
83 continue
84 if frozen_spec is not None and entry.get("frozen_spec") != frozen_spec:
85 continue
86 matches.append(entry)
87 return matches[-1] if matches else None
88
89
90 def find_matching_deploy_health(
91 entries: list[dict[str, Any]],
92 *,
93 phase_id: str,
94 frozen_spec: str | None,
95 ) -> dict[str, Any] | None:
96 """Return the last Mode C match: pass + ≥1 ``deploy_health`` artifact (§PD.3).
97
98 Does not open network or treat ``ref`` as a URL.
99 """
100 matches: list[dict[str, Any]] = []
101 for entry in entries:
102 if entry.get("kind") != "verification_evidence":
103 continue
104 if entry.get("actor_role") != "verifier":
105 continue
106 if entry.get("bv_verdict") != "pass":
107 continue
108 if entry.get("phase_id") != phase_id:
109 continue
110 if frozen_spec is not None and entry.get("frozen_spec") != frozen_spec:
111 continue
112 artifacts = entry.get("artifacts")
113 if not isinstance(artifacts, list):
114 continue
115 if not any(
116 isinstance(item, dict) and item.get("type") == "deploy_health" for item in artifacts
117 ):
118 continue
119 matches.append(entry)
120 return matches[-1] if matches else None
121
122
123 def find_matching_independent_second_review(
124 entries: list[dict[str, Any]],
125 *,
126 phase_id: str,
127 frozen_spec: str | None,
128 producer_session: str | None,
129 ) -> dict[str, Any] | None:
130 """Return the last matching ISR pass entry for Mode D (§ISR.5.3).
131
132 Does not open a network connection, call a model, or read IDE session ids.
133 """
134 matches: list[dict[str, Any]] = []
135 for entry in entries:
136 if entry.get("kind") != "independent_second_review":
137 continue
138 if entry.get("actor_role") != "verifier":
139 continue
140 if entry.get("isr_verdict") != "pass":
141 continue
142 if entry.get("phase_id") != phase_id:
143 continue
144 if frozen_spec is not None and entry.get("frozen_spec") != frozen_spec:
145 continue
146 actor_session = entry.get("actor_session_id")
147 producer_id = entry.get("producer_session_id")
148 if not isinstance(actor_session, str) or not isinstance(producer_id, str):
149 continue
150 if actor_session == producer_id:
151 continue
152 if producer_session is not None:
153 if producer_id != producer_session:
154 continue
155 if actor_session == producer_session:
156 continue
157 matches.append(entry)
158 return matches[-1] if matches else None
159
160
161
162 def find_matching_freeze_review(
163 entries: list[dict[str, Any]],
164 *,
165 phase_id: str,
166 frozen_spec: str | None,
167 artifact_digest: str | None,
168 ) -> dict[str, Any] | None:
169 """Return the last matching freeze_review pass entry (§FRV.6.3).
170
171 Does not open a network connection, call a model, or read IDE session ids.
172 """
173 matches: list[dict[str, Any]] = []
174 for entry in entries:
175 if entry.get("kind") != "freeze_review":
176 continue
177 if entry.get("actor_role") != "verifier":
178 continue
179 if entry.get("gate") != "substantive":
180 continue
181 if entry.get("freeze_verdict") != "pass":
182 continue
183 if entry.get("phase_id") != phase_id:
184 continue
185 if frozen_spec is not None and entry.get("frozen_spec") != frozen_spec:
186 continue
187 if artifact_digest is not None and entry.get("artifact_digest") != artifact_digest:
188 continue
189 producer_id = entry.get("producer_session_id")
190 actor_session = entry.get("actor_session_id")
191 if producer_id is not None:
192 if not isinstance(producer_id, str) or not isinstance(actor_session, str):
193 continue
194 if producer_id == actor_session:
195 continue
196 matches.append(entry)
197 return matches[-1] if matches else None
198
199
200 def _aff_phase_id_ok(value: Any) -> bool:
201 return isinstance(value, str) and bool(value.strip())
202
203
204 def _aff_round_ok(value: Any) -> bool:
205 return type(value) is int and value >= 1
206
207
208 def _aff_reviewer_model_ok(value: Any) -> bool:
209 return isinstance(value, str) and bool(value.strip())
210
211
212 def _adversarial_freeze_eligible(
213 entry: dict[str, Any],
214 *,
215 frozen_spec: str,
216 artifact_digest: str,
217 phase_id: str | None,
218 producer_session: str | None,
219 ) -> bool:
220 """Return True when an AFF entry is eligible for the latest-verdict resolver (§AFF.5.5)."""
221 if entry.get("kind") != "adversarial_freeze":
222 return False
223 verdict = entry.get("aff_verdict")
224 if verdict not in AFF_VERDICTS:
225 return False
226 if entry.get("frozen_spec") != frozen_spec:
227 return False
228 if entry.get("artifact_digest") != artifact_digest:
229 return False
230 if not _aff_phase_id_ok(entry.get("phase_id")):
231 return False
232 if not _aff_round_ok(entry.get("round")):
233 return False
234 if phase_id is not None and entry.get("phase_id") != phase_id:
235 return False
236
237 if verdict == "skip":
238 if entry.get("actor_role") != "owner":
239 return False
240 if "aff_posture" in entry or "producer_session_id" in entry:
241 return False
242 return True
243
244 # pass | findings | blocked
245 if entry.get("actor_role") != "verifier":
246 return False
247 if entry.get("aff_posture") != "attack":
248 return False
249 if not _aff_reviewer_model_ok(entry.get("reviewer_model")):
250 return False
251 actor_session = entry.get("actor_session_id")
252 producer_id = entry.get("producer_session_id")
253 if not isinstance(actor_session, str) or not actor_session.strip():
254 return False
255 if not isinstance(producer_id, str) or not producer_id.strip():
256 return False
257 if actor_session == producer_id:
258 return False
259 if producer_session is not None:
260 if producer_id != producer_session:
261 return False
262 if actor_session == producer_session:
263 return False
264 return True
265
266
267 def find_latest_adversarial_freeze_verdict(
268 entries: list[dict[str, Any]],
269 *,
270 frozen_spec: str,
271 artifact_digest: str,
272 phase_id: str | None = None,
273 producer_session: str | None = None,
274 ) -> dict[str, Any] | None:
275 """Return the last eligible AFF verdict across all four verdicts (§AFF.5.5).
276
277 Does not open a network connection, call a model, or read IDE session ids.
278 """
279 winner: dict[str, Any] | None = None
280 for entry in entries:
281 if _adversarial_freeze_eligible(
282 entry,
283 frozen_spec=frozen_spec,
284 artifact_digest=artifact_digest,
285 phase_id=phase_id,
286 producer_session=producer_session,
287 ):
288 winner = entry
289 return winner
290
291
292 def find_matching_adversarial_freeze_pass(
293 entries: list[dict[str, Any]],
294 *,
295 frozen_spec: str,
296 artifact_digest: str,
297 phase_id: str | None = None,
298 producer_session: str | None = None,
299 ) -> dict[str, Any] | None:
300 """Return the latest eligible verdict only when it is ``pass``."""
301 winner = find_latest_adversarial_freeze_verdict(
302 entries,
303 frozen_spec=frozen_spec,
304 artifact_digest=artifact_digest,
305 phase_id=phase_id,
306 producer_session=producer_session,
307 )
308 if winner is not None and winner.get("aff_verdict") == "pass":
309 return winner
310 return None
311
312
313 def find_matching_adversarial_freeze_skip(
314 entries: list[dict[str, Any]],
315 *,
316 frozen_spec: str,
317 artifact_digest: str,
318 phase_id: str | None = None,
319 ) -> dict[str, Any] | None:
320 """Return the latest eligible verdict only when it is ``skip``."""
321 winner = find_latest_adversarial_freeze_verdict(
322 entries,
323 frozen_spec=frozen_spec,
324 artifact_digest=artifact_digest,
325 phase_id=phase_id,
326 producer_session=None,
327 )
328 if winner is not None and winner.get("aff_verdict") == "skip":
329 return winner
330 return None
331
332
333 def _require_non_empty_str(value: Any, field: str) -> str:
334 if not isinstance(value, str) or not value.strip():
335 raise EntryValidationError(2, f"{field} must be a non-empty string")
336 return value
337
338
339 def _require_exact_positive_int(value: Any, field: str) -> int:
340 """Exact-type positive integer (rejects JSON/Python boolean)."""
341 if type(value) is not int or value < 1:
342 raise EntryValidationError(2, f"{field} must be an integer >= 1")
343 return value
344
345
346 def validate_append_body(*, kind: str, body: dict[str, Any]) -> dict[str, Any]:
347 """Validate and normalize an append body before hashing."""
348 if kind not in ENTRY_KINDS:
349 raise EntryValidationError(2, f"unknown entry kind: {kind}")
350
351 if "entry_hash" in body or "prev_hash" in body:
352 raise EntryValidationError(2, "client must not supply entry_hash or prev_hash")
353
354 body_kind = body.get("kind")
355 if body_kind is not None and body_kind != kind:
356 raise EntryValidationError(2, "body kind must match --kind when present")
357
358 merged = dict(body)
359 merged["kind"] = kind
360
361 if "v" in merged:
362 version = merged["v"]
363 if type(version) is not int or version != 1:
364 raise EntryValidationError(2, "v must be integer 1")
365 else:
366 merged["v"] = 1
367
368 if "ts" in merged:
369 ts = merged["ts"]
370 if not isinstance(ts, str) or not ts.strip():
371 raise EntryValidationError(2, "ts must be a non-empty string when supplied")
372
373 if kind == "genesis":
374 if "actor_role" in merged or "actor_session_id" in merged:
375 raise EntryValidationError(2, "genesis must not carry actor fields")
376 for key in (
377 "assignment",
378 "artifact_sha256",
379 "passed",
380 "evidence",
381 "subject",
382 "ruling",
383 "bound_verdict_hash",
384 "hook",
385 "ok",
386 "reason",
387 "provenance",
388 "phase_id",
389 "frozen_spec",
390 "round",
391 "bv_verdict",
392 "artifacts",
393 "subject_sha256",
394 "isr_verdict",
395 "producer_session_id",
396 "producer_agent_id",
397 "verifier_agent_id",
398 "bound_verification_evidence_hash",
399 "gate",
400 "freeze_verdict",
401 "artifact_digest",
402 "checklist_ids",
403 "findings_count",
404 "aff_verdict",
405 "aff_posture",
406 "bound_freeze_review_hash",
407 "side_check_path",
408 "producer_model",
409 "reviewer_model",
410 "notes",
411 ):
412 if key in merged:
413 raise EntryValidationError(2, f"genesis must not carry {key}")
414 return merged
415
416 actor_role = merged.get("actor_role")
417 if actor_role not in ACTOR_ROLES:
418 raise EntryValidationError(
419 23
420 if kind
421 in {
422 "verdict",
423 "verification_evidence",
424 "independent_second_review",
425 "freeze_review",
426 "adversarial_freeze",
427 }
428 else 2,
429 "invalid or missing actor_role",
430 )
431
432 actor_session = merged.get("actor_session_id")
433 _require_non_empty_str(actor_session, "actor_session_id")
434
435 if kind == "task_assigned":
436 if actor_role != "overseer":
437 raise EntryValidationError(23, "task_assigned requires actor_role=overseer")
438 _require_mapping(merged.get("assignment"), "assignment")
439 elif kind == "verdict":
440 if actor_role != "verifier":
441 raise EntryValidationError(23, "verdict requires actor_role=verifier")
442 _require_non_empty_str(merged.get("artifact_sha256"), "artifact_sha256")
443 passed = merged.get("passed")
444 if not isinstance(passed, bool):
445 raise EntryValidationError(2, "passed must be a boolean")
446 evidence = _require_mapping(merged.get("evidence"), "evidence")
447 reexecuted = evidence.get("reexecuted")
448 if not isinstance(reexecuted, list) or not reexecuted:
449 raise EntryValidationError(24, "evidence.reexecuted must be a non-empty list")
450 if not all(isinstance(item, str) for item in reexecuted):
451 raise EntryValidationError(2, "evidence.reexecuted entries must be strings")
452 elif kind == "dispute_opened":
453 _require_non_empty_str(merged.get("subject"), "subject")
454 elif kind == "overseer_ruling":
455 if actor_role != "overseer":
456 raise EntryValidationError(23, "overseer_ruling requires actor_role=overseer")
457 _require_non_empty_str(merged.get("ruling"), "ruling")
458 elif kind == "approval_recorded":
459 if actor_role != "owner":
460 raise EntryValidationError(23, "approval_recorded requires actor_role=owner")
461 _require_non_empty_str(merged.get("artifact_sha256"), "artifact_sha256")
462 _require_non_empty_str(merged.get("bound_verdict_hash"), "bound_verdict_hash")
463 elif kind == "board_advance":
464 _require_non_empty_str(merged.get("artifact_sha256"), "artifact_sha256")
465 _require_non_empty_str(merged.get("bound_verdict_hash"), "bound_verdict_hash")
466 elif kind == "hook_check":
467 hook = merged.get("hook")
468 if hook not in {"board_done", "handoff", "register"}:
469 raise EntryValidationError(2, "hook_check.hook must be board_done|handoff|register")
470 ok = merged.get("ok")
471 if not isinstance(ok, bool):
472 raise EntryValidationError(2, "ok must be a boolean")
473 reason = merged.get("reason")
474 if reason is not None and not isinstance(reason, str):
475 raise EntryValidationError(2, "reason must be a string when present")
476 elif kind == "verification_evidence":
477 if actor_role != "verifier":
478 raise EntryValidationError(23, "verification_evidence requires actor_role=verifier")
479 _require_non_empty_str(merged.get("phase_id"), "phase_id")
480 _require_non_empty_str(merged.get("frozen_spec"), "frozen_spec")
481 _require_exact_positive_int(merged.get("round"), "round")
482 bv_verdict = merged.get("bv_verdict")
483 if bv_verdict not in BV_VERDICTS:
484 raise EntryValidationError(2, "bv_verdict must be pass|findings|blocked")
485 merged["artifacts"] = validate_verification_artifacts(merged.get("artifacts"))
486 subject_sha256 = merged.get("subject_sha256")
487 if subject_sha256 is not None:
488 _validate_sha256(subject_sha256, "subject_sha256")
489 notes = merged.get("notes")
490 if notes is not None and not isinstance(notes, str):
491 raise EntryValidationError(2, "notes must be a string when present")
492 elif kind == "independent_second_review":
493 if actor_role != "verifier":
494 raise EntryValidationError(
495 23, "independent_second_review requires actor_role=verifier"
496 )
497 _require_non_empty_str(merged.get("phase_id"), "phase_id")
498 _require_non_empty_str(merged.get("frozen_spec"), "frozen_spec")
499 _require_exact_positive_int(merged.get("round"), "round")
500 isr_verdict = merged.get("isr_verdict")
501 if isr_verdict not in ISR_VERDICTS:
502 raise EntryValidationError(2, "isr_verdict must be pass|findings|blocked")
503 producer_session_id = _require_non_empty_str(
504 merged.get("producer_session_id"), "producer_session_id"
505 )
506 if actor_session == producer_session_id:
507 raise EntryValidationError(
508 2, "actor_session_id must differ from producer_session_id"
509 )
510 producer_agent_id = merged.get("producer_agent_id")
511 verifier_agent_id = merged.get("verifier_agent_id")
512 if producer_agent_id is not None and not isinstance(producer_agent_id, str):
513 raise EntryValidationError(2, "producer_agent_id must be a string when present")
514 if verifier_agent_id is not None and not isinstance(verifier_agent_id, str):
515 raise EntryValidationError(2, "verifier_agent_id must be a string when present")
516 if (
517 isinstance(producer_agent_id, str)
518 and isinstance(verifier_agent_id, str)
519 and producer_agent_id == verifier_agent_id
520 ):
521 raise EntryValidationError(
522 2, "producer_agent_id must differ from verifier_agent_id when both present"
523 )
524 bound_hash = merged.get("bound_verification_evidence_hash")
525 if bound_hash is not None:
526 _require_non_empty_str(bound_hash, "bound_verification_evidence_hash")
527 notes = merged.get("notes")
528 if notes is not None and not isinstance(notes, str):
529 raise EntryValidationError(2, "notes must be a string when present")
530
531 elif kind == "freeze_review":
532 if actor_role != "verifier":
533 raise EntryValidationError(23, "freeze_review requires actor_role=verifier")
534 _require_non_empty_str(merged.get("phase_id"), "phase_id")
535 _require_non_empty_str(merged.get("frozen_spec"), "frozen_spec")
536 _require_exact_positive_int(merged.get("round"), "round")
537 if merged.get("gate") != "substantive":
538 raise EntryValidationError(2, "gate must be substantive")
539 freeze_verdict = merged.get("freeze_verdict")
540 if freeze_verdict not in FREEZE_VERDICTS:
541 raise EntryValidationError(2, "freeze_verdict must be pass|findings|blocked")
542 digest = merged.get("artifact_digest")
543 if not isinstance(digest, str) or not re.fullmatch(r"sha256:[0-9a-f]{64}", digest):
544 raise EntryValidationError(2, "artifact_digest must be sha256: + 64 lowercase hex")
545 _require_non_empty_str(merged.get("reviewer_model"), "reviewer_model")
546 checklist_ids = merged.get("checklist_ids")
547 if checklist_ids is not None:
548 if (
549 not isinstance(checklist_ids, list)
550 or not checklist_ids
551 or not all(isinstance(item, str) and item.strip() for item in checklist_ids)
552 ):
553 raise EntryValidationError(2, "checklist_ids must be a non-empty list of non-empty strings")
554 findings_count = merged.get("findings_count")
555 if findings_count is not None:
556 if type(findings_count) is not int or findings_count < 0:
557 raise EntryValidationError(2, "findings_count must be an integer >= 0")
558 producer_session_id = merged.get("producer_session_id")
559 if producer_session_id is not None:
560 producer_session_id = _require_non_empty_str(producer_session_id, "producer_session_id")
561 if actor_session == producer_session_id:
562 raise EntryValidationError(
563 2, "actor_session_id must differ from producer_session_id"
564 )
565 notes = merged.get("notes")
566 if notes is not None and not isinstance(notes, str):
567 raise EntryValidationError(2, "notes must be a string when present")
568
569 elif kind == "adversarial_freeze":
570 aff_verdict = merged.get("aff_verdict")
571 if aff_verdict not in AFF_VERDICTS:
572 raise EntryValidationError(2, "aff_verdict must be pass|findings|blocked|skip")
573 _require_non_empty_str(merged.get("phase_id"), "phase_id")
574 _require_non_empty_str(merged.get("frozen_spec"), "frozen_spec")
575 _require_exact_positive_int(merged.get("round"), "round")
576 digest = merged.get("artifact_digest")
577 if not isinstance(digest, str) or not re.fullmatch(r"sha256:[0-9a-f]{64}", digest):
578 raise EntryValidationError(2, "artifact_digest must be sha256: + 64 lowercase hex")
579
580 if aff_verdict == "skip":
581 if actor_role != "owner":
582 raise EntryValidationError(23, "adversarial_freeze skip requires actor_role=owner")
583 if "aff_posture" in merged:
584 raise EntryValidationError(2, "skip must not carry aff_posture")
585 if "producer_session_id" in merged:
586 raise EntryValidationError(2, "skip must not carry producer_session_id")
587 notes = merged.get("notes")
588 if notes is not None and not isinstance(notes, str):
589 raise EntryValidationError(2, "notes must be a string when present")
590 else:
591 if actor_role != "verifier":
592 raise EntryValidationError(
593 23, "adversarial_freeze pass/findings/blocked requires actor_role=verifier"
594 )
595 aff_posture = merged.get("aff_posture")
596 if aff_posture not in AFF_POSTURES:
597 raise EntryValidationError(2, "aff_posture must be attack")
598 producer_session_id = _require_non_empty_str(
599 merged.get("producer_session_id"), "producer_session_id"
600 )
601 if actor_session == producer_session_id:
602 raise EntryValidationError(
603 2, "actor_session_id must differ from producer_session_id"
604 )
605 _require_non_empty_str(merged.get("reviewer_model"), "reviewer_model")
606 producer_model = merged.get("producer_model")
607 if producer_model is not None and not isinstance(producer_model, str):
608 raise EntryValidationError(2, "producer_model must be a string when present")
609 producer_agent_id = merged.get("producer_agent_id")
610 verifier_agent_id = merged.get("verifier_agent_id")
611 if producer_agent_id is not None and not isinstance(producer_agent_id, str):
612 raise EntryValidationError(2, "producer_agent_id must be a string when present")
613 if verifier_agent_id is not None and not isinstance(verifier_agent_id, str):
614 raise EntryValidationError(2, "verifier_agent_id must be a string when present")
615 if (
616 isinstance(producer_agent_id, str)
617 and isinstance(verifier_agent_id, str)
618 and producer_agent_id == verifier_agent_id
619 ):
620 raise EntryValidationError(
621 2, "producer_agent_id must differ from verifier_agent_id when both present"
622 )
623 bound_hash = merged.get("bound_freeze_review_hash")
624 if bound_hash is not None:
625 _require_non_empty_str(bound_hash, "bound_freeze_review_hash")
626 side_check = merged.get("side_check_path")
627 if side_check is not None:
628 _require_non_empty_str(side_check, "side_check_path")
629 notes = merged.get("notes")
630 if notes is not None and not isinstance(notes, str):
631 raise EntryValidationError(2, "notes must be a string when present")
632
633 if "provenance" in merged:
634 merged["provenance"] = validate_provenance(merged["provenance"])
635
636 return merged
637
638
639 def find_passing_verdict(
640 entries: list[dict[str, Any]],
641 *,
642 artifact_sha256: str,
643 bound_verdict_hash: str,
644 ) -> bool:
645 """Return True when a passing verifier verdict matches the bound hash."""
646 for entry in entries:
647 if entry.get("kind") != "verdict":
648 continue
649 if entry.get("actor_role") != "verifier":
650 continue
651 if entry.get("passed") is not True:
652 continue
653 if entry.get("artifact_sha256") != artifact_sha256:
654 continue
655 if entry.get("entry_hash") == bound_verdict_hash:
656 return True
657 return False
File History 1 commit
sha256:8461d44b77376fbf06fa7c3e085d309e3010fd8d5886d63c63e69ce118811ad4 docs: record AFF-b feature-tip SHAs after AFF-b-ISR commit. Human 4 days ago