Files
archy/docs/OS-OTA-CONTRACT-20261103.md

17 KiB

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.