From 954528a7c5a0e27e2e50b642373168fa0a550aba Mon Sep 17 00:00:00 2001 From: Agent Control Runner Date: Wed, 21 Oct 2026 03:50:39 +0900 Subject: [PATCH] =?UTF-8?q?P28-01-RESTART:=20=E7=8F=BEHEAD=E3=81=AE?= =?UTF-8?q?=E6=80=A7=E8=83=BD=E5=9F=BA=E6=BA=96=EF=BC=9A=E9=80=94=E4=B8=AD?= =?UTF-8?q?=E4=BF=9D=E5=AD=98=E3=83=BB=E5=88=A5PID=E5=86=8D=E9=96=8B?= =?UTF-8?q?=E3=81=AE=E5=86=8D=E7=8F=BE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../p28-01/restart/probe/check_restart.py | 3 + .../p28-01/restart/probe/restart_probe.gd | 11 +++- .../p28-01/restart/probe/test_restart.py | 63 +++++++++++++++++++ 3 files changed, 76 insertions(+), 1 deletion(-) diff --git a/docs/plans/original-completion-2026-10-02/execution/p28-01/restart/probe/check_restart.py b/docs/plans/original-completion-2026-10-02/execution/p28-01/restart/probe/check_restart.py index 8dd6c3e..ce2fb42 100644 --- a/docs/plans/original-completion-2026-10-02/execution/p28-01/restart/probe/check_restart.py +++ b/docs/plans/original-completion-2026-10-02/execution/p28-01/restart/probe/check_restart.py @@ -314,6 +314,8 @@ def json_sha(value): # and Python. Fixed decimal form is valid JSON; keys and strings remain verbatim. def encode(v): if isinstance(v, dict): + require(len({str(k) for k in v}) == len(v), "canonical_key_collision") + require(all(type(k) is str for k in v), "canonical JSON requires text keys") return "{" + ",".join(json.dumps(k, ensure_ascii=False) + ":" + encode(v[k]) for k in sorted(v)) + "}" if isinstance(v, list): @@ -335,6 +337,7 @@ def check_sidecar(public, sidecar): "sidecar root/missing") check_no_metrics(public) full = sidecar["report"] + require(full.get("stop_reason") != "canonical_key_collision", "canonical_key_collision") require(public == redact_report(full), "public redaction differs from W2") restored = copy.deepcopy(public) for path, payload in payload_paths(restored).items(): diff --git a/docs/plans/original-completion-2026-10-02/execution/p28-01/restart/probe/restart_probe.gd b/docs/plans/original-completion-2026-10-02/execution/p28-01/restart/probe/restart_probe.gd index e9b88eb..c864d33 100644 --- a/docs/plans/original-completion-2026-10-02/execution/p28-01/restart/probe/restart_probe.gd +++ b/docs/plans/original-completion-2026-10-02/execution/p28-01/restart/probe/restart_probe.gd @@ -19,6 +19,7 @@ var boundary: Variant = null var at_390_event_verbatim: Array = [] var at_390_pending: Array = [] var redaction_ok := true +var canonical_key_collision := false const SAVE_EVENTS := ["SAVE_CHECKPOINT_SUCCEEDED", "SAVE_CHECKPOINT_FAILED", "SAVE_LOADED", "SAVE_LOAD_FAILED", "SAVE_MIGRATION_SUCCEEDED", "SAVE_MIGRATION_FAILED", "SAVE_RECOVERY_LOADED", "SAVE_RECOVERY_CANDIDATE_LOADED", "SAVE_RECOVERY_FAILED"] @@ -26,7 +27,13 @@ const SAVE_EVENTS := ["SAVE_CHECKPOINT_SUCCEEDED", "SAVE_CHECKPOINT_FAILED", "SA func canonical_json(value: Variant) -> String: if value is Dictionary: var keys: Array = value.keys() - keys.sort() + keys.sort_custom(func(a, b): return str(a) < str(b)) + for index: int in range(1, keys.size()): + if str(keys[index - 1]) == str(keys[index]): + canonical_key_collision = true + # Unwind serialization before emitting failure evidence. + call_deferred("finish", "canonical_key_collision") + return "" var parts: PackedStringArray = [] for key: Variant in keys: parts.append(JSON.stringify(str(key)) + ":" + canonical_json(value[key])) @@ -567,6 +574,8 @@ func public_has_forbidden(value: Variant) -> bool: func finish(reason: String) -> void: if finished: return + if canonical_key_collision: + reason = "canonical_key_collision" finished = true report["stop_reason"] = reason report["key_forms"] = key_forms diff --git a/docs/plans/original-completion-2026-10-02/execution/p28-01/restart/probe/test_restart.py b/docs/plans/original-completion-2026-10-02/execution/p28-01/restart/probe/test_restart.py index fdbbebc..b2b39d3 100644 --- a/docs/plans/original-completion-2026-10-02/execution/p28-01/restart/probe/test_restart.py +++ b/docs/plans/original-completion-2026-10-02/execution/p28-01/restart/probe/test_restart.py @@ -252,6 +252,69 @@ class RestartTests(unittest.TestCase): self.assertIn("test_restart.py", self.inventory_before) self.assertEqual(self.here_inventory(), self.inventory_before) + def actual_canonical_sidecar(self): + relative = Path("native-e524ff2-canonical-failure-r01/report_full.json") + roots = [check.ROOT / "build", + check.ROOT.parent / ".runtime" / "P28-01-RESTART" / "build"] + if os.environ.get("BUILD_OUTPUT_DIR"): + roots.insert(0, Path(os.environ["BUILD_OUTPUT_DIR"])) + path = next((root / relative for root in roots if (root / relative).is_file()), None) + self.assertIsNotNone(path, "required retained native-e524ff2 sidecar is missing") + raw = path.read_bytes() + self.addCleanup(lambda: self.assertEqual(path.read_bytes(), raw)) + return json.loads(raw) + + def test_z1_actual_text_key_order_hash(self): + sidecar = self.actual_canonical_sidecar() + events = sidecar["at_390_event_verbatim"] + self.assertEqual(len(events), 16) + first = events[0] + self.assertEqual(first["event_id"], "domain_event.000000000009") + self.assertEqual(first["absolute_minute"], 362) + self.assertEqual(first["payload"], + {"old_state": 1, "reason": "ENTRY_COMPLETED", "state": 2}) + self.assertEqual(check.json_sha(first), + "9ca80bec0c605ecd288f7671952d30fe66c67f291c5a618ea54623f07394edb9") + for key in ("raw_event_sha256", "canonical_sha256"): + self.assertEqual(sidecar["report"]["at_390"]["events"][0][key], + "f28afa0899d7efea59ab99ae06276ba354779a73cac705fa4801162f27c592d7") + with self.assertRaisesRegex(ValueError, "at390 raw/canonical SHA"): + check.check_sidecar(check.redact_report(sidecar["report"]), sidecar) + + def test_z1_all_actual_event_bindings(self): + original = self.actual_canonical_sidecar() + sidecar = copy.deepcopy(original) + full = sidecar["report"] + self.assertEqual(len(sidecar["at_390_event_verbatim"]), 16) + self.assertEqual(len(full["at_390"]["events"]), 16) + for event, summary in zip(sidecar["at_390_event_verbatim"], full["at_390"]["events"]): + summary["raw_event_sha256"] = check.json_sha(event) + summary["canonical_sha256"] = check.json_sha( + check.canonical_event(event, full["event_id"])) + self.assertEqual(sidecar["at_390_event_verbatim"], original["at_390_event_verbatim"]) + self.assertEqual(check.check_sidecar(check.redact_report(full), sidecar), full) + for index in range(16): + with self.subTest(index=index): + changed = copy.deepcopy(sidecar) + changed["report"]["at_390"]["events"][index]["raw_event_sha256"] = "0" * 64 + with self.assertRaisesRegex(ValueError, "at390 raw/canonical SHA"): + check.check_sidecar(check.redact_report(changed["report"]), changed) + + def test_z2_collision_rejected(self): + for ambiguous in ({1: "numeric", "1": "text"}, + {"nested": [{1: "numeric", "1": "text"}]}): + with self.subTest(value=ambiguous), self.assertRaisesRegex( + ValueError, "canonical_key_collision"): + check.json_sha(ambiguous) + full = report() + full["stop_reason"] = "canonical_key_collision" + public, sidecar = evidence(full) + with self.assertRaisesRegex(ValueError, "canonical_key_collision"): + check.check_report(public, sidecar) + public["stop_reason"] = "complete" + with self.assertRaises(ValueError): + check.check_report(public, sidecar) + def rejects(self, original, mutations, validator): for name, mutation in mutations.items(): with self.subTest(rejection=name):