agent-bios 0.12.0 → 0.13.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/claude/guides/coding-staged-workflow.md +1 -0
- package/claude/guides/review-defect-criteria.md +214 -0
- package/claude/guides/review-request.md +15 -1
- package/codex/guides/coding-staged-workflow.md +1 -0
- package/codex/guides/review-defect-criteria.md +214 -0
- package/codex/guides/review-request.md +15 -1
- package/compose/assemble.py +7 -0
- package/compose/domains.json +1 -0
- package/launch/agent-launch.py +953 -180
- package/package.json +2 -2
- package/provenance.json +1 -1
package/launch/agent-launch.py
CHANGED
|
@@ -937,6 +937,19 @@ SEVERITY_NAME_RULE = (
|
|
|
937
937
|
# forbidding the labels that trigger it, which is why `<`, `-` and `=` stay registrable.
|
|
938
938
|
SEVERITY_ARROW = " => "
|
|
939
939
|
SEVERITY_CLAUSE_MARKER = "; severities "
|
|
940
|
+
# The criterion discipline clause. A CONSTANT, not a slot: the criterion's content is
|
|
941
|
+
# per-review and lives in the packet (which packet_sha256 binds), while the contract is
|
|
942
|
+
# per-launch and golden-pinned — so what renders here is only the discipline, identical
|
|
943
|
+
# for every method and every criterion. Placed BEFORE the severity clause in the row,
|
|
944
|
+
# because the severity clause is parsed as everything after its marker.
|
|
945
|
+
CRITERION_CLAUSE_MARKER = "; criterion "
|
|
946
|
+
CRITERION_CLAUSE = (
|
|
947
|
+
f"{CRITERION_CLAUSE_MARKER}declared in the packet: compile it with "
|
|
948
|
+
f"--compile-criterion, prefer a dispatch whose host schema flag probed present "
|
|
949
|
+
f"(--check-schema-flag), emit receipts under REVIEW_CRITERION_SCHEMA so emission "
|
|
950
|
+
f"refuses unclassified findings, and fold returned rows against the declared enum "
|
|
951
|
+
f"without guessing a class"
|
|
952
|
+
)
|
|
940
953
|
# The panel that carries corpus/registration text, and the scrolling body that holds it.
|
|
941
954
|
# Named once so the screen, its CSS, its key bindings and the check that drives them cannot
|
|
942
955
|
# drift apart.
|
|
@@ -1127,6 +1140,17 @@ class ReviewMechanism:
|
|
|
1127
1140
|
def parse_review_method(method_id: str, raw: Any, context: str) -> ReviewMethod:
|
|
1128
1141
|
if not isinstance(raw, dict):
|
|
1129
1142
|
raise LaunchError(f"{context} must be a table")
|
|
1143
|
+
# The method ID is the one authored value core PREFIXES to the rendered row, so the
|
|
1144
|
+
# formatted-body marker checks never see it — a registry key carrying a clause
|
|
1145
|
+
# marker forged a second decodable clause beside core's (criterion round, #1; the
|
|
1146
|
+
# same door round 20 #5 opened for the plan marker, closed there on the whole row).
|
|
1147
|
+
for marker in (SEVERITY_CLAUSE_MARKER, CONTROLS_CLAUSE_MARKER, CRITERION_CLAUSE_MARKER):
|
|
1148
|
+
if marker in method_id:
|
|
1149
|
+
raise LaunchError(
|
|
1150
|
+
f"{context}: the method id contains {marker!r}, which core owns for a "
|
|
1151
|
+
f"rendered clause; an id carrying it would forge a second clause in "
|
|
1152
|
+
f"every row it prefixes"
|
|
1153
|
+
)
|
|
1130
1154
|
unknown = sorted(set(raw) - METHOD_KEYS)
|
|
1131
1155
|
if unknown:
|
|
1132
1156
|
raise LaunchError(f"{context}: unknown method key(s): {', '.join(unknown)}")
|
|
@@ -1307,6 +1331,12 @@ def validate_instruction_slots(template: str, context: str) -> set:
|
|
|
1307
1331
|
f"{context}: contains {CONTROLS_CLAUSE_MARKER!r}, which core appends from the "
|
|
1308
1332
|
f"method's declared controls; a second clause would contradict it"
|
|
1309
1333
|
)
|
|
1334
|
+
if CRITERION_CLAUSE_MARKER in template:
|
|
1335
|
+
raise LaunchError(
|
|
1336
|
+
f"{context}: contains {CRITERION_CLAUSE_MARKER!r}, which core appends when the "
|
|
1337
|
+
f"plan declares a criterion-disciplined review; a second clause would "
|
|
1338
|
+
f"contradict the plan's toggle"
|
|
1339
|
+
)
|
|
1310
1340
|
refuse_plan_marker(template, context)
|
|
1311
1341
|
return used
|
|
1312
1342
|
|
|
@@ -1513,9 +1543,16 @@ def derive_review_mechanism(
|
|
|
1513
1543
|
)
|
|
1514
1544
|
|
|
1515
1545
|
|
|
1516
|
-
def render_review_method(
|
|
1546
|
+
def render_review_method(
|
|
1547
|
+
method: ReviewMethod, mechanism: ReviewMechanism, criterion: bool = False
|
|
1548
|
+
) -> str:
|
|
1517
1549
|
"""The generic renderer. Every method — shipped or third-party — reaches the
|
|
1518
|
-
contract through this one function, with the exact seat already resolved.
|
|
1550
|
+
contract through this one function, with the exact seat already resolved.
|
|
1551
|
+
|
|
1552
|
+
`criterion` is the plan's toggle, threaded rather than read from anywhere global:
|
|
1553
|
+
when the preset declares a criterion-disciplined review, core appends the one
|
|
1554
|
+
discipline clause to every row, method-blind, the way the severity translation is
|
|
1555
|
+
appended — an author never writes it and a slot never carries it."""
|
|
1519
1556
|
binding = mechanism.binding
|
|
1520
1557
|
slots = {
|
|
1521
1558
|
"command": mechanism.command or "",
|
|
@@ -1561,7 +1598,8 @@ def render_review_method(method: ReviewMethod, mechanism: ReviewMechanism) -> st
|
|
|
1561
1598
|
# them as literals, and a perspective label carrying `; controls trials=1` rendered a
|
|
1562
1599
|
# second controls clause beside core's (round 19, #9). Every substitution passes here.
|
|
1563
1600
|
for marker, owner in ((CONTROLS_CLAUSE_MARKER, "the method's declared controls"),
|
|
1564
|
-
(REVIEW_PLAN_MARKER, "the contract's canonical review record")
|
|
1601
|
+
(REVIEW_PLAN_MARKER, "the contract's canonical review record"),
|
|
1602
|
+
(CRITERION_CLAUSE_MARKER, "the plan's criterion discipline")):
|
|
1565
1603
|
if marker in body:
|
|
1566
1604
|
raise LaunchError(
|
|
1567
1605
|
f"review method {method.method_id!r} renders {marker!r} in its body, which "
|
|
@@ -1570,7 +1608,7 @@ def render_review_method(method: ReviewMethod, mechanism: ReviewMechanism) -> st
|
|
|
1570
1608
|
rendered = (
|
|
1571
1609
|
f"{method.method_id}: {body} "
|
|
1572
1610
|
f"[{mechanism.shape}; {binding.model}/{binding.effort}"
|
|
1573
|
-
f"{controls_clause(method)}{severity_translation(method)}]"
|
|
1611
|
+
f"{controls_clause(method)}{criterion_clause(criterion)}{severity_translation(method)}]"
|
|
1574
1612
|
)
|
|
1575
1613
|
# The assembled ROW, after the body. The body check above catches a SLOT value and says
|
|
1576
1614
|
# so; what it cannot see is the part core itself prefixes — the METHOD ID, a user-chosen
|
|
@@ -1623,6 +1661,16 @@ def severity_vocabulary(method: ReviewMethod) -> str:
|
|
|
1623
1661
|
return ", ".join(method.severity_emits)
|
|
1624
1662
|
|
|
1625
1663
|
|
|
1664
|
+
def criterion_clause(active: bool) -> str:
|
|
1665
|
+
"""The discipline clause, or "" when the plan declares no criterion.
|
|
1666
|
+
|
|
1667
|
+
Core-owned and constant for the reason severity_translation is core-appended: a
|
|
1668
|
+
third-party method carries it without its author remembering to, and the gate can
|
|
1669
|
+
subtract the exact substring. Inside the seat bracket, BEFORE the severity clause,
|
|
1670
|
+
which is parsed as everything after its own marker."""
|
|
1671
|
+
return CRITERION_CLAUSE if active else ""
|
|
1672
|
+
|
|
1673
|
+
|
|
1626
1674
|
def severity_translation(method: ReviewMethod) -> str:
|
|
1627
1675
|
"""The clause telling the reader how to move this reviewer's severities onto the
|
|
1628
1676
|
canonical ladder, or "" when it reports on the ladder already.
|
|
@@ -1743,7 +1791,8 @@ def independence_grade(reviewer: ReviewBinding, main: ReviewBinding) -> str:
|
|
|
1743
1791
|
|
|
1744
1792
|
|
|
1745
1793
|
def _resolve_one(
|
|
1746
|
-
method: ReviewMethod, binding: ReviewBinding, main: ReviewBinding,
|
|
1794
|
+
method: ReviewMethod, binding: ReviewBinding, main: ReviewBinding,
|
|
1795
|
+
config: dict[str, Any], criterion: bool = False,
|
|
1747
1796
|
) -> ReviewMethodReport:
|
|
1748
1797
|
"""One method against the main seat. An optional method is NEVER silently rebound
|
|
1749
1798
|
to a different seat: if its mechanism cannot be derived it is dropped, and the
|
|
@@ -1823,7 +1872,7 @@ def _resolve_one(
|
|
|
1823
1872
|
return ReviewMethodReport(
|
|
1824
1873
|
method.method_id, status, grade, "; ".join(notes),
|
|
1825
1874
|
binding.model, binding.effort, binding.provider, mechanism.adapter,
|
|
1826
|
-
render_review_method(method, mechanism), method_controls(method),
|
|
1875
|
+
render_review_method(method, mechanism, criterion), method_controls(method),
|
|
1827
1876
|
tuple(mechanism.evidence),
|
|
1828
1877
|
)
|
|
1829
1878
|
|
|
@@ -1844,7 +1893,7 @@ def best_review_grade(rows) -> str:
|
|
|
1844
1893
|
|
|
1845
1894
|
def resolve_composable_review(
|
|
1846
1895
|
review: ReviewPlan, main: ReviewBinding, config: dict[str, Any],
|
|
1847
|
-
methods: dict[str, ReviewMethod],
|
|
1896
|
+
methods: dict[str, ReviewMethod], criterion: bool = False,
|
|
1848
1897
|
) -> ReviewReport:
|
|
1849
1898
|
"""Base panel plus the method map, each graded against the main seat.
|
|
1850
1899
|
|
|
@@ -1858,7 +1907,7 @@ def resolve_composable_review(
|
|
|
1858
1907
|
if panel is None:
|
|
1859
1908
|
raise LaunchError(f"the {PANEL_METHOD} method is required and is not registered")
|
|
1860
1909
|
base_binding = review.base_binding or main
|
|
1861
|
-
base = _resolve_one(panel, base_binding, main, config)
|
|
1910
|
+
base = _resolve_one(panel, base_binding, main, config, criterion)
|
|
1862
1911
|
# Authored-but-unsatisfiable is a fallback too, not just an undeliverable mechanism.
|
|
1863
1912
|
# Without this the §1 rule below was unreachable for the commonest case — a preset
|
|
1864
1913
|
# naming a provider the profile has no host for never got here at all, because the
|
|
@@ -1866,7 +1915,7 @@ def resolve_composable_review(
|
|
|
1866
1915
|
fell_back = review.base_unseated is not None
|
|
1867
1916
|
if base.status == STATUS_DROPPED and review.base_binding is not None:
|
|
1868
1917
|
# Try the same floor on the main's own seat before giving up on review.
|
|
1869
|
-
base = _resolve_one(panel, main, main, config)
|
|
1918
|
+
base = _resolve_one(panel, main, main, config, criterion)
|
|
1870
1919
|
fell_back = True
|
|
1871
1920
|
if fell_back and base.status != STATUS_DROPPED:
|
|
1872
1921
|
detail = (
|
|
@@ -1905,7 +1954,7 @@ def resolve_composable_review(
|
|
|
1905
1954
|
if unseated
|
|
1906
1955
|
else "no binding: the method names no verifier seat"))
|
|
1907
1956
|
continue
|
|
1908
|
-
rows.append(_resolve_one(method, binding, main, config))
|
|
1957
|
+
rows.append(_resolve_one(method, binding, main, config, criterion))
|
|
1909
1958
|
return ReviewReport(base, tuple(rows), best_review_grade((base, *rows)))
|
|
1910
1959
|
|
|
1911
1960
|
|
|
@@ -2375,6 +2424,12 @@ RECEIPT_KEYS = frozenset({
|
|
|
2375
2424
|
"provider", "model", "effort", "exit_status", "passes", "ordering_seed", "swap_group",
|
|
2376
2425
|
# What the tool reported back, keyed by the field names its offer declared.
|
|
2377
2426
|
"evidence",
|
|
2427
|
+
# The digest of the compiled criterion schema the emission validated this result
|
|
2428
|
+
# against — present ONLY on a schema-route dispatch (REVIEW_CRITERION_SCHEMA was
|
|
2429
|
+
# set), read by --verify-receipts, which recompiles the packet's criterion and
|
|
2430
|
+
# refuses a mismatch. Absent on a prose-route dispatch, which is disclosed, never
|
|
2431
|
+
# refused: host schema capability is per-machine and not retroactively provable.
|
|
2432
|
+
"criterion_schema_sha256",
|
|
2378
2433
|
})
|
|
2379
2434
|
# The BUNDLE's grammar, closed for the reason the receipt's is. The reader took its four
|
|
2380
2435
|
# anchors and ignored everything else, so a key it does not know rode along unjudged and
|
|
@@ -2460,6 +2515,16 @@ def _receipt_structure_reason(receipt: Any) -> str:
|
|
|
2460
2515
|
f"records exit_status as {type(status).__name__}, not an integer — a boolean is "
|
|
2461
2516
|
f"not an exit status, and JSON false is not exit 0"
|
|
2462
2517
|
)
|
|
2518
|
+
# Form here, in the door the adjudicator AND the fold share, so a folded pair of
|
|
2519
|
+
# matching non-hashes cannot agree its way past the per-receipt check.
|
|
2520
|
+
if "criterion_schema_sha256" in receipt and not is_sha256_hex(
|
|
2521
|
+
receipt["criterion_schema_sha256"]
|
|
2522
|
+
):
|
|
2523
|
+
return (
|
|
2524
|
+
f"records criterion_schema_sha256={receipt['criterion_schema_sha256']!r}, "
|
|
2525
|
+
f"which is not a SHA-256 hash — nothing was hashed, so no schema validated "
|
|
2526
|
+
f"this result"
|
|
2527
|
+
)
|
|
2463
2528
|
return ""
|
|
2464
2529
|
|
|
2465
2530
|
|
|
@@ -2496,7 +2561,7 @@ def _raw_receipt_reason(receipt: Any) -> str:
|
|
|
2496
2561
|
|
|
2497
2562
|
def _receipt_reason(
|
|
2498
2563
|
receipt: Any, row: "ReviewMethodReport | None", bundle: dict, seen: set,
|
|
2499
|
-
controls: dict, required_evidence: tuple = (),
|
|
2564
|
+
controls: dict, required_evidence: tuple = (), criterion_digest: "str | None" = None,
|
|
2500
2565
|
) -> str:
|
|
2501
2566
|
"""Why this receipt cannot be credited, or "" when it can.
|
|
2502
2567
|
|
|
@@ -2545,6 +2610,22 @@ def _receipt_reason(
|
|
|
2545
2610
|
)
|
|
2546
2611
|
if packet != bundle.get("packet_sha256"):
|
|
2547
2612
|
return "hashes a different packet than the one the plan declared"
|
|
2613
|
+
# Only when the packet's criterion is in hand: the digest is recompiled from the
|
|
2614
|
+
# packet's own record, so a mismatch means the schema the host was handed was
|
|
2615
|
+
# compiled from some OTHER criterion — a stricter or looser enum than the one this
|
|
2616
|
+
# packet declares. With no packet supplied there is nothing to recompile, and the
|
|
2617
|
+
# rendering says the claim is unbound rather than treating it as proven.
|
|
2618
|
+
if (
|
|
2619
|
+
criterion_digest is not None
|
|
2620
|
+
and "criterion_schema_sha256" in receipt
|
|
2621
|
+
and receipt["criterion_schema_sha256"] != criterion_digest
|
|
2622
|
+
):
|
|
2623
|
+
return (
|
|
2624
|
+
f"was validated against a schema hashing "
|
|
2625
|
+
f"{receipt['criterion_schema_sha256']}, and recompiling this packet's "
|
|
2626
|
+
f"declared criterion yields {criterion_digest} — the schema enforced was "
|
|
2627
|
+
f"not compiled from the criterion this packet declares"
|
|
2628
|
+
)
|
|
2548
2629
|
for field in ("provider", "model", "effort"):
|
|
2549
2630
|
if receipt.get(field) != getattr(row, field):
|
|
2550
2631
|
return (
|
|
@@ -2639,7 +2720,7 @@ def _receipt_reason(
|
|
|
2639
2720
|
|
|
2640
2721
|
def verify_review_receipts(
|
|
2641
2722
|
report: ReviewReport, bundle: Any, required_controls: dict[str, dict],
|
|
2642
|
-
required_evidence: dict[str, tuple],
|
|
2723
|
+
required_evidence: dict[str, tuple], criterion_digest: "str | None" = None,
|
|
2643
2724
|
) -> tuple[ReviewReport, tuple[ReceiptVerdict, ...]]:
|
|
2644
2725
|
"""Adjudicate a review against its receipts.
|
|
2645
2726
|
|
|
@@ -2860,6 +2941,7 @@ def verify_review_receipts(
|
|
|
2860
2941
|
# The row's own snapshot, never the caller's recomputation — see the drift
|
|
2861
2942
|
# refusal above.
|
|
2862
2943
|
row.evidence if row else (),
|
|
2944
|
+
criterion_digest,
|
|
2863
2945
|
)
|
|
2864
2946
|
if not reason and isinstance(receipt.get("dispatch_id"), str):
|
|
2865
2947
|
seen.add(receipt["dispatch_id"])
|
|
@@ -2908,7 +2990,8 @@ def verify_review_receipts(
|
|
|
2908
2990
|
|
|
2909
2991
|
|
|
2910
2992
|
def render_receipt_verdicts(
|
|
2911
|
-
report: ReviewReport, verdicts: tuple[ReceiptVerdict, ...],
|
|
2993
|
+
report: ReviewReport, verdicts: tuple[ReceiptVerdict, ...],
|
|
2994
|
+
packet_bound: bool = False, criterion_note: str = "",
|
|
2912
2995
|
) -> str:
|
|
2913
2996
|
# Coverage is printed as a count, not only as a word: `partial` tells a reader the set
|
|
2914
2997
|
# was incomplete and `1/3` tells them how incomplete, which is the difference between
|
|
@@ -2952,6 +3035,10 @@ def render_receipt_verdicts(
|
|
|
2952
3035
|
" grade_derivation=claimed — the plan does not serialize the main seat, so a row's "
|
|
2953
3036
|
"independence grade is the launch's own claim and is not recomputed here"
|
|
2954
3037
|
)
|
|
3038
|
+
if criterion_note:
|
|
3039
|
+
# Empty on every non-criterion adjudication, so the rendering is byte-identical
|
|
3040
|
+
# to today's whenever the plan declared no discipline.
|
|
3041
|
+
lines.append(criterion_note)
|
|
2955
3042
|
for verdict in verdicts:
|
|
2956
3043
|
state = "ACHIEVED" if verdict.accepted else "PROPOSED"
|
|
2957
3044
|
lines.append(
|
|
@@ -2962,6 +3049,426 @@ def render_receipt_verdicts(
|
|
|
2962
3049
|
return "\n".join(lines)
|
|
2963
3050
|
|
|
2964
3051
|
|
|
3052
|
+
# ── criterion discipline (defect-criterion stage 6) ─────────────────────────
|
|
3053
|
+
# The criterion is PER-REVIEW: its content rides the packet, never the contract or the
|
|
3054
|
+
# config. What core owns is the deterministic subset — the document grammar, the
|
|
3055
|
+
# compiled findings schema, the packet record line, and the findings check that receipt
|
|
3056
|
+
# emission runs. Whether a golden is persuasive or a stop condition genuinely reachable
|
|
3057
|
+
# stays semantic and is never adjudicated here.
|
|
3058
|
+
CRITERION_RECORD_SCHEMA = "ReviewCriterion/v1"
|
|
3059
|
+
CRITERION_RECORD_MARKER = f"{CRITERION_RECORD_SCHEMA}: "
|
|
3060
|
+
# The guide's eight fields plus the two the machine subset needs named apart: the enum
|
|
3061
|
+
# and its one stop-relevant member. Closed, and every key required — the guide's own
|
|
3062
|
+
# rule is that an empty cell makes a hunch, not a criterion.
|
|
3063
|
+
CRITERION_KEYS = frozenset({
|
|
3064
|
+
"name", "observer", "defect", "classes", "stop_class", "evidence",
|
|
3065
|
+
"non_defects", "stop_condition", "misclassification_cost", "goldens",
|
|
3066
|
+
})
|
|
3067
|
+
CRITERION_GOLDEN_KEYS = frozenset({"kind", "case", "why", "date", "provenance"})
|
|
3068
|
+
CRITERION_GOLDEN_KINDS = ("positive", "negative", "boundary")
|
|
3069
|
+
# The guide's admission bar: ≥2 positive, ≥2 negative, ≥1 boundary.
|
|
3070
|
+
CRITERION_GOLDEN_MINIMUMS = {"positive": 2, "negative": 2, "boundary": 1}
|
|
3071
|
+
CRITERION_PROVENANCE = frozenset({"measured", "constructed"})
|
|
3072
|
+
|
|
3073
|
+
|
|
3074
|
+
def _utf8_encodable(value: str) -> bool:
|
|
3075
|
+
"""False for a string no UTF-8 artifact can carry — a JSON escape can smuggle a
|
|
3076
|
+
lone surrogate through json.loads, and every encode after it crashes."""
|
|
3077
|
+
try:
|
|
3078
|
+
value.encode("utf-8")
|
|
3079
|
+
except UnicodeEncodeError:
|
|
3080
|
+
return False
|
|
3081
|
+
return True
|
|
3082
|
+
|
|
3083
|
+
|
|
3084
|
+
def validate_criterion(document: Any, context: str) -> None:
|
|
3085
|
+
"""Refuse a criterion document that fails the DECIDABLE subset of the guide schema,
|
|
3086
|
+
by field name. Nothing downstream receives a half-criterion: there is no fallback
|
|
3087
|
+
to "no criterion" and no default for any cell."""
|
|
3088
|
+
if not isinstance(document, dict):
|
|
3089
|
+
raise LaunchError(f"{context} must be a JSON object")
|
|
3090
|
+
# The whole document must be UTF-8-encodable BEFORE any field check: JSON escapes
|
|
3091
|
+
# admit lone surrogates that json.loads accepts and every later encode —
|
|
3092
|
+
# the canonical record line, the compiled schema bytes — crashes on with a
|
|
3093
|
+
# traceback instead of a named refusal (criterion round 2, #0).
|
|
3094
|
+
try:
|
|
3095
|
+
json.dumps(document, ensure_ascii=False).encode("utf-8")
|
|
3096
|
+
except UnicodeEncodeError as exc:
|
|
3097
|
+
raise LaunchError(
|
|
3098
|
+
f"{context} is not UTF-8-encodable ({exc}); a lone surrogate cannot ride "
|
|
3099
|
+
f"the record line or the compiled schema"
|
|
3100
|
+
) from exc
|
|
3101
|
+
missing = sorted(CRITERION_KEYS - set(document))
|
|
3102
|
+
if missing:
|
|
3103
|
+
raise LaunchError(
|
|
3104
|
+
f"{context} is missing {', '.join(missing)}; an empty cell makes a hunch, "
|
|
3105
|
+
f"not a criterion"
|
|
3106
|
+
)
|
|
3107
|
+
unknown = sorted(set(document) - CRITERION_KEYS)
|
|
3108
|
+
if unknown:
|
|
3109
|
+
raise LaunchError(f"{context} carries unknown key(s): {', '.join(unknown)}")
|
|
3110
|
+
for field in ("name", "observer", "defect", "evidence", "stop_condition",
|
|
3111
|
+
"misclassification_cost"):
|
|
3112
|
+
value = document[field]
|
|
3113
|
+
if not isinstance(value, str) or not value.strip():
|
|
3114
|
+
raise LaunchError(f"{context}.{field} must be a non-empty string")
|
|
3115
|
+
classes = document["classes"]
|
|
3116
|
+
if (
|
|
3117
|
+
not isinstance(classes, list) or len(classes) < 2
|
|
3118
|
+
or not all(isinstance(value, str) and value for value in classes)
|
|
3119
|
+
):
|
|
3120
|
+
raise LaunchError(
|
|
3121
|
+
f"{context}.classes must list at least two non-empty class names — the "
|
|
3122
|
+
f"stop-relevant class and the relief valves that keep it honest"
|
|
3123
|
+
)
|
|
3124
|
+
if len(set(classes)) != len(classes):
|
|
3125
|
+
raise LaunchError(f"{context}.classes repeats a value")
|
|
3126
|
+
for value in classes:
|
|
3127
|
+
if value != value.strip():
|
|
3128
|
+
raise LaunchError(
|
|
3129
|
+
f"{context}.classes[{value!r}] may not carry leading or trailing "
|
|
3130
|
+
f"whitespace, which no reader can see"
|
|
3131
|
+
)
|
|
3132
|
+
stop = document["stop_class"]
|
|
3133
|
+
if not isinstance(stop, str) or stop not in classes:
|
|
3134
|
+
raise LaunchError(
|
|
3135
|
+
f"{context}.stop_class must name exactly one member of classes; "
|
|
3136
|
+
f"got {stop!r}"
|
|
3137
|
+
)
|
|
3138
|
+
non_defects = document["non_defects"]
|
|
3139
|
+
if (
|
|
3140
|
+
not isinstance(non_defects, list) or not non_defects
|
|
3141
|
+
or not all(isinstance(value, str) and value.strip() for value in non_defects)
|
|
3142
|
+
):
|
|
3143
|
+
raise LaunchError(
|
|
3144
|
+
f"{context}.non_defects must be a non-empty list of the defect-lookalikes "
|
|
3145
|
+
f"this criterion excludes"
|
|
3146
|
+
)
|
|
3147
|
+
goldens = document["goldens"]
|
|
3148
|
+
if not isinstance(goldens, list):
|
|
3149
|
+
raise LaunchError(f"{context}.goldens must be a list")
|
|
3150
|
+
counts = {kind: 0 for kind in CRITERION_GOLDEN_KINDS}
|
|
3151
|
+
for index, golden in enumerate(goldens):
|
|
3152
|
+
slot = f"{context}.goldens[{index}]"
|
|
3153
|
+
if not isinstance(golden, dict):
|
|
3154
|
+
raise LaunchError(f"{slot} must be an object")
|
|
3155
|
+
missing = sorted(CRITERION_GOLDEN_KEYS - set(golden))
|
|
3156
|
+
if missing:
|
|
3157
|
+
raise LaunchError(f"{slot} is missing {', '.join(missing)}")
|
|
3158
|
+
unknown = sorted(set(golden) - CRITERION_GOLDEN_KEYS)
|
|
3159
|
+
if unknown:
|
|
3160
|
+
raise LaunchError(f"{slot} carries unknown key(s): {', '.join(unknown)}")
|
|
3161
|
+
kind = golden["kind"]
|
|
3162
|
+
if kind not in CRITERION_GOLDEN_KINDS:
|
|
3163
|
+
raise LaunchError(
|
|
3164
|
+
f"{slot}.kind must be one of {', '.join(CRITERION_GOLDEN_KINDS)}; "
|
|
3165
|
+
f"got {kind!r}"
|
|
3166
|
+
)
|
|
3167
|
+
for field in ("case", "why"):
|
|
3168
|
+
if not isinstance(golden[field], str) or not golden[field].strip():
|
|
3169
|
+
raise LaunchError(f"{slot}.{field} must be a non-empty string")
|
|
3170
|
+
date = golden["date"]
|
|
3171
|
+
if not isinstance(date, str):
|
|
3172
|
+
raise LaunchError(f"{slot}.date must be a string")
|
|
3173
|
+
try:
|
|
3174
|
+
datetime.date.fromisoformat(date)
|
|
3175
|
+
except ValueError as exc:
|
|
3176
|
+
raise LaunchError(f"{slot}.date must be an ISO date: {exc}") from exc
|
|
3177
|
+
if golden["provenance"] not in CRITERION_PROVENANCE:
|
|
3178
|
+
raise LaunchError(
|
|
3179
|
+
f"{slot}.provenance must be measured or constructed; "
|
|
3180
|
+
f"got {golden['provenance']!r}"
|
|
3181
|
+
)
|
|
3182
|
+
counts[kind] += 1
|
|
3183
|
+
short = [
|
|
3184
|
+
f"{kind} {counts[kind]}/{minimum}"
|
|
3185
|
+
for kind, minimum in CRITERION_GOLDEN_MINIMUMS.items()
|
|
3186
|
+
if counts[kind] < minimum
|
|
3187
|
+
]
|
|
3188
|
+
if short:
|
|
3189
|
+
raise LaunchError(
|
|
3190
|
+
f"{context}.goldens falls short of the admission bar ({', '.join(short)}); "
|
|
3191
|
+
f"the bar is never weakened to admit an entry"
|
|
3192
|
+
)
|
|
3193
|
+
|
|
3194
|
+
|
|
3195
|
+
def criterion_schema(classes: list) -> dict:
|
|
3196
|
+
"""The compiled findings schema — a pure function of the enum, which is what makes
|
|
3197
|
+
`--verify-receipts` able to recompile it from the packet's record and compare
|
|
3198
|
+
digests. Both object levels reject unknown fields; `findings` may be empty."""
|
|
3199
|
+
return {
|
|
3200
|
+
"type": "object",
|
|
3201
|
+
"additionalProperties": False,
|
|
3202
|
+
"required": ["findings"],
|
|
3203
|
+
"properties": {
|
|
3204
|
+
"findings": {
|
|
3205
|
+
"type": "array",
|
|
3206
|
+
"items": {
|
|
3207
|
+
"type": "object",
|
|
3208
|
+
"additionalProperties": False,
|
|
3209
|
+
"required": ["class", "finding"],
|
|
3210
|
+
"properties": {
|
|
3211
|
+
"class": {"enum": list(classes)},
|
|
3212
|
+
"finding": {"type": "string", "minLength": 1},
|
|
3213
|
+
},
|
|
3214
|
+
},
|
|
3215
|
+
},
|
|
3216
|
+
},
|
|
3217
|
+
}
|
|
3218
|
+
|
|
3219
|
+
|
|
3220
|
+
def criterion_schema_bytes(document: dict) -> bytes:
|
|
3221
|
+
"""Canonical bytes for the compiled schema: sorted keys, no whitespace, one
|
|
3222
|
+
trailing newline. Byte-identical on every compile of the same document — proven by
|
|
3223
|
+
the double-compile control — because the receipt digest is compared against a
|
|
3224
|
+
recompilation."""
|
|
3225
|
+
schema = criterion_schema(document["classes"])
|
|
3226
|
+
return (
|
|
3227
|
+
json.dumps(schema, ensure_ascii=False, sort_keys=True, separators=(",", ":"))
|
|
3228
|
+
+ "\n"
|
|
3229
|
+
).encode("utf-8")
|
|
3230
|
+
|
|
3231
|
+
|
|
3232
|
+
def criterion_record_line(document: dict) -> str:
|
|
3233
|
+
"""The one line the packet carries: the canonical serialization of the document
|
|
3234
|
+
behind the record marker, the same shape the ReviewPlan/v1 record rides in."""
|
|
3235
|
+
return CRITERION_RECORD_MARKER + json.dumps(
|
|
3236
|
+
document, ensure_ascii=False, sort_keys=True, separators=(",", ":")
|
|
3237
|
+
)
|
|
3238
|
+
|
|
3239
|
+
|
|
3240
|
+
def extract_criterion(text: str, context: str) -> dict:
|
|
3241
|
+
"""The criterion the packet declares, from its record line — exactly one.
|
|
3242
|
+
|
|
3243
|
+
Zero records is a refusal rather than None on purpose: this is only called when
|
|
3244
|
+
the plan's rows carry the discipline clause, and a launch that declared the
|
|
3245
|
+
discipline cannot verify without the criterion."""
|
|
3246
|
+
# LINE-anchored, because the packet is free text: an unanchored find took a prose
|
|
3247
|
+
# mention — "for example, ReviewCriterion/v1: {...}" mid-sentence — as the declared
|
|
3248
|
+
# record (criterion round, #5). The record is a line the compiler printed: marker at
|
|
3249
|
+
# line start, one JSON value, nothing else before the newline.
|
|
3250
|
+
starts = []
|
|
3251
|
+
index = text.find(CRITERION_RECORD_MARKER)
|
|
3252
|
+
while index >= 0:
|
|
3253
|
+
if index == 0 or text[index - 1] == "\n":
|
|
3254
|
+
starts.append(index)
|
|
3255
|
+
index = text.find(CRITERION_RECORD_MARKER, index + 1)
|
|
3256
|
+
if not starts:
|
|
3257
|
+
raise LaunchError(
|
|
3258
|
+
f"{context} carries no {CRITERION_RECORD_SCHEMA} record line, so the "
|
|
3259
|
+
f"criterion the plan's discipline clause declares is nowhere in the bytes "
|
|
3260
|
+
f"the reviewers consumed"
|
|
3261
|
+
)
|
|
3262
|
+
if len(starts) > 1:
|
|
3263
|
+
raise LaunchError(
|
|
3264
|
+
f"{context} carries more than one {CRITERION_RECORD_SCHEMA} record; a mixed "
|
|
3265
|
+
f"packet is split into one packet per criterion, so exactly one is the claim "
|
|
3266
|
+
f"this can adjudicate"
|
|
3267
|
+
)
|
|
3268
|
+
start = starts[0] + len(CRITERION_RECORD_MARKER)
|
|
3269
|
+
try:
|
|
3270
|
+
document, consumed = json.JSONDecoder().raw_decode(text[start:])
|
|
3271
|
+
except json.JSONDecodeError as exc:
|
|
3272
|
+
raise LaunchError(
|
|
3273
|
+
f"{context} carries a {CRITERION_RECORD_SCHEMA} record that does not parse: "
|
|
3274
|
+
f"{exc}"
|
|
3275
|
+
) from exc
|
|
3276
|
+
trailing = text[start + consumed:].split("\n", 1)[0]
|
|
3277
|
+
if trailing.strip():
|
|
3278
|
+
raise LaunchError(
|
|
3279
|
+
f"{context} carries a {CRITERION_RECORD_SCHEMA} record line with trailing "
|
|
3280
|
+
f"content ({trailing.strip()[:40]!r}); the record is the whole line, so "
|
|
3281
|
+
f"extra text on it is a different claim than the compiler printed"
|
|
3282
|
+
)
|
|
3283
|
+
validate_criterion(document, f"{context}'s {CRITERION_RECORD_SCHEMA} record")
|
|
3284
|
+
return document
|
|
3285
|
+
|
|
3286
|
+
|
|
3287
|
+
def criterion_classes_of_schema(schema: Any, context: str) -> list:
|
|
3288
|
+
"""The enum a compiled schema file carries — accepted only when the whole schema
|
|
3289
|
+
equals what compiling that enum produces, so a hand-edited or foreign schema is
|
|
3290
|
+
refused rather than partially read."""
|
|
3291
|
+
try:
|
|
3292
|
+
classes = schema["properties"]["findings"]["items"]["properties"]["class"]["enum"]
|
|
3293
|
+
except (TypeError, KeyError):
|
|
3294
|
+
raise LaunchError(
|
|
3295
|
+
f"{context} is not a compiled criterion schema; compile one with "
|
|
3296
|
+
f"--compile-criterion"
|
|
3297
|
+
) from None
|
|
3298
|
+
# Self-consistency alone lets the schema supply its own authority: an exact
|
|
3299
|
+
# criterion_schema(['anything']) equals recompiling its own enum although the
|
|
3300
|
+
# compiler can never produce it — validate_criterion refuses every one-class
|
|
3301
|
+
# document (criterion round, #2). The enum must also be one the compiler admits.
|
|
3302
|
+
if (
|
|
3303
|
+
not isinstance(classes, list) or len(classes) < 2
|
|
3304
|
+
or len(set(classes)) != len(classes)
|
|
3305
|
+
or not all(
|
|
3306
|
+
isinstance(value, str) and value and value == value.strip()
|
|
3307
|
+
and _utf8_encodable(value)
|
|
3308
|
+
for value in classes
|
|
3309
|
+
)
|
|
3310
|
+
):
|
|
3311
|
+
raise LaunchError(
|
|
3312
|
+
f"{context} carries an enum --compile-criterion can never produce (fewer "
|
|
3313
|
+
f"than two classes, or a duplicate, empty, whitespace-padded, or "
|
|
3314
|
+
f"non-UTF-8-encodable name); it was not written by the compiler — "
|
|
3315
|
+
f"recompile rather than editing the artifact"
|
|
3316
|
+
)
|
|
3317
|
+
if schema != criterion_schema(classes):
|
|
3318
|
+
raise LaunchError(
|
|
3319
|
+
f"{context} differs from what compiling its own enum produces, so it was "
|
|
3320
|
+
f"not written by --compile-criterion; recompile rather than editing the "
|
|
3321
|
+
f"artifact"
|
|
3322
|
+
)
|
|
3323
|
+
return list(classes)
|
|
3324
|
+
|
|
3325
|
+
|
|
3326
|
+
def findings_violations(result: Any, classes: list) -> list:
|
|
3327
|
+
"""Why this result is not a findings record under the declared enum — every
|
|
3328
|
+
violation named, empty when it conforms. Deterministic and total: no salvage, no
|
|
3329
|
+
guessed class, no partial admission."""
|
|
3330
|
+
violations = []
|
|
3331
|
+
if not isinstance(result, dict):
|
|
3332
|
+
return [f"the result is {type(result).__name__}, not an object"]
|
|
3333
|
+
unknown = sorted(set(result) - {"findings"})
|
|
3334
|
+
if unknown:
|
|
3335
|
+
violations.append(f"unknown result key(s): {', '.join(unknown)}")
|
|
3336
|
+
findings = result.get("findings")
|
|
3337
|
+
if not isinstance(findings, list):
|
|
3338
|
+
violations.append("the result carries no findings array")
|
|
3339
|
+
return violations
|
|
3340
|
+
for index, finding in enumerate(findings):
|
|
3341
|
+
slot = f"findings[{index}]"
|
|
3342
|
+
if not isinstance(finding, dict):
|
|
3343
|
+
violations.append(f"{slot} is {type(finding).__name__}, not an object")
|
|
3344
|
+
continue
|
|
3345
|
+
unknown = sorted(set(finding) - {"class", "finding"})
|
|
3346
|
+
if unknown:
|
|
3347
|
+
violations.append(f"{slot} carries unknown key(s): {', '.join(unknown)}")
|
|
3348
|
+
if "class" not in finding:
|
|
3349
|
+
violations.append(f"{slot} carries no class; a row is never admitted unclassified")
|
|
3350
|
+
elif finding["class"] not in classes:
|
|
3351
|
+
violations.append(
|
|
3352
|
+
f"{slot} carries class {finding['class']!r}, which is not in the "
|
|
3353
|
+
f"declared enum ({', '.join(classes)})"
|
|
3354
|
+
)
|
|
3355
|
+
prose = finding.get("finding")
|
|
3356
|
+
if not isinstance(prose, str) or not prose.strip():
|
|
3357
|
+
violations.append(f"{slot}.finding must be a non-empty string")
|
|
3358
|
+
return violations
|
|
3359
|
+
|
|
3360
|
+
|
|
3361
|
+
def compile_criterion_command(criterion_file: str, schema_out: str) -> int:
|
|
3362
|
+
"""Validate a criterion document and publish its compiled findings schema.
|
|
3363
|
+
|
|
3364
|
+
Prints the packet record line the dispatching agent pastes verbatim, and the
|
|
3365
|
+
digest receipt emission will stamp — so the three artifacts (packet record,
|
|
3366
|
+
schema file, receipt digest) cannot drift from one authoring."""
|
|
3367
|
+
path = pathlib.Path(criterion_file).expanduser()
|
|
3368
|
+
try:
|
|
3369
|
+
document = json.loads(path.read_text(encoding="utf-8"))
|
|
3370
|
+
except (OSError, UnicodeDecodeError, json.JSONDecodeError) as exc:
|
|
3371
|
+
raise LaunchError(f"cannot read criterion document {path}: {exc}") from exc
|
|
3372
|
+
validate_criterion(document, str(path))
|
|
3373
|
+
payload = criterion_schema_bytes(document)
|
|
3374
|
+
target = pathlib.Path(schema_out).expanduser()
|
|
3375
|
+
try:
|
|
3376
|
+
target.parent.mkdir(parents=True, exist_ok=True)
|
|
3377
|
+
publish_atomically(target, payload)
|
|
3378
|
+
except OSError as exc:
|
|
3379
|
+
raise LaunchError(f"cannot write compiled schema into {target}: {exc}") from exc
|
|
3380
|
+
print(criterion_record_line(document))
|
|
3381
|
+
print(f"criterion_schema_sha256={hashlib.sha256(payload).hexdigest()} {target}")
|
|
3382
|
+
return 0
|
|
3383
|
+
|
|
3384
|
+
|
|
3385
|
+
def check_findings_command(result_file: str, schema_file: str) -> int:
|
|
3386
|
+
"""The structural accepting channel, standalone: refuse a findings result that
|
|
3387
|
+
fails the compiled schema, each violation by name. The same check runs inside
|
|
3388
|
+
--emit-receipt when REVIEW_CRITERION_SCHEMA is set, so bypassing this one only
|
|
3389
|
+
forfeits the receipt."""
|
|
3390
|
+
schema_path = pathlib.Path(schema_file).expanduser()
|
|
3391
|
+
try:
|
|
3392
|
+
schema = json.loads(schema_path.read_text(encoding="utf-8"))
|
|
3393
|
+
except (OSError, UnicodeDecodeError, json.JSONDecodeError) as exc:
|
|
3394
|
+
raise LaunchError(f"cannot read compiled schema {schema_path}: {exc}") from exc
|
|
3395
|
+
classes = criterion_classes_of_schema(schema, str(schema_path))
|
|
3396
|
+
result_path = pathlib.Path(result_file).expanduser()
|
|
3397
|
+
try:
|
|
3398
|
+
result = json.loads(result_path.read_text(encoding="utf-8"))
|
|
3399
|
+
except (OSError, UnicodeDecodeError) as exc:
|
|
3400
|
+
raise LaunchError(f"cannot read result {result_path}: {exc}") from exc
|
|
3401
|
+
except json.JSONDecodeError as exc:
|
|
3402
|
+
print(f"REFUSED: {result_path} is not JSON, so no finding in it carries a class: {exc}")
|
|
3403
|
+
return 1
|
|
3404
|
+
violations = findings_violations(result, classes)
|
|
3405
|
+
if violations:
|
|
3406
|
+
for violation in violations:
|
|
3407
|
+
print(f"REFUSED: {violation}")
|
|
3408
|
+
return 1
|
|
3409
|
+
count = len(result["findings"])
|
|
3410
|
+
print(f"OK: {count} finding{'s' if count != 1 else ''} conform to the declared enum")
|
|
3411
|
+
return 0
|
|
3412
|
+
|
|
3413
|
+
|
|
3414
|
+
# The host CLIs' structured-output flags, and how each registers them. A TABLE plus a
|
|
3415
|
+
# PROBE rather than an offer key: a declared capability is documentation, and the rule
|
|
3416
|
+
# is probe-over-docs — the flag either appears in the installed binary's registered
|
|
3417
|
+
# options or the route renders prose discipline.
|
|
3418
|
+
HOST_SCHEMA_FLAGS = {
|
|
3419
|
+
"codex": (("exec", "--help"), "--output-schema"),
|
|
3420
|
+
"claude": (("--help",), "--json-schema"),
|
|
3421
|
+
}
|
|
3422
|
+
SCHEMA_FLAG_ABSENT_EXIT = 4
|
|
3423
|
+
|
|
3424
|
+
|
|
3425
|
+
def check_schema_flag_command(host: str, config_path: pathlib.Path) -> int:
|
|
3426
|
+
"""Probe the resolved backend binary for its structured-output flag.
|
|
3427
|
+
|
|
3428
|
+
Exit 0 when the flag is registered, SCHEMA_FLAG_ABSENT_EXIT when the binary runs
|
|
3429
|
+
and does not register it — absence is a finding, not an error — and a LaunchError
|
|
3430
|
+
when nothing could be probed at all."""
|
|
3431
|
+
if host not in HOST_SCHEMA_FLAGS:
|
|
3432
|
+
raise LaunchError(
|
|
3433
|
+
f"no schema flag is known for host {host!r}; known hosts: "
|
|
3434
|
+
f"{', '.join(sorted(HOST_SCHEMA_FLAGS))}"
|
|
3435
|
+
)
|
|
3436
|
+
config = load_config(config_path)
|
|
3437
|
+
command = config.get("backends", {}).get(host, {}).get("command")
|
|
3438
|
+
if not isinstance(command, str) or not command:
|
|
3439
|
+
raise LaunchError(f"config names no backend command for host {host!r}")
|
|
3440
|
+
help_args, flag = HOST_SCHEMA_FLAGS[host]
|
|
3441
|
+
try:
|
|
3442
|
+
probe = subprocess.run(
|
|
3443
|
+
[command, *help_args], capture_output=True, text=True, timeout=60,
|
|
3444
|
+
)
|
|
3445
|
+
except (OSError, subprocess.TimeoutExpired) as exc:
|
|
3446
|
+
raise LaunchError(f"cannot probe {command!r} for {flag}: {exc}") from exc
|
|
3447
|
+
# A failed help run proves nothing about the flag — and its ERROR text can carry
|
|
3448
|
+
# the very token being searched for ("error: unknown option --output-schema"), so
|
|
3449
|
+
# scanning it reported present on the known-opposite input (criterion round, #7).
|
|
3450
|
+
if probe.returncode != 0:
|
|
3451
|
+
detail = (probe.stderr or probe.stdout).strip()[:160]
|
|
3452
|
+
raise LaunchError(
|
|
3453
|
+
f"cannot probe {command!r} for {flag}: help exited {probe.returncode}"
|
|
3454
|
+
+ (f" ({detail})" if detail else "")
|
|
3455
|
+
+ " — presence cannot be read from a failed probe"
|
|
3456
|
+
)
|
|
3457
|
+
text = (probe.stdout + probe.stderr).replace(",", " ")
|
|
3458
|
+
registered = any(
|
|
3459
|
+
token == flag or token.startswith(f"{flag}=")
|
|
3460
|
+
for token in text.split()
|
|
3461
|
+
)
|
|
3462
|
+
if registered:
|
|
3463
|
+
print(f"schema flag {flag} for host {host}: present ({command})")
|
|
3464
|
+
return 0
|
|
3465
|
+
print(
|
|
3466
|
+
f"schema flag {flag} for host {host}: absent ({command} registers no such "
|
|
3467
|
+
f"option; dispatch this route as prose discipline)"
|
|
3468
|
+
)
|
|
3469
|
+
return SCHEMA_FLAG_ABSENT_EXIT
|
|
3470
|
+
|
|
3471
|
+
|
|
2965
3472
|
# ── receipt production (the adapter side of the same contract) ──────────────
|
|
2966
3473
|
# Everything above adjudicates receipts; nothing produced one. That gap is the whole
|
|
2967
3474
|
# defect: the only party able to write a receipt was whoever ran the review, so the
|
|
@@ -2983,6 +3490,12 @@ RECEIPT_METHOD_ENV = "REVIEW_METHOD_ID"
|
|
|
2983
3490
|
# multi-pass method, and a third-party one gets them right by not participating.
|
|
2984
3491
|
RECEIPT_SEED_ENV = "REVIEW_ORDERING_SEED"
|
|
2985
3492
|
RECEIPT_SWAP_ENV = "REVIEW_SWAP_GROUP"
|
|
3493
|
+
# The compiled criterion schema for THIS dispatch, set by the dispatch wrapper only
|
|
3494
|
+
# when it passed the schema to the host. Set, emission validates the result against it
|
|
3495
|
+
# and refuses to write a receipt for a class-less or out-of-enum finding — so the only
|
|
3496
|
+
# path to a receipt IS the structural accepting channel, and a bypassed check is an
|
|
3497
|
+
# unproven dispatch. Unset — every prose route — emission behaves exactly as today.
|
|
3498
|
+
RECEIPT_CRITERION_ENV = "REVIEW_CRITERION_SCHEMA"
|
|
2986
3499
|
# The probe adjudicates a one-row plan, and every plan's one required row is the base
|
|
2987
3500
|
# panel wearing its reserved id (`_review_identity_reason`). It had a name of its own —
|
|
2988
3501
|
# `adapter-conformance-probe` — which made the probe the only plan in the system whose
|
|
@@ -3021,9 +3534,10 @@ def _sha256_file(path: str) -> str:
|
|
|
3021
3534
|
return digest.hexdigest()
|
|
3022
3535
|
|
|
3023
3536
|
|
|
3024
|
-
def publish_atomically(target: pathlib.Path,
|
|
3025
|
-
|
|
3026
|
-
|
|
3537
|
+
def publish_atomically(target: pathlib.Path, payload: str | bytes,
|
|
3538
|
+
mode: int | None = None) -> None:
|
|
3539
|
+
"""Write `payload` at `target` so a reader sees the old file or the complete new one,
|
|
3540
|
+
and a failure leaves neither a partial file nor a temporary.
|
|
3027
3541
|
|
|
3028
3542
|
`write_text` creates the FINAL name and then fills it, so a concurrent reader — and
|
|
3029
3543
|
the receipt fold globs exactly its directory — could observe a truncated record, and a
|
|
@@ -3036,10 +3550,17 @@ def publish_atomically(target: pathlib.Path, text: str) -> None:
|
|
|
3036
3550
|
the preset save wrote its temporary and published it with no `try`, so an `os.replace`
|
|
3037
3551
|
that failed left a complete temporary file beside the untouched preset file, for the
|
|
3038
3552
|
next save's glob or the next reader to find (spec round 2, #6). The removal never
|
|
3039
|
-
masks the error that caused it.
|
|
3553
|
+
masks the error that caused it. The registration wizard was the third copy, with
|
|
3554
|
+
neither the boundary nor the cleanup (writable-scratch round, #6) — it publishes
|
|
3555
|
+
BYTES it snapshotted and re-applies the file's prior mode, hence the two optionals."""
|
|
3040
3556
|
temporary = target.with_name(f".{target.name}.{os.getpid()}.tmp")
|
|
3041
3557
|
try:
|
|
3042
|
-
|
|
3558
|
+
if isinstance(payload, bytes):
|
|
3559
|
+
temporary.write_bytes(payload)
|
|
3560
|
+
else:
|
|
3561
|
+
temporary.write_text(payload, encoding="utf-8")
|
|
3562
|
+
if mode is not None:
|
|
3563
|
+
temporary.chmod(mode)
|
|
3043
3564
|
os.replace(temporary, target)
|
|
3044
3565
|
except BaseException:
|
|
3045
3566
|
try:
|
|
@@ -3075,18 +3596,79 @@ def emit_receipt_command(
|
|
|
3075
3596
|
if not key or not separator:
|
|
3076
3597
|
raise LaunchError(f"--evidence takes key=value, got {item!r}")
|
|
3077
3598
|
fields[key] = value
|
|
3599
|
+
# Present-but-empty is a configuration error, never a silent prose downgrade: an
|
|
3600
|
+
# exported empty value made the whole validation vacuous while the caller believed
|
|
3601
|
+
# it armed (criterion round, #3 — the empty-variable class the corpus already
|
|
3602
|
+
# names for shell checks).
|
|
3603
|
+
criterion_env = os.environ.get(RECEIPT_CRITERION_ENV)
|
|
3604
|
+
if criterion_env is not None and not criterion_env.strip():
|
|
3605
|
+
raise LaunchError(
|
|
3606
|
+
f"{RECEIPT_CRITERION_ENV} is set but empty; unset it for a prose-route "
|
|
3607
|
+
f"dispatch or point it at a compiled schema"
|
|
3608
|
+
)
|
|
3609
|
+
# Only a SUCCESSFUL dispatch is validated and stamped: a failed dispatch's receipt
|
|
3610
|
+
# records the failure and is already refused credit for exit != 0, and refusing to
|
|
3611
|
+
# write it would erase the failure record — while stamping it would claim a
|
|
3612
|
+
# validation that never ran.
|
|
3613
|
+
criterion_schema_path = criterion_env if status == 0 else None
|
|
3614
|
+
# ONE byte snapshot each for the schema and the result: validating one read and
|
|
3615
|
+
# hashing a second let the digest bind bytes the validation never saw — the same
|
|
3616
|
+
# false PASS whether the swap is a race or an adversary (criterion round, #4).
|
|
3617
|
+
criterion_digest = None
|
|
3618
|
+
result_bytes = None
|
|
3619
|
+
if criterion_schema_path:
|
|
3620
|
+
schema_file = pathlib.Path(criterion_schema_path).expanduser()
|
|
3621
|
+
try:
|
|
3622
|
+
schema_bytes = schema_file.read_bytes()
|
|
3623
|
+
except OSError as exc:
|
|
3624
|
+
raise LaunchError(
|
|
3625
|
+
f"{RECEIPT_CRITERION_ENV} names {schema_file}, which is not a readable "
|
|
3626
|
+
f"compiled schema: {exc}"
|
|
3627
|
+
) from exc
|
|
3628
|
+
try:
|
|
3629
|
+
schema = json.loads(schema_bytes)
|
|
3630
|
+
except (UnicodeDecodeError, json.JSONDecodeError) as exc:
|
|
3631
|
+
raise LaunchError(
|
|
3632
|
+
f"{RECEIPT_CRITERION_ENV} names {schema_file}, which is not a readable "
|
|
3633
|
+
f"compiled schema: {exc}"
|
|
3634
|
+
) from exc
|
|
3635
|
+
classes = criterion_classes_of_schema(schema, str(schema_file))
|
|
3636
|
+
try:
|
|
3637
|
+
result_bytes = pathlib.Path(result_file).expanduser().read_bytes()
|
|
3638
|
+
except OSError as exc:
|
|
3639
|
+
raise LaunchError(f"cannot read result {result_file}: {exc}") from exc
|
|
3640
|
+
try:
|
|
3641
|
+
result = json.loads(result_bytes)
|
|
3642
|
+
except (UnicodeDecodeError, json.JSONDecodeError) as exc:
|
|
3643
|
+
raise LaunchError(
|
|
3644
|
+
f"no receipt: the result is not JSON, so no finding in it carries a "
|
|
3645
|
+
f"class from the declared enum ({exc})"
|
|
3646
|
+
) from exc
|
|
3647
|
+
violations = findings_violations(result, classes)
|
|
3648
|
+
if violations:
|
|
3649
|
+
raise LaunchError(
|
|
3650
|
+
"no receipt: " + "; ".join(violations) + " — a result the schema "
|
|
3651
|
+
"refuses is not published, and runtime never guesses a class"
|
|
3652
|
+
)
|
|
3653
|
+
criterion_digest = hashlib.sha256(schema_bytes).hexdigest()
|
|
3078
3654
|
dispatch_id = uuid.uuid4().hex
|
|
3079
3655
|
receipt = {
|
|
3080
3656
|
"schema": RECEIPT_SCHEMA,
|
|
3081
3657
|
"method_id": method_id,
|
|
3082
3658
|
"dispatch_id": dispatch_id,
|
|
3083
3659
|
"packet_sha256": _sha256_file(packet_file),
|
|
3084
|
-
|
|
3660
|
+
# The bytes the validation saw, when it ran; the file, when it did not.
|
|
3661
|
+
"result_sha256": (
|
|
3662
|
+
hashlib.sha256(result_bytes).hexdigest()
|
|
3663
|
+
if result_bytes is not None else _sha256_file(result_file)
|
|
3664
|
+
),
|
|
3085
3665
|
"provider": provider,
|
|
3086
3666
|
"model": model,
|
|
3087
3667
|
"effort": effort,
|
|
3088
3668
|
"exit_status": status,
|
|
3089
3669
|
}
|
|
3670
|
+
if criterion_digest:
|
|
3671
|
+
receipt["criterion_schema_sha256"] = criterion_digest
|
|
3090
3672
|
if fields:
|
|
3091
3673
|
receipt["evidence"] = fields
|
|
3092
3674
|
for key, name in (("ordering_seed", RECEIPT_SEED_ENV), ("swap_group", RECEIPT_SWAP_ENV)):
|
|
@@ -3214,7 +3796,12 @@ def _merge_method_passes(method_id: str, group: list[dict], main_dispatch_id: st
|
|
|
3214
3796
|
# `passes`, and for a lone receipt it is naturally the one-element set. Round 4 made
|
|
3215
3797
|
# the multi-pass record faithful and this is the same sentence applied to the twin
|
|
3216
3798
|
# F1 already names: asked of EACH receipt, singleton included.
|
|
3217
|
-
for field_name in (
|
|
3799
|
+
for field_name in (
|
|
3800
|
+
"provider", "model", "effort", "packet_sha256", "criterion_schema_sha256",
|
|
3801
|
+
):
|
|
3802
|
+
# criterion_schema_sha256 included with PRESENCE counted as a value: passes
|
|
3803
|
+
# validated against different schemas — or one validated and one not — ran
|
|
3804
|
+
# under different criteria and cannot be one method's passes.
|
|
3218
3805
|
values = {json.dumps(receipt.get(field_name), sort_keys=True) for receipt in group}
|
|
3219
3806
|
if len(values) > 1:
|
|
3220
3807
|
raise LaunchError(
|
|
@@ -3578,7 +4165,8 @@ def routed_preset_names(presets: dict[str, Any]) -> set[str]:
|
|
|
3578
4165
|
"""
|
|
3579
4166
|
return {
|
|
3580
4167
|
name for name, preset in presets.items()
|
|
3581
|
-
if isinstance(preset, dict)
|
|
4168
|
+
if isinstance(preset, dict)
|
|
4169
|
+
and ("mission" in preset or "trigger" in preset or "criterion" in preset)
|
|
3582
4170
|
}
|
|
3583
4171
|
|
|
3584
4172
|
|
|
@@ -4519,7 +5107,7 @@ def host_models(config: dict[str, Any], host: str, tiers: dict[str, Any]) -> lis
|
|
|
4519
5107
|
|
|
4520
5108
|
def resolve_review_for_plan(
|
|
4521
5109
|
review: ReviewPlan, config: dict[str, Any], host: str, main_model: str,
|
|
4522
|
-
main_effort: str, context: str,
|
|
5110
|
+
main_effort: str, context: str, criterion: bool = False,
|
|
4523
5111
|
) -> tuple[dict, "ReviewReport | None"]:
|
|
4524
5112
|
"""(method registry, resolved report) for a review against the main seat.
|
|
4525
5113
|
|
|
@@ -4527,6 +5115,16 @@ def resolve_review_for_plan(
|
|
|
4527
5115
|
what a fresh launch would; two copies of this would let the Custom hub show a report
|
|
4528
5116
|
the next launch disagrees with."""
|
|
4529
5117
|
if review.source != "composable":
|
|
5118
|
+
if criterion:
|
|
5119
|
+
# The clause renders only through composable review rows, so on a legacy
|
|
5120
|
+
# plan the toggle is accepted and then does nothing — a false success the
|
|
5121
|
+
# operator reads as discipline in force (criterion round, #0).
|
|
5122
|
+
raise LaunchError(
|
|
5123
|
+
f"{context} declares criterion = true, but its review uses the legacy "
|
|
5124
|
+
f"schema; the discipline clause renders only through composable review "
|
|
5125
|
+
f"rows, so the toggle would be silently inert — author a composable "
|
|
5126
|
+
f"[review] block or drop the toggle"
|
|
5127
|
+
)
|
|
4530
5128
|
return {}, None
|
|
4531
5129
|
provider = config["hosts"][host].get("provider")
|
|
4532
5130
|
if not isinstance(provider, str) or not provider:
|
|
@@ -4536,7 +5134,7 @@ def resolve_review_for_plan(
|
|
|
4536
5134
|
)
|
|
4537
5135
|
methods = load_review_methods(config)
|
|
4538
5136
|
main_seat = ReviewBinding(provider, host, main_model, main_effort)
|
|
4539
|
-
return methods, resolve_composable_review(review, main_seat, config, methods)
|
|
5137
|
+
return methods, resolve_composable_review(review, main_seat, config, methods, criterion)
|
|
4540
5138
|
|
|
4541
5139
|
|
|
4542
5140
|
def effective_review_family(
|
|
@@ -4578,6 +5176,7 @@ def apply_review_plan(plan: dict[str, Any], config: dict[str, Any], review: Revi
|
|
|
4578
5176
|
methods, report = resolve_review_for_plan(
|
|
4579
5177
|
review, config, plan["host"], plan["tiers"][plan["main_tier"]]["model"],
|
|
4580
5178
|
tier_effort(plan, plan["main_tier"]), f"custom.{plan['preset']}",
|
|
5179
|
+
plan.get("criterion", False),
|
|
4581
5180
|
)
|
|
4582
5181
|
plan["review_plan"] = review
|
|
4583
5182
|
# Editing on one host replaces that host's arm and leaves the others exactly as
|
|
@@ -4792,8 +5391,18 @@ def build_plan(config: dict[str, Any], host: str, preset_name: str) -> dict[str,
|
|
|
4792
5391
|
# isolated mechanism is an invalid launch, and that has to fail while the plan is
|
|
4793
5392
|
# being built rather than halfway through printing a contract.
|
|
4794
5393
|
main_effort = frontier_effort if main_tier == "frontier" else tiers[main_tier]["effort"]
|
|
5394
|
+
# The criterion-discipline toggle. A BOOLEAN, deliberately: the criterion itself is
|
|
5395
|
+
# per-review and rides the packet, which packet_sha256 binds — a preset carrying its
|
|
5396
|
+
# content would put per-review text into the per-launch contract the golden pins.
|
|
5397
|
+
criterion = preset.get("criterion", False)
|
|
5398
|
+
if not isinstance(criterion, bool):
|
|
5399
|
+
raise LaunchError(
|
|
5400
|
+
f"presets.{preset_name}.criterion must be a boolean toggle; the criterion "
|
|
5401
|
+
f"itself is declared in the review packet, never in the preset"
|
|
5402
|
+
)
|
|
4795
5403
|
review_methods, review_report = resolve_review_for_plan(
|
|
4796
|
-
review, config, host, tiers[main_tier]["model"], main_effort,
|
|
5404
|
+
review, config, host, tiers[main_tier]["model"], main_effort,
|
|
5405
|
+
f"presets.{preset_name}", criterion,
|
|
4797
5406
|
)
|
|
4798
5407
|
codex_policy = preset.get("codex_execution_policy")
|
|
4799
5408
|
claude_policy = preset.get("claude_permission_mode")
|
|
@@ -4886,6 +5495,7 @@ def build_plan(config: dict[str, Any], host: str, preset_name: str) -> dict[str,
|
|
|
4886
5495
|
},
|
|
4887
5496
|
"mission": mission,
|
|
4888
5497
|
"trigger": trigger,
|
|
5498
|
+
"criterion": criterion,
|
|
4889
5499
|
}
|
|
4890
5500
|
|
|
4891
5501
|
|
|
@@ -5303,32 +5913,39 @@ def _trial_registration(
|
|
|
5303
5913
|
through the genuine load_config — same merge order, same validation, same
|
|
5304
5914
|
collision refusal. Returns None on acceptance, the reader's message on refusal."""
|
|
5305
5915
|
trial = pathlib.Path(tempfile.mkdtemp(prefix="agent-launch-register-"))
|
|
5306
|
-
|
|
5307
|
-
|
|
5308
|
-
|
|
5309
|
-
|
|
5310
|
-
shutil.copy(source, trial / sibling)
|
|
5311
|
-
methods_source = user_methods_path(config_path)
|
|
5312
|
-
existing = methods_source.read_text(encoding="utf-8") if methods_source.is_file() else USER_METHODS_HEADER
|
|
5313
|
-
candidate = trial / USER_METHODS_NAME
|
|
5314
|
-
try:
|
|
5315
|
-
candidate.write_text(existing + block, encoding="utf-8")
|
|
5316
|
-
except OSError as exc:
|
|
5317
|
-
# A read-only config dir or a full disk lost every answer the
|
|
5318
|
-
# user had just typed. Reported like any other refusal instead.
|
|
5319
|
-
raise LaunchError(f"cannot write {candidate}: {exc}") from exc
|
|
5320
|
-
candidate.chmod(0o600)
|
|
5916
|
+
# One finally over EVERYTHING after the mkdtemp — the copies, the candidate
|
|
5917
|
+
# write, both readers: the trial is a scratch workspace, and every exit used
|
|
5918
|
+
# to leave it behind, one directory per attempt, for the life of the temp
|
|
5919
|
+
# area (writable-scratch round, #7).
|
|
5321
5920
|
try:
|
|
5322
|
-
|
|
5323
|
-
|
|
5324
|
-
|
|
5325
|
-
|
|
5326
|
-
|
|
5327
|
-
|
|
5328
|
-
|
|
5329
|
-
|
|
5330
|
-
|
|
5331
|
-
|
|
5921
|
+
shutil.copy(config_path, trial / config_path.name)
|
|
5922
|
+
for sibling in (USER_PRESETS_NAME, USER_LAUNCHER_NAME):
|
|
5923
|
+
source = config_path.with_name(sibling)
|
|
5924
|
+
if source.is_file():
|
|
5925
|
+
shutil.copy(source, trial / sibling)
|
|
5926
|
+
methods_source = user_methods_path(config_path)
|
|
5927
|
+
existing = methods_source.read_text(encoding="utf-8") if methods_source.is_file() else USER_METHODS_HEADER
|
|
5928
|
+
candidate = trial / USER_METHODS_NAME
|
|
5929
|
+
try:
|
|
5930
|
+
candidate.write_text(existing + block, encoding="utf-8")
|
|
5931
|
+
except OSError as exc:
|
|
5932
|
+
# A read-only config dir or a full disk lost every answer the
|
|
5933
|
+
# user had just typed. Reported like any other refusal instead.
|
|
5934
|
+
raise LaunchError(f"cannot write {candidate}: {exc}") from exc
|
|
5935
|
+
candidate.chmod(0o600)
|
|
5936
|
+
try:
|
|
5937
|
+
trial_config = load_config(trial / config_path.name)
|
|
5938
|
+
# The REAL pipeline is both readers, not the first one. load_config accepts a
|
|
5939
|
+
# method whose instructions name a slot core does not provide; load_review_methods
|
|
5940
|
+
# — the reader every launch actually goes through — refuses it. Trialling only the
|
|
5941
|
+
# first wrote the block, told the user it was live, and left the refusal for the
|
|
5942
|
+
# next launch. A trial that does not run the reader that decides is not a trial.
|
|
5943
|
+
load_review_methods(trial_config)
|
|
5944
|
+
except LaunchError as exc:
|
|
5945
|
+
return str(exc)
|
|
5946
|
+
return None
|
|
5947
|
+
finally:
|
|
5948
|
+
shutil.rmtree(trial, ignore_errors=True)
|
|
5332
5949
|
|
|
5333
5950
|
|
|
5334
5951
|
def register_reviewer_wizard(
|
|
@@ -5550,60 +6167,67 @@ def register_reviewer_wizard(
|
|
|
5550
6167
|
back_hint=t("corpus.back.hint"))
|
|
5551
6168
|
return False
|
|
5552
6169
|
mode = (write_target.stat().st_mode & 0o777) if existed else 0o600
|
|
5553
|
-
temporary = write_target.with_name(
|
|
5554
|
-
f".{write_target.name}.{os.getpid()}.tmp"
|
|
5555
|
-
)
|
|
5556
|
-
temporary.write_bytes(before + block.encode())
|
|
5557
|
-
temporary.chmod(mode)
|
|
5558
|
-
os.replace(temporary, write_target)
|
|
5559
|
-
# Unreachable while the trial is the real pipeline — and
|
|
5560
|
-
# load-bearing exactly when it is not: a post-write failure must
|
|
5561
|
-
# restore the pre-write state (bytes, mode, or ABSENCE) and say
|
|
5562
|
-
# so, never leave a corrupt registry behind a green screen.
|
|
5563
6170
|
try:
|
|
5564
|
-
|
|
5565
|
-
|
|
5566
|
-
|
|
5567
|
-
|
|
5568
|
-
#
|
|
5569
|
-
#
|
|
5570
|
-
#
|
|
5571
|
-
|
|
5572
|
-
|
|
5573
|
-
|
|
5574
|
-
|
|
5575
|
-
|
|
5576
|
-
|
|
5577
|
-
|
|
5578
|
-
|
|
5579
|
-
|
|
5580
|
-
|
|
5581
|
-
|
|
5582
|
-
|
|
5583
|
-
|
|
5584
|
-
|
|
5585
|
-
|
|
5586
|
-
|
|
5587
|
-
|
|
6171
|
+
# The shared primitive owns the temporary's lifecycle: on any
|
|
6172
|
+
# failure it removes its own temp and re-raises. A directory at
|
|
6173
|
+
# the target, a permission flip, a full disk — each used to
|
|
6174
|
+
# escape here as a raw OSError, PAST the retained-answer loop,
|
|
6175
|
+
# with the completed temporary left beside the untouched file.
|
|
6176
|
+
# Publication failure is a refusal like any other: the reader's
|
|
6177
|
+
# loop keeps the answers, and the screen shows the OS's words.
|
|
6178
|
+
publish_atomically(write_target, before + block.encode(), mode)
|
|
6179
|
+
except OSError as exc:
|
|
6180
|
+
verdict = f"cannot write {write_target}: {exc}"
|
|
6181
|
+
if verdict is None:
|
|
6182
|
+
# Unreachable while the trial is the real pipeline — and
|
|
6183
|
+
# load-bearing exactly when it is not: a post-write failure must
|
|
6184
|
+
# restore the pre-write state (bytes, mode, or ABSENCE) and say
|
|
6185
|
+
# so, never leave a corrupt registry behind a green screen.
|
|
6186
|
+
try:
|
|
6187
|
+
fresh = load_config(config_path)
|
|
6188
|
+
except LaunchError:
|
|
6189
|
+
fresh = None
|
|
6190
|
+
if fresh is None or answers["id"] not in load_review_methods(fresh):
|
|
6191
|
+
# Restore ONLY when the file still holds exactly our write:
|
|
6192
|
+
# unconditional rollback would destroy an edit that landed in
|
|
6193
|
+
# the meantime; if the bytes moved, the human owns the merge.
|
|
6194
|
+
current = (
|
|
6195
|
+
write_target.read_bytes() if write_target.is_file() else b""
|
|
5588
6196
|
)
|
|
5589
|
-
|
|
5590
|
-
|
|
5591
|
-
|
|
5592
|
-
|
|
5593
|
-
|
|
5594
|
-
|
|
5595
|
-
|
|
5596
|
-
|
|
5597
|
-
|
|
5598
|
-
|
|
5599
|
-
|
|
5600
|
-
|
|
5601
|
-
|
|
5602
|
-
|
|
5603
|
-
|
|
5604
|
-
|
|
5605
|
-
|
|
5606
|
-
|
|
6197
|
+
if current != before + block.encode():
|
|
6198
|
+
# Its own sentence, because this branch deliberately does NOT
|
|
6199
|
+
# roll back — someone else's edit landed and the human owns the
|
|
6200
|
+
# merge. Sharing the restore branch's message told the user the
|
|
6201
|
+
# file had been restored in the one case where it was left
|
|
6202
|
+
# exactly as the other writer wrote it.
|
|
6203
|
+
_corpus_info(
|
|
6204
|
+
ui, t("wizard.title"), [t("wizard.postwrite.raced.line")],
|
|
6205
|
+
back_hint=t("corpus.back.hint"),
|
|
6206
|
+
)
|
|
6207
|
+
return False
|
|
6208
|
+
if existed:
|
|
6209
|
+
restore = write_target.with_name(
|
|
6210
|
+
f".{write_target.name}.{os.getpid()}.restore"
|
|
6211
|
+
)
|
|
6212
|
+
restore.write_bytes(before)
|
|
6213
|
+
restore.chmod(mode)
|
|
6214
|
+
os.replace(restore, write_target)
|
|
6215
|
+
else:
|
|
6216
|
+
write_target.unlink(missing_ok=True)
|
|
6217
|
+
_corpus_info(ui, t("wizard.title"), [t("wizard.postwrite.line")],
|
|
6218
|
+
back_hint=t("corpus.back.hint"))
|
|
6219
|
+
return False # pre-write state restored above
|
|
6220
|
+
if verdict is None:
|
|
6221
|
+
config.clear()
|
|
6222
|
+
config.update(fresh)
|
|
6223
|
+
registry.clear()
|
|
6224
|
+
registry.update(load_review_methods(config))
|
|
6225
|
+
_corpus_info(
|
|
6226
|
+
ui, t("wizard.title"),
|
|
6227
|
+
[t("wizard.done.line").format(method_id=answers["id"])],
|
|
6228
|
+
back_hint=t("corpus.back.hint"),
|
|
6229
|
+
)
|
|
6230
|
+
return True
|
|
5607
6231
|
action = choose(
|
|
5608
6232
|
t("wizard.refused.title"),
|
|
5609
6233
|
[
|
|
@@ -6039,6 +6663,13 @@ def preset_from_plan(
|
|
|
6039
6663
|
"codex_execution_policy": plan["codex_execution_policy"],
|
|
6040
6664
|
"claude_permission_mode": plan["claude_permission_mode"],
|
|
6041
6665
|
}
|
|
6666
|
+
if plan.get("criterion"):
|
|
6667
|
+
# Save As from a criterion-toggled plan silently dropped the discipline: the
|
|
6668
|
+
# routed-name guard covers same-name shadowing, not a fresh name, and the
|
|
6669
|
+
# reload defaulted the absent field to false while Save reported success
|
|
6670
|
+
# (criterion round, #8). Written only when true, so every existing saved
|
|
6671
|
+
# preset stays byte-identical.
|
|
6672
|
+
fields["criterion"] = True
|
|
6042
6673
|
if review_block is None:
|
|
6043
6674
|
# Carrying both schemas on one preset is a validation error, so the legacy
|
|
6044
6675
|
# names appear only when there is no authored block to write.
|
|
@@ -6140,21 +6771,25 @@ def preset_from_plan(
|
|
|
6140
6771
|
f"is not an effort — saving would drop it and {other} would reload at "
|
|
6141
6772
|
f"its host default. Fix that entry in the launch profile and save again."
|
|
6142
6773
|
)
|
|
6774
|
+
# A non-table where the normalized home would go — `tier_overrides.<host>` or
|
|
6775
|
+
# its `frontier` entry authored as a value. The serializer now carries such a
|
|
6776
|
+
# value VERBATIM (spec round 7, #3), so this normalizer cannot write into it
|
|
6777
|
+
# (`setdefault("frontier", {})` on a string was round 24, #8's raw TypeError)
|
|
6778
|
+
# and cannot skip past it either: skipping used to be safe only because the
|
|
6779
|
+
# renderer refused the block, and with that refusal gone a skip would drop
|
|
6780
|
+
# the authored `frontier_effort` in silence. When the authored effort is that
|
|
6781
|
+
# host's default there is nothing to write and the carry is faithful; when it
|
|
6782
|
+
# is not, the home is genuinely occupied and the save refuses naming both —
|
|
6783
|
+
# the one case D-20260817-105da4 keeps a named refusal for.
|
|
6143
6784
|
block = scoped.get(other)
|
|
6785
|
+
occupied = None
|
|
6786
|
+
existing = None
|
|
6144
6787
|
if block is not None and not isinstance(block, dict):
|
|
6145
|
-
|
|
6146
|
-
|
|
6147
|
-
|
|
6148
|
-
|
|
6149
|
-
|
|
6150
|
-
# Left exactly as authored, for the reason the host-block branch above says
|
|
6151
|
-
# in the same words: the serializer owns unwritable shapes and names them.
|
|
6152
|
-
# This normalizer wrote into the block regardless, so `setdefault("frontier",
|
|
6153
|
-
# {})` handed back the STRING and `["effort"] =` left Save as a raw
|
|
6154
|
-
# TypeError — past the named boundary its workhorse twin arrives through
|
|
6155
|
-
# (round 24, #8). Nothing is dropped by skipping: the renderer's `_table`
|
|
6156
|
-
# door refuses this very entry by name a moment later.
|
|
6157
|
-
continue
|
|
6788
|
+
occupied = f"tier_overrides.{other}"
|
|
6789
|
+
else:
|
|
6790
|
+
existing = (block or {}).get("frontier")
|
|
6791
|
+
if existing is not None and not isinstance(existing, dict):
|
|
6792
|
+
occupied = f"tier_overrides.{other}.frontier"
|
|
6158
6793
|
if isinstance(existing, dict) and existing.get("effort") not in (None, effort):
|
|
6159
6794
|
# The contradiction build_plan already refuses, reached from the host that
|
|
6160
6795
|
# does not launch it. Named here rather than resolved: picking one of two
|
|
@@ -6179,6 +6814,19 @@ def preset_from_plan(
|
|
|
6179
6814
|
.get("frontier", {}).get("effort")
|
|
6180
6815
|
):
|
|
6181
6816
|
continue
|
|
6817
|
+
if occupied is not None:
|
|
6818
|
+
# AFTER the default elision on purpose, unlike the contradiction above: a
|
|
6819
|
+
# non-table home states no effort to contradict, so a default-valued
|
|
6820
|
+
# authoring writes no line and reloads faithfully. A non-default one has
|
|
6821
|
+
# exactly one home and that home is occupied by a value the save carries
|
|
6822
|
+
# verbatim — writing both is impossible, and dropping either is the
|
|
6823
|
+
# silence S4 forbids.
|
|
6824
|
+
raise LaunchError(
|
|
6825
|
+
f"preset {name!r}: {other} authors frontier_effort={effort!r}, whose "
|
|
6826
|
+
f"saved home is tier_overrides.{other}.frontier.effort, and {occupied} "
|
|
6827
|
+
f"holds a non-table value the save carries verbatim — it cannot write "
|
|
6828
|
+
f"both. Remove one of them in the launch profile and save again."
|
|
6829
|
+
)
|
|
6182
6830
|
scoped.setdefault(other, {}).setdefault("frontier", {})["effort"] = effort
|
|
6183
6831
|
return fields, scoped, review_block
|
|
6184
6832
|
|
|
@@ -6270,11 +6918,11 @@ def render_preset_block(
|
|
|
6270
6918
|
tier_overrides: dict[str, Any],
|
|
6271
6919
|
review_block: dict[str, Any] | None = None,
|
|
6272
6920
|
) -> str:
|
|
6273
|
-
# One door for every table
|
|
6274
|
-
#
|
|
6275
|
-
#
|
|
6276
|
-
#
|
|
6277
|
-
#
|
|
6921
|
+
# One door for every place only a table can stand — an arm's `methods`, its `base`,
|
|
6922
|
+
# each binding. Save failures are contracted to arrive as LaunchError at the settings
|
|
6923
|
+
# hub, never a raw escape or a silent filter (round 22, #9). The tier overrides and
|
|
6924
|
+
# whole review arms no longer pass through it: a non-table node there is not
|
|
6925
|
+
# malformed, it is a VALUE its parent table spells (spec round 7, #2/#3).
|
|
6278
6926
|
def _table(value, where):
|
|
6279
6927
|
if not isinstance(value, dict):
|
|
6280
6928
|
raise LaunchError(
|
|
@@ -6287,51 +6935,58 @@ def render_preset_block(
|
|
|
6287
6935
|
lines = [f"[presets.{_toml_key(name)}]"]
|
|
6288
6936
|
for key, value in fields.items():
|
|
6289
6937
|
lines.append(f"{key} = {_toml_scalar(value)}")
|
|
6290
|
-
|
|
6291
|
-
|
|
6292
|
-
|
|
6293
|
-
|
|
6294
|
-
|
|
6295
|
-
|
|
6296
|
-
|
|
6297
|
-
|
|
6298
|
-
|
|
6299
|
-
|
|
6300
|
-
|
|
6301
|
-
|
|
6302
|
-
|
|
6303
|
-
|
|
6304
|
-
|
|
6305
|
-
|
|
6306
|
-
|
|
6307
|
-
|
|
6938
|
+
if tier_overrides:
|
|
6939
|
+
# The whole tree through the SAME recursive emitter the review arms use, from the
|
|
6940
|
+
# `tier_overrides` root down. Verbatim carry has no depth limit (spec round 3, #6;
|
|
6941
|
+
# round 24, #7) and no FLOOR either: a non-table node at any depth —
|
|
6942
|
+
# `tier_overrides.claude = "x"`, or `…codex.workhorse = [1, 2]` — is TOML the
|
|
6943
|
+
# launcher accepts, since only the active host's blocks are validated at load,
|
|
6944
|
+
# and TOML spells it perfectly well as a value in its parent table; refusing it
|
|
6945
|
+
# at Save made an accepted profile unsaveable, which is S4's complement (spec
|
|
6946
|
+
# round 7, #3 — the per-tier loop this replaces held a table door at exactly the
|
|
6947
|
+
# two depths the emitter now writes as values). The active host's blocks arrive
|
|
6948
|
+
# normalized from `preset_from_plan`, so for them this emits the same
|
|
6949
|
+
# `[…tier_overrides.<host>.<tier>]` tables it always did; every leaf key goes
|
|
6950
|
+
# through `_toml_key` and every leaf value through `_arm_scalar`, so a genuinely
|
|
6951
|
+
# unwritable entry is refused naming its full `tier_overrides.…` path.
|
|
6952
|
+
_emit_arm_table(
|
|
6953
|
+
lines, f"presets.{_toml_key(name)}.tier_overrides",
|
|
6954
|
+
tier_overrides, name, "tier_overrides",
|
|
6955
|
+
)
|
|
6308
6956
|
if review_block is not None:
|
|
6309
6957
|
# One sub-table per binding, and the method id is quoted: it is a user-chosen
|
|
6310
6958
|
# name, so a bare key would break on anything a TOML bare key cannot hold.
|
|
6311
6959
|
# Adding method N therefore appends exactly one table and touches nothing else.
|
|
6312
6960
|
arms = review_block.get(REVIEW_ARMS_KEY)
|
|
6961
|
+
# A whole arm authored as a VALUE — `hosts.codex = [1, 2]` — is TOML the launcher
|
|
6962
|
+
# accepts, because only the launching arm is parsed, and TOML writes it back as a
|
|
6963
|
+
# value in the parent hosts table; so that is what this does, rather than the
|
|
6964
|
+
# refusal that made an accepted profile unsaveable (spec round 7, #2). Emitted
|
|
6965
|
+
# before the table arms for the reason every emitter here puts scalars first; a
|
|
6966
|
+
# value with no TOML form at all is refused through `_arm_scalar`, naming
|
|
6967
|
+
# `review.hosts.<host>`.
|
|
6968
|
+
value_arms = (
|
|
6969
|
+
{host: arm for host, arm in arms.items() if not isinstance(arm, dict)}
|
|
6970
|
+
if arms else {}
|
|
6971
|
+
)
|
|
6972
|
+
if value_arms:
|
|
6973
|
+
lines.append("")
|
|
6974
|
+
lines.append(f"[presets.{_toml_key(name)}.review.{REVIEW_ARMS_KEY}]")
|
|
6975
|
+
for host in sorted(value_arms, key=_toml_key):
|
|
6976
|
+
lines.append(
|
|
6977
|
+
f"{_toml_key(host)} = "
|
|
6978
|
+
f"{_arm_scalar(value_arms[host], name, f'review.{REVIEW_ARMS_KEY}.{host}')}"
|
|
6979
|
+
)
|
|
6313
6980
|
for prefix, block in (
|
|
6314
6981
|
sorted((f"review.{REVIEW_ARMS_KEY}.{_toml_key(host)}", arm)
|
|
6315
|
-
for host, arm in arms.items())
|
|
6982
|
+
for host, arm in arms.items() if isinstance(arm, dict))
|
|
6316
6983
|
if arms else [("review", review_block)]
|
|
6317
6984
|
):
|
|
6318
|
-
# An untouched arm is written back exactly as authored, and "as authored"
|
|
6319
|
-
# includes shapes this cannot serialize: a scalar where a table belongs made
|
|
6320
|
-
# `"base" in block` a raw TypeError out of Save, after the launch had already
|
|
6321
|
-
# happened. Save failures are contracted to arrive as LaunchError at the
|
|
6322
|
-
# settings hub, so this one does too, naming the arm to look at.
|
|
6323
|
-
if not isinstance(block, dict):
|
|
6324
|
-
raise LaunchError(
|
|
6325
|
-
f"preset {name!r}: the review arm at {prefix!r} is "
|
|
6326
|
-
f"{type(block).__name__}, not a table — it cannot be written back. "
|
|
6327
|
-
f"Fix that entry in the launch profile and save again."
|
|
6328
|
-
)
|
|
6329
6985
|
# `.get`, because an untouched arm is written back exactly as authored and
|
|
6330
6986
|
# a raw block need not carry both keys — only the edited arm is re-derived.
|
|
6331
|
-
# The nested shapes get
|
|
6332
|
-
#
|
|
6333
|
-
#
|
|
6334
|
-
# `methods` escape as AttributeError past the same boundary.
|
|
6987
|
+
# The nested shapes each get a door: `base`, `methods` and each binding are
|
|
6988
|
+
# tables or the save names the entry (round 19, #11), which once escaped as
|
|
6989
|
+
# a raw AttributeError from `.items()` on a scalar `methods`.
|
|
6335
6990
|
methods = _table(block.get("methods", {}), f"{prefix}.methods")
|
|
6336
6991
|
# Everything else the arm carries. An untouched arm is promised back VERBATIM
|
|
6337
6992
|
# and this serializer knew exactly two keys, so any other top-level field the
|
|
@@ -7799,14 +8454,17 @@ def child_agent_registrations(
|
|
|
7799
8454
|
"""THE Codex child-registration projection — `(tier, description, config path, config
|
|
7800
8455
|
content)` per spawnable tier, in the order the backend receives them.
|
|
7801
8456
|
|
|
7802
|
-
Pure
|
|
7803
|
-
consumers — `run_contract` states it,
|
|
7804
|
-
|
|
7805
|
-
|
|
7806
|
-
|
|
7807
|
-
one tier at another tier's
|
|
7808
|
-
|
|
7809
|
-
|
|
8457
|
+
Pure in what it writes — nothing — but not in what it reads: the authored template
|
|
8458
|
+
files and `XDG_CACHE_HOME`. One value, three consumers — `run_contract` states it,
|
|
8459
|
+
`codex_agent_configs` materialises the content at the path, and `project_args` writes
|
|
8460
|
+
the description and that path into argv — for the same reason `review_mcp_servers` is
|
|
8461
|
+
one value: the template a tier resolves to decides BOTH halves of what the backend is
|
|
8462
|
+
handed, and neither reached the contract. Repointing one tier at another tier's
|
|
8463
|
+
template therefore changed the child's description and its config path under a
|
|
8464
|
+
byte-identical contract, which is the one thing the contract's own tail may not be
|
|
8465
|
+
false about (spec round 6, L7). And one CALL: `project_args` computes this once and
|
|
8466
|
+
hands the same list to all three, because a projection that rereads its sources gives
|
|
8467
|
+
each extra call a chance to answer differently (spec round 7, L7).
|
|
7810
8468
|
|
|
7811
8469
|
The path is the config's IDENTITY, not an incidental location: its directory is a
|
|
7812
8470
|
digest of the whole rendered set, so any edit to any template moves every path — and
|
|
@@ -7883,11 +8541,16 @@ def child_agent_registrations(
|
|
|
7883
8541
|
|
|
7884
8542
|
|
|
7885
8543
|
def codex_agent_configs(
|
|
7886
|
-
plan: dict[str, Any], materialize: bool
|
|
8544
|
+
plan: dict[str, Any], materialize: bool,
|
|
8545
|
+
registrations: list[tuple[str, str, pathlib.Path, str]] | None = None,
|
|
7887
8546
|
) -> dict[str, tuple[pathlib.Path, str]]:
|
|
7888
8547
|
"""The registration projection, written to disk. Everything decided is decided above;
|
|
7889
|
-
this adds only the bytes and the failure modes writing them has.
|
|
7890
|
-
|
|
8548
|
+
this adds only the bytes and the failure modes writing them has. A caller that also
|
|
8549
|
+
renders the contract passes the projection it rendered, so what is materialised is
|
|
8550
|
+
the value the contract states rather than a second computation of it (spec round 7,
|
|
8551
|
+
L7)."""
|
|
8552
|
+
if registrations is None:
|
|
8553
|
+
registrations = child_agent_registrations(plan)
|
|
7891
8554
|
cache_root = registrations[0][2].parent
|
|
7892
8555
|
# Every filesystem error here becomes a LaunchError, because `materialize` is exactly
|
|
7893
8556
|
# the difference between --dry-run and a real launch: the projection skips these writes
|
|
@@ -8063,7 +8726,18 @@ def _cross_review_route(
|
|
|
8063
8726
|
return " ".join(parts)
|
|
8064
8727
|
|
|
8065
8728
|
|
|
8066
|
-
def run_contract(
|
|
8729
|
+
def run_contract(
|
|
8730
|
+
plan: dict[str, Any],
|
|
8731
|
+
mcp_registrations: list[tuple[str, str, list[str]]] | None = None,
|
|
8732
|
+
child_registrations: list[tuple[str, str, pathlib.Path, str]] | None = None,
|
|
8733
|
+
) -> str:
|
|
8734
|
+
"""The launch contract. A caller that also builds argv — `project_args` — passes the
|
|
8735
|
+
MCP and child registration projections it computed, so this text and that argv are
|
|
8736
|
+
generated from ONE call of each producer: the producers reread template files, the
|
|
8737
|
+
environment and PATH, so calling them once per consumer let a mid-launch change put
|
|
8738
|
+
one value here and another in argv under a byte-identical contract (spec round 7,
|
|
8739
|
+
L7). A caller that renders only the contract omits them and the projections are
|
|
8740
|
+
computed here — there is no argv beside the text to diverge from."""
|
|
8067
8741
|
main_tier = plan["main_tier"]
|
|
8068
8742
|
inactive = inactive_tiers(plan)
|
|
8069
8743
|
if plan["delegation"]:
|
|
@@ -8152,7 +8826,9 @@ def run_contract(plan: dict[str, Any]) -> str:
|
|
|
8152
8826
|
# Rendered from the whole triple, quoted as JSON, because reconciling this against argv
|
|
8153
8827
|
# is the point: a command or an argument carrying a space would otherwise read as two.
|
|
8154
8828
|
# Empty on every route that registers nothing, which includes every legacy one.
|
|
8155
|
-
registrations =
|
|
8829
|
+
registrations = (
|
|
8830
|
+
review_mcp_servers(plan) if mcp_registrations is None else mcp_registrations
|
|
8831
|
+
)
|
|
8156
8832
|
mcp_clause = ""
|
|
8157
8833
|
if registrations:
|
|
8158
8834
|
rendered = "; ".join(
|
|
@@ -8181,7 +8857,11 @@ def run_contract(plan: dict[str, Any]) -> str:
|
|
|
8181
8857
|
if plan["delegation"] and plan["host"] == "codex":
|
|
8182
8858
|
rendered_children = "; ".join(
|
|
8183
8859
|
f"{tier} = {json.dumps(description)} at {json.dumps(str(path))}"
|
|
8184
|
-
for tier, description, path, _ in
|
|
8860
|
+
for tier, description, path, _ in (
|
|
8861
|
+
child_agent_registrations(plan)
|
|
8862
|
+
if child_registrations is None
|
|
8863
|
+
else child_registrations
|
|
8864
|
+
)
|
|
8185
8865
|
)
|
|
8186
8866
|
child_clause = (
|
|
8187
8867
|
f"Codex child agent registrations, in the order the backend receives them "
|
|
@@ -8340,9 +9020,12 @@ def review_mcp_servers(plan: dict[str, Any]) -> list[tuple[str, str, list[str]]]
|
|
|
8340
9020
|
|
|
8341
9021
|
THE registration projection, and the only one: `run_contract` states this value and
|
|
8342
9022
|
both argv builders write it, so the contract and the args cannot describe different
|
|
8343
|
-
servers
|
|
8344
|
-
|
|
8345
|
-
|
|
9023
|
+
servers — and `project_args` computes it ONCE, handing the same list to the contract
|
|
9024
|
+
and to argv, because command resolution rereads PATH and a second call is a second
|
|
9025
|
+
answer (spec round 7, L7). It carries the args as well as the name and command
|
|
9026
|
+
because a triple is what each backend receives — leaving them to a constant read at
|
|
9027
|
+
the argv site put a third of the registration outside the value the contract is
|
|
9028
|
+
generated from.
|
|
8346
9029
|
|
|
8347
9030
|
A rename here is a real change to what runs: the name below becomes the backend's
|
|
8348
9031
|
`mcp_servers.<name>` namespace, which is how a tool call addresses the server."""
|
|
@@ -8565,7 +9248,21 @@ def project_args(plan: dict[str, Any], materialize_agents: bool = True) -> list[
|
|
|
8565
9248
|
return []
|
|
8566
9249
|
main = plan["tiers"][plan["main_tier"]]
|
|
8567
9250
|
main_effort = tier_effort(plan, plan["main_tier"])
|
|
8568
|
-
|
|
9251
|
+
# Each registration projection is computed ONCE and every consumer below receives the
|
|
9252
|
+
# same value. One producer was not enough: the contract render and the argv builders
|
|
9253
|
+
# each CALLED it, and the producers reread template files, `XDG_CACHE_HOME` and PATH —
|
|
9254
|
+
# so a change landing between the two calls put one value in the contract and another
|
|
9255
|
+
# in argv, under a tail that promises what is described is what runs (spec round 7,
|
|
9256
|
+
# L7). The child projection is computed exactly where both of its consumers live:
|
|
9257
|
+
# codex argv with delegation on, and the contract's child clause, which renders under
|
|
9258
|
+
# the same condition.
|
|
9259
|
+
mcp_registrations = review_mcp_servers(plan)
|
|
9260
|
+
child_registrations = (
|
|
9261
|
+
child_agent_registrations(plan)
|
|
9262
|
+
if plan["delegation"] and host == "codex"
|
|
9263
|
+
else None
|
|
9264
|
+
)
|
|
9265
|
+
contract = run_contract(plan, mcp_registrations, child_registrations)
|
|
8569
9266
|
if host == "codex":
|
|
8570
9267
|
args = [
|
|
8571
9268
|
"--model", main["model"],
|
|
@@ -8582,12 +9279,14 @@ def project_args(plan: dict[str, Any], materialize_agents: bool = True) -> list[
|
|
|
8582
9279
|
policy_args = ["--sandbox", policy]
|
|
8583
9280
|
args += policy_args
|
|
8584
9281
|
if plan["delegation"]:
|
|
8585
|
-
for tier, (path, description) in codex_agent_configs(
|
|
9282
|
+
for tier, (path, description) in codex_agent_configs(
|
|
9283
|
+
plan, materialize_agents, child_registrations
|
|
9284
|
+
).items():
|
|
8586
9285
|
args += [
|
|
8587
9286
|
"-c", f"agents.{tier}.description={json.dumps(description)}",
|
|
8588
9287
|
"-c", f"agents.{tier}.config_file={json.dumps(str(path))}",
|
|
8589
9288
|
]
|
|
8590
|
-
for name, command, server_args in
|
|
9289
|
+
for name, command, server_args in mcp_registrations:
|
|
8591
9290
|
args += [
|
|
8592
9291
|
"-c", f"mcp_servers.{name}.enabled=true",
|
|
8593
9292
|
"-c", f"mcp_servers.{name}.command={json.dumps(command)}",
|
|
@@ -8609,7 +9308,7 @@ def project_args(plan: dict[str, Any], materialize_agents: bool = True) -> list[
|
|
|
8609
9308
|
else:
|
|
8610
9309
|
policy_args = ["--permission-mode", policy]
|
|
8611
9310
|
args += policy_args
|
|
8612
|
-
servers =
|
|
9311
|
+
servers = mcp_registrations
|
|
8613
9312
|
if servers:
|
|
8614
9313
|
mcp_config = {
|
|
8615
9314
|
"mcpServers": {
|
|
@@ -8851,6 +9550,28 @@ def parse_args(argv: list[str]) -> argparse.Namespace:
|
|
|
8851
9550
|
"--fold-receipts", nargs=3, metavar=("DIR", "PACKET_FILE", "MAIN_DISPATCH_ID"),
|
|
8852
9551
|
help=f"fold a directory of receipts into one {RECEIPT_BUNDLE_SCHEMA} bundle on stdout",
|
|
8853
9552
|
)
|
|
9553
|
+
# The criterion side of the same per-review file surface. Compile turns a criterion
|
|
9554
|
+
# document into the canonical findings schema and prints the packet record line;
|
|
9555
|
+
# check-findings is the structural accepting channel standalone; check-schema-flag
|
|
9556
|
+
# probes the installed backend for its structured-output flag, because a declared
|
|
9557
|
+
# capability is docs and the rule is probe-over-docs.
|
|
9558
|
+
parser.add_argument(
|
|
9559
|
+
"--compile-criterion", nargs=2, metavar=("CRITERION_FILE", "SCHEMA_OUT"),
|
|
9560
|
+
help=f"validate a criterion document, write its compiled findings schema, and "
|
|
9561
|
+
f"print the {CRITERION_RECORD_SCHEMA} record line the packet carries",
|
|
9562
|
+
)
|
|
9563
|
+
parser.add_argument(
|
|
9564
|
+
"--check-findings", nargs=2, metavar=("RESULT_FILE", "SCHEMA_FILE"),
|
|
9565
|
+
help="refuse a findings result that fails the compiled criterion schema, each "
|
|
9566
|
+
"violation by name; the same check runs inside --emit-receipt when "
|
|
9567
|
+
f"${RECEIPT_CRITERION_ENV} is set",
|
|
9568
|
+
)
|
|
9569
|
+
parser.add_argument(
|
|
9570
|
+
"--check-schema-flag", metavar="PROBE_HOST",
|
|
9571
|
+
help="probe the resolved backend binary for its structured-output flag; exit 0 "
|
|
9572
|
+
f"when registered, {SCHEMA_FLAG_ABSENT_EXIT} when the binary runs and does "
|
|
9573
|
+
f"not register it",
|
|
9574
|
+
)
|
|
8854
9575
|
# REMAINDER and not "+": the adapter's own command line carries flags, and "+" stops
|
|
8855
9576
|
# at the first token starting with a dash — which handed `--model` to the host
|
|
8856
9577
|
# positional and rejected the run before the adapter was ever dispatched.
|
|
@@ -8866,7 +9587,8 @@ def parse_args(argv: list[str]) -> argparse.Namespace:
|
|
|
8866
9587
|
args = parser.parse_args(argv)
|
|
8867
9588
|
hostless = (
|
|
8868
9589
|
args.verify_receipts or args.emit_receipt or args.fold_receipts
|
|
8869
|
-
or args.check_adapter
|
|
9590
|
+
or args.check_adapter or args.compile_criterion or args.check_findings
|
|
9591
|
+
or args.check_schema_flag
|
|
8870
9592
|
)
|
|
8871
9593
|
if args.host is None and not hostless:
|
|
8872
9594
|
parser.error("the following arguments are required: host")
|
|
@@ -8941,9 +9663,25 @@ def verify_receipts_command(
|
|
|
8941
9663
|
# under audit. Both halves, because either alone is satisfiable by the wrong thing —
|
|
8942
9664
|
# a digest agreeing with the anchor says these bytes were hashed, and the plan the
|
|
8943
9665
|
# bytes carry says WHICH review they were hashed for.
|
|
9666
|
+
# Whether the plan declared the criterion discipline: read from the rows' rendered
|
|
9667
|
+
# instructions, where the launch snapshotted it — a ReviewPlan/v1 field would be a
|
|
9668
|
+
# schema change restating what the instruction already carries.
|
|
9669
|
+
criterion_active = any(
|
|
9670
|
+
CRITERION_CLAUSE_MARKER in row.instruction
|
|
9671
|
+
for row in (report.base, *report.methods)
|
|
9672
|
+
if row.status != STATUS_DROPPED
|
|
9673
|
+
)
|
|
9674
|
+
criterion_digest: "str | None" = None
|
|
8944
9675
|
if packet_path is not None:
|
|
8945
9676
|
packet_file = pathlib.Path(packet_path).expanduser()
|
|
8946
|
-
|
|
9677
|
+
# ONE byte snapshot: hashing the path and then reopening it as text let the
|
|
9678
|
+
# equality speak for one file state and the plan/criterion extraction for
|
|
9679
|
+
# another (criterion round, #6 — the emit-side twin is #4).
|
|
9680
|
+
try:
|
|
9681
|
+
packet_bytes = packet_file.read_bytes()
|
|
9682
|
+
except OSError as exc:
|
|
9683
|
+
raise LaunchError(f"cannot read the packet {packet_file}: {exc}") from exc
|
|
9684
|
+
digest = hashlib.sha256(packet_bytes).hexdigest()
|
|
8947
9685
|
anchor = bundle.get("packet_sha256") if isinstance(bundle, dict) else None
|
|
8948
9686
|
if digest != anchor:
|
|
8949
9687
|
raise LaunchError(
|
|
@@ -8952,8 +9690,8 @@ def verify_receipts_command(
|
|
|
8952
9690
|
f"these bytes"
|
|
8953
9691
|
)
|
|
8954
9692
|
try:
|
|
8955
|
-
packet_text =
|
|
8956
|
-
except
|
|
9693
|
+
packet_text = packet_bytes.decode("utf-8")
|
|
9694
|
+
except UnicodeDecodeError as exc:
|
|
8957
9695
|
raise LaunchError(f"cannot read the packet {packet_file}: {exc}") from exc
|
|
8958
9696
|
carried = extract_review_plan_v1(packet_text)
|
|
8959
9697
|
if carried is None:
|
|
@@ -8966,6 +9704,16 @@ def verify_receipts_command(
|
|
|
8966
9704
|
f"the supplied packet carries a different {REVIEW_PLAN_SCHEMA} record than "
|
|
8967
9705
|
f"{plan_path}; these receipts evidence a review of another plan"
|
|
8968
9706
|
)
|
|
9707
|
+
if criterion_active:
|
|
9708
|
+
# A plan whose rows carry the discipline clause declared a criterion, and a
|
|
9709
|
+
# packet that omits the record cannot verify: extract_criterion refuses the
|
|
9710
|
+
# absence, the malformed record and the second record each by name. The
|
|
9711
|
+
# digest is a RECOMPILATION from the packet's own bytes — never read off
|
|
9712
|
+
# the receipts, which are the artifacts under audit.
|
|
9713
|
+
document = extract_criterion(packet_text, "the supplied packet")
|
|
9714
|
+
criterion_digest = hashlib.sha256(
|
|
9715
|
+
criterion_schema_bytes(document)
|
|
9716
|
+
).hexdigest()
|
|
8969
9717
|
# The offer that SERVES THE ROW'S HOST, selected by `select_capability_offer` — the
|
|
8970
9718
|
# same function `derive_review_mechanism` seats a method with, rather than a second
|
|
8971
9719
|
# loop written to the same description. The first offer for the operation was taken
|
|
@@ -9020,9 +9768,26 @@ def verify_receipts_command(
|
|
|
9020
9768
|
method, capability, row_host
|
|
9021
9769
|
)["evidence"]
|
|
9022
9770
|
verified, verdicts = verify_review_receipts(
|
|
9023
|
-
report, bundle, required_controls, required_evidence
|
|
9771
|
+
report, bundle, required_controls, required_evidence, criterion_digest
|
|
9024
9772
|
)
|
|
9025
|
-
|
|
9773
|
+
if not criterion_active:
|
|
9774
|
+
criterion_note = ""
|
|
9775
|
+
elif criterion_digest is not None:
|
|
9776
|
+
criterion_note = (
|
|
9777
|
+
" criterion=declared and bound: receipts carrying criterion_schema_sha256 "
|
|
9778
|
+
"were held to the schema recompiled from the packet's record; a receipt "
|
|
9779
|
+
"without it ran prose discipline — steering, not structural enforcement"
|
|
9780
|
+
)
|
|
9781
|
+
else:
|
|
9782
|
+
criterion_note = (
|
|
9783
|
+
" criterion=declared but UNBOUND: no packet was supplied, so the declared "
|
|
9784
|
+
"criterion was recompiled against nothing and no schema digest above was "
|
|
9785
|
+
"adjudicated"
|
|
9786
|
+
)
|
|
9787
|
+
print(render_receipt_verdicts(
|
|
9788
|
+
verified, verdicts, packet_bound=packet_path is not None,
|
|
9789
|
+
criterion_note=criterion_note,
|
|
9790
|
+
))
|
|
9026
9791
|
return 0 if verified.achievement == ACHIEVEMENT_COMPLETE else 1
|
|
9027
9792
|
|
|
9028
9793
|
|
|
@@ -9072,6 +9837,14 @@ def main(argv: list[str]) -> int:
|
|
|
9072
9837
|
return emit_receipt_command(*args.emit_receipt, evidence=args.evidence)
|
|
9073
9838
|
if args.fold_receipts:
|
|
9074
9839
|
return fold_receipts_command(*args.fold_receipts)
|
|
9840
|
+
if args.compile_criterion:
|
|
9841
|
+
return compile_criterion_command(*args.compile_criterion)
|
|
9842
|
+
if args.check_findings:
|
|
9843
|
+
return check_findings_command(*args.check_findings)
|
|
9844
|
+
if args.check_schema_flag:
|
|
9845
|
+
return check_schema_flag_command(
|
|
9846
|
+
args.check_schema_flag, args.config.expanduser()
|
|
9847
|
+
)
|
|
9075
9848
|
if args.check_adapter:
|
|
9076
9849
|
if len(args.check_adapter) < 2:
|
|
9077
9850
|
raise LaunchError("--check-adapter takes a seat and then the adapter command")
|