Skip to content

Commit 028c2e7

Browse files
PhysShellclaude
andcommitted
fix(certify): green — bridge-authorized R_C and the pre-rename originals boundary
R1. removed_all_own001 is now required to equal the bridge-authorized R_C, not merely baseline - postimage. _authorized_removed reconstructs the candidate bridge Step-12-locally, mirroring the frozen fix_delta._bridge_r_c: each accepted candidate's K=(file, component, event, handler) — component the containing type's simple name, event source.event — maps to a validated baseline group (single distinct observation shape, exact eligible/observation cardinality, no mixed convert/manual under one K), and R_C draws one observation per converted candidate. An unrelated non-subscription OWN001 that also vanished is now DELTA_BINDING even when honestly listed and matching baseline - postimage. R2. A second, originals-only revalidation runs immediately before the atomic rename. revalidate_certification_inputs is split into _revalidate_originals (the five inputs + the original bundle + the original --ref-dir closure re-derived from the caller directories) plus the materialized-slot check; publish_certification gains a pre_rename hook invoked after the staging guard and immediately before os.rename; run_certify wires it with a fresh throwaway scratch dir (the execution root is already gone) that is strictly removed afterwards. A privileged mutation of any authoritative input between the first revalidation and publication is now ISOLATION. Tier A: 111/111. ruff + mypy --strict clean. Frozen Steps 0-11 untouched. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JxKjqdGEFzq4UzZupw379G
1 parent 857af59 commit 028c2e7

1 file changed

Lines changed: 115 additions & 27 deletions

File tree

‎ownlang/fix_certify.py‎

Lines changed: 115 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,9 @@
3737
import os
3838
import re
3939
import stat
40+
import tempfile
4041
from collections import Counter
42+
from collections.abc import Callable
4143
from typing import Any
4244

