diff --git a/Engine/build-locked-engine.sh b/Engine/build-locked-engine.sh index 9f3a4ffd..40cb5c67 100755 --- a/Engine/build-locked-engine.sh +++ b/Engine/build-locked-engine.sh @@ -95,7 +95,8 @@ done < <( ) stage=$(mktemp -d /private/tmp/omarchy-locked-engine-build.XXXXXX) -trap 'rm -rf -- "$stage"' EXIT +temporary= +trap 'rm -rf -- "$stage"; if [[ -n $temporary ]]; then rm -f -- "$temporary"; fi' EXIT cp -a "$checkout/." "$stage/" git -C "$stage" apply "$engine_root/$(jq -r '.downstream_overlay.patch.path' "$lock")" @@ -143,11 +144,10 @@ source_date_epoch=$(jq -r '.build_toolchain.source_date_epoch' "$lock") --mtime="@$source_date_epoch" --owner=0 --group=0 --numeric-owner \ -C "$stage/package" -cf - . | "$gzip" -n -9 >"$temporary" chmod 644 "$temporary" -mv -f "$temporary" "$artifact" -python3 "$engine_root/verify-archive-modes.py" "$artifact" +python3 "$engine_root/verify-archive-modes.py" "$temporary" -actual_size=$(stat -f '%z' "$artifact") -actual_digest=$(/usr/bin/shasum -a 256 "$artifact") +actual_size=$(stat -f '%z' "$temporary") +actual_digest=$(/usr/bin/shasum -a 256 "$temporary") actual_digest=${actual_digest%% *} expected_filename=$(jq -er '.validation_artifact.filename' "$lock") expected_size=$(jq -er '.validation_artifact.size_bytes' "$lock") @@ -172,4 +172,7 @@ if [[ $actual_digest != "$expected_digest" ]]; then echo "Built engine digest does not reproduce source lock" >&2 exit 1 fi +# A failed rebuild must never replace an earlier authenticated artifact. +mv -f "$temporary" "$artifact" +temporary= printf '%s %s %s bytes\n' "$actual_digest" "$artifact" "$actual_size" diff --git a/Engine/overlay/src/omarchy_asahi.py b/Engine/overlay/src/omarchy_asahi.py index 84fa9250..63ae4301 100644 --- a/Engine/overlay/src/omarchy_asahi.py +++ b/Engine/overlay/src/omarchy_asahi.py @@ -428,6 +428,9 @@ def rewrite_existing_content(self, plan): names = payload.namelist() if len(names) != len(set(names)): raise AsahiAdapterError("ambiguous repair payload") + replacements = [] + # Validate the entire member table before opening any disk for write. + # A missing later image must not leave earlier partitions replaced. for role in ("stub", "efi", "boot", "root"): expected = self.manifest["replacement_content"][role] member = expected["payload_member"] @@ -450,6 +453,15 @@ def rewrite_existing_content(self, plan): info = payload.getinfo(member) if info.file_size != expected["size_bytes"]: raise AsahiAdapterError("repair payload size changed") + try: + # Check local headers, encryption and decoder availability + # without decompressing multi-gigabyte images twice. + with payload.open(info): + pass + except (RuntimeError, NotImplementedError, zipfile.BadZipFile) as error: + raise AsahiAdapterError("unsupported repair payload member") from error + replacements.append((role, expected, info)) + for role, expected, info in replacements: identifier = self._partition_identifier(role) digest = hashlib.sha256() written = 0 @@ -809,6 +821,12 @@ def prepare_target(self, plan): name=plan.source_identifier, free=False, ) + if ( + source.offset + source.size != plan.offset_bytes + plan.length_bytes + or source.offset >= plan.offset_bytes + or source.type != "Apple_APFS" + ): + raise AsahiAdapterError("approved source partition changed") new_size = source.size - plan.length_bytes if new_size < plan.minimum_container_bytes: raise AsahiAdapterError( @@ -858,13 +876,8 @@ def install_stub_and_esp(self, plan): ) return self._installed_evidence(plan) - def validate_installed_checkpoint( - self, - plan, - target_evidence, - installed_evidence, - ): - """Re-read the exact completed stage-one state without mutation.""" + def validate_prepared_checkpoint(self, plan, target_evidence): + """Reconcile the recorded APFS identity before resuming any mutation.""" self._require_preflight() target = self._parse_checkpoint_evidence( target_evidence, @@ -876,6 +889,36 @@ def validate_installed_checkpoint( "uuid", }, ) + if target["plan_digest"] != plan.plan_digest: + raise AsahiAdapterError("installed checkpoint plan changed") + self._refresh_parts() + try: + prepared = self._find_prepared_target(plan) + except AsahiAdapterError as error: + raise AsahiAdapterError( + "prepared resume target does not match checkpoint" + ) from error + # Bind the adapter only after every recorded identity field matches. + actual = { + "plan_digest": plan.plan_digest, + "partition_identifier": prepared.name, + "offset_bytes": prepared.offset, + "size_bytes": prepared.size, + "uuid": prepared.uuid, + } + if actual != target: + raise AsahiAdapterError("prepared resume target does not match checkpoint") + self.target_part = prepared + self.installer.part = prepared + + def validate_installed_checkpoint( + self, + plan, + target_evidence, + installed_evidence, + ): + """Re-read the exact completed stage-one state without mutation.""" + self.validate_prepared_checkpoint(plan, target_evidence) installed = self._parse_checkpoint_evidence( installed_evidence, { @@ -887,18 +930,9 @@ def validate_installed_checkpoint( "populated_partitions", }, ) - if ( - target["plan_digest"] != plan.plan_digest - or installed["plan_digest"] != plan.plan_digest - ): + if installed["plan_digest"] != plan.plan_digest: raise AsahiAdapterError("installed checkpoint plan changed") - self._refresh_parts() - self.target_part = self._find_prepared_target(plan) - self.installer.part = self.target_part - if self._target_evidence(plan) != target_evidence: - raise AsahiAdapterError("prepared target identity changed") - recorded = installed["populated_partitions"] if ( not isinstance(recorded, list) diff --git a/Engine/overlay/src/omarchy_repair.py b/Engine/overlay/src/omarchy_repair.py index cb93917f..ab39ceae 100644 --- a/Engine/overlay/src/omarchy_repair.py +++ b/Engine/overlay/src/omarchy_repair.py @@ -468,6 +468,10 @@ def _validate_manifest(manifest): }: raise InPlaceRepairError("invalid preserved repair content") continue + capacity = next(item["size_bytes"] for item in partitions if item["role"] == role) + if any(identity["size_bytes"] > capacity or identity["size_bytes"] % 4096 + for identity in (before, after)): + raise InPlaceRepairError("invalid repair content size") if ( not isinstance(member, str) or not member diff --git a/Engine/overlay/src/omarchy_stage1.py b/Engine/overlay/src/omarchy_stage1.py index 24022bc1..32d7c286 100644 --- a/Engine/overlay/src/omarchy_stage1.py +++ b/Engine/overlay/src/omarchy_stage1.py @@ -71,6 +71,20 @@ def run_stage1(plan, journal, adapter): if completed is not None: return completed + # Historical inventory admits a resumed run, but cannot establish that its + # prepared target still belongs to this install. Re-read checkpoint identity + # before recording any new mutation intent. Fresh runs need no readback. + if "apfs-target-prepared" in journal.checkpoints: + arguments = [plan, journal.checkpoint_evidence("apfs-target-prepared")] + validator_name = "validate_prepared_checkpoint" + if "stub-and-esp-installed" in journal.checkpoints: + validator_name = "validate_installed_checkpoint" + arguments.append(journal.checkpoint_evidence("stub-and-esp-installed")) + validator = getattr(adapter, validator_name, None) + if not callable(validator): + raise Stage1Error(f"adapter does not implement {validator_name}") + validator(*arguments) + # The privileged helper runs with umask 077 so nothing it creates is # readable by other users. The stub volume and its ESP are not the # helper's private state: macOS and the unprivileged inspection that diff --git a/Engine/overlay/tests/fixtures/repair-manifest-canary-2004.json b/Engine/overlay/tests/fixtures/repair-manifest-canary-2004.json new file mode 100644 index 00000000..6fd315f1 --- /dev/null +++ b/Engine/overlay/tests/fixtures/repair-manifest-canary-2004.json @@ -0,0 +1 @@ +{"approved_extent":{"length_bytes":137438953472,"offset_bytes":857747943424,"source_identifier":"disk0s2"},"device_identifier":"apple,j314s","disk_identity":"sha256:0f0f46de025fa26858187abf44fdb0dc88f35b5551ff3f7ac511c7de7b7b7f9f","existing_content":{"boot":{"sha256":"sha256:1d1388e963387936b61ba940d27605a12432032dcd9d36c9f86620dd407aec4b","size_bytes":2147483648},"efi":{"sha256":"sha256:a8d18241c9876f4571f7a5c365fef22e5aabb7999d1267bb2f6251fa8b098a9b","size_bytes":524288000},"root":{"sha256":"sha256:8dd7ac3d1789d871a40092f1a13fb7ab0fb4aab2b73e387a4c66c249f61ef83f","size_bytes":34359738368},"stub":{"sha256":"sha256:8315c0037745a0df14dfc8898c9258c0cc59fa3aeedd7c9409710b3db1e217df","size_bytes":2499805184}},"operation":"repair-installed-system","partitions":[{"filesystem":{"label":"Omarchy","type":"apfs","uuid":"0fb80edb-92e4-4c26-8b34-227ff4503576"},"identifier":"disk0s3","offset_bytes":857747943424,"partition_type":"Apple_APFS","partition_uuid":"0fb80edb-92e4-4c26-8b34-227ff4503576","role":"stub","size_bytes":2499805184},{"filesystem":{"label":"EFI - OMARC","type":"fat32","uuid":"4f4d-5801"},"identifier":"disk0s4","offset_bytes":860247748608,"partition_type":"EFI","partition_uuid":"319ef3d8-7176-4d33-9851-6a0144328e1c","role":"efi","size_bytes":524288000},{"filesystem":{"label":"OMARCHY_BOOT","type":"ext4","uuid":"4f4d5801-424f-4f54-8000-000000000001"},"identifier":"disk0s5","offset_bytes":860772036608,"partition_type":"Linux Filesystem","partition_uuid":"7620a7b4-8548-46fd-8ec5-847aa15c2da3","role":"boot","size_bytes":2147483648},{"filesystem":{"label":"OMARCHY_ROOT","type":"btrfs","uuid":"4f4d5801-524f-4f54-8000-000000000001"},"identifier":"disk0s6","offset_bytes":862919520256,"partition_type":"Linux Filesystem","partition_uuid":"76d01f38-70e2-4cb5-81fc-a9b41d2d479a","role":"root","size_bytes":132267376640}],"repair_id":"m1-canary-2004-0.6.0","replacement_content":{"boot":{"payload_member":"boot.img","sha256":"sha256:6a3b0a54f9f588d615a96510fb3f5a33a14897ac9a7ea6026792f877d9a7a7f9","size_bytes":2147483648},"efi":{"payload_member":null,"sha256":"sha256:a8d18241c9876f4571f7a5c365fef22e5aabb7999d1267bb2f6251fa8b098a9b","size_bytes":524288000},"root":{"payload_member":"root.img","sha256":"sha256:395a3db964c58733dd2e820baa04e3b3a0ed39025c3240867e431bdd3af7a8b7","size_bytes":34359738368},"stub":{"payload_member":null,"sha256":"sha256:8315c0037745a0df14dfc8898c9258c0cc59fa3aeedd7c9409710b3db1e217df","size_bytes":2499805184}},"schema_version":1,"semantic_contract":{"boot_policy_retry":"checkpoint-bound","partitioning":"forbidden","readback":"exhaustive"},"store_identifier":"disk0"} \ No newline at end of file diff --git a/Engine/overlay/tests/test_omarchy_asahi.py b/Engine/overlay/tests/test_omarchy_asahi.py index c19dc231..ea6c73bd 100644 --- a/Engine/overlay/tests/test_omarchy_asahi.py +++ b/Engine/overlay/tests/test_omarchy_asahi.py @@ -333,6 +333,43 @@ def test_changed_free_extent_is_rejected_before_partition_creation(self): self.assertEqual(dutil.add_calls, []) + def test_resize_geometry_drift_is_rejected_before_mutation(self): + source = FakePart("disk0s2", offset=0, size=500 * GIB, free=False) + plan = SimpleNamespace(**{ + **self.plan.__dict__, "candidate_kind": "resize", + "source_identifier": source.name, "minimum_container_bytes": 320 * GIB, + }) + dutil = FakeDiskUtil([[source]]) + adapter = self._adapter(FakeInstaller(dutil)) + adapter.preflight(plan) + with self.assertRaisesRegex(AsahiAdapterError, "approved source partition changed"): + adapter.prepare_target(plan) + self.assertEqual(dutil.resize_calls, []) + self.assertEqual(dutil.add_calls, []) + + def test_prepared_checkpoint_rejects_each_changed_identity_field(self): + initial = self._adapter(FakeInstaller(FakeDiskUtil([[self.free]]))) + initial.preflight(self.plan) + evidence = initial.prepare_target(self.plan) + for field, value in ( + ("uuid", "DIFFERENT-UUID"), ("name", "disk0s9"), + ("size", 2 * GIB + 4096), ("offset", self.plan.offset_bytes + 4096), + ("type", "Linux Filesystem"), + ): + with self.subTest(field=field): + part = FakePart("disk0s4", offset=self.plan.offset_bytes, + size=2 * GIB, free=False) + setattr(part, field, value) + installer = FakeInstaller(FakeDiskUtil([[part]])) + adapter = self._adapter(installer) + adapter.preflight(self.plan) + with self.assertRaisesRegex( + AsahiAdapterError, "prepared resume target does not match checkpoint" + ): + adapter.validate_prepared_checkpoint(self.plan, evidence) + self.assertIsNone(adapter.target_part) + self.assertFalse(any(call[0] == "prepare_volume" for call in installer.ins.calls)) + def test_retry_reconciles_the_exact_prepared_apfs_partition(self): prepared = FakePart( "disk0s4", @@ -725,6 +762,20 @@ def test_readback_is_exhaustive_for_declared_writes_and_skips_preserved_content( ): self.adapter.validate_repaired_content(self.plan) + def test_invalid_later_repair_member_does_not_write_earlier_partition(self): + for field, value in (("payload_member", "repair/missing.img"), ("size_bytes", 4096)): + with self.subTest(field=field): + expected = self.manifest["replacement_content"]["root"] + original = expected[field] + expected[field] = value + try: + with self.assertRaises(AsahiAdapterError): + self.adapter.rewrite_existing_content(self.plan) + self.assertEqual(self.content["disk0s5"], b"boot-old") + self.assertEqual(self.opened_partitions, []) + finally: + expected[field] = original + @staticmethod def _identity(content): return { diff --git a/Engine/overlay/tests/test_omarchy_repair.py b/Engine/overlay/tests/test_omarchy_repair.py index b5ac0a09..2f2b4cea 100644 --- a/Engine/overlay/tests/test_omarchy_repair.py +++ b/Engine/overlay/tests/test_omarchy_repair.py @@ -96,6 +96,39 @@ def test_exact_existing_layout_produces_one_repair_candidate(self): self.assertEqual(candidate["length_bytes"], 137_438_953_472) self.assertRegex(candidate["identity_digest"], r"^sha256:[0-9a-f]{64}$") + def test_recorded_physical_canary_manifest_remains_a_repair_candidate(self): + # Exact published bytes and provenance are recorded in docs/engine-hardening.md. + raw = (Path(__file__).parent / "fixtures" / + "repair-manifest-canary-2004.json").read_bytes() + self.assertEqual( + hashlib.sha256(raw).hexdigest(), + "3fd0b3033d91666965e157a0fa271523aa5cafa3826721f53789c13d8edaa059", + ) + self.manifest = json.loads(raw) + inventory = collect_repair_inventory( + SimpleNamespace(sys_disk="disk0", parts=self.parts), + self.manifest, + disk_identity_reader=lambda _: self.manifest["disk_identity"], + filesystem_identity_reader=self._filesystem_identity, + ) + self.assertEqual(len(inventory["candidates"]), 1) + self.assertEqual(inventory["candidates"][0]["kind"], "repair") + self.assertEqual(inventory["candidates"][0]["length_bytes"], 137_438_953_472) + + def test_repair_image_must_fit_its_partition_and_raw_device_alignment(self): + for side in ("existing_content", "replacement_content"): + for size in (1, 4097, self.parts[3].size + 4096): + with self.subTest(side=side, size=size): + manifest = self._manifest() + manifest[side]["boot"]["size_bytes"] = size + with self.assertRaisesRegex(InPlaceRepairError, "repair content size"): + collect_repair_inventory( + SimpleNamespace(sys_disk="disk0", parts=self.parts), + manifest, + disk_identity_reader=lambda _: "sha256:" + "5" * 64, + filesystem_identity_reader=self._filesystem_identity, + ) + def test_release_and_canary_share_the_concrete_repair_executor_adapter(self): plan = SimpleNamespace(plan_digest="a" * 64) release_adapter = FakeRepairAdapter() diff --git a/Engine/overlay/tests/test_omarchy_stage1.py b/Engine/overlay/tests/test_omarchy_stage1.py index 7cdf4722..805f3b7b 100644 --- a/Engine/overlay/tests/test_omarchy_stage1.py +++ b/Engine/overlay/tests/test_omarchy_stage1.py @@ -134,12 +134,43 @@ def test_recovery_handoff_can_retry_after_authentication_failure(self): outcome = run_stage1(self.plan, resumed, retry) self.assertEqual(outcome, "awaiting_recovery") - self.assertEqual(retry.calls, ["prepare_recovery_handoff"]) + self.assertEqual(retry.calls, [ + "validate_installed_checkpoint", "prepare_recovery_handoff", + ]) self.assertEqual( resumed.completion_outcome, "awaiting_recovery", ) + def test_resume_validates_prepared_target_before_recording_mutation_intent(self): + self.journal.event("apfs_preparation_started") + self.journal.checkpoint("apfs-target-prepared", "apfs_preparation", b"target") + adapter = RecordingStage1Adapter(failure="validate_prepared_checkpoint") + before = self.path.read_bytes() + with self.assertRaisesRegex(RuntimeError, "synthetic crash"): + run_stage1(self.plan, self.journal, adapter) + self.assertEqual(adapter.calls, ["validate_prepared_checkpoint"]) + self.assertEqual(self.path.read_bytes(), before) + self.assertFalse(self.journal.has_event("stub_and_esp_started")) + + def test_resume_validates_installed_content_before_recovery(self): + first = RecordingStage1Adapter(failure="prepare_recovery_handoff") + with self.assertRaises(RuntimeError): + run_stage1(self.plan, self.journal, first) + retry = RecordingStage1Adapter(failure="validate_installed_checkpoint") + with self.assertRaisesRegex(RuntimeError, "synthetic crash"): + run_stage1(self.plan, Journal(str(self.path)), retry) + self.assertEqual(retry.calls, ["validate_installed_checkpoint"]) + + def test_resume_without_a_checkpoint_validator_fails_closed(self): + self.journal.event("apfs_preparation_started") + self.journal.checkpoint("apfs-target-prepared", "apfs_preparation", b"target") + adapter = RecordingStage1Adapter() + adapter.validate_prepared_checkpoint = None + with self.assertRaisesRegex(Stage1Error, "validate_prepared_checkpoint"): + run_stage1(self.plan, self.journal, adapter) + self.assertEqual(adapter.calls, []) + def test_explicit_recovery_retry_validates_checkpoint_then_runs_bless_only(self): target_evidence = b'{"partition_identifier":"disk0s4"}' installed_evidence = b'{"populated_partitions":["disk0s5"]}' @@ -277,9 +308,13 @@ def validate_installed_checkpoint( target_evidence, installed_evidence, ): - self.calls.append("validate_installed_checkpoint") + self._record("validate_installed_checkpoint", plan) self.validated_evidence = (target_evidence, installed_evidence) + def validate_prepared_checkpoint(self, plan, target_evidence): + self._record("validate_prepared_checkpoint", plan) + self.validated_evidence = target_evidence + def _record(self, name, plan): self.calls.append(name) if self.failure == name: diff --git a/Engine/rebuild-python-overlay.py b/Engine/rebuild-python-overlay.py index 3046760d..a51c3491 100644 --- a/Engine/rebuild-python-overlay.py +++ b/Engine/rebuild-python-overlay.py @@ -15,7 +15,7 @@ BASE_SHA256 = '9e9277384b6c9e8b269cc79b1b24df7bfcdcbb898a596a677b74d1d18050aebe' BASE_COMMIT = 'f0469cea0899f3efed8efead604174c7a53c4451' -VERSION = 'v0.9.2-omarchy.28' +VERSION = 'v0.9.2-omarchy.29' _SPEC = importlib.util.spec_from_file_location( 'verify_source_lock', Path(__file__).resolve().parent / 'verify-source-lock.py') diff --git a/Engine/source-lock.json b/Engine/source-lock.json index a3b34a86..92170212 100644 --- a/Engine/source-lock.json +++ b/Engine/source-lock.json @@ -141,7 +141,7 @@ } }, "downstream_overlay": { - "version": "v0.9.2-omarchy.28", + "version": "v0.9.2-omarchy.29", "capability": "candidate_bound_full_os_stage_one_authenticated_recovery", "engine_modes": [ "inspect", @@ -171,7 +171,7 @@ { "path": "overlay/src/omarchy_asahi.py", "destination": "src/omarchy_asahi.py", - "sha256": "a5d887b1d07a1977ff397980d247c9138a61cfffb675ce51aa18d07589030f0c" + "sha256": "f35d60c78897ca04e39d81d920f64bcdf2853571256d63e6d519332ad74b6d3d" }, { "path": "overlay/src/omarchy_contract.py", @@ -191,7 +191,7 @@ { "path": "overlay/src/omarchy_repair.py", "destination": "src/omarchy_repair.py", - "sha256": "145feb79909ca056b6baf9bc6b67560f3714b97a63deaf95c2bbcf68979a10f9" + "sha256": "23c837f3090a93796718172291d8cfbbdd4082b2ce3b940df51a7bf970186d44" }, { "path": "overlay/src/omarchy_runtime.py", @@ -201,12 +201,12 @@ { "path": "overlay/src/omarchy_stage1.py", "destination": "src/omarchy_stage1.py", - "sha256": "206f1decf3176359f0e11b8a4d2afdc2997ff24fa9498fa057ddd2d75b295ea5" + "sha256": "c787a1e901c1c94f805ccce68123eaa532d454141f3b5ebf9c43afb410f912b8" }, { "path": "overlay/tests/test_omarchy_asahi.py", "destination": "tests/test_omarchy_asahi.py", - "sha256": "bf57c80bf26bbfc00a3cb24d1a5f8fc1196e8c155789fd4af97dddc0e4b7292a" + "sha256": "99ff0d7d433caab3910110b2ceaf260c080c7879d8c47a2b27c35cc6200b017e" }, { "path": "overlay/tests/test_omarchy_contract.py", @@ -226,7 +226,7 @@ { "path": "overlay/tests/test_omarchy_repair.py", "destination": "tests/test_omarchy_repair.py", - "sha256": "e88084083405308ccf5ea2a08383bf02c18a4d6d15e441a7b7d44afe37c4234b" + "sha256": "79de8c9746f21ec67b7294276c716f5b8c87cc5b37279d6ed633b9c4d3b94b24" }, { "path": "overlay/tests/test_omarchy_replace.py", @@ -241,7 +241,7 @@ { "path": "overlay/tests/test_omarchy_stage1.py", "destination": "tests/test_omarchy_stage1.py", - "sha256": "170989306f796c8baab19e715613be380f24f0646be2c6d8ffc565e2e676e4c0" + "sha256": "11d852eae2205b24197e5c838c608d295d9d254cf2ae80d843b91494a5c21d9c" }, { "path": "overlay/src/omarchy_image.py", @@ -258,7 +258,7 @@ "build_recipe": [ { "path": "build-locked-engine.sh", - "sha256": "1c1618fcf4a3d162d50f7560cc245be96c56d2ae4b68a0ef64fd3afbd07214c6" + "sha256": "1468cbe8b53739028122510e9779a05c6e58fcbb360787161b631b84f9891012" }, { "path": "verify-archive-modes.py", @@ -270,13 +270,13 @@ }, { "path": "rebuild-python-overlay.py", - "sha256": "3cc29ff5c7bd834b40a50ef2271417efcd33bc7bc747b11c743d5ba2042017c2" + "sha256": "63d6e35170a71e1533c9c5d0f7bb837837283bfd27f169aded10a30cfb10822e" } ], "validation_artifact": { - "filename": "installer-v0.9.2-omarchy.28.tar.gz", - "size_bytes": 17843348, - "sha256": "0cf1aa87760f90a545298b7cef737c9b497f2cad421d79ac59f557a81f2eb146", + "filename": "installer-v0.9.2-omarchy.29.tar.gz", + "size_bytes": 17844091, + "sha256": "3a87e43b023e050c725d2e005bddd3721cf00f5e77104635e6a1e35411804d50", "reproducibility_scope": "two-identical-python-overlay-repacks-authenticated-base", "signature": "absent", "metadata_sha256": "2e6181ce6b6e17c11039e04bfadade8d10ae0e11889885ad8c9960b68f179a5d" diff --git a/Engine/tests/test_build_locked_engine.py b/Engine/tests/test_build_locked_engine.py new file mode 100644 index 00000000..93dce15b --- /dev/null +++ b/Engine/tests/test_build_locked_engine.py @@ -0,0 +1,109 @@ +"""Exercise final-artifact admission with disposable, non-building tool fixtures.""" + +import hashlib +import io +import json +import os +from pathlib import Path +import shlex +import shutil +import subprocess +import sys +import tarfile +import tempfile +import unittest + + +class LockedEnginePublicationTests(unittest.TestCase): + def test_only_a_verified_build_replaces_the_previous_artifact(self): + for failure in ("mode", "size", "digest", None): + with self.subTest(failure=failure), tempfile.TemporaryDirectory() as temp: + root = Path(temp) + engine, checkout, cache, output, tools = ( + root / name for name in ("engine", "checkout", "cache", "output", "tools") + ) + for directory in (engine, checkout, cache, output, tools, root / "target"): + directory.mkdir() + script = engine / "build-locked-engine.sh" + shutil.copyfile(Path(__file__).resolve().parents[1] / script.name, script) + (engine / "verify-source-lock.py").write_text("") + shutil.copyfile(Path(__file__).resolve().parents[1] / "verify-archive-modes.py", + engine / "verify-archive-modes.py") + (engine / "metadata.json").write_text("{}") + (checkout / ".git").mkdir() + (checkout / "tests").mkdir() + (checkout / "tests/test_omarchy_fixture.py").write_text( + "import unittest\nclass Fixture(unittest.TestCase):\n def test_ready(self): pass\n") + (checkout / "src").mkdir() + for name in ("main.py", "omarchy_fixture.py"): + (checkout / "src" / name).write_text("") + certificate = checkout / "package/Frameworks/Python.framework/Versions/Current/etc/openssl/cert.pem" + certificate.parent.mkdir(parents=True) + certificate.write_text("fixture") + (checkout / "dl").mkdir() + (checkout / "dl/certifi-cacert-2026.07.22.pem").write_text("fixture") + self.executable(checkout / "build.sh", "exit 0") + + archive = root / "archive.tar" + with tarfile.open(archive, "w") as tar: + member = tarfile.TarInfo("fixture") + member.size = 3 + member.mode = 0o666 if failure == "mode" else 0o644 + tar.addfile(member, io.BytesIO(b"new")) + payload = archive.read_bytes() + tool_records = {} + for name in ("clang", "lld", "rustc", "cargo", "make", "bsdtar", "cpio", "gtar", "gzip", "seven_zip"): + body = 'if [[ ${1:-} == --version ]]; then echo "fixture 1"; exit; fi\n' + if name == "rustc": + body += "echo " + shlex.quote(str(root / "target")) + elif name == "seven_zip": + body += "printf '\\nfixture 1\\n'" + elif name == "gtar": + body += "cat " + shlex.quote(str(archive)) + elif name == "gzip": + body += "cat" + else: + body += "exit 0" + self.executable(tools / name, body) + tool_records[name] = {"path": str(tools / name), "version": "fixture 1"} + self.executable(tools / "git", "exit 0") + # Keep the macOS build script's scratch directory portable and isolated. + self.executable(tools / "mktemp", + 'if [[ ${1:-} == -d ]]; then exec /usr/bin/mktemp -d "$TMPDIR/build.XXXXXX"; ' + 'else exec /usr/bin/mktemp "$@"; fi') + self.executable(tools / "stat", "exec " + shlex.quote(sys.executable) + + ' -c \'import os,sys; print(os.stat(sys.argv[-1]).st_size)\' "$@"') + filename = "installer-fixture.tar.gz" + lock = { + "build_toolchain": {"tools": tool_records, "rust_target": "fixture", "source_date_epoch": 0}, + "build_inputs": [], + "downstream_overlay": {"patch": {"path": "unused.patch"}, "files": [], + "metadata": {"path": "metadata.json"}, "version": "fixture"}, + "validation_artifact": {"filename": filename, + "size_bytes": len(payload) + (failure == "size"), + "sha256": "0" * 64 if failure == "digest" else hashlib.sha256(payload).hexdigest()}, + } + (engine / "source-lock.json").write_text(json.dumps(lock)) + final = output / filename + final.write_bytes(b"previous-good-artifact") + result = subprocess.run( + ["bash", str(script), str(checkout), str(cache), str(output)], + env={**os.environ, "PATH": str(tools) + os.pathsep + os.environ["PATH"], "TMPDIR": str(root)}, + capture_output=True, text=True, timeout=30, + ) + self.assertEqual(result.returncode == 0, failure is None, result.stdout + result.stderr) + if failure: + expected = {"mode": "unsafe engine archive mode", "size": "size does not reproduce", + "digest": "digest does not reproduce"}[failure] + self.assertIn(expected, result.stdout + result.stderr) + self.assertEqual(final.read_bytes(), payload if failure is None else b"previous-good-artifact") + self.assertEqual(list(output.glob(".installer.*")), []) + + @staticmethod + def executable(path, body): + path.write_text("#!/bin/bash\nset -euo pipefail\n" + body + "\n") + path.chmod(0o755) + + +if __name__ == "__main__": + unittest.main() diff --git a/Packaging/build-app.sh b/Packaging/build-app.sh index 5ef6bd31..89011cfd 100755 --- a/Packaging/build-app.sh +++ b/Packaging/build-app.sh @@ -46,8 +46,8 @@ helper_identifier="$INSTALLER_HELPER_IDENTIFIER" app_name="$INSTALLER_APP_NAME.app" app_executable_name="OmarchyAppleInstallerApp" daemon_plist_name="$helper_identifier.plist" -engine_file_name="installer-v0.9.2-omarchy.28.tar.gz" -engine_digest="0cf1aa87760f90a545298b7cef737c9b497f2cad421d79ac59f557a81f2eb146" +engine_file_name="installer-v0.9.2-omarchy.29.tar.gz" +engine_digest="3a87e43b023e050c725d2e005bddd3721cf00f5e77104635e6a1e35411804d50" if [[ $signing_identity == "-" ]]; then client_requirement="identifier \"$app_identifier\"" diff --git a/Sources/OmarchyAppleInstaller/ValidationEngineArtifact.swift b/Sources/OmarchyAppleInstaller/ValidationEngineArtifact.swift index 0396ad71..87772128 100644 --- a/Sources/OmarchyAppleInstaller/ValidationEngineArtifact.swift +++ b/Sources/OmarchyAppleInstaller/ValidationEngineArtifact.swift @@ -18,11 +18,11 @@ /// An installation engine fix ships in a catalog without rebuilding /// and re-notarizing the app. Nothing may require them to be equal. public struct ValidationEngineArtifactLocator: Sendable { - public static let version = "v0.9.2-omarchy.28" - public static let fileName = "installer-v0.9.2-omarchy.28.tar.gz" + public static let version = "v0.9.2-omarchy.29" + public static let fileName = "installer-v0.9.2-omarchy.29.tar.gz" public static let expectedDigest = - "sha256:0cf1aa87760f90a545298b7cef737c9b497f2cad421d79ac59f557a81f2eb146" - public static let expectedSizeBytes: UInt64 = 17_843_348 + "sha256:3a87e43b023e050c725d2e005bddd3721cf00f5e77104635e6a1e35411804d50" + public static let expectedSizeBytes: UInt64 = 17_844_091 public init() {} diff --git a/Tests/OmarchyAppleInstallerTrustCoreTests/ValidationEngineArtifactTests.swift b/Tests/OmarchyAppleInstallerTrustCoreTests/ValidationEngineArtifactTests.swift index fafa9b55..df7e2bcc 100644 --- a/Tests/OmarchyAppleInstallerTrustCoreTests/ValidationEngineArtifactTests.swift +++ b/Tests/OmarchyAppleInstallerTrustCoreTests/ValidationEngineArtifactTests.swift @@ -8,19 +8,19 @@ func testM3CapableInspectionIdentityIsPinnedExactly() { XCTAssertEqual( ValidationEngineArtifactLocator.version, - "v0.9.2-omarchy.28" + "v0.9.2-omarchy.29" ) XCTAssertEqual( ValidationEngineArtifactLocator.fileName, - "installer-v0.9.2-omarchy.28.tar.gz" + "installer-v0.9.2-omarchy.29.tar.gz" ) XCTAssertEqual( ValidationEngineArtifactLocator.expectedDigest, - "sha256:0cf1aa87760f90a545298b7cef737c9b497f2cad421d79ac59f557a81f2eb146" + "sha256:3a87e43b023e050c725d2e005bddd3721cf00f5e77104635e6a1e35411804d50" ) XCTAssertEqual( ValidationEngineArtifactLocator.expectedSizeBytes, - 17_843_348 + 17_844_091 ) } diff --git a/docs/engine-hardening.md b/docs/engine-hardening.md new file mode 100644 index 00000000..fbdce5cd --- /dev/null +++ b/docs/engine-hardening.md @@ -0,0 +1,31 @@ +# Engine overlay hardening + +These engine overlay changes were split out of [PR 29](https://github.com/omacom/omarchy-mac-installer/pull/29), whose app-side hardening is recorded in `docs/battle-testing.md`. They change locked engine inputs, so they ship with a refreshed source lock and the reproducibly rebuilt engine `.29`. + +## Changes and regression coverage + +| Failure | Prevention | Regression coverage | +| --- | --- | --- | +| A resumed run encounters another APFS partition at the same location | Reconcile the saved target evidence before the next mutation; give a specific checkpoint-mismatch diagnostic and preserve the journal | Changed UUID, identifier, size, offset and type all produce the named failure; journal bytes remain unchanged; simulation retains the preparation checkpoint without enabling a fresh install | +| Container geometry changes during preflight | Check the source endpoint and APFS type before resizing | Changed source endpoint produces zero resize and partition-creation calls | +| A repair ZIP lacks a later image after an earlier partition has already been overwritten | Check every replacement's member, local header, size and decoder before opening any writable target; require manifest image sizes to fit their partitions and 4 KiB raw-device alignment | Missing/mis-sized later member leaves the earlier partition untouched; oversized and unaligned images produce no repair candidate | +| A non-reproducible engine build replaces the previous good output | Validate the temporary archive's modes, size and digest before atomic promotion; clean up on failure | Whole-script fixture uses mocked build tools and verifies the previous bytes survive all three rejection paths | + +## Repair manifest compatibility and producer contract + +The fixture `Engine/overlay/tests/fixtures/repair-manifest-canary-2004.json` preserves the exact bytes of a real manifest from the recorded physical M1 canary sequence 2004. It was extracted from [`sequence-2004-state.tar.gz` at source revision `92a9054f4565b37739ac3bd4f0fb4fcf8bd48625`](https://github.com/maralcbr/omarchy-mx-mac/blob/92a9054f4565b37739ac3bd4f0fb4fcf8bd48625/evidence/apple-silicon/2026-08-29-m1-canary-2004/sequence-2004-state.tar.gz), member `sequence-2004-ed71f0ec-1f5b-41c0-bb68-d27d89c54070/candidate/repair-manifest-2002.json`. The archive SHA-256 is `bfe1e461dae4745255e82cf281786a2cd554a9c0846ff5a400a6404e46ea88e5`; the exact manifest SHA-256 is `3fd0b3033d91666965e157a0fa271523aa5cafa3826721f53789c13d8edaa059`, pinned by the regression test. + +For every rewritten role (`replacement_content[role].payload_member` is non-null), producers must describe both the existing hashed content and replacement image as positive whole-byte sizes no larger than that role's partition and divisible by 4096. The payload member's uncompressed size must equal the replacement size. Preserved roles have a null member and identical existing/replacement content identities. This is the consumer's required contract, checked before repair writes; it is not a claim that an unavailable producer implementation enforces it. + +The recorded manifest meets that contract: boot has 2,147,483,648 existing and replacement bytes in a 2,147,483,648-byte partition; root has 34,359,738,368 existing and replacement bytes in a 132,267,376,640-byte partition. Both are 4096-aligned. Stub and EFI are preserved. The test passes the unmodified manifest through repair candidate discovery with mocked live disk/filesystem inspection and expects one candidate. It is a compatibility characterization, not a new failure reproduced against the old implementation. This archive was explicitly a non-release physical canary; it establishes compatibility with that real consumed artifact, not all published or future repair manifests. No current production manifest or producer implementation is inferred from it. A subsequent [release and producer evidence search](repair-manifest-release-research.md) verified signed public catalogs and inspected image-producer source, but did not locate a released repair manifest or its generator. No released repair manifest has shipped and repair has only run in canaries, so there is no producer to test against. The checksum-pinned canary fixture and the contract above are the accepted compatibility evidence, and that contract is what any future producer must meet. + +## Release boundary + +These edits ship in engine `.29` (`installer-v0.9.2-omarchy.29.tar.gz`, 17,844,091 bytes, SHA-256 `3a87e43b023e050c725d2e005bddd3721cf00f5e77104635e6a1e35411804d50`), recorded in `Engine/source-lock.json` with the refreshed source hashes and reproduced as described in `docs/extraction.md`. The packager, Swift locator and release-input templates pin `.29`. Before release, validate the assembled package and qualify interrupted stage-one resume and repair on authorized supported hardware. `apple,j614s` remains blocked. + +PR 29 teaches the app the engine's `prepared resume target does not match checkpoint` diagnostic, so no further Swift change is needed when this engine ships after it. + +## Remaining engine work + +1. **Repair durability (before enabling physical repair).** The repair writer still uses `fsync`; the fresh-install image writer has the macOS raw-device synchronization primitive. Reuse that primitive with a fake flush seam and verify it on authorized hardware. Malformed member tables now fail before writes, but corruption or I/O failure encountered during actual writing can still leave a partial repair. +2. **Geometry identity boundary.** The new resize check rejects endpoint drift but does not introduce a new UUID-bearing plan format or an OS-level lock against another disk management process. Revalidate live resize limits/identity as close to mutation as practical; qualify behavior if another process changes the disk. The existing engine and diskutil checks remain necessary. diff --git a/docs/extraction.md b/docs/extraction.md index 3f638f48..3859451f 100644 --- a/docs/extraction.md +++ b/docs/extraction.md @@ -81,4 +81,8 @@ Swift now withholds the resize drift margin before adopting the recommended (dou The catalog's installation engine is `installer-v0.9.2-omarchy.28.tar.gz`, 17,843,348 bytes, SHA-256 `0cf1aa87760f90a545298b7cef737c9b497f2cad421d79ac59f557a81f2eb146`: the `.27` overlay with the Recovery step from #39 added. Two repacks with macOS `/usr/bin/python3` 3.9.6 produced identical bytes; compared with `.27`, only `omarchy_asahi.py`, `omarchy_runtime.py` and `version.tag` changed. The bundled inspection pins and packager select `.28` too, so the release scripts, which take the catalog engine from `Packaging/build-app.sh`, publish the engine the templates name. -Ship installer **2.1.0 or later** together with the new engine and a signed catalog whose `installer.minimumVersion` is at least **2.1.0**. Both release-input templates carry this gate, and the catalog generator refuses a lower minimum for `.18` or newer engines in this lineage. Older installers then show the update-required message before decoding recommendation fields. The bundled inspection pins now select `.28`; signed production catalogs and frozen private-test catalogs are not changed by this source update. Artifact publication, catalog signing and physical installation qualification remain separate release steps. +## Engine overlay hardening (.29) + +Engine `.29` carries the overlay hardening split from PR 29 (see [engine-hardening.md](engine-hardening.md)): resumed stage one reconciles the saved APFS target before any mutation, preflight rejects a changed source container, and repair validates every payload member and image size before opening a disk for write. The pinned archive is `installer-v0.9.2-omarchy.29.tar.gz`, 17,844,091 bytes, SHA-256 `3a87e43b023e050c725d2e005bddd3721cf00f5e77104635e6a1e35411804d50`. Two repacks with macOS `/usr/bin/python3` 3.9.6 produced identical bytes from the authenticated `.14` base and locked v0.9.2 checkout; the same host and interpreter first reproduced the recorded `.28` digest exactly. Compared with `.28`, only `omarchy_asahi.py`, `omarchy_repair.py`, `omarchy_stage1.py` and `version.tag` changed, and the three modules match the repository sources byte for byte. The bundled inspection pins, packager and both release-input templates select `.29`. + +Ship installer **2.1.0 or later** together with the new engine and a signed catalog whose `installer.minimumVersion` is at least **2.1.0**. Both release-input templates carry this gate, and the catalog generator refuses a lower minimum for `.18` or newer engines in this lineage. Older installers then show the update-required message before decoding recommendation fields. The bundled inspection pins now select `.29`; signed production catalogs and frozen private-test catalogs are not changed by this source update. Artifact publication, catalog signing and physical installation qualification remain separate release steps. diff --git a/docs/repair-manifest-release-research.md b/docs/repair-manifest-release-research.md new file mode 100644 index 00000000..00503e52 --- /dev/null +++ b/docs/repair-manifest-release-research.md @@ -0,0 +1,59 @@ +# Repair manifest release evidence search + +Observed on 2026-09-30 IST (2026-09-29 19:07 UTC). This note records a bounded, read-only first-party artifact search for the PR #29 repair compatibility review. It does not establish that a released repair manifest does not exist. + +## Result + +The public MX Mac Stable and RC catalogs were obtained and their Ed25519 signatures verified against the repository's existing trust root. Their authenticated installer metadata was also obtained. None of these inspected catalogs or metadata files contains a repair manifest or a reference to one. This is useful release evidence, but it does not close Marcelo's request for a producer or released repair manifest. The installer metadata's partition size is not the repair manifest's `existing_content[role].size_bytes` contract. + +## Authenticated release metadata + +[Stable](https://downloads.aicodelabs.com.au/channels/stable/catalog.signed.json) and [RC](https://downloads.aicodelabs.com.au/channels/rc/catalog.signed.json) both returned HTTP 200 and identical 45,336-byte envelopes. The envelope SHA-256 is `16e8b90caaf20d7c61a3b37db2909d5efe29eb9087457d48330898929d369abb`; the exact decoded catalog SHA-256 is `b595332e1b94a8ab39421e6d8808d3d67953df6fb5527f8608ac6a6a03c95507`. The catalog reports schema 4, sequence `1790210231`, issued `2026-09-24T00:37:11Z`, and evidence revision `4.0.4-mac.1.20260924-rc`. + +Verification used `scripts/catalog-signing.swift verify` on the exact decoded catalog bytes and detached decoded signature, with the tracked `Release/trust-root.ed25519.pub` (SHA-256 `0e86a878c2445009981a182a4642ec22ae14ab4d8365fad4b038cafc4802dc1b`). The verifier printed `catalog_signature=passed`. The catalog was not rewritten or re-signed. Signature success authenticates these observed bytes; it is not a freshness or installation qualification claim. + +The catalog lists engine, installer metadata, and payload artifacts. Its complete decoded JSON contains no repair field or repair artifact reference. The [authenticated installer metadata](https://downloads.aicodelabs.com.au/releases/os-v4.0.4-mac.1.20260924-rc/installer_data.json) is 1,004 bytes and matches the signed catalog's SHA-256 `abce392d6956e87d2fc0701d4ca42b966789494bc0647cc7062ece48a2190f6d`. It specifies EFI partition size `524288000B`, Boot partition size `2147483648B` with image `boot.img`, and Root partition minimum `34359738368B` with image `root.img` and `expand: true`. All listed partition sizes are 4096-aligned. This file is fresh-install metadata, not a repair manifest, and supplies no `existing_content[role].size_bytes` or `replacement_content[role].size_bytes` fields. + +[RC Aurora](https://downloads.aicodelabs.com.au/channels/rc-aurora/catalog.signed.json) returned HTTP 200, sequence `1790127482`, issued `2026-09-23T01:38:02Z`. Its 4,596-byte envelope SHA-256 is `60d23a73ee94b70d0b880a9c4e2b1d9563cbb1e73270ffbbb15cc9f3f6810cbc`; decoded catalog SHA-256 is `4775d914df51ad1bda92e0dcceb1302d445d6603a7e1a20655b0e51281777062`. Verification against the same tracked trust root also printed `catalog_signature=passed`. It has no repair field or repair artifact reference. + +## Limited payload index inspection + +The signed Stable catalog points to [this OS ZIP](https://downloads.aicodelabs.com.au/releases/os-v4.0.4-mac.1.20260924-rc/omarchy-2026.09.23-aarch64-apple-silicon-mac-rc-os-package.zip), with size `3875382788` and SHA-256 `0920b622f67295c6a3fa7a73ba942fa6b170b4f9504a173054c2f69683909aa4`. Only its last 65,557 bytes were fetched. The server returned HTTP 206 with `Content-Range: bytes 3875317231-3875382787/3875382788`. Those range bytes have SHA-256 `a69d126683fb996008e40a035e8687f529593c36a0a079e17485d16891ba5fd6`. + +The end-of-central-directory record specifies 14 entries and a 1,201-byte central directory at offset `3875381565`; the retrieved range includes that complete directory. It lists ESP files, `boot.img`, `root.img`, and `omarchy-volume.icns`, with no repair manifest member. The directory reports uncompressed `boot.img` size `2147483648` and ZIP64 `root.img` size `14344519680`, both 4096-aligned. The Root image size is different from the minimum Root partition size in installer metadata; these values must not be conflated. + +These observations do not authenticate the ZIP's full digest, because the whole ZIP was deliberately not downloaded. They also cannot exclude files inside the image filesystems or prove a production repair manifest's field contract. + +## Other first-party search evidence + +The [publisher's GitHub releases API](https://api.github.com/repos/maralcbr/omarchy-mx-mac/releases) returned 29 public releases containing 126 release assets. No asset name identifies a repair manifest. The latest listed [runtime release](https://github.com/maralcbr/omarchy-mx-mac/releases/tag/v4.0.4-mac.1) directs users to the publisher's signed installer and image channels; no release in this inspected listing supplies a repair-manifest asset. This is an asset-name and release-note search, not inspection of every package's contents or an assertion about private artifacts. + +The [MX Mac Edge catalog](https://downloads.aicodelabs.com.au/channels/edge/catalog.signed.json) and this repository's configured [standalone Edge catalog](https://downloads.aicodelabs.com.au/omarchy-mac/channels/edge/catalog.signed.json) both returned HTTP 404. Initial sandbox requests could not resolve external hosts. A permitted read-only network retry reached the servers. Python urllib requests returned HTTP 403, while curl obtained the successful catalogs above; the 403 responses are not treated as evidence that those catalogs are absent. + +No release configuration, trust root, engine lock, payload, source code, test, or physical machine was modified. No signing credentials were used, and nothing was published. + +## Producer source trace + +The installer is a consumer of an externally supplied repair manifest. At the reviewed public PR head, [`PreparationSpecification.repairManifestSourcePath`](https://github.com/omacom/omarchy-mac-installer/blob/51ad0d83513d39a499c9c167c856189248584f50/Sources/OmarchyCanaryCandidateTool/main.swift#L29-L34) supplies a file that the [candidate tool passes to the bundle writer](https://github.com/omacom/omarchy-mac-installer/blob/51ad0d83513d39a499c9c167c856189248584f50/Sources/OmarchyCanaryCandidateTool/main.swift#L214-L220). The [release staging script](https://github.com/omacom/omarchy-mac-installer/blob/51ad0d83513d39a499c9c167c856189248584f50/scripts/publish-m1-release#L181-L192) likewise copies its optional `--repair-manifest` input. Neither location computes the manifest's existing or replacement content sizes. + +The original publisher's recursive source trees were inspected at the [extraction revision `4db862d`](https://github.com/maralcbr/omarchy-mx-mac/tree/4db862da7a0957758c504c5a3e041202019dbabf), [portable proof revision `8add634`](https://github.com/maralcbr/omarchy-mx-mac/tree/8add634536b87ff9121327b4972af787ea5b3cfc), and [M4 handoff revision `cf0606c`](https://github.com/maralcbr/omarchy-mx-mac/tree/cf0606c65cce20f123eb37cf465eacdb6168862b). Their repair/canary file paths identify the same consumer tools, tests, and historical non-release canary archive already characterized in [engine-hardening.md](engine-hardening.md#repair-manifest-compatibility-and-producer-contract). Path inspection is not a complete content search of those repositories. Searches of GitHub's default-branch code index did not locate a repair-manifest producer; historical branches and unindexed/private source are not excluded by that result. + +The public image builder is available at [`maralcbr/omarchy-pkgs`, revision `7e2f6cf`](https://github.com/maralcbr/omarchy-pkgs/blob/7e2f6cfec1b3e412ad4fd6ed9611be275a246b30/bin/build-mac-image). Its `seal_images` function rounds Root image bytes up to a multiple of 1,048,576, rejects sizes exceeding the 34,359,738,368-byte Root partition minimum, and creates the Boot image at 2,147,483,648 bytes (lines 1000–1026). Both image lengths are therefore 4096-aligned. Its `write_installer_data` function emits fresh-install partition metadata (lines 1055–1066). This is direct image-producer evidence, but no repair manifest is generated in the inspected script. It does not establish how another tool chooses `existing_content[role].size_bytes`, nor that every released image came from this revision. The historical Apple image-builder tree at [`omarchy-iso` revision `7d27f99`](https://github.com/maralcbr/omarchy-iso/tree/7d27f996988eb640a0a74f67d76a1400a0cf3a80) also has image and installer-metadata producer paths but no repair-manifest-named file. + +## Published engine archive follow-up + +The published [`v0.8.0-m1` engine archive](https://github.com/maralcbr/omarchy-mx-mac/releases/download/v0.8.0-m1/installer-v0.9.0-omarchy.7.tar.gz) was downloaded and its SHA-256 `063fd0765fb2057384d9653f7bf547b0471af31fc764e039d578d4fef6dce4d5` matched that release's [`SHA256SUMS`](https://github.com/maralcbr/omarchy-mx-mac/releases/download/v0.8.0-m1/SHA256SUMS). This is a published checksum comparison, not independent signature verification. Its 1,544 archive members include no filename containing `repair` or `manifest`. The inspected `.py`, `.sh`, `.json`, and `.md` members smaller than 1 MB contain no `existing_content` or `repair_manifest` reference. The archive was read without extraction or execution. This excludes a producer or sample in those inspected members of that release; it does not establish the contents of every release or embedded artifact. The downloaded archive and checksum file are retained locally at `/private/tmp/pr29-shipped-engine/`. + +PR #29's review and comment endpoints were also rechecked. Marcelo's review still requires a producer or real shipped manifest and supplies no artifact location; no issue comments were present and Scott's four inline comments do not supply one. The next evidence source needs to be supplied by the producer owner or a release custodian. Contacting them has not been authorized, and no message was posted. + +## Resolution + +Marcelo accepted this evidence in the PR #45 review. No released repair manifest has ever shipped and repair has only run in canaries, so there is no producer to test against. The checksum-pinned canary fixture and the consumer contract in [engine-hardening.md](engine-hardening.md) close the request, and that contract is what any future producer must meet. + +Stronger evidence would require either an exact released repair manifest with provenance to the release that distributed it, or the actual repair-manifest producer at an identifiable revision with representative inputs and generated output. The output must let a test check both `existing_content[role].size_bytes` and `replacement_content[role].size_bytes` for rewritten roles against their partition capacity and 4096 alignment, plus replacement image length. A fresh-install catalog, image-builder arithmetic, or a newly invented manifest cannot establish that contract for existing repair output. + +The existing checksum-pinned physical canary fixture remains the only real repair manifest obtained in this investigation. Its explicitly non-release provenance prevents treating it as a released compatibility sample. No change to the consumer's safety checks or additional synthetic fixture is justified by the evidence found here. + +## Retained local evidence + +The fetched catalogs, decoded signed bytes and signatures, installer metadata, release listing, ZIP tail and response headers, source trees, and inspected image-builder script are retained under `.build/review-evidence/pr29/manifest-research/` in the local checkout. `SHA256SUMS` records their exact local identities. This ignored evidence directory is not shipped or committed; the source URLs and hashes above are the durable report. No full-suite rerun is needed for this documentation-only investigation because application, engine, and test source are unchanged. diff --git a/scripts/release-inputs-aurora.template.json b/scripts/release-inputs-aurora.template.json index 12896f5e..684ec067 100644 --- a/scripts/release-inputs-aurora.template.json +++ b/scripts/release-inputs-aurora.template.json @@ -1,8 +1,8 @@ { "payload_name": "omarchy-2026.09.13-aarch64-apple-silicon-aurora-os-package.zip", - "engine_name": "installer-v0.9.2-omarchy.28.tar.gz", + "engine_name": "installer-v0.9.2-omarchy.29.tar.gz", "metadata_name": "installer_data.json", - "engine_version": "v0.9.2-omarchy.28", + "engine_version": "v0.9.2-omarchy.29", "evidence_revision": "4.0.3-mac.2.20260913-aurora", "asahi_installer_tag": "v0.9.2", "asahi_installer_revision": "dffbb38ef0c00c0431c609ecd8a00f42deb5b24c", diff --git a/scripts/release-inputs.template.json b/scripts/release-inputs.template.json index a72dd846..0c04ca6c 100644 --- a/scripts/release-inputs.template.json +++ b/scripts/release-inputs.template.json @@ -1,8 +1,8 @@ { "payload_name": "omarchy-2026.09.13-aarch64-apple-silicon-asahi-os-package.zip", - "engine_name": "installer-v0.9.2-omarchy.28.tar.gz", + "engine_name": "installer-v0.9.2-omarchy.29.tar.gz", "metadata_name": "installer_data.json", - "engine_version": "v0.9.2-omarchy.28", + "engine_version": "v0.9.2-omarchy.29", "evidence_revision": "4.0.3-mac.1.20260913", "asahi_installer_tag": "v0.9.2", "asahi_installer_revision": "dffbb38ef0c00c0431c609ecd8a00f42deb5b24c",