diff --git a/infra/deploy-runner/nodedc-b2-vps-deploy b/infra/deploy-runner/nodedc-b2-vps-deploy index 91d8641..afa79f3 100755 --- a/infra/deploy-runner/nodedc-b2-vps-deploy +++ b/infra/deploy-runner/nodedc-b2-vps-deploy @@ -197,6 +197,25 @@ PHASE_FILE_SHA256 = { }, } +# Exact immutable baselines from terminally accepted predecessor artifacts. +# This is intentionally keyed by both patch id and artifact digest so a later +# artifact cannot inherit compatibility from a merely similar deployment. +ACCEPTED_PREDECESSOR_FILE_SHA256 = { + "foundation": { + ( + "device-edge-vps-foundation-20260806-003", + "1be852f144e9f0fea32af70bebd07a2607b6a1818825094bd4c1b4062064716a", + ): { + "vps/config/00-nodedc-b2-vps.conf": + "cc94d0579f85d0af9746b9ce760bc72980f4a22fb59027e1f5f9c7bf3aaebd64", + "vps/config/nftables-foundation.conf": + "4d44f902d8d98d1aa8506fca9d9582e700f6424def2b1d667ab2cd5a5ee84934", + "deployment/device-edge-vps-foundation-v1.json": + "317c98b42520fff3238275908482de7aa611b4ee41c6b1f8062abd2730ab072a", + }, + }, +} + class DeployError(RuntimeError): pass @@ -455,8 +474,24 @@ def assert_management_key(): die("verified Mac management key is missing") +def phase_file_sha256(phase: str): + expected = dict(PHASE_FILE_SHA256[phase]) + accepted = [ + record + for record in journal_records(APPLIED_JOURNAL) + if record.get("phase") == phase and record.get("status") == "ok" + ] + if len(accepted) == 1: + record = accepted[0] + identity = (record.get("patch"), record.get("sha256")) + expected.update( + ACCEPTED_PREDECESSOR_FILE_SHA256.get(phase, {}).get(identity, {}) + ) + return expected + + def source_file_state(phase: str): - expected = PHASE_FILE_SHA256[phase] + expected = phase_file_sha256(phase) actual = {} for relative, digest in expected.items(): path = LIVE_ROOT / relative diff --git a/infra/deploy-runner/test_device_edge_vps_artifact.py b/infra/deploy-runner/test_device_edge_vps_artifact.py index 93c91dc..db399d4 100644 --- a/infra/deploy-runner/test_device_edge_vps_artifact.py +++ b/infra/deploy-runner/test_device_edge_vps_artifact.py @@ -261,6 +261,50 @@ class DeviceEdgeVpsArtifactTest(unittest.TestCase): self.assertIn("command_transport=disabled", rendered) self.assertIn("gelios=untouched", rendered) + def test_source_baseline_is_pinned_to_the_exact_accepted_predecessor(self): + with tempfile.TemporaryDirectory(prefix="nodedc-vps-baseline-") as directory: + journal = Path(directory) / "applied.jsonl" + old_journal = RUNNER.APPLIED_JOURNAL + RUNNER.APPLIED_JOURNAL = journal + try: + journal.write_text(json.dumps({ + "patch": "device-edge-vps-foundation-20260806-003", + "phase": "foundation", + "sha256": ( + "1be852f144e9f0fea32af70bebd07a2607b6a1818825094bd4c1b4062064716a" + ), + "status": "ok", + }) + "\n", encoding="utf-8") + expected = RUNNER.phase_file_sha256("foundation") + self.assertEqual( + expected["vps/config/00-nodedc-b2-vps.conf"], + "cc94d0579f85d0af9746b9ce760bc72980f4a22fb59027e1f5f9c7bf3aaebd64", + ) + self.assertEqual( + expected["vps/config/nftables-foundation.conf"], + "4d44f902d8d98d1aa8506fca9d9582e700f6424def2b1d667ab2cd5a5ee84934", + ) + self.assertEqual( + expected["deployment/device-edge-vps-foundation-v1.json"], + "317c98b42520fff3238275908482de7aa611b4ee41c6b1f8062abd2730ab072a", + ) + + journal.write_text(json.dumps({ + "patch": "device-edge-vps-foundation-20260806-003", + "phase": "foundation", + "sha256": "0" * 64, + "status": "ok", + }) + "\n", encoding="utf-8") + unexpected = RUNNER.phase_file_sha256("foundation") + self.assertEqual( + unexpected["vps/config/00-nodedc-b2-vps.conf"], + RUNNER.PHASE_FILE_SHA256[ + "foundation" + ]["vps/config/00-nodedc-b2-vps.conf"], + ) + finally: + RUNNER.APPLIED_JOURNAL = old_journal + def test_units_and_firewalls_keep_the_required_boundaries(self): source_root = SCRIPT_DIR.parent.parent / "device-plane" foundation = (source_root / "vps/config/nftables-foundation.conf").read_text()