4345
from ownlang import fix_delta as fd
@@ -337,7 +339,7 @@ def _bind_gate(gate_bytes: bytes, auth: Any, plan_bytes: bytes, manifest_data: b
337339

338340

339341
def _bind_delta(delta_bytes: bytes, auth: Any, plan_bytes: bytes, candidates_bytes: bytes,
340-
hashes: dict[str, str], gate_sha256: str, rel: str,
342+
candidates: dict[str, Any], hashes: dict[str, str], gate_sha256: str, rel: str,
341343
class_fqn: str) -> dict[str, Any]:
342344
"""Bind the Step 10 delta-result as the upstream authority. The frozen fix_target.bind_delta
343345
proves canonical bytes, the exact top-level + consumed shapes, all seventeen checks pass, and
@@ -377,7 +379,7 @@ def _bind_delta(delta_bytes: bytes, auth: Any, plan_bytes: bytes, candidates_byt
377379
expected_kind = "single-file+refdirs" if scope.get("reference_dir_count") else "single-file"
378380
if scope.get("closure_kind") != expected_kind:
379381
raise CertifyError(cat, "delta-result.json closure_kind is not its derived kind")
380-
_validate_delta_representable(delta, auth, cat)
382+
_validate_delta_representable(delta, auth, candidates, cat)
381383
return delta
382384

383385

@@ -517,11 +519,63 @@ def _check_self_manifest(files: Any, digest: Any, cat: str, name: str) -> None:
517519
raise CertifyError(cat, f"{name} deployment manifest digest is not self-consistent")
518520

519521

520-
def _validate_delta_representable(delta: dict[str, Any], auth: Any, cat: str) -> None:
522+
def _bridge_key_of(candidate: dict[str, Any]) -> tuple[str, str, str, str]:
523+
"""The K=(file, component, event, handler) the frozen core runner derives for a candidate:
524+
component is the containing type's simple name, event is source.event (or event when the source
525+
is 'this')."""
526+
src = candidate["source"]
527+
event = candidate["event"] if src == "this" else f"{src}.{candidate['event']}"
528+
return (candidate["file"], candidate["containing_type"].rsplit(".", 1)[-1], event,
529+
candidate["handler"])
530+
531+
532+
def _authorized_removed(convert: list[str], manual: list[str], candidates: dict[str, Any],
533+
baseline_all001: list[dict[str, Any]], cat: str) -> Counter[str]:
534+
"""Reconstruct R_C — the multiset of core OWN001 observations AUTHORIZED for removal — from the
535+
accepted candidate bridge, mirroring the frozen fix_delta._bridge_r_c: every accepted candidate
536+
maps through K to a validated baseline group (a single distinct observation shape, exact
537+
eligible/observation cardinality, no mixed convert/manual under one K); R_C draws one
538+
observation per converted candidate. This is the frozen 'removed core == authorized'
539+
authorization, so an unrelated non-subscription OWN001 that also vanished cannot ride a
540+
hash-consistent delta."""
541+
by_fid = {c["finding_id"]: _bridge_key_of(c) for c in candidates["candidates"]}
542+
core_by_k: dict[tuple[str, str, str, str], list[dict[str, Any]]] = {}
543+
for obs in baseline_all001:
544+
core_by_k.setdefault((obs["file"], obs["component"], obs["event"], obs["handler"]),
545+
[]).append(obs)
546+
convert_set = set(convert)
547+
action_of: dict[tuple[str, str, str, str], set[str]] = {}
548+
accepted_by_k: Counter[tuple[str, str, str, str]] = Counter()
549+
for fid in convert + manual:
550+
key = by_fid.get(fid)
551+
if key is None:
552+
raise CertifyError(cat, f"accepted candidate {fid} has no bridge key")
553+
action_of.setdefault(key, set()).add("convert" if fid in convert_set else "manual")
554+
accepted_by_k[key] += 1
555+
for key, actions in action_of.items():
556+
if len(actions) > 1:
557+
raise CertifyError(cat, f"bridge key {key} mixes convert and manual candidates")
558+
for key, count in accepted_by_k.items():
559+
group = core_by_k.get(key, [])
560+
if not group:
561+
raise CertifyError(cat, f"accepted candidate bridge key {key} has no baseline OWN001")
562+
if len({fd._ckey(o) for o in group}) != 1:
563+
raise CertifyError(cat, f"bridge key {key} maps to multiple distinct observations")
564+
if len(group) != count:
565+
raise CertifyError(cat, f"eligible/observation cardinality mismatch for {key}")
566+
r_c: Counter[str] = Counter()
567+
for fid in convert:
568+
r_c[fd._ckey(core_by_k[by_fid[fid]][0])] += 1
569+
return r_c
570+
571+
572+
def _validate_delta_representable(delta: dict[str, Any], auth: Any, candidates: dict[str, Any],
573+
cat: str) -> None:
521574
"""The representable Step 10 invariants beyond the frozen consumed-shape binding: the closed
522575
baseline / postimage / delta observation schemas, deterministic canonical ordering, the
523-
subscription / core-multiset / OWN050 / idempotence equations, and the self-describing
524-
toolchain-manifest digests. Every OWN001 observation carries a boolean advisory (O1)."""
576+
subscription / core-multiset / OWN050 / idempotence equations (including that the removed core
577+
OWN001 multiset equals the bridge-authorized R_C, not merely baseline - postimage), and the
578+
self-describing toolchain-manifest digests. Every OWN001 observation's advisory is a boolean."""
525579
convert = sorted(auth.applied)
526580
manual = sorted(auth.manual)
527581
c_set, m_set = set(convert), set(manual)
@@ -565,8 +619,14 @@ def _validate_delta_representable(delta: dict[str, Any], auth: Any, cat: str) ->
565619
p_all = Counter(fd._ckey(o) for o in images["postimage"]["all"])
566620
if p_all - b_all:
567621
raise CertifyError(cat, "a new core OWN001 observation appeared in the postimage")
568-
if Counter(fd._ckey(o) for o in removed) != (b_all - p_all):
622+
removed_multiset = Counter(fd._ckey(o) for o in removed)
623+
if removed_multiset != (b_all - p_all):
569624
raise CertifyError(cat, "removed_all_own001 != the baseline-minus-postimage multiset")
625+
# the removed core OWN001 must be EXACTLY those authorized for conversion (R_C from the
626+
# candidate bridge) — an unrelated OWN001 that also vanished is a DELTA_BINDING refusal.
627+
r_c = _authorized_removed(convert, manual, candidates, images["baseline"]["all"], cat)
628+
if removed_multiset != r_c:
629+
raise CertifyError(cat, "removed_all_own001 != the bridge-authorized R_C")
570630
b50 = Counter(fd._ckey(o) for o in images["baseline"]["own050"])
571631
p50 = Counter(fd._ckey(o) for o in images["postimage"]["own050"])
572632
if p50 - b50:
@@ -707,21 +767,20 @@ def _bind_reference(work: str, ref_dirs: list[str], delta: dict[str, Any],
707767
return slot_dirs, evidence
708768

709769

710-
# --- original-authority revalidation (deepened in the enforcement commit) -----------
770+
# --- original-authority revalidation ------------------------------------------------
711771

712772

713-
def revalidate_certification_inputs(
773+
def _revalidate_originals(
714774
plan_path: str, candidates_path: str, gate_path: str, delta_path: str, target_path: str,
715775
bundle_phys: str, rel: str, ref_dirs: list[str], plan_bytes: bytes, candidates_bytes: bytes,
716776
gate_bytes: bytes, delta_bytes: bytes, target_bytes: bytes, manifest_data: bytes,
717-
patch_bytes: bytes, postimage_bytes: bytes, work: str, slot_dirs: list[str],
718-
slot_evidence: list[dict[str, Any]]) -> None:
719-
"""Before publication, re-check that every authoritative input still equals what was bound —
720-
the plain input files, the ORIGINAL bundle (exact layout + manifest / patch / postimage bytes),
721-
the ORIGINAL --ref-dir closures re-derived from the caller directories, and the materialized
722-
reference slots. Validation of the materialized copies is never treated as proof the caller
723-
inputs stayed unchanged: the originals are re-scanned in their own right. Any post-binding drift
724-
(including a change in an originally-empty ref-dir) is ISOLATION."""
777+
patch_bytes: bytes, postimage_bytes: bytes, slot_evidence: list[dict[str, Any]],
778+
scratch: str) -> None:
779+
"""Re-check every ORIGINAL authoritative input equals what was bound: the five input files, the
780+
original bundle (exact layout + manifest / patch / postimage bytes), and the original --ref-dir
781+
closure re-derived from the caller directories (into `scratch`, a fresh dir) against the initial
782+
snapshot. Any post-binding drift (including a change in an originally-empty ref-dir) is
783+
ISOLATION. This is the originals-only boundary; the materialized copies are never proof."""
725784
for path, original, label in ((plan_path, plan_bytes, "--plan"),
726785
(candidates_path, candidates_bytes, "--candidates"),
727786
(gate_path, gate_bytes, "--gate"),
@@ -730,7 +789,6 @@ def revalidate_certification_inputs(
730789
if _snap(path, ISOLATION, label) != original:
731790
raise CertifyError(ISOLATION, f"{label} changed during certification")
732791

733-
# the ORIGINAL bundle: exact entry set + postimage subtree + the three artifacts' bytes.
734792
_require_bundle_layout(bundle_phys, rel, ISOLATION)
735793
if _snap(os.path.join(bundle_phys, "apply-manifest.json"), ISOLATION,
736794
"apply-manifest.json") != manifest_data:
@@ -741,16 +799,26 @@ def revalidate_certification_inputs(
741799
"postimage") != postimage_bytes:
742800
raise CertifyError(ISOLATION, "the original postimage changed after binding")
743801

744-
# the ORIGINAL --ref-dirs: re-derive the whole closure and require the initial snapshot exactly.
745802
try:
746-
_reval_dirs, reval_evidence = fd.snapshot_reference_closure(
747-
os.path.join(work, "reval"), ref_dirs)
803+
_reval_dirs, reval_evidence = fd.snapshot_reference_closure(scratch, ref_dirs)
748804
except fd.DeltaError as exc:
749805
raise CertifyError(ISOLATION, str(exc)) from exc
750806
if reval_evidence != slot_evidence:
751807
raise CertifyError(ISOLATION, "an original --ref-dir closure changed after binding")
752808

753-
# the MATERIALIZED reference slots.
809+
810+
def revalidate_certification_inputs(
811+
plan_path: str, candidates_path: str, gate_path: str, delta_path: str, target_path: str,
812+
bundle_phys: str, rel: str, ref_dirs: list[str], plan_bytes: bytes, candidates_bytes: bytes,
813+
gate_bytes: bytes, delta_bytes: bytes, target_bytes: bytes, manifest_data: bytes,
814+
patch_bytes: bytes, postimage_bytes: bytes, work: str, slot_dirs: list[str],
815+
slot_evidence: list[dict[str, Any]]) -> None:
816+
"""The full pre-publication revalidation: the originals-only boundary plus the MATERIALIZED
817+
reference slots (re-hashed from the execution root). Any post-binding drift is ISOLATION."""
818+
_revalidate_originals(plan_path, candidates_path, gate_path, delta_path, target_path,
819+
bundle_phys, rel, ref_dirs, plan_bytes, candidates_bytes, gate_bytes,
820+
delta_bytes, target_bytes, manifest_data, patch_bytes, postimage_bytes,
821+
slot_evidence, os.path.join(work, "reval"))
754822
for i, ev in enumerate(slot_evidence):
755823
dll = os.path.join(slot_dirs[i], ev["relative_path"].rsplit("/", 1)[-1])
756824
if _sha_bytes(_snap(dll, ISOLATION, "reference slot")) != ev["sha256"]:
@@ -805,11 +873,14 @@ def _claim_workdir_strict(parent_phys: str) -> str:
805873
raise CertifyError(PUBLICATION, "could not claim a work directory")
806874

807875

808-
def publish_certification(out: str, protected: list[str], evidence_bytes: bytes) -> str:
876+
def publish_certification(out: str, protected: list[str], evidence_bytes: bytes,
877+
pre_rename: Callable[[], None] | None = None) -> str:
809878
"""Stage EXACTLY certification-result.json in a claimed private work directory off the output
810-
parent and publish it with ONE atomic rename. OUTPUT_DIR is absent on refusal; an existing
811-
OUTPUT_DIR is PUBLICATION; any staging / cleanup OSError is PUBLICATION. No filesystem operation
812-
runs after a successful rename."""
879+
parent and publish it with ONE atomic rename. `pre_rename`, if given, is the final
880+
originals-only revalidation and runs immediately before the rename (after the staging guard), so
881+
a privileged mutation of an authoritative input between the first revalidation and publication
882+
is still caught. OUTPUT_DIR is absent on refusal; an existing OUTPUT_DIR is PUBLICATION; any
883+
staging / cleanup OSError is PUBLICATION. No filesystem op runs after a successful rename."""
813884
try:
814885
out_phys, parent_phys, _root = _out_parent(out, out)
815886
except GateError as exc:
@@ -828,6 +899,8 @@ def publish_certification(out: str, protected: list[str], evidence_bytes: bytes)
828899
raise CertifyError(PUBLICATION,
829900
"the out-dir destination changed before publication")
830901
_require_single(workdir)
902+
if pre_rename is not None: # the final originals-only boundary, just before the rename
903+
pre_rename()
831904
os.rename(workdir, out_phys)
832905
except CertifyError:
833906
raise
@@ -949,7 +1022,7 @@ def run_certify(plan_path: str, candidates_path: str, bundle: str, gate_path: st
9491022
pre_sha256, binfo["post_sha256"])
9501023

9511024
# [5] delta (DELTA_BINDING).
952-
delta = _bind_delta(delta_bytes, auth, plan_bytes, candidates_bytes, hashes,
1025+
delta = _bind_delta(delta_bytes, auth, plan_bytes, candidates_bytes, candidates, hashes,
9531026
hashes["gate_result_sha256"], rel, class_fqn)
9541027
hashes["delta_result_sha256"] = _sha_bytes(delta_bytes)
9551028

@@ -980,12 +1053,27 @@ def run_certify(plan_path: str, candidates_path: str, bundle: str, gate_path: st
9801053
slot_evidence, converted)
9811054
evidence_bytes = _canonical_bytes(evidence)
9821055
publish_protected = [binfo["bundle_phys"], work, *input_parents, *ref_dirs]
1056+
1057+
def _pre_rename() -> None:
1058+
# the SECOND originals-only boundary, run inside publish immediately before the atomic
1059+
# rename. The execution root is already gone, so the closure is re-derived into a fresh
1060+
# throwaway scratch (outside every protected root) that is strictly removed afterwards.
1061+
scratch = tempfile.mkdtemp(prefix="owen-certify-reval-")
1062+
try:
1063+
_revalidate_originals(
1064+
plan_path, candidates_path, gate_path, delta_path, target_path,
1065+
binfo["bundle_phys"], rel, ref_dirs, plan_bytes, candidates_bytes, gate_bytes,
1066+
delta_bytes, target_bytes, binfo["manifest_data"], binfo["patch_bytes"],
1067+
binfo["postimage_bytes"], slot_evidence, os.path.join(scratch, "reval"))
1068+
finally:
1069+
_rmtree_strict(scratch)
1070+
9831071
try:
9841072
ft._remove_root_strict(work)
9851073
except ft.TargetError as exc:
9861074
raise CertifyError(exc.category, str(exc)) from exc
9871075
work_removed = True
988-
return publish_certification(out, publish_protected, evidence_bytes)
1076+
return publish_certification(out, publish_protected, evidence_bytes, _pre_rename)
9891077
except BaseException:
9901078
if not work_removed:
9911079
try:

0 commit comments

Comments
 (0)