@ccoalm/ccl-skills 0.7.0 → 0.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -2
- package/dist/assets/marketplace/plugins/ccl-skills/skills/app-cross-platform-dev/SKILL.md +8 -7
- package/dist/assets/marketplace/plugins/ccl-skills/skills/app-cross-platform-dev/references/mobile-quality-release.md +1 -1
- package/dist/assets/marketplace/plugins/ccl-skills/skills/code-review/SKILL.md +16 -17
- package/dist/assets/marketplace/plugins/ccl-skills/skills/code-review/references/client-routing.md +1 -1
- package/dist/assets/marketplace/plugins/ccl-skills/skills/code-review/references/staged-review-contract.md +195 -7
- package/dist/assets/marketplace/plugins/ccl-skills/skills/code-review/references/timeout-auth-and-capabilities.md +3 -3
- package/dist/assets/marketplace/plugins/ccl-skills/skills/code-review/scripts/claude_review.sh +13 -5
- package/dist/assets/marketplace/plugins/ccl-skills/skills/code-review/scripts/codex_review.sh +9 -3
- package/dist/assets/marketplace/plugins/ccl-skills/skills/code-review/scripts/kimi_review.sh +9 -3
- package/dist/assets/marketplace/plugins/ccl-skills/skills/code-review/scripts/normalize_review_timeout.sh +22 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/code-review/scripts/opencode_review.sh +9 -3
- package/dist/assets/marketplace/plugins/ccl-skills/skills/code-review/scripts/review_gate.py +1540 -129
- package/dist/assets/marketplace/plugins/ccl-skills/skills/code-review/scripts/test_claude_review_probe.sh +8 -3
- package/dist/assets/marketplace/plugins/ccl-skills/skills/code-review/scripts/test_review_client_compat.py +76 -1
- package/dist/assets/marketplace/plugins/ccl-skills/skills/code-review/scripts/test_review_gate.sh +1858 -3
- package/dist/assets/marketplace/plugins/ccl-skills/skills/code-review/scripts/test_update_review_plan_intent.sh +789 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/code-review/scripts/update_review_plan_intent.py +513 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/go-microservice-dev/SKILL.md +4 -1
- package/dist/assets/marketplace/plugins/ccl-skills/skills/llm-inference-integration/SKILL.md +2 -1
- package/dist/assets/marketplace/plugins/ccl-skills/skills/miniapp-product-dev/SKILL.md +11 -10
- package/dist/assets/marketplace/plugins/ccl-skills/skills/nodejs-service-dev/SKILL.md +64 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/nodejs-service-dev/agents/openai.yaml +4 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/nodejs-service-dev/references/async-lifecycle-and-performance.md +72 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/nodejs-service-dev/references/runtime-and-project-contract.md +58 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/nodejs-service-dev/references/source-map.md +41 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/nodejs-service-dev/references/verification-diagnostics-and-security.md +63 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/product-rd-workflow/SKILL.md +8 -10
- package/dist/assets/marketplace/plugins/ccl-skills/skills/product-rd-workflow/references/design-routing-and-readiness.md +10 -14
- package/dist/assets/marketplace/plugins/ccl-skills/skills/product-rd-workflow/references/verify-developer-experience.md +1 -1
- package/dist/assets/marketplace/plugins/ccl-skills/skills/product-ui-ux-design/SKILL.md +135 -86
- package/dist/assets/marketplace/plugins/ccl-skills/skills/product-ui-ux-design/references/behavioral-aesthetic-logic.md +66 -80
- package/dist/assets/marketplace/plugins/ccl-skills/skills/product-ui-ux-design/references/delivery-contract.md +275 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/product-ui-ux-design/references/design-execution-checklist.md +88 -214
- package/dist/assets/marketplace/plugins/ccl-skills/skills/product-ui-ux-design/references/design-impl-naming-and-versioning.md +2 -2
- package/dist/assets/marketplace/plugins/ccl-skills/skills/product-ui-ux-design/references/design-intake-and-acceptance.md +10 -8
- package/dist/assets/marketplace/plugins/ccl-skills/skills/product-ui-ux-design/references/design-system-source-of-truth.md +4 -5
- package/dist/assets/marketplace/plugins/ccl-skills/skills/product-ui-ux-design/references/external-ui-ux-quality-benchmarks.md +112 -95
- package/dist/assets/marketplace/plugins/ccl-skills/skills/product-ui-ux-design/references/frontend-code-evidence-map.md +30 -21
- package/dist/assets/marketplace/plugins/ccl-skills/skills/product-ui-ux-design/references/interaction-design-patterns.md +22 -3
- package/dist/assets/marketplace/plugins/ccl-skills/skills/product-ui-ux-design/references/layout-recipes-and-screenshot-acceptance.md +20 -17
- package/dist/assets/marketplace/plugins/ccl-skills/skills/product-ui-ux-design/references/multi-project-token-consistency.md +7 -9
- package/dist/assets/marketplace/plugins/ccl-skills/skills/product-ui-ux-design/references/multi-stack-strategy.md +14 -10
- package/dist/assets/marketplace/plugins/ccl-skills/skills/product-ui-ux-design/references/operational-processing-workflows.md +2 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/product-ui-ux-design/references/platform-mobile-patterns.md +1 -1
- package/dist/assets/marketplace/plugins/ccl-skills/skills/product-ui-ux-design/references/product-lifecycle-acceptance-and-iteration.md +9 -6
- package/dist/assets/marketplace/plugins/ccl-skills/skills/product-ui-ux-design/references/product-surface-patterns.md +3 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/product-ui-ux-design/references/source-map.md +37 -10
- package/dist/assets/marketplace/plugins/ccl-skills/skills/product-ui-ux-design/references/tokens-and-components.md +7 -1
- package/dist/assets/marketplace/plugins/ccl-skills/skills/product-ui-ux-design/references/ui-ux-audit.md +8 -5
- package/dist/assets/marketplace/plugins/ccl-skills/skills/product-ui-ux-design/references/ui-ux-design-development.md +16 -5
- package/dist/assets/marketplace/plugins/ccl-skills/skills/product-ui-ux-design/references/visual-craft.md +4 -2
- package/dist/assets/marketplace/plugins/ccl-skills/skills/python-service-dev/SKILL.md +4 -1
- package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/SKILL.md +4 -4
- package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/references/dual-track-review-gate.md +95 -5
- package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/references/extraction-quickstart.md +11 -9
- package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/references/r0-leakage-audit.md +102 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/references/source-register.md +54 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/references/source-to-skill-extraction.md +8 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/references/uiux-judgment-extraction.md +6 -6
- package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/references/validation-and-landing.md +4 -3
- package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/scripts/check-ccl-skills.sh +69 -2
- package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/scripts/extraction_review_gate.sh +22 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/scripts/impact-chain-gate.rb +49 -4
- package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/scripts/obligation-ledger.py +2748 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/scripts/register-firing-path-resolution.rb +20 -5
- package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/scripts/shared_git_surface_gate.py +1142 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/scripts/test_check_ccl_regressions.sh +17 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/scripts/test_check_ccl_skill_catalog.sh +41 -4
- package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/scripts/test_ci_checkout_ref_binding.sh +120 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/scripts/test_entrypoint_domain_scan_terms.sh +82 -8
- package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/scripts/test_extraction_review_gate.sh +336 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/scripts/test_impact_chain_self_adjudication.sh +82 -10
- package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/scripts/test_obligation_ledger.sh +1416 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/scripts/test_obligation_ledger_repo_audit.sh +57 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/scripts/test_register_firing_path_wiring.sh +141 -4
- package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/scripts/test_routing_pointer_integrity.sh +3 -1
- package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/scripts/test_shared_git_surface_gate.sh +1696 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/scripts/test_uiux_delivery_contract.sh +2117 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/scripts/test_uiux_loading_budget.sh +316 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/scripts/test_validate_extraction_review_state.sh +1176 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/scripts/test_validate_skill_cross_refs.sh +31 -1
- package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/scripts/validate-skill.sh +9 -4
- package/dist/assets/marketplace/plugins/ccl-skills/skills/skill-extraction-workflow/scripts/validate_extraction_review_state.py +980 -0
- package/dist/assets/marketplace/plugins/ccl-skills/skills/terminal-cli-dev/SKILL.md +8 -6
- package/dist/assets/marketplace/plugins/ccl-skills/skills/testing-strategy/SKILL.md +8 -7
- package/dist/assets/marketplace/plugins/ccl-skills/skills/testing-strategy/references/client-runtime-test-matrices.md +10 -2
- package/dist/assets/marketplace/plugins/ccl-skills/skills/web-react-dev/SKILL.md +6 -5
- package/dist/assets/marketplace/plugins/ccl-skills/skills/web-react-dev/references/complex-workspace-patterns.md +1 -1
- package/dist/assets/release.json +175 -70
- package/package.json +1 -1
|
@@ -0,0 +1,980 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Validate a receipt-bound terminal ledger for an extraction review lane."""
|
|
3
|
+
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
|
|
6
|
+
import argparse
|
|
7
|
+
import hashlib
|
|
8
|
+
import json
|
|
9
|
+
import os
|
|
10
|
+
import re
|
|
11
|
+
import stat
|
|
12
|
+
import sys
|
|
13
|
+
import unicodedata
|
|
14
|
+
from datetime import datetime
|
|
15
|
+
from pathlib import Path
|
|
16
|
+
from typing import Any, NoReturn
|
|
17
|
+
|
|
18
|
+
MAX_LEDGER_BYTES = 128_000
|
|
19
|
+
MAX_RESULT_BYTES = 1_000_000
|
|
20
|
+
MAX_EVIDENCE_BYTES = 128_000
|
|
21
|
+
OBJECT_ID_RE = re.compile(r"(?:[0-9a-f]{40}|[0-9a-f]{64})")
|
|
22
|
+
SHA256_RE = re.compile(r"[0-9a-f]{64}")
|
|
23
|
+
RFC3339_RE = re.compile(
|
|
24
|
+
r"\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?(?:Z|[+-]\d{2}:\d{2})"
|
|
25
|
+
)
|
|
26
|
+
TERMINAL_STATES = {
|
|
27
|
+
"ready_for_human_decision",
|
|
28
|
+
"continuation_authorization_required",
|
|
29
|
+
"baseline_race",
|
|
30
|
+
}
|
|
31
|
+
EXTERNAL_REVIEW_STATES = {"reviewed", "findings_pending", "post_review_budget"}
|
|
32
|
+
KNOWN_REVIEW_STATES = EXTERNAL_REVIEW_STATES | {"self_reviewed"}
|
|
33
|
+
DISPOSITIONS = {
|
|
34
|
+
"fixed",
|
|
35
|
+
"source_refuted",
|
|
36
|
+
"accepted_tradeoff",
|
|
37
|
+
"pre_existing_out_of_scope",
|
|
38
|
+
"needs_human_decision",
|
|
39
|
+
"open",
|
|
40
|
+
}
|
|
41
|
+
RESOLVED_DISPOSITIONS = {
|
|
42
|
+
"fixed",
|
|
43
|
+
"source_refuted",
|
|
44
|
+
"accepted_tradeoff",
|
|
45
|
+
"pre_existing_out_of_scope",
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
class StateError(Exception):
|
|
50
|
+
pass
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def fail(message: str) -> NoReturn:
|
|
54
|
+
raise StateError(message)
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def bounded_text(value: object, field: str, maximum: int = 1000) -> str:
|
|
58
|
+
if not isinstance(value, str) or value != value.strip() or not value:
|
|
59
|
+
fail(f"{field} must be a non-empty normalized string")
|
|
60
|
+
# Interior control characters would otherwise be echoed into stderr
|
|
61
|
+
# diagnostics (terminal-escape injection on the error path). C1 controls
|
|
62
|
+
# (NEL, CSI) and Unicode line/paragraph separators forge line breaks too.
|
|
63
|
+
if any(
|
|
64
|
+
ord(char) < 0x20 or 0x7F <= ord(char) <= 0x9F or char in "\u2028\u2029"
|
|
65
|
+
for char in value
|
|
66
|
+
):
|
|
67
|
+
fail(f"{field} must not contain control characters")
|
|
68
|
+
# Default-ignorable format characters (ZWSP, word joiner, bidi controls)
|
|
69
|
+
# let visually identical class keys or predicates register as distinct,
|
|
70
|
+
# splitting one recurrence class below its sweep threshold.
|
|
71
|
+
if any(unicodedata.category(char) == "Cf" for char in value):
|
|
72
|
+
fail(f"{field} must not contain format characters")
|
|
73
|
+
try:
|
|
74
|
+
value.encode("utf-8")
|
|
75
|
+
except UnicodeEncodeError:
|
|
76
|
+
fail(f"{field} must be valid UTF-8 text")
|
|
77
|
+
if len(value) > maximum:
|
|
78
|
+
fail(f"{field} exceeds {maximum} characters")
|
|
79
|
+
return value
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def object_id(value: object, field: str) -> str:
|
|
83
|
+
text = bounded_text(value, field, 64)
|
|
84
|
+
if OBJECT_ID_RE.fullmatch(text) is None:
|
|
85
|
+
fail(f"{field} must be a lowercase 40- or 64-character hexadecimal object id")
|
|
86
|
+
return text
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def sha256(value: object, field: str) -> str:
|
|
90
|
+
text = bounded_text(value, field, 64)
|
|
91
|
+
if SHA256_RE.fullmatch(text) is None:
|
|
92
|
+
fail(f"{field} must be a lowercase 64-character SHA-256 digest")
|
|
93
|
+
return text
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def exact_object(value: object, fields: set[str], label: str) -> dict[str, Any]:
|
|
97
|
+
if not isinstance(value, dict) or set(value) != fields:
|
|
98
|
+
fail(f"{label} must contain exactly: {', '.join(sorted(fields))}")
|
|
99
|
+
return value
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def read_regular(path: Path, *, label: str, maximum: int) -> bytes:
|
|
103
|
+
if not hasattr(os, "O_NOFOLLOW") or not hasattr(os, "O_NONBLOCK"):
|
|
104
|
+
fail(f"platform cannot open {label} with no-follow and non-blocking safety")
|
|
105
|
+
try:
|
|
106
|
+
fd = os.open(
|
|
107
|
+
path,
|
|
108
|
+
os.O_RDONLY
|
|
109
|
+
| os.O_NOFOLLOW
|
|
110
|
+
| getattr(os, "O_CLOEXEC", 0)
|
|
111
|
+
| os.O_NONBLOCK,
|
|
112
|
+
)
|
|
113
|
+
except (OSError, UnicodeError, ValueError) as exc:
|
|
114
|
+
fail(f"{label} is unreadable: {exc}")
|
|
115
|
+
try:
|
|
116
|
+
try:
|
|
117
|
+
info = os.fstat(fd)
|
|
118
|
+
except OSError as exc:
|
|
119
|
+
fail(f"cannot inspect {label}: {exc}")
|
|
120
|
+
if not stat.S_ISREG(info.st_mode) or info.st_nlink != 1:
|
|
121
|
+
fail(f"{label} must be a singly linked regular file")
|
|
122
|
+
if info.st_size > maximum:
|
|
123
|
+
fail(f"{label} exceeds {maximum} bytes")
|
|
124
|
+
chunks: list[bytes] = []
|
|
125
|
+
remaining = maximum + 1
|
|
126
|
+
while remaining:
|
|
127
|
+
try:
|
|
128
|
+
chunk = os.read(fd, remaining)
|
|
129
|
+
except OSError as exc:
|
|
130
|
+
fail(f"cannot read {label}: {exc}")
|
|
131
|
+
if not chunk:
|
|
132
|
+
break
|
|
133
|
+
chunks.append(chunk)
|
|
134
|
+
remaining -= len(chunk)
|
|
135
|
+
raw = b"".join(chunks)
|
|
136
|
+
finally:
|
|
137
|
+
try:
|
|
138
|
+
os.close(fd)
|
|
139
|
+
except OSError:
|
|
140
|
+
pass
|
|
141
|
+
if len(raw) > maximum:
|
|
142
|
+
fail(f"{label} exceeds {maximum} bytes")
|
|
143
|
+
return raw
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def decode_json(raw: bytes, *, label: str) -> dict[str, Any]:
|
|
147
|
+
def unique_object(pairs: list[tuple[str, Any]]) -> dict[str, Any]:
|
|
148
|
+
value: dict[str, Any] = {}
|
|
149
|
+
for key, item in pairs:
|
|
150
|
+
if key in value:
|
|
151
|
+
fail(f"{label} contains duplicate object key: {key}")
|
|
152
|
+
value[key] = item
|
|
153
|
+
return value
|
|
154
|
+
|
|
155
|
+
def reject_constant(constant: str) -> NoReturn:
|
|
156
|
+
fail(f"{label} contains a non-standard JSON constant: {constant}")
|
|
157
|
+
|
|
158
|
+
try:
|
|
159
|
+
value = json.loads(
|
|
160
|
+
raw.decode("utf-8"),
|
|
161
|
+
object_pairs_hook=unique_object,
|
|
162
|
+
parse_constant=reject_constant,
|
|
163
|
+
)
|
|
164
|
+
except StateError:
|
|
165
|
+
raise
|
|
166
|
+
except (UnicodeError, json.JSONDecodeError) as exc:
|
|
167
|
+
fail(f"{label} must be UTF-8 JSON: {exc}")
|
|
168
|
+
if not isinstance(value, dict):
|
|
169
|
+
fail(f"{label} must be a JSON object")
|
|
170
|
+
return value
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
def sibling_name(value: object, field: str) -> str:
|
|
174
|
+
name = bounded_text(value, field, 255)
|
|
175
|
+
if Path(name).is_absolute() or Path(name).name != name or "/" in name or "\\" in name:
|
|
176
|
+
fail(f"{field} must name a file in the ledger directory")
|
|
177
|
+
return name
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
def load_sibling(
|
|
181
|
+
ledger_dir: Path,
|
|
182
|
+
*,
|
|
183
|
+
file_value: object,
|
|
184
|
+
digest_value: object,
|
|
185
|
+
label: str,
|
|
186
|
+
maximum: int,
|
|
187
|
+
) -> bytes:
|
|
188
|
+
name = sibling_name(file_value, f"{label}.file")
|
|
189
|
+
expected = sha256(digest_value, f"{label}.sha256")
|
|
190
|
+
raw = read_regular(ledger_dir / name, label=label, maximum=maximum)
|
|
191
|
+
actual = hashlib.sha256(raw).hexdigest()
|
|
192
|
+
if actual != expected:
|
|
193
|
+
fail(f"{label} digest does not match {name}")
|
|
194
|
+
return raw
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
def canonical_hash(value: object, label: str) -> str:
|
|
198
|
+
try:
|
|
199
|
+
encoded = json.dumps(
|
|
200
|
+
value,
|
|
201
|
+
ensure_ascii=False,
|
|
202
|
+
sort_keys=True,
|
|
203
|
+
separators=(",", ":"),
|
|
204
|
+
).encode()
|
|
205
|
+
except (TypeError, ValueError, UnicodeError) as exc:
|
|
206
|
+
fail(f"{label} is not canonical JSON: {exc}")
|
|
207
|
+
return hashlib.sha256(encoded).hexdigest()
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
def parse_rfc3339(value: object, field: str) -> datetime:
|
|
211
|
+
text = bounded_text(value, field, 100)
|
|
212
|
+
if RFC3339_RE.fullmatch(text) is None:
|
|
213
|
+
fail(f"{field} must be a strict RFC3339 timestamp")
|
|
214
|
+
try:
|
|
215
|
+
parsed = datetime.fromisoformat(text[:-1] + "+00:00" if text.endswith("Z") else text)
|
|
216
|
+
except ValueError as exc:
|
|
217
|
+
fail(f"{field} must be a valid RFC3339 timestamp: {exc}")
|
|
218
|
+
if parsed.utcoffset() is None:
|
|
219
|
+
fail(f"{field} must include an RFC3339 UTC offset")
|
|
220
|
+
return parsed
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
def load(path: Path) -> tuple[dict[str, Any], Path]:
|
|
224
|
+
raw = read_regular(path, label="ledger", maximum=MAX_LEDGER_BYTES)
|
|
225
|
+
value = decode_json(raw, label="ledger")
|
|
226
|
+
payload = exact_object(
|
|
227
|
+
value,
|
|
228
|
+
{
|
|
229
|
+
"schema_version",
|
|
230
|
+
"candidate_sha256",
|
|
231
|
+
"controller_receipts",
|
|
232
|
+
"completion_receipt",
|
|
233
|
+
"base_attestations",
|
|
234
|
+
"autonomous_round",
|
|
235
|
+
"controller_review_state",
|
|
236
|
+
"finding_classes",
|
|
237
|
+
"unreviewed_delta",
|
|
238
|
+
"closeout_state",
|
|
239
|
+
},
|
|
240
|
+
"ledger",
|
|
241
|
+
)
|
|
242
|
+
return payload, path.absolute().parent
|
|
243
|
+
|
|
244
|
+
|
|
245
|
+
def validate_scope(receipt: dict[str, Any], label: str) -> str:
|
|
246
|
+
scope = exact_object(
|
|
247
|
+
receipt.get("review_scope"),
|
|
248
|
+
{
|
|
249
|
+
"schema_version",
|
|
250
|
+
"intent_sha256",
|
|
251
|
+
"acceptance_sha256",
|
|
252
|
+
"stage",
|
|
253
|
+
"review_depth",
|
|
254
|
+
"risk_tags",
|
|
255
|
+
"challenge_budget",
|
|
256
|
+
"wording_only_proof_sha256",
|
|
257
|
+
"wording_only_scope_sha256",
|
|
258
|
+
},
|
|
259
|
+
f"{label}.review_scope",
|
|
260
|
+
)
|
|
261
|
+
if scope["schema_version"] != 3 or type(scope["schema_version"]) is not int:
|
|
262
|
+
fail(f"{label}.review_scope.schema_version must be 3")
|
|
263
|
+
sha256(scope["intent_sha256"], f"{label}.review_scope.intent_sha256")
|
|
264
|
+
sha256(scope["acceptance_sha256"], f"{label}.review_scope.acceptance_sha256")
|
|
265
|
+
stage = bounded_text(scope["stage"], f"{label}.review_scope.stage", 80)
|
|
266
|
+
depth = bounded_text(scope["review_depth"], f"{label}.review_scope.review_depth", 80)
|
|
267
|
+
risks = scope["risk_tags"]
|
|
268
|
+
if not isinstance(risks, list):
|
|
269
|
+
fail(f"{label}.review_scope.risk_tags must be an array")
|
|
270
|
+
normalized_risks = [
|
|
271
|
+
bounded_text(item, f"{label}.review_scope.risk_tags", 100) for item in risks
|
|
272
|
+
]
|
|
273
|
+
if len(normalized_risks) != len(set(normalized_risks)):
|
|
274
|
+
fail(f"{label}.review_scope.risk_tags contains duplicates")
|
|
275
|
+
if scope["challenge_budget"] != 2 or type(scope["challenge_budget"]) is not int:
|
|
276
|
+
fail(f"{label}.review_scope.challenge_budget must be 2")
|
|
277
|
+
if (
|
|
278
|
+
scope["wording_only_proof_sha256"] is not None
|
|
279
|
+
or scope["wording_only_scope_sha256"] is not None
|
|
280
|
+
or "wording_only_proof_sha256" not in receipt
|
|
281
|
+
or receipt["wording_only_proof_sha256"] is not None
|
|
282
|
+
or "wording_only_scope" not in receipt
|
|
283
|
+
or receipt["wording_only_scope"] is not None
|
|
284
|
+
):
|
|
285
|
+
fail(f"{label} must bind the non-wording extraction scope")
|
|
286
|
+
recorded = sha256(receipt.get("review_scope_sha256"), f"{label}.review_scope_sha256")
|
|
287
|
+
if canonical_hash(scope, f"{label}.review_scope") != recorded:
|
|
288
|
+
fail(f"{label}.review_scope_sha256 does not reproduce review_scope")
|
|
289
|
+
if (
|
|
290
|
+
receipt.get("stage") != stage
|
|
291
|
+
or receipt.get("review_depth") != depth
|
|
292
|
+
or receipt.get("risk_tags") != normalized_risks
|
|
293
|
+
):
|
|
294
|
+
fail(f"{label} top-level scope fields contradict review_scope")
|
|
295
|
+
return recorded
|
|
296
|
+
|
|
297
|
+
|
|
298
|
+
def validate_controller_receipts(
|
|
299
|
+
payload: dict[str, Any], ledger_dir: Path
|
|
300
|
+
) -> tuple[list[dict[str, Any]], list[str], dict[str, list[str]], str, str]:
|
|
301
|
+
refs = payload["controller_receipts"]
|
|
302
|
+
if not isinstance(refs, list) or not 1 <= len(refs) <= 3:
|
|
303
|
+
fail("controller_receipts must contain one to three ordered Agent rounds")
|
|
304
|
+
if payload["autonomous_round"] != len(refs) or type(payload["autonomous_round"]) is not int:
|
|
305
|
+
fail("autonomous_round must equal the ordered controller receipt count")
|
|
306
|
+
|
|
307
|
+
receipts: list[dict[str, Any]] = []
|
|
308
|
+
receipt_hashes: list[str] = []
|
|
309
|
+
finding_hashes: dict[str, list[str]] = {}
|
|
310
|
+
chain_id: str | None = None
|
|
311
|
+
scope_hash: str | None = None
|
|
312
|
+
ledger_candidate = sha256(
|
|
313
|
+
payload["candidate_sha256"], "ledger.candidate_sha256"
|
|
314
|
+
)
|
|
315
|
+
for expected_index, value in enumerate(refs, start=1):
|
|
316
|
+
ref = exact_object(
|
|
317
|
+
value, {"sequence", "file", "sha256"}, f"controller_receipts[{expected_index - 1}]"
|
|
318
|
+
)
|
|
319
|
+
if ref["sequence"] != expected_index or type(ref["sequence"]) is not int:
|
|
320
|
+
fail("controller receipt sequence must be contiguous from 1")
|
|
321
|
+
receipt_hash = sha256(ref["sha256"], f"controller_receipts[{expected_index - 1}].sha256")
|
|
322
|
+
raw = load_sibling(
|
|
323
|
+
ledger_dir,
|
|
324
|
+
file_value=ref["file"],
|
|
325
|
+
digest_value=receipt_hash,
|
|
326
|
+
label=f"controller receipt {expected_index}",
|
|
327
|
+
maximum=MAX_RESULT_BYTES,
|
|
328
|
+
)
|
|
329
|
+
receipt = decode_json(raw, label=f"controller receipt {expected_index}")
|
|
330
|
+
if receipt.get("schema_version") != 3 or type(receipt.get("schema_version")) is not int:
|
|
331
|
+
fail(f"controller receipt {expected_index} schema_version must be 3")
|
|
332
|
+
expected_mode = "review" if expected_index == 1 else "challenge"
|
|
333
|
+
if receipt.get("mode") != expected_mode:
|
|
334
|
+
fail(f"controller receipt {expected_index} must have mode {expected_mode}")
|
|
335
|
+
if receipt.get("status") not in {"passed", "findings"}:
|
|
336
|
+
fail(f"controller receipt {expected_index} must have status passed or findings")
|
|
337
|
+
if receipt.get("review_chain_tracked") is not True:
|
|
338
|
+
fail(f"controller receipt {expected_index} must belong to a tracked chain")
|
|
339
|
+
current_chain = bounded_text(
|
|
340
|
+
receipt.get("review_chain_id"), f"controller receipt {expected_index}.review_chain_id", 120
|
|
341
|
+
)
|
|
342
|
+
if chain_id is None:
|
|
343
|
+
chain_id = current_chain
|
|
344
|
+
elif current_chain != chain_id:
|
|
345
|
+
fail(f"controller receipt {expected_index} review_chain_id changed")
|
|
346
|
+
if receipt.get("autonomous_review_index") != expected_index or type(
|
|
347
|
+
receipt.get("autonomous_review_index")
|
|
348
|
+
) is not int:
|
|
349
|
+
fail(f"controller receipt {expected_index} autonomous_review_index is not contiguous")
|
|
350
|
+
expected_challenge_index = 0 if expected_index == 1 else expected_index - 1
|
|
351
|
+
if receipt.get("challenge_index") != expected_challenge_index or type(
|
|
352
|
+
receipt.get("challenge_index")
|
|
353
|
+
) is not int:
|
|
354
|
+
fail(f"controller receipt {expected_index} challenge_index is invalid")
|
|
355
|
+
if receipt.get("challenge_budget") != 2 or type(receipt.get("challenge_budget")) is not int:
|
|
356
|
+
fail(f"controller receipt {expected_index} challenge_budget must be 2")
|
|
357
|
+
if receipt.get("autonomous_review_budget") != 3 or type(
|
|
358
|
+
receipt.get("autonomous_review_budget")
|
|
359
|
+
) is not int:
|
|
360
|
+
fail(f"controller receipt {expected_index} autonomous_review_budget must be 3")
|
|
361
|
+
expected_remaining = 3 - expected_index
|
|
362
|
+
if receipt.get("autonomous_reviews_remaining") != expected_remaining or type(
|
|
363
|
+
receipt.get("autonomous_reviews_remaining")
|
|
364
|
+
) is not int:
|
|
365
|
+
fail(f"controller receipt {expected_index} autonomous_reviews_remaining is invalid")
|
|
366
|
+
if receipt.get("autonomous_review_allowed") is not (expected_remaining > 0):
|
|
367
|
+
fail(f"controller receipt {expected_index} autonomous_review_allowed is invalid")
|
|
368
|
+
prior = receipt.get("prior_review_result_sha256")
|
|
369
|
+
if prior != receipt_hashes:
|
|
370
|
+
fail(f"controller receipt {expected_index} prior_review_result_sha256 is not the complete ordered prefix")
|
|
371
|
+
candidate = sha256(
|
|
372
|
+
receipt.get("candidate_sha256"), f"controller receipt {expected_index}.candidate_sha256"
|
|
373
|
+
)
|
|
374
|
+
packet = sha256(
|
|
375
|
+
receipt.get("packet_sha256"), f"controller receipt {expected_index}.packet_sha256"
|
|
376
|
+
)
|
|
377
|
+
if packet != candidate:
|
|
378
|
+
fail(f"controller receipt {expected_index} packet_sha256 must equal candidate_sha256")
|
|
379
|
+
# Every counted round must have inspected the exact final candidate;
|
|
380
|
+
# a chain whose review round saw an earlier candidate is not a review
|
|
381
|
+
# of the candidate this ledger closes out.
|
|
382
|
+
if candidate != ledger_candidate:
|
|
383
|
+
fail(
|
|
384
|
+
f"controller receipt {expected_index} does not bind the ledger candidate"
|
|
385
|
+
)
|
|
386
|
+
current_scope_hash = validate_scope(receipt, f"controller receipt {expected_index}")
|
|
387
|
+
if scope_hash is None:
|
|
388
|
+
scope_hash = current_scope_hash
|
|
389
|
+
elif current_scope_hash != scope_hash:
|
|
390
|
+
fail(f"controller receipt {expected_index} review scope changed")
|
|
391
|
+
|
|
392
|
+
findings = receipt.get("findings")
|
|
393
|
+
if not isinstance(findings, list):
|
|
394
|
+
fail(f"controller receipt {expected_index}.findings must be an array")
|
|
395
|
+
current_findings: list[str] = []
|
|
396
|
+
current_finding_set: set[str] = set()
|
|
397
|
+
for finding_index, finding in enumerate(findings):
|
|
398
|
+
if not isinstance(finding, dict):
|
|
399
|
+
fail(f"controller receipt {expected_index}.findings[{finding_index}] must be an object")
|
|
400
|
+
finding_hash = canonical_hash(
|
|
401
|
+
finding, f"controller receipt {expected_index}.findings[{finding_index}]"
|
|
402
|
+
)
|
|
403
|
+
if finding_hash in current_finding_set:
|
|
404
|
+
fail(f"controller receipt {expected_index} repeats a canonical finding")
|
|
405
|
+
current_finding_set.add(finding_hash)
|
|
406
|
+
current_findings.append(finding_hash)
|
|
407
|
+
if receipt["status"] == "passed" and findings:
|
|
408
|
+
fail(f"controller receipt {expected_index} passed status cannot carry findings")
|
|
409
|
+
if receipt["status"] == "findings" and not findings:
|
|
410
|
+
fail(f"controller receipt {expected_index} findings status requires findings")
|
|
411
|
+
state = bounded_text(
|
|
412
|
+
receipt.get("review_state"), f"controller receipt {expected_index}.review_state", 80
|
|
413
|
+
)
|
|
414
|
+
if state not in EXTERNAL_REVIEW_STATES:
|
|
415
|
+
fail(f"controller receipt {expected_index} has unknown controller review_state {state}")
|
|
416
|
+
expected_state = (
|
|
417
|
+
"post_review_budget"
|
|
418
|
+
if receipt["status"] == "findings" and expected_index == 3
|
|
419
|
+
else "findings_pending"
|
|
420
|
+
if receipt["status"] == "findings"
|
|
421
|
+
else "reviewed"
|
|
422
|
+
)
|
|
423
|
+
if state != expected_state:
|
|
424
|
+
fail(f"controller receipt {expected_index} review_state contradicts status and round")
|
|
425
|
+
if receipt.get("human_decision_required") is not (state == "post_review_budget"):
|
|
426
|
+
fail(f"controller receipt {expected_index} human_decision_required contradicts review_state")
|
|
427
|
+
|
|
428
|
+
receipts.append(receipt)
|
|
429
|
+
receipt_hashes.append(receipt_hash)
|
|
430
|
+
finding_hashes[receipt_hash] = current_findings
|
|
431
|
+
|
|
432
|
+
if chain_id is None or scope_hash is None:
|
|
433
|
+
fail("controller receipt chain is empty")
|
|
434
|
+
return receipts, receipt_hashes, finding_hashes, chain_id, scope_hash
|
|
435
|
+
|
|
436
|
+
|
|
437
|
+
def validate_completion_receipt(
|
|
438
|
+
value: object,
|
|
439
|
+
ledger_dir: Path,
|
|
440
|
+
receipts: list[dict[str, Any]],
|
|
441
|
+
receipt_hashes: list[str],
|
|
442
|
+
chain_id: str,
|
|
443
|
+
scope_hash: str,
|
|
444
|
+
) -> dict[str, Any] | None:
|
|
445
|
+
if value is None:
|
|
446
|
+
return None
|
|
447
|
+
ref = exact_object(value, {"file", "sha256"}, "completion_receipt")
|
|
448
|
+
raw = load_sibling(
|
|
449
|
+
ledger_dir,
|
|
450
|
+
file_value=ref["file"],
|
|
451
|
+
digest_value=ref["sha256"],
|
|
452
|
+
label="completion receipt",
|
|
453
|
+
maximum=MAX_RESULT_BYTES,
|
|
454
|
+
)
|
|
455
|
+
receipt = decode_json(raw, label="completion receipt")
|
|
456
|
+
final = receipts[-1]
|
|
457
|
+
if (
|
|
458
|
+
receipt.get("schema_version") != 3
|
|
459
|
+
or type(receipt.get("schema_version")) is not int
|
|
460
|
+
or receipt.get("mode") != "complete"
|
|
461
|
+
or receipt.get("status") != "passed"
|
|
462
|
+
or receipt.get("review_state") != "self_reviewed"
|
|
463
|
+
or receipt.get("completion_gated") is not False
|
|
464
|
+
or receipt.get("next_action") != "complete"
|
|
465
|
+
or receipt.get("findings") != []
|
|
466
|
+
):
|
|
467
|
+
fail("completion receipt must be a passed self_reviewed complete result")
|
|
468
|
+
if final.get("status") != "passed" or final.get("findings") != []:
|
|
469
|
+
fail("completion receipt cannot close a final external receipt with findings")
|
|
470
|
+
if receipt.get("review_chain_tracked") is not True or receipt.get("review_chain_id") != chain_id:
|
|
471
|
+
fail("completion receipt review_chain_id does not match the controller chain")
|
|
472
|
+
if receipt.get("challenge_budget") != 2 or type(receipt.get("challenge_budget")) is not int:
|
|
473
|
+
fail("completion receipt challenge_budget must be 2")
|
|
474
|
+
if receipt.get("autonomous_review_budget") != 3 or type(
|
|
475
|
+
receipt.get("autonomous_review_budget")
|
|
476
|
+
) is not int:
|
|
477
|
+
fail("completion receipt autonomous_review_budget must be 3")
|
|
478
|
+
if receipt.get("autonomous_review_index") != len(receipts) or type(
|
|
479
|
+
receipt.get("autonomous_review_index")
|
|
480
|
+
) is not int:
|
|
481
|
+
fail("completion receipt autonomous_review_index does not match the final round")
|
|
482
|
+
expected_remaining = 3 - len(receipts)
|
|
483
|
+
if receipt.get("autonomous_reviews_remaining") != expected_remaining or type(
|
|
484
|
+
receipt.get("autonomous_reviews_remaining")
|
|
485
|
+
) is not int:
|
|
486
|
+
fail("completion receipt autonomous_reviews_remaining does not match the final round")
|
|
487
|
+
if receipt.get("autonomous_review_allowed") is not False:
|
|
488
|
+
fail("completion receipt must disable further autonomous review")
|
|
489
|
+
if receipt.get("prior_review_result_sha256") != receipt_hashes[:-1]:
|
|
490
|
+
fail("completion receipt prior_review_result_sha256 does not match the final external receipt")
|
|
491
|
+
if receipt.get("completion_review_result_sha256") != receipt_hashes[-1]:
|
|
492
|
+
fail("completion receipt completion_review_result_sha256 does not identify the final external receipt")
|
|
493
|
+
candidate = sha256(receipt.get("candidate_sha256"), "completion receipt.candidate_sha256")
|
|
494
|
+
packet = sha256(receipt.get("packet_sha256"), "completion receipt.packet_sha256")
|
|
495
|
+
if packet != candidate or candidate != final.get("candidate_sha256"):
|
|
496
|
+
fail("completion receipt does not bind the exact final candidate")
|
|
497
|
+
if validate_scope(receipt, "completion receipt") != scope_hash:
|
|
498
|
+
fail("completion receipt review scope changed")
|
|
499
|
+
return receipt
|
|
500
|
+
|
|
501
|
+
|
|
502
|
+
def validate_base_attestations(
|
|
503
|
+
payload: dict[str, Any],
|
|
504
|
+
ledger_dir: Path,
|
|
505
|
+
receipt_hashes: list[str],
|
|
506
|
+
closeout: str,
|
|
507
|
+
) -> int:
|
|
508
|
+
attestations = payload["base_attestations"]
|
|
509
|
+
if not isinstance(attestations, list) or not attestations:
|
|
510
|
+
fail("base_attestations must be a non-empty array")
|
|
511
|
+
base_shas: list[str] = []
|
|
512
|
+
mapped_receipts: list[str] = []
|
|
513
|
+
mapped_receipt_bases: list[str] = []
|
|
514
|
+
expected_remote: str | None = None
|
|
515
|
+
expected_ref: str | None = None
|
|
516
|
+
previous_time: datetime | None = None
|
|
517
|
+
base_changes = 0
|
|
518
|
+
second_drift_sequence: int | None = None
|
|
519
|
+
for index, item in enumerate(attestations, start=1):
|
|
520
|
+
row = exact_object(
|
|
521
|
+
item,
|
|
522
|
+
{
|
|
523
|
+
"sequence",
|
|
524
|
+
"remote",
|
|
525
|
+
"ref",
|
|
526
|
+
"sha",
|
|
527
|
+
"confirmed_at",
|
|
528
|
+
"controller_receipt_sha256",
|
|
529
|
+
"evidence_file",
|
|
530
|
+
"evidence_sha256",
|
|
531
|
+
},
|
|
532
|
+
f"base_attestations[{index - 1}]",
|
|
533
|
+
)
|
|
534
|
+
if row["sequence"] != index or type(row["sequence"]) is not int:
|
|
535
|
+
fail("base attestation sequence must be contiguous from 1")
|
|
536
|
+
remote = bounded_text(row["remote"], f"base_attestations[{index - 1}].remote", 200)
|
|
537
|
+
ref = bounded_text(row["ref"], f"base_attestations[{index - 1}].ref", 300)
|
|
538
|
+
if expected_remote is None:
|
|
539
|
+
expected_remote, expected_ref = remote, ref
|
|
540
|
+
elif remote != expected_remote or ref != expected_ref:
|
|
541
|
+
fail("base attestations must use the same remote and ref")
|
|
542
|
+
base_sha = object_id(row["sha"], f"base_attestations[{index - 1}].sha")
|
|
543
|
+
if base_shas and base_sha != base_shas[-1]:
|
|
544
|
+
base_changes += 1
|
|
545
|
+
if base_changes == 2:
|
|
546
|
+
second_drift_sequence = index
|
|
547
|
+
confirmed = parse_rfc3339(
|
|
548
|
+
row["confirmed_at"], f"base_attestations[{index - 1}].confirmed_at"
|
|
549
|
+
)
|
|
550
|
+
if previous_time is not None and confirmed <= previous_time:
|
|
551
|
+
fail("base attestation confirmed_at values must strictly increase")
|
|
552
|
+
previous_time = confirmed
|
|
553
|
+
raw = load_sibling(
|
|
554
|
+
ledger_dir,
|
|
555
|
+
file_value=row["evidence_file"],
|
|
556
|
+
digest_value=row["evidence_sha256"],
|
|
557
|
+
label=f"base attestation {index} evidence",
|
|
558
|
+
maximum=MAX_EVIDENCE_BYTES,
|
|
559
|
+
)
|
|
560
|
+
expected_raw = f"{base_sha}\t{ref}\n".encode()
|
|
561
|
+
if raw != expected_raw:
|
|
562
|
+
fail(f"base attestation {index} evidence does not contain canonical ls-remote output")
|
|
563
|
+
receipt_hash_value = row["controller_receipt_sha256"]
|
|
564
|
+
if receipt_hash_value is not None:
|
|
565
|
+
if second_drift_sequence is not None:
|
|
566
|
+
fail(
|
|
567
|
+
"a controller receipt is mapped at or after the second base drift"
|
|
568
|
+
)
|
|
569
|
+
mapped_receipts.append(
|
|
570
|
+
sha256(
|
|
571
|
+
receipt_hash_value,
|
|
572
|
+
f"base_attestations[{index - 1}].controller_receipt_sha256",
|
|
573
|
+
)
|
|
574
|
+
)
|
|
575
|
+
mapped_receipt_bases.append(base_sha)
|
|
576
|
+
base_shas.append(base_sha)
|
|
577
|
+
if mapped_receipts != receipt_hashes:
|
|
578
|
+
fail("base attestations must map every controller receipt exactly once in order")
|
|
579
|
+
second_drift = base_changes >= 2
|
|
580
|
+
if second_drift != (closeout == "baseline_race"):
|
|
581
|
+
fail("the second base drift must terminate as baseline_race, and baseline_race requires two ordered base changes")
|
|
582
|
+
if (
|
|
583
|
+
closeout != "baseline_race"
|
|
584
|
+
and mapped_receipt_bases[-1] != base_shas[-1]
|
|
585
|
+
):
|
|
586
|
+
fail(
|
|
587
|
+
"the final controller receipt does not consume the latest attested base"
|
|
588
|
+
)
|
|
589
|
+
return base_changes
|
|
590
|
+
|
|
591
|
+
|
|
592
|
+
def validate_disposition_evidence(
|
|
593
|
+
ledger_dir: Path,
|
|
594
|
+
*,
|
|
595
|
+
file_value: object,
|
|
596
|
+
digest_value: object,
|
|
597
|
+
candidate: str,
|
|
598
|
+
receipt_hash: str,
|
|
599
|
+
finding_hash: str,
|
|
600
|
+
disposition: str,
|
|
601
|
+
class_key: str,
|
|
602
|
+
occurrence_index: int,
|
|
603
|
+
class_pairs: list[tuple[str, str]],
|
|
604
|
+
unresolved_pairs: set[tuple[str, str]],
|
|
605
|
+
) -> list[tuple[str, str]]:
|
|
606
|
+
label = f"finding class {class_key} disposition evidence {occurrence_index + 1}"
|
|
607
|
+
raw = load_sibling(
|
|
608
|
+
ledger_dir,
|
|
609
|
+
file_value=file_value,
|
|
610
|
+
digest_value=digest_value,
|
|
611
|
+
label=label,
|
|
612
|
+
maximum=MAX_EVIDENCE_BYTES,
|
|
613
|
+
)
|
|
614
|
+
evidence = exact_object(
|
|
615
|
+
decode_json(raw, label=label),
|
|
616
|
+
{
|
|
617
|
+
"schema_version",
|
|
618
|
+
"candidate_sha256",
|
|
619
|
+
"receipt_sha256",
|
|
620
|
+
"finding_sha256",
|
|
621
|
+
"disposition",
|
|
622
|
+
"resolves_occurrences",
|
|
623
|
+
"evidence",
|
|
624
|
+
},
|
|
625
|
+
label,
|
|
626
|
+
)
|
|
627
|
+
if evidence["schema_version"] != 1 or type(evidence["schema_version"]) is not int:
|
|
628
|
+
fail(f"{label}.schema_version must be 1")
|
|
629
|
+
if sha256(evidence["candidate_sha256"], f"{label}.candidate_sha256") != candidate:
|
|
630
|
+
fail(f"{label} is stale for the current candidate")
|
|
631
|
+
if sha256(evidence["receipt_sha256"], f"{label}.receipt_sha256") != receipt_hash:
|
|
632
|
+
fail(f"{label} does not bind its controller receipt")
|
|
633
|
+
if sha256(evidence["finding_sha256"], f"{label}.finding_sha256") != finding_hash:
|
|
634
|
+
fail(f"{label} does not bind its controller finding")
|
|
635
|
+
if bounded_text(evidence["disposition"], f"{label}.disposition", 80) != disposition:
|
|
636
|
+
fail(f"{label} does not bind its disposition")
|
|
637
|
+
|
|
638
|
+
refs = evidence["resolves_occurrences"]
|
|
639
|
+
if not isinstance(refs, list) or not refs:
|
|
640
|
+
fail(f"{label}.resolves_occurrences must be a non-empty array")
|
|
641
|
+
resolved_pairs: list[tuple[str, str]] = []
|
|
642
|
+
seen_resolved: set[tuple[str, str]] = set()
|
|
643
|
+
for resolved_index, value in enumerate(refs):
|
|
644
|
+
resolved = exact_object(
|
|
645
|
+
value,
|
|
646
|
+
{"receipt_sha256", "finding_sha256"},
|
|
647
|
+
f"{label}.resolves_occurrences[{resolved_index}]",
|
|
648
|
+
)
|
|
649
|
+
pair = (
|
|
650
|
+
sha256(
|
|
651
|
+
resolved["receipt_sha256"],
|
|
652
|
+
f"{label}.resolves_occurrences[{resolved_index}].receipt_sha256",
|
|
653
|
+
),
|
|
654
|
+
sha256(
|
|
655
|
+
resolved["finding_sha256"],
|
|
656
|
+
f"{label}.resolves_occurrences[{resolved_index}].finding_sha256",
|
|
657
|
+
),
|
|
658
|
+
)
|
|
659
|
+
if pair in seen_resolved:
|
|
660
|
+
fail(f"{label} repeats a resolved occurrence")
|
|
661
|
+
if pair not in class_pairs:
|
|
662
|
+
fail(f"{label} resolves an occurrence outside the current class prefix")
|
|
663
|
+
if pair not in unresolved_pairs:
|
|
664
|
+
fail(f"{label} resolves an occurrence that is not currently unresolved")
|
|
665
|
+
seen_resolved.add(pair)
|
|
666
|
+
resolved_pairs.append(pair)
|
|
667
|
+
current_pair = (receipt_hash, finding_hash)
|
|
668
|
+
if current_pair not in seen_resolved:
|
|
669
|
+
fail(f"{label} must resolve its current occurrence")
|
|
670
|
+
expected_order = [pair for pair in class_pairs if pair in seen_resolved]
|
|
671
|
+
if resolved_pairs != expected_order:
|
|
672
|
+
fail(f"{label}.resolves_occurrences must follow finding class order")
|
|
673
|
+
|
|
674
|
+
evidence_items = evidence["evidence"]
|
|
675
|
+
if not isinstance(evidence_items, list) or not evidence_items:
|
|
676
|
+
fail(f"{label}.evidence must be a non-empty array")
|
|
677
|
+
normalized_evidence = [
|
|
678
|
+
bounded_text(value, f"{label}.evidence[{index}]", 1000)
|
|
679
|
+
for index, value in enumerate(evidence_items)
|
|
680
|
+
]
|
|
681
|
+
if len(normalized_evidence) != len(set(normalized_evidence)):
|
|
682
|
+
fail(f"{label}.evidence contains duplicates")
|
|
683
|
+
return resolved_pairs
|
|
684
|
+
|
|
685
|
+
|
|
686
|
+
def validate_finding_classes(
|
|
687
|
+
payload: dict[str, Any],
|
|
688
|
+
ledger_dir: Path,
|
|
689
|
+
receipt_findings: dict[str, list[str]],
|
|
690
|
+
candidate: str,
|
|
691
|
+
closeout: str,
|
|
692
|
+
) -> bool:
|
|
693
|
+
classes = payload["finding_classes"]
|
|
694
|
+
if not isinstance(classes, list):
|
|
695
|
+
fail("finding_classes must be an array")
|
|
696
|
+
expected_pairs = {
|
|
697
|
+
(receipt_hash, finding_hash)
|
|
698
|
+
for receipt_hash, findings in receipt_findings.items()
|
|
699
|
+
for finding_hash in findings
|
|
700
|
+
}
|
|
701
|
+
seen_pairs: set[tuple[str, str]] = set()
|
|
702
|
+
seen_keys: set[str] = set()
|
|
703
|
+
seen_predicates: dict[str, str] = {}
|
|
704
|
+
any_unresolved = False
|
|
705
|
+
finding_order = {
|
|
706
|
+
(receipt_hash, finding_hash): (receipt_index, finding_index)
|
|
707
|
+
for receipt_index, (receipt_hash, findings) in enumerate(receipt_findings.items())
|
|
708
|
+
for finding_index, finding_hash in enumerate(findings)
|
|
709
|
+
}
|
|
710
|
+
for class_index, item in enumerate(classes):
|
|
711
|
+
row = exact_object(
|
|
712
|
+
item,
|
|
713
|
+
{"key", "root_cause_predicate", "affected_surface", "occurrences", "authoritative_sweep"},
|
|
714
|
+
f"finding_classes[{class_index}]",
|
|
715
|
+
)
|
|
716
|
+
key = bounded_text(row["key"], f"finding_classes[{class_index}].key", 120)
|
|
717
|
+
if key in seen_keys:
|
|
718
|
+
fail(f"duplicate finding class key: {key}")
|
|
719
|
+
seen_keys.add(key)
|
|
720
|
+
predicate = bounded_text(
|
|
721
|
+
row["root_cause_predicate"],
|
|
722
|
+
f"finding_classes[{class_index}].root_cause_predicate",
|
|
723
|
+
2000,
|
|
724
|
+
)
|
|
725
|
+
normalized_predicate = " ".join(predicate.casefold().split())
|
|
726
|
+
prior_key = seen_predicates.get(normalized_predicate)
|
|
727
|
+
if prior_key is not None:
|
|
728
|
+
fail(
|
|
729
|
+
"duplicate normalized root_cause_predicate across finding classes: "
|
|
730
|
+
f"{prior_key}, {key}"
|
|
731
|
+
)
|
|
732
|
+
seen_predicates[normalized_predicate] = key
|
|
733
|
+
bounded_text(
|
|
734
|
+
row["affected_surface"], f"finding_classes[{class_index}].affected_surface", 500
|
|
735
|
+
)
|
|
736
|
+
occurrences = row["occurrences"]
|
|
737
|
+
if not isinstance(occurrences, list) or not occurrences:
|
|
738
|
+
fail(f"finding_classes[{class_index}].occurrences must be non-empty")
|
|
739
|
+
class_order: list[tuple[int, int]] = []
|
|
740
|
+
class_pairs: list[tuple[str, str]] = []
|
|
741
|
+
unresolved_pairs: set[tuple[str, str]] = set()
|
|
742
|
+
human_decision_pairs: set[tuple[str, str]] = set()
|
|
743
|
+
for occurrence_index, occurrence in enumerate(occurrences):
|
|
744
|
+
occurrence_label = (
|
|
745
|
+
f"finding_classes[{class_index}].occurrences[{occurrence_index}]"
|
|
746
|
+
)
|
|
747
|
+
if not isinstance(occurrence, dict):
|
|
748
|
+
fail(f"{occurrence_label} must be an object")
|
|
749
|
+
disposition = bounded_text(
|
|
750
|
+
occurrence.get("disposition"),
|
|
751
|
+
f"{occurrence_label}.disposition",
|
|
752
|
+
80,
|
|
753
|
+
)
|
|
754
|
+
if disposition not in DISPOSITIONS:
|
|
755
|
+
fail(f"finding class {key} has an unknown disposition")
|
|
756
|
+
occurrence_fields = {
|
|
757
|
+
"receipt_sha256",
|
|
758
|
+
"finding_sha256",
|
|
759
|
+
"disposition",
|
|
760
|
+
}
|
|
761
|
+
if disposition in RESOLVED_DISPOSITIONS:
|
|
762
|
+
occurrence_fields |= {
|
|
763
|
+
"disposition_evidence_file",
|
|
764
|
+
"disposition_evidence_sha256",
|
|
765
|
+
}
|
|
766
|
+
occurrence_row = exact_object(
|
|
767
|
+
occurrence,
|
|
768
|
+
occurrence_fields,
|
|
769
|
+
occurrence_label,
|
|
770
|
+
)
|
|
771
|
+
receipt_hash = sha256(
|
|
772
|
+
occurrence_row["receipt_sha256"],
|
|
773
|
+
f"finding_classes[{class_index}].occurrences[{occurrence_index}].receipt_sha256",
|
|
774
|
+
)
|
|
775
|
+
finding_hash = sha256(
|
|
776
|
+
occurrence_row["finding_sha256"],
|
|
777
|
+
f"finding_classes[{class_index}].occurrences[{occurrence_index}].finding_sha256",
|
|
778
|
+
)
|
|
779
|
+
pair = (receipt_hash, finding_hash)
|
|
780
|
+
if pair not in expected_pairs:
|
|
781
|
+
fail(f"finding class {key} occurrence does not identify a finding in its controller receipt")
|
|
782
|
+
if pair in seen_pairs:
|
|
783
|
+
fail(f"controller finding {finding_hash} is classified more than once")
|
|
784
|
+
seen_pairs.add(pair)
|
|
785
|
+
class_order.append(finding_order[pair])
|
|
786
|
+
class_pairs.append(pair)
|
|
787
|
+
unresolved_pairs.add(pair)
|
|
788
|
+
if disposition == "needs_human_decision":
|
|
789
|
+
human_decision_pairs.add(pair)
|
|
790
|
+
if disposition in RESOLVED_DISPOSITIONS:
|
|
791
|
+
resolved_pairs = validate_disposition_evidence(
|
|
792
|
+
ledger_dir,
|
|
793
|
+
file_value=occurrence_row["disposition_evidence_file"],
|
|
794
|
+
digest_value=occurrence_row["disposition_evidence_sha256"],
|
|
795
|
+
candidate=candidate,
|
|
796
|
+
receipt_hash=receipt_hash,
|
|
797
|
+
finding_hash=finding_hash,
|
|
798
|
+
disposition=disposition,
|
|
799
|
+
class_key=key,
|
|
800
|
+
occurrence_index=occurrence_index,
|
|
801
|
+
class_pairs=class_pairs,
|
|
802
|
+
unresolved_pairs=unresolved_pairs,
|
|
803
|
+
)
|
|
804
|
+
if human_decision_pairs.intersection(resolved_pairs):
|
|
805
|
+
fail(
|
|
806
|
+
f"finding class {key} local evidence cannot resolve a "
|
|
807
|
+
"needs_human_decision occurrence"
|
|
808
|
+
)
|
|
809
|
+
unresolved_pairs.difference_update(resolved_pairs)
|
|
810
|
+
if class_order != sorted(class_order):
|
|
811
|
+
fail(f"finding class {key} occurrences are not in controller receipt order")
|
|
812
|
+
any_unresolved |= bool(unresolved_pairs)
|
|
813
|
+
|
|
814
|
+
sweep = row["authoritative_sweep"]
|
|
815
|
+
if len(occurrences) >= 3:
|
|
816
|
+
sweep_row = exact_object(
|
|
817
|
+
sweep,
|
|
818
|
+
{
|
|
819
|
+
"candidate_sha256",
|
|
820
|
+
"manifest_file",
|
|
821
|
+
"manifest_sha256",
|
|
822
|
+
"searched_set",
|
|
823
|
+
"unmatched_instances",
|
|
824
|
+
},
|
|
825
|
+
f"finding_classes[{class_index}].authoritative_sweep",
|
|
826
|
+
)
|
|
827
|
+
if sha256(
|
|
828
|
+
sweep_row["candidate_sha256"],
|
|
829
|
+
f"finding_classes[{class_index}].authoritative_sweep.candidate_sha256",
|
|
830
|
+
) != candidate:
|
|
831
|
+
fail(f"finding class {key} sweep is stale for the current candidate")
|
|
832
|
+
searched = sweep_row["searched_set"]
|
|
833
|
+
if not isinstance(searched, list) or not searched:
|
|
834
|
+
fail(f"finding class {key} third occurrence requires a non-empty searched_set")
|
|
835
|
+
normalized = [
|
|
836
|
+
bounded_text(value, f"finding class {key} searched_set", 500)
|
|
837
|
+
for value in searched
|
|
838
|
+
]
|
|
839
|
+
if len(normalized) != len(set(normalized)):
|
|
840
|
+
fail(f"finding class {key} searched_set contains duplicates")
|
|
841
|
+
unmatched = sweep_row["unmatched_instances"]
|
|
842
|
+
if type(unmatched) is not int or unmatched < 0:
|
|
843
|
+
fail(f"finding class {key} unmatched_instances must be a non-negative integer")
|
|
844
|
+
manifest_raw = load_sibling(
|
|
845
|
+
ledger_dir,
|
|
846
|
+
file_value=sweep_row["manifest_file"],
|
|
847
|
+
digest_value=sweep_row["manifest_sha256"],
|
|
848
|
+
label=f"finding class {key} sweep manifest",
|
|
849
|
+
maximum=MAX_EVIDENCE_BYTES,
|
|
850
|
+
)
|
|
851
|
+
manifest = exact_object(
|
|
852
|
+
decode_json(manifest_raw, label=f"finding class {key} sweep manifest"),
|
|
853
|
+
{"schema_version", "candidate_sha256", "searched_set", "unmatched_instances"},
|
|
854
|
+
f"finding class {key} sweep manifest",
|
|
855
|
+
)
|
|
856
|
+
if manifest["schema_version"] != 1 or type(manifest["schema_version"]) is not int:
|
|
857
|
+
fail(f"finding class {key} sweep manifest schema_version must be 1")
|
|
858
|
+
if sha256(manifest["candidate_sha256"], f"finding class {key} sweep manifest candidate") != candidate:
|
|
859
|
+
fail(f"finding class {key} sweep manifest is stale")
|
|
860
|
+
if manifest["searched_set"] != normalized:
|
|
861
|
+
fail(f"finding class {key} searched_set does not match its sweep manifest")
|
|
862
|
+
unmatched_items = manifest["unmatched_instances"]
|
|
863
|
+
if not isinstance(unmatched_items, list):
|
|
864
|
+
fail(f"finding class {key} sweep manifest unmatched_instances must be an array")
|
|
865
|
+
normalized_unmatched = [
|
|
866
|
+
bounded_text(value, f"finding class {key} unmatched instance", 500)
|
|
867
|
+
for value in unmatched_items
|
|
868
|
+
]
|
|
869
|
+
if len(normalized_unmatched) != len(set(normalized_unmatched)):
|
|
870
|
+
fail(f"finding class {key} sweep manifest repeats an unmatched instance")
|
|
871
|
+
if len(normalized_unmatched) != unmatched:
|
|
872
|
+
fail(f"finding class {key} unmatched_instances count does not match its sweep manifest")
|
|
873
|
+
if closeout == "ready_for_human_decision" and unmatched != 0:
|
|
874
|
+
fail(f"finding class {key} ready sweep must report zero unmatched instances")
|
|
875
|
+
elif sweep is not None:
|
|
876
|
+
fail(f"finding class {key} has a sweep before its third occurrence")
|
|
877
|
+
if seen_pairs != expected_pairs:
|
|
878
|
+
fail("finding_classes omits controller findings")
|
|
879
|
+
return any_unresolved
|
|
880
|
+
|
|
881
|
+
|
|
882
|
+
def validate(payload: dict[str, Any], ledger_dir: Path) -> tuple[str, int, int]:
|
|
883
|
+
if payload["schema_version"] != 3 or type(payload["schema_version"]) is not int:
|
|
884
|
+
fail("schema_version must be 3")
|
|
885
|
+
candidate = sha256(payload["candidate_sha256"], "candidate_sha256")
|
|
886
|
+
closeout = bounded_text(payload["closeout_state"], "closeout_state", 80)
|
|
887
|
+
if closeout not in TERMINAL_STATES:
|
|
888
|
+
fail("closeout_state is not an extraction terminal state")
|
|
889
|
+
|
|
890
|
+
receipts, receipt_hashes, receipt_findings, chain_id, scope_hash = (
|
|
891
|
+
validate_controller_receipts(payload, ledger_dir)
|
|
892
|
+
)
|
|
893
|
+
if receipts[-1]["candidate_sha256"] != candidate:
|
|
894
|
+
fail("candidate_sha256 must equal the final controller receipt candidate")
|
|
895
|
+
completion = validate_completion_receipt(
|
|
896
|
+
payload["completion_receipt"],
|
|
897
|
+
ledger_dir,
|
|
898
|
+
receipts,
|
|
899
|
+
receipt_hashes,
|
|
900
|
+
chain_id,
|
|
901
|
+
scope_hash,
|
|
902
|
+
)
|
|
903
|
+
base_changes = validate_base_attestations(
|
|
904
|
+
payload, ledger_dir, receipt_hashes, closeout
|
|
905
|
+
)
|
|
906
|
+
any_unresolved = validate_finding_classes(
|
|
907
|
+
payload, ledger_dir, receipt_findings, candidate, closeout
|
|
908
|
+
)
|
|
909
|
+
|
|
910
|
+
delta = payload["unreviewed_delta"]
|
|
911
|
+
if not isinstance(delta, list):
|
|
912
|
+
fail("unreviewed_delta must be an array")
|
|
913
|
+
for index, value in enumerate(delta):
|
|
914
|
+
bounded_text(value, f"unreviewed_delta[{index}]", 500)
|
|
915
|
+
|
|
916
|
+
if closeout == "ready_for_human_decision" and completion is None:
|
|
917
|
+
fail("ready_for_human_decision requires a completion receipt")
|
|
918
|
+
if closeout == "continuation_authorization_required" and completion is not None:
|
|
919
|
+
fail("continuation_authorization_required cannot carry a completion receipt")
|
|
920
|
+
if closeout == "baseline_race" and completion is not None:
|
|
921
|
+
fail("baseline_race cannot carry a completion receipt")
|
|
922
|
+
|
|
923
|
+
controller_state = bounded_text(
|
|
924
|
+
payload["controller_review_state"], "controller_review_state", 80
|
|
925
|
+
)
|
|
926
|
+
if controller_state not in KNOWN_REVIEW_STATES:
|
|
927
|
+
fail(f"unknown controller review_state {controller_state}")
|
|
928
|
+
derived_state = completion["review_state"] if completion is not None else receipts[-1]["review_state"]
|
|
929
|
+
if controller_state != derived_state:
|
|
930
|
+
fail("controller_review_state does not match the referenced final receipt")
|
|
931
|
+
|
|
932
|
+
if closeout == "ready_for_human_decision":
|
|
933
|
+
if len(receipts) < 2:
|
|
934
|
+
fail("ready_for_human_decision ready requires at least one tracked challenge")
|
|
935
|
+
if completion is None:
|
|
936
|
+
fail("ready_for_human_decision requires a completion receipt")
|
|
937
|
+
if completion["candidate_sha256"] != candidate or receipts[-1]["candidate_sha256"] != candidate:
|
|
938
|
+
fail("ready_for_human_decision completion receipt must bind the exact final candidate")
|
|
939
|
+
if any_unresolved or delta:
|
|
940
|
+
fail("ready_for_human_decision requires no unresolved finding occurrence and no unreviewed delta")
|
|
941
|
+
elif closeout == "continuation_authorization_required":
|
|
942
|
+
final = receipts[-1]
|
|
943
|
+
if (
|
|
944
|
+
len(receipts) != 3
|
|
945
|
+
or final.get("status") != "findings"
|
|
946
|
+
or final.get("review_state") != "post_review_budget"
|
|
947
|
+
or final.get("human_decision_required") is not True
|
|
948
|
+
):
|
|
949
|
+
fail("continuation_authorization_required requires round 3 findings in post_review_budget")
|
|
950
|
+
elif closeout == "baseline_race" and not delta:
|
|
951
|
+
fail("baseline_race requires a non-empty unreviewed_delta")
|
|
952
|
+
|
|
953
|
+
return closeout, len(payload["finding_classes"]), base_changes
|
|
954
|
+
|
|
955
|
+
|
|
956
|
+
def main() -> int:
|
|
957
|
+
parser = argparse.ArgumentParser(description=__doc__)
|
|
958
|
+
parser.add_argument("state_file", type=Path)
|
|
959
|
+
args = parser.parse_args()
|
|
960
|
+
try:
|
|
961
|
+
payload, ledger_dir = load(args.state_file)
|
|
962
|
+
state, class_count, base_changes = validate(payload, ledger_dir)
|
|
963
|
+
except StateError as exc:
|
|
964
|
+
print(f"extraction_review_state_invalid: {exc}", file=sys.stderr)
|
|
965
|
+
return 1
|
|
966
|
+
except (OSError, UnicodeError, ValueError):
|
|
967
|
+
print(
|
|
968
|
+
"extraction_review_state_invalid: local evidence read or encoding failed",
|
|
969
|
+
file=sys.stderr,
|
|
970
|
+
)
|
|
971
|
+
return 1
|
|
972
|
+
print(
|
|
973
|
+
"extraction_review_state_ok: "
|
|
974
|
+
f"closeout_state={state} finding_classes={class_count} base_changes={base_changes}"
|
|
975
|
+
)
|
|
976
|
+
return 0
|
|
977
|
+
|
|
978
|
+
|
|
979
|
+
if __name__ == "__main__":
|
|
980
|
+
raise SystemExit(main())
|