3737import os
3838import re
3939import stat
40+ import tempfile
4041from collections import Counter
42+ from collections .abc import Callable
4143from typing import Any
4244
4345from 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
339341def _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