# OS OTA contract, version 1 (2026-11-03) Owner: OS executor lane. Integration contract for admission/release/UI lanes. Implementation is candidate source, not installed/release acceptance. ## D09 binding and packaging Optional protected D09 binding `os_update` has exactly `version: 1`, `plan_sha256`, `manifest_sha256`, `authority_digest`, `verifier_sha256` (lowercase SHA256). These values must be admitted by the outside owner, never copied from an unauthenticated application request. Owner/admission now consumes the existing status-lane wire/schema. The worker calls `os_update_executor.stage(binding, action)` before stopping the manager (prepare/download/verify), then after STOPPED and C01 quiescence (apply). RESTORE cannot undo an OS transaction. Protected input directory is `/var/lib/archipelago-os-update/plans/PLAN_SHA256/`: `plan.json`, `manifest.json`. The fixed release verifier is `/usr/libexec/archipelago-os-release-verifier`, invoked `ceremony verify MANIFEST`; its protected, externally appointed digest is pinned in the binding. It must be the existing pinned-release-root verifier, not an application-selected executable. Manifest `os_update` is exactly `{version: 1, plan_sha256: HEX}`. Manifest signature and exact bytes are verified before network/package effects. Production has no unsigned flag. Disposable tests inject an adapter, never production authentication. ### Independent state custody (2026-11-03 correction) All privileged OS inputs, journals, APT caches, retained boot evidence and verifier custody live under root-controlled `/var/lib/archipelago-os-update`, independent of application-owned `/var/lib/archipelago`. Executor lock: `/var/lib/archipelago-os-update/executor.lock`. The owner postboot policy grants write access only to that lock and `transactions/` (plus the real dpkg lock files). Neither application data nor its ownership is modified to make this work. Any entry at the retired `/var/lib/archipelago/os-update` path, including an empty directory or dangling symlink, refuses provisioning, execution and qualification. Uninspectable/symlinked legacy ancestry also refuses. Existing legacy state needs separately reviewed root migration/reconciliation against independently retained consumption history. No automatic migration, deletion, copying, reset, clearance bit or replay is provided. App-controlled absence of old files is not authority to override the existing independent D09 consumption history. New directories are created relative to a protected parent descriptor only after its ancestry is checked. Live directory descriptors pin inode/mount/ownership; directory replacement and changed/removed journal bytes refuse further effects. A newly created transaction durably writes sequence zero. An already-existing transaction directory without a journal is an interrupted/foreign state, never an invitation to initialize or reuse its authority. Root provisioner `--admission` installs exact fixed incoming `/var/lib/archipelago-d09/incoming/os-update/{plan.json,manifest.json,preservation.json,verifier}` inputs (the D09 ROOT constant is authoritative). Existing differing inputs or verifier are refused, never overwritten. The native ELF verifier hash must match both the externally admitted binding and its already-appointed prerequisite native backend artifact on first installation, rather than an arbitrary newly supplied ELF. Protected `verifier-custody.json` retains that initial artifact pin, so later releases can keep the same verifier without falsely rebinding it to a new backend. All signed manifest component name/version/hash/ size tuples must equal D09 `release_identity`, including `os-update-plan.json`. The owner package includes all eight `os_update_*.py` modules; `python3-apt` is a required installed prerequisite, not opportunistically installed by admission. ## Plan JSON v1 Exact top-level fields: The fields below are required. The sole optional field `manager_reexec` is specified in the manager-continuity section; omission grants no bus recovery. * `version` = 1; `suite` = `bookworm` or `trixie` (must equal installed codename); `architecture` = `amd64` or `arm64` (must equal dpkg architecture). Hosts with configured foreign dpkg architectures are refused in v1 rather than silently omitting their upgrade indexes. Vendor/Pi kernels are not qualified. * `before_sha256`: hash of exact `dpkg-query -W` inventory (fixed format in code). * `repositories`: array of `{pocket, inrelease_sha256, indexes}`. Pocket is suite, suite-updates, or suite-security. Each index is `{path, sha256, size}` naming an uncompressed `COMPONENT/binary-ARCH/Packages`. HTTPS Debian endpoints and archive keyring are fixed by executor. Debian InRelease signature → Release SHA256 index → package stanza SHA256 → downloaded .deb is verified and retained. Exact index hashes are release-plan bound. Expired metadata is rejected by APT. `Packages.xz` is also supported; its compressed hash/size is the Release entry. All three pockets and all four Debian components (main, contrib, non-free, non-free-firmware) are required, with one index each, so an omitted pocket or component cannot silently turn this into a curated subset upgrade. * `packages`: complete changed-package array of `{name, architecture, version, sha256, size, filename, pocket}` matching authenticated Packages stanzas. * `transaction`: sorted simulation records `{operation, name, version}` where operation is `Inst`, `Remv`, or `Conf`; removal version is the installed version. This is the entire fixed APT `dist-upgrade` solver result, not a curated install. * `removals`: sorted explicitly authorized removed package names. No purge, autoremove, downgrade, release upgrade or automatic repair. * `custody`: array `{name, disposition}` covering every installed/changed package in the protected custody set; disposition is `retain` or `upgrade-reviewed`. Kernel image packages already installed are always retained. Custody removals are refused. Conflicts must resolve to this exact reviewed transaction. Upgrading a kernel metapackage is supported. Replacing an existing versioned kernel image in place is refused: retaining only its old vmlinuz while replacing same-release modules would not provide a sound old-kernel fallback. Such plans need a separately qualified boot-environment preservation implementation. * `service_policy` = `reviewed-maintenance-window`: package maintainer scripts may interrupt services as reviewed Debian root authority. This is not a sandbox for untrusted code. Caller must have authority for that interruption; package scripts must not restore/start operator-stopped apps or reinstall removed apps. * `preservation_sha256`: protected external preservation receipt hash. Receipt must attest wallets/data/identities/settings and stopped/uninstalled intent; its checks are outside apt and cannot be substituted by a database rollback. * `boot`: `{running_release, retained_files, expected_release, package}`. `package` is the exact kernel package object with the same fields as a `packages` entry; its full Debian archive chain is verified even if this kernel is already installed. Retained files map absolute `/boot/` paths to SHA256 and must include running vmlinuz and initrd plus GRUB config. No old image removal is permitted. Expected release must be explicitly appointed; new boot files/provenance are recorded after apply. A package-only transaction may retain the current kernel, but still requires authenticated archive/payload evidence and a fresh post-maintenance boot. `preservation.json` is protected, externally appointed evidence whose exact bytes are hashed by `preservation_sha256`. It must contain the binding's `authority_digest`, `service_policy`, and `preserve_wallets_data_identity_stop_uninstall: true`. Any legacy `exclusive_package_writer` field conveys no locking authority. Admission probes actual POSIX frontend and inner dpkg locks and records PID/inode ownership. Apply reacquires those locks through same-process libapt, checks inventory and both full solver results while holding them, and keeps the frontend lock through the dpkg inner-lock handoff and installed verification. No unlock/relock transfer to apt-get, Debug::NoLocking, fake lock files, or root ready bit. Maintainer scripts retain reviewed Debian package authority; the remaining preservation fields are authority assertions, not executor proofs that arbitrary scripts preserve data. No application may appoint this receipt. After boot, `postboot-preservation.json` must be exactly `{version: 1, authority_digest, plan_sha256, boot_id, preservation_verified: true}` from the external observer. ## Durable status v1 `/var/lib/archipelago-os-update/transactions/AUTHORITY_DIGEST/status.json`: `version`, `plan_sha256`, `authority_digest`, `phase`, `sequence`, `events`. Events contain `phase`, `details`; statuses are PREPARE, DOWNLOAD, VERIFIED, APPLY_INTENT, VERIFY, REBOOT_REQUIRED, POSTBOOT_CONFIRMED, RECONCILIATION_REQUIRED. The durable APPLY_INTENT consumes authority before invoking apt/dpkg. Any lost reply/crash/partial dpkg failure after it requires external reconciliation; never automatically rerun maintainer scripts or `dpkg --configure -a`. Fixed `reconcile` operation records current inventory and `dpkg --audit` without executing maintainer scripts; any repair still needs separately reviewed external authority. It does not clear consumption or turn an uncertain apply into success. Re-entry after verified completion is observation-only. Status is independent of D09 userspace completion: reboot-required is not running-kernel acceptance. Production worker journals also require a private authenticated `os_checkpoint` acknowledgement from the live owner before continuing. The owner records sequence, phase and status digest in its anchored head, rejects replay/gaps and changed preimages, and includes the checkpoint history in external readmission evidence. In particular, an old VERIFIED journal cannot reauthorize a previously recorded APPLY_INTENT. Inspect refuses unanchored journal state. Disposable standalone executor fixtures do not substitute for this composed owner authority. Postboot confirmation requires a different boot ID, exact expected uname and the exact Debian build version, unchanged recorded kernel/module/initrd/GRUB/EFI payload hashes, plus externally supplied preservation observation. It is operational provenance, not measured boot. No automatic reboot. No DB snapshot rollback. Existing host_fixups protected refusal remains intact. ## Owner status, observation and lifetime ### Explicit Debian manager reexec permission An optional signed `manager_reexec` plan object has exactly: `version: 1`, `operation: "debian-systemd-daemon-reexec"`, `package: "systemd"`, `package_version` (exact Debian version), `architecture` (plan architecture), `expected_executable_sha256` (exact target systemd executable), and `max_unavailable_seconds` (integer 1..120). If systemd changes, its version/arch must equal the changed-package entry; otherwise preflight checks the installed reviewed package version and current PID1 executable against the exact plan hash. The ordinary maintenance policy does not imply this permission. This field must be included before plan hashing/manifest signing. At APPLY_INTENT the live owner pins PID1, itself, the exact already-dispatched worker and their cgroup directory identities. It records real unit invocation, MainPID, control-group and no-job observations while the app is stopped, and retains the worker's frontend-lock identity. During package effects the owner does not redispatch. A disconnected manager is durably UNCERTAIN and blocks checkpoint/lifecycle advancement until the same live references, boot, epoch, unit/job identities and allowed executable are proved over a Unix connection authenticated as PID1. Fixed LoadUnit queries load metadata only, never start jobs. The first interruption sets one CLOCK_BOOTTIME deadline; subsequent interruptions cannot renew it. Timeout, unexpected bus peer, owner/worker loss or unit/job drift holds fail-closed. The owner survives a failed stop request without claiming that the stop occurred. It retains live references on a held window; a restarted owner cannot seed continuity from the durable ARMED/UNCERTAIN record. Existing external readmission remains necessary, and replay of an old readmission cannot clear a later hold. Manager history is append-only in the anchored D09 head and is part of existing external consumption-history binding. Only OS checkpoint reply waiting is extended to 150 seconds for this bounded proof operation. It grants no new lifetime or package retry. A completed OS journal is preserved even when the completion reply is lost. C01 time/epoch/ power checks, release authentication and actual postboot gates are unchanged. The authenticated optional `inspect_response.os_update` uses the existing exact wire fields: version, supported, plan_sha256, phase, installed_os, installed_kernel, running_kernel, reboot_required, post_boot_verified. Capability is emitted only after protected inputs, signed release/component binding, verifier custody and runtime qualification. Installed OS/base-files and installed target kernel are live dpkg observations; a not-yet-installed target kernel is null and does not make an admitted plan unsupported. Root-only `archipelago-d09-provision --os-postboot` asks the live owner to rehash boot/module payloads and inspect exact current uname/Debian build/boot ID alongside external preservation evidence. The executor lock serializes observation with OS effects. The owner appends the resulting status digest and boot ID to its anchored ledger. Each confirmed status projection reobserves current boot evidence; an old POSTBOOT_CONFIRMED journal cannot report verified on a later boot. A successor cannot discard unresolved predecessor OS boot/preservation state. OS-bearing workers have an explicit six-hour RuntimeMaxSec, independent of short RPC reply timeouts, TimeoutStartSec=infinity and KillMode=control-group. Exhaustion retains the existing durable prepare/apply intent for reconciliation; it grants no retry. C01 epoch/time/power checks remain in force. Worker limits are CPU2, 8 GiB memory and zero swap. No automatic reboot is provided. Composed immutable artifact, installed boot and operator acceptance still require qualification. Neither a helper fixture nor userspace OTA completion closes them. ## Qualification checkpoint * 15 focused source tests pass, including D09 prepare/apply reachability, signature/hash/partial-suite refusal, consumption, and no maintainer-script replay. * Real disposable APT/dpkg integration passed in `/tmp/opencode/os-ota-disposable-20261103-r4/result.json`: dependency upgrade, explicit conflict removal, durable partial postinst failure, single script invocation across replay, and fixture wallet/decision preservation. This uses a fixture-only unsigned local repository; production manifest/repository authentication and boot are explicitly not covered by that result. * Subsequent current-source integration rerun and independent real Debian archive chain qualification were blocked by the shared heavy lock (exit 75). The latter fixture is `scripts/tests/os_update_debian_chain.py`; no pass is claimed. * No installed-node apt/kernel/reboot, release signing/publication, or commits. ### Owner integration continuation Current continuation: **30 focused tests**, **25 lifecycle tests**, and **11 review correction tests** passed; the successor/reactivation suite also passed **23 tests** (some cases overlap). Actual rootless libapt/dpkg execution passed in `/home/archipelago/Reports/opencode-full-release-20261029/os-owner-disposable-r6/`: real lock evidence, dependency upgrade/removal, partial postinst failure and no replay. Production signature and actual boot are explicitly outside that fixture's acceptance. `Acquire::Languages "none";` fixes the installed-VM private mirror translation failure without changing signature/hash/date controls. Details and retained failed attempts are in `/home/archipelago/Reports/OS-OTA-OWNER-INTEGRATION-20261103.md`. Sibling-root continuation: **41 focused tests** passed, including actual package local-import closure and preservation of `c01_uplink.py`. Real isolated root/UID1000 filesystem integration passed **15 custody/replay checks** in `/home/archipelago/Reports/opencode-full-release-20261029/os-ota-custody-r2/`. This is storage/authority qualification, not a signature, package-apply or boot pass. No unsigned production approval or test signing root was introduced. Exact downstream paths and remaining artifact gates are recorded in `/home/archipelago/Reports/OS-OTA-CUSTODY-PATH-20261103.md`. Manager-reexec continuation: **48 focused source tests** and existing lifecycle/ review/successor suites passed, plus **13 C01 handoff/protocol tests**. Installed-PID1 component fixture `os-manager-pid1-r2` passed six cases including real dpkg → Debian systemd `libc-upgrade` trigger → PID1 reexec, two bus interruptions with one unchanged owner/epoch/anchor, timeout, bad bus peer, worker replacement, owner death/restart and unplanned-error refusal. This uses synthetic root-appointed component control tuples, not release authentication or a complete native C01/OTA acceptance. Snapshot, evidence, final-source distinctions and required new signed-plan field are recorded in `/home/archipelago/Reports/OS-OTA-MANAGER-REEXEC-20261103.md`.