#!/usr/bin/env python3 """Fail-closed cross-code-line release migration planning. The migration plan is data, not a shell script. It binds an admitted source to the exact currently observed staging code lines, the compatible state contract, and the rollback/verification contract. The plan can then be embedded in the existing signed ReleasePlan so Owner approval covers migration semantics without creating a second release authority. """ from __future__ import annotations import argparse import hashlib import json import os import re import sqlite3 import stat import subprocess import tempfile from dataclasses import asdict, dataclass from pathlib import Path from typing import Any, Callable, Mapping, Sequence OID_RE = re.compile(r"^[0-9a-f]{40}$") SHA_RE = re.compile(r"^[0-9a-f]{64}$") IDENT_RE = re.compile(r"^[A-Za-z0-9][A-Za-z0-9_.:@/-]{2,255}$") PLAN_SCHEMA = "aios.release-migration-plan.v1" REQUIREMENT_SCHEMA = "aios.release-migration-requirement.v1" PROFILE_SCHEMA = "aios.release-migration-profile.v1" ALLOWED_STATE_MODES = frozenset({"REUSE_IN_PLACE_WITH_SNAPSHOT", "PRESERVE_UNUSED"}) REQUIRED_PRECONDITIONS = ( "writer_disabled", "task_quiescent", "state_snapshot_verified", "credentials_ready", "rollback_ready", ) REQUIRED_VERIFICATION_CONTRACTS = ( "runtime_identity_exact", "task_principal_no_bearer_e2e", "restart_recovery", "credential_projection", "release_consistency_no_unknown", ) class MigrationPlanError(RuntimeError): """Migration evidence is missing, malformed, stale, or unsafe.""" class MigrationPlanRequired(MigrationPlanError): """A cross-code-line release cannot proceed without an exact migration plan.""" def _canonical_json(value: object) -> bytes: return (json.dumps(value, sort_keys=True, separators=(",", ":")) + "\n").encode("utf-8") def _sha(value: object, label: str) -> str: text = str(value or "").lower() if not SHA_RE.fullmatch(text): raise MigrationPlanError(f"{label}_invalid") return text def _oid(value: object, label: str) -> str: text = str(value or "").lower() if not OID_RE.fullmatch(text): raise MigrationPlanError(f"{label}_invalid") return text def _ident(value: object, label: str) -> str: text = str(value or "") if not IDENT_RE.fullmatch(text): raise MigrationPlanError(f"{label}_invalid") return text def _secure_json(path: Path, *, expected_owner_uid: int | None = 0) -> dict[str, Any]: try: metadata = path.lstat() except OSError as exc: raise MigrationPlanError(f"secure_json_unavailable:{path}") from exc if path.is_symlink() or not stat.S_ISREG(metadata.st_mode): raise MigrationPlanError("secure_json_regular_file_required") if expected_owner_uid is not None and os.name != "nt" and metadata.st_uid != expected_owner_uid: raise MigrationPlanError("secure_json_owner_invalid") if os.name != "nt" and metadata.st_mode & 0o022: raise MigrationPlanError("secure_json_mode_invalid") try: value = json.loads(path.read_text(encoding="utf-8")) except (OSError, UnicodeDecodeError, json.JSONDecodeError) as exc: raise MigrationPlanError("secure_json_invalid") from exc if not isinstance(value, dict): raise MigrationPlanError("secure_json_object_required") return value def _durable_json(path: Path, value: Mapping[str, Any], *, mode: int = 0o640) -> None: path.parent.mkdir(parents=True, exist_ok=True) encoded = _canonical_json(dict(value)) if path.exists(): current = path.read_bytes() if current != encoded: raise MigrationPlanError("migration_record_conflict") return temporary = path.with_name(path.name + ".tmp") descriptor = os.open(temporary, os.O_WRONLY | os.O_CREAT | os.O_EXCL, mode) try: with os.fdopen(descriptor, "wb") as handle: handle.write(encoded) handle.flush() os.fsync(handle.fileno()) os.replace(temporary, path) if os.name != "nt": os.chmod(path, mode) directory_fd = os.open(path.parent, os.O_RDONLY) try: os.fsync(directory_fd) finally: os.close(directory_fd) finally: try: temporary.unlink() except FileNotFoundError: pass def _hash_files(root: Path, files: Sequence[str]) -> str: payload: dict[str, str] = {} resolved_root = root.resolve(strict=True) for relative in files: rel = Path(relative) if rel.is_absolute() or ".." in rel.parts: raise MigrationPlanError("interface_path_invalid") path = (resolved_root / rel).resolve(strict=True) try: path.relative_to(resolved_root) except ValueError as exc: raise MigrationPlanError("interface_path_escape") from exc if path.is_symlink() or not path.is_file(): raise MigrationPlanError("interface_file_invalid") payload[relative] = hashlib.sha256(path.read_bytes()).hexdigest() return hashlib.sha256(_canonical_json(payload)).hexdigest() def _git_blob(git_dir: Path, source_oid: str, relative: str) -> bytes: rel = Path(relative) if rel.is_absolute() or ".." in rel.parts: raise MigrationPlanError("target_interface_path_invalid") completed = subprocess.run( ["git", f"--git-dir={git_dir}", "show", f"{source_oid}:{relative}"], check=False, capture_output=True, timeout=30, env={"PATH": os.environ.get("PATH", "")}, ) if completed.returncode != 0: raise MigrationPlanError("target_interface_unavailable") return bytes(completed.stdout) def _hash_git_files(git_dir: Path, source_oid: str, files: Sequence[str]) -> str: payload = { relative: hashlib.sha256(_git_blob(git_dir, source_oid, relative)).hexdigest() for relative in files } return hashlib.sha256(_canonical_json(payload)).hexdigest() def _systemd_properties(service: str) -> dict[str, Any]: _ident(service, "service") completed = subprocess.run( [ "systemctl", "show", service, "--property=WorkingDirectory", "--property=Environment", "--property=FragmentPath", "--property=ActiveState", ], check=False, capture_output=True, text=True, timeout=20, env={"PATH": os.environ.get("PATH", "")}, ) if completed.returncode != 0: raise MigrationPlanError("systemd_observation_unavailable") result: dict[str, Any] = {} for line in completed.stdout.splitlines(): if "=" in line: key, value = line.split("=", 1) result[key] = value if result.get("ActiveState") != "active": raise MigrationPlanError("component_not_active") environment: dict[str, str] = {} for token in str(result.get("Environment") or "").split(): if "=" in token: key, value = token.split("=", 1) environment[key] = value.strip('"') result["environment"] = environment return result def _sqlite_schema(connection: sqlite3.Connection) -> dict[str, Any]: tables = [ row[0] for row in connection.execute( "SELECT name FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%' ORDER BY name" ) ] result: dict[str, Any] = {} for table in tables: if not re.fullmatch(r"[A-Za-z_][A-Za-z0-9_]*", table): raise MigrationPlanError("sqlite_table_name_invalid") columns = [] for row in connection.execute(f"PRAGMA table_info({table})"): columns.append( { "cid": int(row[0]), "name": str(row[1]), "type": str(row[2]), "notnull": int(row[3]), "default": row[4], "pk": int(row[5]), } ) result[table] = { "columns": columns, "row_count": int(connection.execute(f"SELECT COUNT(*) FROM {table}").fetchone()[0]), } return result def _schema_digest(schema: Mapping[str, Any]) -> str: shape = { table: {"columns": value["columns"]} for table, value in sorted(schema.items()) } return hashlib.sha256(_canonical_json(shape)).hexdigest() def _assert_additive_sqlite(before: Mapping[str, Any], after: Mapping[str, Any]) -> None: for table, old in before.items(): if table not in after: raise MigrationPlanError("sqlite_table_removed") old_columns = {column["name"]: column for column in old["columns"]} new_columns = {column["name"]: column for column in after[table]["columns"]} for name, column in old_columns.items(): if new_columns.get(name) != column: raise MigrationPlanError("sqlite_column_changed") for name, column in new_columns.items(): if name in old_columns: continue # A new NOT NULL column is backward compatible only with a default. if column["notnull"] and column["default"] is None: raise MigrationPlanError("sqlite_new_column_unsafe") if int(after[table]["row_count"]) != int(old["row_count"]): raise MigrationPlanError("sqlite_row_count_changed_during_probe") @dataclass(frozen=True) class RuntimeComponentBinding: component_id: str service: str code_line_id: str source_oid: str artifact_sha256: str | None interface_sha256: str def __post_init__(self) -> None: _ident(self.component_id, "component_id") _ident(self.service, "service") _ident(self.code_line_id, "code_line_id") _oid(self.source_oid, "source_oid") if self.artifact_sha256 is not None: _sha(self.artifact_sha256, "artifact_sha256") _sha(self.interface_sha256, "interface_sha256") def to_dict(self) -> dict[str, Any]: return asdict(self) @classmethod def from_dict(cls, value: Mapping[str, Any]) -> "RuntimeComponentBinding": return cls(**dict(value)) @dataclass(frozen=True) class StateMigrationContract: state_id: str state_ref: str mode: str before_schema_sha256: str after_schema_sha256: str snapshot_name: str snapshot_sha256: str snapshot_required: bool rollback_compatible: bool def __post_init__(self) -> None: _ident(self.state_id, "state_id") _ident(self.state_ref, "state_ref") if self.mode not in ALLOWED_STATE_MODES: raise MigrationPlanError("state_mode_invalid") _sha(self.before_schema_sha256, "before_schema_sha256") _sha(self.after_schema_sha256, "after_schema_sha256") _sha(self.snapshot_sha256, "snapshot_sha256") snapshot = Path(self.snapshot_name) if ( snapshot.is_absolute() or ".." in snapshot.parts or not snapshot.parts or str(snapshot).startswith(".") ): raise MigrationPlanError("snapshot_name_invalid") if self.snapshot_required is not True or self.rollback_compatible is not True: raise MigrationPlanError("state_recovery_contract_invalid") def to_dict(self) -> dict[str, Any]: return asdict(self) @classmethod def from_dict(cls, value: Mapping[str, Any]) -> "StateMigrationContract": return cls(**dict(value)) @dataclass(frozen=True) class MigrationPlan: migration_id: str version: str candidate_id: str repository_identity: str source_admission_id: str source_commit: str tree_oid: str profile_id: str profile_sha256: str from_components: tuple[RuntimeComponentBinding, ...] to_components: tuple[RuntimeComponentBinding, ...] state_contracts: tuple[StateMigrationContract, ...] activation_schema: str activation_sha256: str rollback_window_seconds: int preconditions: tuple[str, ...] = REQUIRED_PRECONDITIONS verification_contracts: tuple[str, ...] = REQUIRED_VERIFICATION_CONTRACTS runtime_switch_capability: str = "runtime_switch" rollback_contract: str = "restore_previous_runtime_and_state_snapshot.v1" forward_recovery_contract: str = "forward_recovery_after_verified_rollback_block.v1" writer_expected: bool = False direct_db_allowed: bool = False direct_runtime_overwrite_allowed: bool = False def __post_init__(self) -> None: _ident(self.migration_id, "migration_id") _ident(self.version, "version") _ident(self.candidate_id, "candidate_id") if self.repository_identity != "aios/aios": raise MigrationPlanError("repository_identity_invalid") _ident(self.source_admission_id, "source_admission_id") _oid(self.source_commit, "source_commit") _oid(self.tree_oid, "tree_oid") _ident(self.profile_id, "profile_id") _sha(self.profile_sha256, "profile_sha256") _ident(self.activation_schema, "activation_schema") _sha(self.activation_sha256, "activation_sha256") if not 60 <= int(self.rollback_window_seconds) <= 86400: raise MigrationPlanError("rollback_window_invalid") if tuple(self.preconditions) != REQUIRED_PRECONDITIONS: raise MigrationPlanError("migration_preconditions_invalid") if tuple(self.verification_contracts) != REQUIRED_VERIFICATION_CONTRACTS: raise MigrationPlanError("migration_verification_contracts_invalid") if self.runtime_switch_capability != "runtime_switch": raise MigrationPlanError("migration_capability_invalid") if self.writer_expected or self.direct_db_allowed or self.direct_runtime_overwrite_allowed: raise MigrationPlanError("migration_unsafe_authority") if not self.from_components or not self.to_components or not self.state_contracts: raise MigrationPlanError("migration_contract_incomplete") from_ids = {item.component_id for item in self.from_components} to_ids = {item.component_id for item in self.to_components} if from_ids != to_ids or len(from_ids) != len(self.from_components) or len(to_ids) != len(self.to_components): raise MigrationPlanError("migration_component_binding_invalid") if all( next(item.code_line_id for item in self.from_components if item.component_id == target.component_id) == target.code_line_id for target in self.to_components ): raise MigrationPlanError("migration_not_required") @property def canonical_payload(self) -> dict[str, Any]: return { "schema": PLAN_SCHEMA, "migration_id": self.migration_id, "version": self.version, "candidate_id": self.candidate_id, "repository_identity": self.repository_identity, "source_admission_id": self.source_admission_id, "source_commit": self.source_commit, "tree_oid": self.tree_oid, "profile_id": self.profile_id, "profile_sha256": self.profile_sha256, "from_components": [item.to_dict() for item in self.from_components], "to_components": [item.to_dict() for item in self.to_components], "state_contracts": [item.to_dict() for item in self.state_contracts], "activation_schema": self.activation_schema, "activation_sha256": self.activation_sha256, "rollback_window_seconds": self.rollback_window_seconds, "preconditions": list(self.preconditions), "verification_contracts": list(self.verification_contracts), "runtime_switch_capability": self.runtime_switch_capability, "rollback_contract": self.rollback_contract, "forward_recovery_contract": self.forward_recovery_contract, "writer_expected": self.writer_expected, "direct_db_allowed": self.direct_db_allowed, "direct_runtime_overwrite_allowed": self.direct_runtime_overwrite_allowed, } @property def sha256(self) -> str: return hashlib.sha256(_canonical_json(self.canonical_payload)).hexdigest() def to_dict(self) -> dict[str, Any]: return {**self.canonical_payload, "sha256": self.sha256} @classmethod def from_dict(cls, value: Mapping[str, Any]) -> "MigrationPlan": raw = dict(value) if raw.pop("schema", None) != PLAN_SCHEMA: raise MigrationPlanError("migration_plan_schema_invalid") claimed = raw.pop("sha256", None) raw["from_components"] = tuple(RuntimeComponentBinding.from_dict(item) for item in raw["from_components"]) raw["to_components"] = tuple(RuntimeComponentBinding.from_dict(item) for item in raw["to_components"]) raw["state_contracts"] = tuple(StateMigrationContract.from_dict(item) for item in raw["state_contracts"]) raw["preconditions"] = tuple(raw.get("preconditions") or ()) raw["verification_contracts"] = tuple(raw.get("verification_contracts") or ()) plan = cls(**raw) if claimed is not None and claimed != plan.sha256: raise MigrationPlanError("migration_plan_sha256_mismatch") return plan def assert_release_binding( self, *, version: str, candidate_id: str, source_commit: str, tree_oid: str, repository_identity: str, ) -> None: expected = ( self.version, self.candidate_id, self.source_commit, self.tree_oid, self.repository_identity, ) actual = (version, candidate_id, source_commit, tree_oid, repository_identity) if actual != expected: raise MigrationPlanError("migration_release_binding_mismatch") @dataclass(frozen=True) class MigrationRequirement: version: str candidate_id: str source_commit: str repository_identity: str required: bool reason_code: str profile_id: str profile_sha256: str migration_plan_sha256: str | None @property def canonical_payload(self) -> dict[str, Any]: return { "schema": REQUIREMENT_SCHEMA, **asdict(self), } @property def sha256(self) -> str: return hashlib.sha256(_canonical_json(self.canonical_payload)).hexdigest() def to_dict(self) -> dict[str, Any]: return {**self.canonical_payload, "sha256": self.sha256} @classmethod def from_dict(cls, value: Mapping[str, Any]) -> "MigrationRequirement": raw = dict(value) if raw.pop("schema", None) != REQUIREMENT_SCHEMA: raise MigrationPlanError("migration_requirement_schema_invalid") claimed = raw.pop("sha256", None) requirement = cls(**raw) _ident(requirement.version, "version") _ident(requirement.candidate_id, "candidate_id") _oid(requirement.source_commit, "source_commit") if requirement.repository_identity != "aios/aios": raise MigrationPlanError("migration_requirement_repository_invalid") _ident(requirement.reason_code, "reason_code") _ident(requirement.profile_id, "profile_id") _sha(requirement.profile_sha256, "profile_sha256") if requirement.required: _sha(requirement.migration_plan_sha256, "migration_plan_sha256") elif requirement.migration_plan_sha256 is not None: raise MigrationPlanError("unexpected_migration_plan") if claimed is not None and claimed != requirement.sha256: raise MigrationPlanError("migration_requirement_sha256_mismatch") return requirement class FileMigrationPlanResolver: """Read-only resolver used by the trusted builder. A profile declares that a release version is cross-code-line. Once a version is declared, absence of requirement/plan state is fail-closed. """ def __init__( self, root: Path, *, profile_root: Path, expected_owner_uid: int | None = 0, ): self.root = Path(root) self.profile_root = Path(profile_root) self.expected_owner_uid = expected_owner_uid def _profile_for_version(self, version: str) -> dict[str, Any] | None: matches = [] if not self.profile_root.is_dir(): return None for path in sorted(self.profile_root.glob("*.json")): profile = _secure_json(path, expected_owner_uid=self.expected_owner_uid) if profile.get("schema") != PROFILE_SCHEMA: raise MigrationPlanError("migration_profile_schema_invalid") if profile.get("version") == version: matches.append(profile) if len(matches) > 1: raise MigrationPlanError("migration_profile_ambiguous") return matches[0] if matches else None def resolve(self, version: str, candidate_id: str, source_commit: str) -> MigrationPlan | None: profile = self._profile_for_version(version) if profile is None: return None base = self.root / source_commit requirement_path = base / "requirement.json" plan_path = base / "plan.json" if not requirement_path.is_file(): raise MigrationPlanRequired("MIGRATION_PLAN_REQUIRED") requirement = MigrationRequirement.from_dict( _secure_json(requirement_path, expected_owner_uid=self.expected_owner_uid) ) expected = (version, candidate_id, source_commit, "aios/aios") actual = ( requirement.version, requirement.candidate_id, requirement.source_commit, requirement.repository_identity, ) if actual != expected: raise MigrationPlanError("migration_requirement_binding_mismatch") if requirement.profile_id != profile.get("profile_id"): raise MigrationPlanError("migration_profile_binding_mismatch") profile_sha = hashlib.sha256(_canonical_json(profile)).hexdigest() if requirement.profile_sha256 != profile_sha: raise MigrationPlanError("migration_profile_digest_mismatch") if not requirement.required: return None if not plan_path.is_file(): raise MigrationPlanRequired("MIGRATION_PLAN_REQUIRED") plan = MigrationPlan.from_dict( _secure_json(plan_path, expected_owner_uid=self.expected_owner_uid) ) plan.assert_release_binding( version=version, candidate_id=candidate_id, source_commit=source_commit, tree_oid=plan.tree_oid, repository_identity="aios/aios", ) if requirement.migration_plan_sha256 != plan.sha256: raise MigrationPlanError("migration_requirement_plan_mismatch") return plan class MigrationPreSwitchValidator: """Validates a root-owned live proof against the signed migration plan.""" def __init__(self, proof_resolver: Callable[[MigrationPlan], Mapping[str, Any]]): self.proof_resolver = proof_resolver def validate(self, migration_plan: MigrationPlan | None) -> dict[str, Any]: if migration_plan is None: return {"required": False, "verified": True} proof = dict(self.proof_resolver(migration_plan)) snapshot_digests = sorted( contract.snapshot_sha256 for contract in migration_plan.state_contracts ) required = { "migration_plan_sha256": migration_plan.sha256, "migration_id": migration_plan.migration_id, "source_commit": migration_plan.source_commit, "tree_oid": migration_plan.tree_oid, "activation_sha256": migration_plan.activation_sha256, "state_snapshot_sha256s": snapshot_digests, "writer_disabled": True, "task_quiescent": True, "state_snapshot_verified": True, "credentials_ready": True, "rollback_ready": True, } failed = [key for key, expected in required.items() if proof.get(key) != expected] if failed: raise MigrationPlanError( "MIGRATION_PRE_SWITCH_PROOF_INVALID:" + ",".join(sorted(failed)) ) return {"required": True, "verified": True, **proof} class HostMigrationCompiler: def __init__( self, *, authority_git_dir: Path, profile_path: Path, output_root: Path, expected_owner_uid: int | None = 0, ): self.authority_git_dir = Path(authority_git_dir) self.profile_path = Path(profile_path) self.output_root = Path(output_root) self.expected_owner_uid = expected_owner_uid def _profile(self) -> dict[str, Any]: profile = _secure_json(self.profile_path, expected_owner_uid=self.expected_owner_uid) required = { "schema", "profile_id", "version", "repository", "current_components", "target_components", "activation_file", "state_ref", "state_db", "state_mode", "rollback_window_seconds", } if set(profile) != required or profile.get("schema") != PROFILE_SCHEMA: raise MigrationPlanError("migration_profile_fields_invalid") if profile.get("repository") != "aios/aios": raise MigrationPlanError("migration_profile_repository_invalid") _ident(profile.get("profile_id"), "profile_id") _ident(profile.get("version"), "version") if profile.get("state_mode") not in ALLOWED_STATE_MODES: raise MigrationPlanError("migration_profile_state_mode_invalid") return profile @staticmethod def _component_from_live(spec: Mapping[str, Any]) -> RuntimeComponentBinding: required = { "component_id", "service", "code_line_id", "expected_source_oid", "source_oid_env", "artifact_sha_env", "interface_files", "marker_files", } if set(spec) != required: raise MigrationPlanError("current_component_profile_invalid") properties = _systemd_properties(str(spec["service"])) environment = properties["environment"] source_oid = _oid(environment.get(str(spec["source_oid_env"])), "current_source_oid") expected_oid = _oid(spec["expected_source_oid"], "expected_source_oid") if source_oid != expected_oid: raise MigrationPlanError("CURRENT_RUNTIME_DRIFT") working = Path(str(properties.get("WorkingDirectory") or "")) if not working.is_absolute() or not working.is_dir(): raise MigrationPlanError("current_working_directory_invalid") for marker in spec["marker_files"]: if not (working / str(marker)).is_file(): raise MigrationPlanError("current_code_line_marker_missing") interface = _hash_files(working, tuple(str(item) for item in spec["interface_files"])) raw_artifact = environment.get(str(spec["artifact_sha_env"])) artifact = str(raw_artifact or "").lower() artifact_value = artifact if SHA_RE.fullmatch(artifact) else None return RuntimeComponentBinding( component_id=str(spec["component_id"]), service=str(spec["service"]), code_line_id=str(spec["code_line_id"]), source_oid=source_oid, artifact_sha256=artifact_value, interface_sha256=interface, ) def _component_from_target( self, spec: Mapping[str, Any], *, source_commit: str ) -> RuntimeComponentBinding: required = {"component_id", "service", "code_line_id", "interface_files"} if set(spec) != required: raise MigrationPlanError("target_component_profile_invalid") interface_files = tuple(str(item) for item in spec["interface_files"]) interface = _hash_git_files(self.authority_git_dir, source_commit, interface_files) return RuntimeComponentBinding( component_id=str(spec["component_id"]), service=str(spec["service"]), code_line_id=str(spec["code_line_id"]), source_oid=source_commit, artifact_sha256=None, interface_sha256=interface, ) @staticmethod def _state_compatibility( state_db: Path, state_ref: str, *, snapshot_path: Path, snapshot_name: str, ) -> StateMigrationContract: from aios_task_admission_bootstrap import TaskAdmissionStore if state_db.is_symlink() or not state_db.is_file(): raise MigrationPlanError("state_db_invalid") snapshot_path.parent.mkdir(parents=True, exist_ok=True) with tempfile.TemporaryDirectory(prefix="aios-migration-probe-") as temp_dir: temp_root = Path(temp_dir) consistent_snapshot = temp_root / "pre-migration.sqlite3" probe_path = temp_root / "schema-probe.sqlite3" with sqlite3.connect( f"file:{state_db}?mode=ro", uri=True ) as live, sqlite3.connect(consistent_snapshot) as copy: live.backup(copy) with sqlite3.connect(consistent_snapshot) as frozen: before = _sqlite_schema(frozen) # Probe target initialization only on a second private copy. The # rollback snapshot remains byte-for-byte pre-migration state. with sqlite3.connect(consistent_snapshot) as source, sqlite3.connect( probe_path ) as probe: source.backup(probe) TaskAdmissionStore(probe_path) with sqlite3.connect(probe_path) as migrated: after = _sqlite_schema(migrated) required_tables = { "task_admission_envelopes", "task_admission_grants", "task_admission_receipts", } if not required_tables.issubset(after): raise MigrationPlanError("INCOMPATIBLE_STATE_SCHEMA") _assert_additive_sqlite(before, after) snapshot_bytes = consistent_snapshot.read_bytes() snapshot_sha = hashlib.sha256(snapshot_bytes).hexdigest() if snapshot_path.exists(): if snapshot_path.is_symlink() or not snapshot_path.is_file(): raise MigrationPlanError("migration_snapshot_invalid") if hashlib.sha256(snapshot_path.read_bytes()).hexdigest() != snapshot_sha: raise MigrationPlanError("migration_snapshot_conflict") else: temporary = snapshot_path.with_name(snapshot_path.name + ".tmp") descriptor = os.open( temporary, os.O_WRONLY | os.O_CREAT | os.O_EXCL, 0o640, ) try: with os.fdopen(descriptor, "wb") as handle: handle.write(snapshot_bytes) handle.flush() os.fsync(handle.fileno()) os.replace(temporary, snapshot_path) if os.name != "nt": os.chmod(snapshot_path, 0o640) directory_fd = os.open(snapshot_path.parent, os.O_RDONLY) try: os.fsync(directory_fd) finally: os.close(directory_fd) finally: try: temporary.unlink() except FileNotFoundError: pass return StateMigrationContract( state_id="task-admission", state_ref=state_ref, mode="REUSE_IN_PLACE_WITH_SNAPSHOT", before_schema_sha256=_schema_digest(before), after_schema_sha256=_schema_digest(after), snapshot_name=snapshot_name, snapshot_sha256=snapshot_sha, snapshot_required=True, rollback_compatible=True, ) def compile( self, *, source_admission: Mapping[str, Any], candidate_id: str, ) -> tuple[MigrationRequirement, MigrationPlan]: profile = self._profile() version = str(source_admission.get("version") or "") source_commit = _oid(source_admission.get("source_commit"), "source_commit") tree_oid = _oid(source_admission.get("tree_oid"), "tree_oid") admission_id = _ident(source_admission.get("admission_id"), "source_admission_id") if ( version != profile["version"] or source_admission.get("repository") != "aios/aios" ): raise MigrationPlanError("source_admission_profile_mismatch") _ident(candidate_id, "candidate_id") profile_sha = hashlib.sha256(_canonical_json(profile)).hexdigest() from_components = tuple( self._component_from_live(item) for item in profile["current_components"] ) to_components = tuple( self._component_from_target(item, source_commit=source_commit) for item in profile["target_components"] ) if {item.component_id for item in from_components} != {item.component_id for item in to_components}: raise MigrationPlanError("migration_component_profile_mismatch") required = any( next(old.code_line_id for old in from_components if old.component_id == new.component_id) != new.code_line_id for new in to_components ) if not required: raise MigrationPlanError("migration_profile_does_not_require_transition") activation_path = Path(str(profile["activation_file"])) activation = _secure_json(activation_path, expected_owner_uid=self.expected_owner_uid) if activation.get("schema") != "aios.task-admission-host-activation.v1": raise MigrationPlanError("activation_schema_invalid") state_path = Path(str(profile["state_db"])) if str(activation.get("state_db") or "") != str(state_path): raise MigrationPlanError("activation_state_db_mismatch") root = self.output_root / source_commit snapshot_name = "snapshots/task-admission.sqlite3" state_contract = self._state_compatibility( state_path, str(profile["state_ref"]), snapshot_path=root / snapshot_name, snapshot_name=snapshot_name, ) migration_id = ( "migration-" + hashlib.sha256( f"{version}\0{candidate_id}\0{source_commit}\0{profile_sha}".encode("utf-8") ).hexdigest()[:24] ) plan = MigrationPlan( migration_id=migration_id, version=version, candidate_id=candidate_id, repository_identity="aios/aios", source_admission_id=admission_id, source_commit=source_commit, tree_oid=tree_oid, profile_id=str(profile["profile_id"]), profile_sha256=profile_sha, from_components=from_components, to_components=to_components, state_contracts=(state_contract,), activation_schema=str(activation["schema"]), activation_sha256=hashlib.sha256(_canonical_json(activation)).hexdigest(), rollback_window_seconds=int(profile["rollback_window_seconds"]), ) requirement = MigrationRequirement( version=version, candidate_id=candidate_id, source_commit=source_commit, repository_identity="aios/aios", required=True, reason_code="CROSS_CODE_LINE", profile_id=str(profile["profile_id"]), profile_sha256=profile_sha, migration_plan_sha256=plan.sha256, ) _durable_json(root / "requirement.json", requirement.to_dict()) _durable_json(root / "plan.json", plan.to_dict()) return requirement, plan def _verified_source_admission( *, source_admission_root: Path, source_commit: str, builder_policy: Path, credential_name: str = "source_admission_hmac", ) -> dict[str, Any]: """Resolve one source admission through its existing HMAC verifier. The migration planner never trusts a caller-selected admission JSON path. It consumes only a root-owned admission store and the independent ``source_admission_hmac`` credential projected by systemd. """ from release_source_admission import open_source_admission_store credentials = os.environ.get("CREDENTIALS_DIRECTORY", "").strip() if not credentials: raise MigrationPlanError("source_admission_credentials_unavailable") credential_path = Path(credentials) / credential_name try: hmac_key = credential_path.read_bytes().strip() except OSError as exc: raise MigrationPlanError("source_admission_hmac_unavailable") from exc try: store = open_source_admission_store( source_admission_root, repository="aios/aios", policy=builder_policy, hmac_key=hmac_key, expected_owner_uid=0 if os.name != "nt" else None, writable=False, ) record = store.lookup_source(source_commit) except Exception as exc: raise MigrationPlanError("source_admission_verification_failed") from exc if record is None: raise MigrationPlanRequired("SOURCE_ADMISSION_REQUIRED") return dict(record) def main() -> int: parser = argparse.ArgumentParser() parser.add_argument("--authority-git-dir", required=True, type=Path) parser.add_argument("--profile", required=True, type=Path) parser.add_argument("--output-root", required=True, type=Path) parser.add_argument("--source-admission-root", required=True, type=Path) parser.add_argument("--builder-policy", required=True, type=Path) parser.add_argument("--source-commit", required=True) parser.add_argument("--candidate-id", required=True) args = parser.parse_args() source_commit = _oid(args.source_commit, "source_commit") compiler = HostMigrationCompiler( authority_git_dir=args.authority_git_dir, profile_path=args.profile, output_root=args.output_root, expected_owner_uid=0 if os.name != "nt" else None, ) requirement, plan = compiler.compile( source_admission=_verified_source_admission( source_admission_root=args.source_admission_root, source_commit=source_commit, builder_policy=args.builder_policy, ), candidate_id=args.candidate_id, ) print( json.dumps( { "status": "MIGRATION_PLAN_READY", "migration_plan_sha256": plan.sha256, "requirement_sha256": requirement.sha256, "source_commit": plan.source_commit, "version": plan.version, }, sort_keys=True, ) ) return 0 if __name__ == "__main__": try: raise SystemExit(main()) except Exception as exc: print( json.dumps( { "status": "FAIL_CLOSED", "error": type(exc).__name__, "reason": str(exc), }, sort_keys=True, ) ) raise SystemExit(2)