diff --git a/docs/a0-apt-arc-addendum.md b/docs/a0-apt-arc-addendum.md new file mode 100644 index 0000000..6880e64 --- /dev/null +++ b/docs/a0-apt-arc-addendum.md @@ -0,0 +1,121 @@ +# A0 addendum — packaging the apt-mutation arc + +Status: **plan / decisions recorded. Implementation held for review.** +Companion to `a0-packaging-plan.md`; read that first. + +A0-dev packaged and VM-proved the **read-only** Runix stack +(runix/pkgstate/rsystemd/rctl/broker + the `runix-stack` metapackage). The +**apt-mutation arc** is now complete end to end — `pkgexec` (privileged effectors + +the `runix-apt-preview` planner), `pkgops` (the unprivileged issuer), and `rctl`'s +`apt.*` surface — but none of it is in the A0 packaging graph. This addendum records +the distribution decisions for the arc and scopes the A0-dev integration (step 1). +It does **not** build anything; the packaging work is held for review. + +## Decisions (Troy, 2026-08-20) + +1. **One versioned trust domain for the production mutation stack.** `pkgexec`, the + audit broker, and `pkgops` are distributed **together through the signed cornball + apt repository** — not split across CRAN and a repo. `pkgops` (pure R) **may + later** also be carried on CRAN, optionally, but the production mutation stack has + a single signed apt channel. + + This refines `a0-packaging-plan.md`'s "the R packages' canonical home is CRAN" + lean. That still holds for the **read-only** R packages. The **mutation** stack + unifies on the apt repo because its two privileged C artifacts — the broker and + `pkgexec` — can never be CRAN-hosted, so CRAN alone can never deliver the boundary; + one trust domain beats two for the dangerous path. + +2. **`runix-apt` is a separate opt-in metapackage.** Mutation capability is **not** + added to `runix-stack`. A host gains effectors only by explicitly + `apt install runix-apt`. Rationale: the effectors are privileged and group-gated + (`runix-apt-autonomous`); a host that wants the read-only typed surface must not + acquire a mutation boundary as a side effect of installing `runix-stack`. + +3. **A0-release stays deferred** until the broader validation bar is met (see + "Validation gate" below). This is independent of the distribution choice: the + signed public archive is not built until the arc has earned the ceremony. + +## Extended packaging graph (A0-dev) + +Adds to the `a0-packaging-plan.md` table (same debhelper conventions: `r-api-4.0` + +`r-base-core (>= 4.6.0)` on every R binary, `${shlibs:Depends}` for native libs, +`3.0 (native)` source format for A0-dev, `amd64` first). + +| deb | arch | Depends | Recommends | +|---|---|---|---| +| `pkgexec` | any | `${shlibs:Depends}` (libapt-pkg, jansson, OpenSSL), `polkit`, `dpkg`, `adduser`, `${misc:Depends}` | | +| `r-cornball-pkgops` | all | `r-base-core (>= 4.6.0)`, `r-api-4.0`, `r-cornball-runix`, `r-cornball-pkgstate`, `r-cornball-janssonr` | `pkgexec` | +| `runix-apt` (meta) | all | `r-cornball-pkgops`, `pkgexec` (exact `=` versions) | | + +Notes: + +- **`pkgexec` already ships a debhelper `.deb`** (0.0.4, `Architecture: any`, deps + via `${shlibs:Depends}`, `postinst` creates the `runix-apt-autonomous` group). It + is *not* a CRAN artifact. Integration is folding it into the A0 orchestrator + + provenance manifest, not from-scratch packaging. +- **`pkgops` needs the planner at runtime** (`runix-apt-preview`, shipped by + `pkgexec`), so its `SystemRequirements: pkgexec` becomes a package dependency. A + `pkgops` without `pkgexec` can preview nothing and commit nothing, so the honest + install unit is the **`runix-apt` metapackage** that bundles both — hence + `pkgexec` as a `pkgops` Recommends and an exact-versioned `runix-apt` Depends. +- **`rctl` is unchanged.** It already declares `pkgops` as a runtime-detected + `Suggests`; with `pkgops` absent, `rctl apt.*` reports the subsystem absent and the + read-only surface is unaffected. `pkgops` arrives via `runix-apt`, **not** through + `runix-stack`'s Recommends (which stay pkgstate/rsystemd). +- **The broker** is already packaged and tagged (`v0.0.1` `.deb`) and is shared by + both stacks (the read-only audit path and the mutation effect-receipt path). + `runix-apt` pulls it transitively through `runix`. + +## A0-dev integration — step 1 scope (HELD for review) + +Goal: `pkgexec` + `pkgops` + `runix-apt` build as debhelper `.deb`s and +install/upgrade/remove from **local files on a disposable VM**, with the apt-mutation +gates passing against the **apt-installed** stack (not the source tree). No signing, +no public repository, no CRAN — that is A0-release, deferred. + +1. **`pkgexec` into the orchestrator.** It has `debian/` already; wire it into the + A0 build so it emits the source package + `.buildinfo` + SHA-256 provenance like + the rest. Verify `dpkg-shlibdeps` resolves libapt-pkg / jansson / OpenSSL / polkit, + and that `postinst` creates `runix-apt-autonomous` idempotently. `Architecture: + any` (amd64 first). +2. **`pkgops` debhelper packaging.** New `debian/` — `Architecture: all`, the r-cran + ABI metadata, Depends on `r-cornball-runix`/`pkgstate`/`janssonr` + `pkgexec`. + Native source format for A0-dev. +3. **`runix-apt` metapackage.** Meta `.deb` depending on `r-cornball-pkgops` + + `pkgexec` at exact `=` versions — the opt-in mutation install unit, referenced by + nothing in `runix-stack`. +4. **VM local-file acceptance (extend the A0-dev `deploy/canary` harness).** On a + clean disposable guest: `apt install ./runix-stack*.deb ./runix-apt*.deb` from + local files; upgrade old→new; remove/purge (polkit rules + group cleanup; the + `/var/log/runix` audit sink survives per the existing operator-owned rule); then + run the apt-mutation §7 gates + polkit matrix against the **apt-installed** stack, + closing "runs from source" → "installs and runs as `.deb`s" for the mutation path. + **Disposable VM only, never the troy-g5 host** ([[troy-g5-canary]] discipline: + guest only, redacted evidence, independent teardown). +5. **Provenance.** Pin source commits and record artifact SHA-256 in the run + manifest (existing A0-dev discipline). + +CI: a `packaging` gate that builds the arc `.deb`s, installs from local files, and +drives one unprivileged→`pkexec` mutation — mirroring the read-only packaging gate. + +**Explicitly out of step 1:** signing, the public/signed repository, CRAN submission, +unattended upgrades, the troy-g5 host, and any real-fleet deployment. + +## Validation gate (before A0-release, not step 1) + +A0-release waits on "replace the Python admin layer" confidence, which the current +VM proof (happy path + refusals + the §7/polkit gates on the mutation primitive) does +not yet establish. The bar: + +- the full fleet pipeline from `roadmap.md` — collect → join → preview → approve → + **apply** → verify → **resume the failures by operation id** → durable audit — run + end to end over **real** packages, not just the synthetic canary target; +- **upgrade / rollback** of mutated package state; +- **multi-arch** (the record grammar already keys `package:arch`; prove it on a real + multi-arch host); +- **failure injection** — interrupted transactions, dpkg-broken pre-state, lock + contention, broker/persist failures — verifying the outcome-before-signal and + reconcile-by-cid discipline under fault. + +Only once that holds does the signed A0-release archive (keys, `Valid-Until`, +rotation, bootstrap — designed in `a0-packaging-plan.md`) get built. diff --git a/docs/a0-packaging-plan.md b/docs/a0-packaging-plan.md index bb81330..8a2faab 100644 --- a/docs/a0-packaging-plan.md +++ b/docs/a0-packaging-plan.md @@ -26,6 +26,14 @@ before**. Until then the existing `Trusted: yes` channels are **experimental**. packages, `shlibs:Depends`, `.buildinfo`) is exactly what a Debian submission and A0-release both need — the same source packages feed all of it. +> **apt-arc addendum (2026-08-20) — `a0-apt-arc-addendum.md`.** The CRAN-canonical +> lean above holds for the **read-only** R packages. The **mutation stack** +> (`pkgexec` + broker + `pkgops`) is distributed **together through the signed apt +> repo** as one versioned trust domain (its privileged C artifacts can never be +> CRAN-hosted), and ships as a **separate opt-in `runix-apt` metapackage** — never +> folded into `runix-stack`. `pkgops` may later join CRAN optionally. See the +> addendum for the extended packaging graph and the A0-dev integration scope. + --- # A0-dev (now) diff --git a/docs/pkgops-implementation-plan.md b/docs/pkgops-implementation-plan.md index 7ab22e5..51d6bf0 100644 --- a/docs/pkgops-implementation-plan.md +++ b/docs/pkgops-implementation-plan.md @@ -46,7 +46,7 @@ between 2→3 and 3→4; slice 1 is independent and lands first as the low-risk slice 1 pkgstate::dpkg_selections() [DONE — pkgstate #10, 0.0.1.9] slice 2 runix native effect-session API [C in common core; critical-path enabler] slice 3 pkgops package [needs 1 + 2] -slice 4 rctl apt.* surface [needs 3; LAST, per contract §8] +slice 4 rctl apt.* surface [DONE — rctl #12, 0.0.1.8] ``` - **Slice 1** unblocks hold/unhold verification and touches nothing risky. Good @@ -356,6 +356,21 @@ Mirrors the `host.*`/`packages.*` pattern just landed. pkgops is a runtime-detec contract's terminal envelope. Advertised via `rctl capabilities`. Built only once the R API is stable. +**DONE (2026-08-20, rctl #12, rctl 0.0.1.8).** 18 operations (nine +`apt.-preview` + nine `apt.`) in `rctl/R/dispatch.R`, plus +`ERROR_PASSTHROUGH` widened for pkgops's `verb`/`plan_hash`/`status`/ +`effect_issued`/`detail`. As-built refinements: authorization is **always machine +mode** (`interactive = FALSE`) — rctl runs non-interactively with no polkit agent, +so a denial or `approval_required` surfaces as a terminal exit-1 envelope, never a +prompt; `--preview` (the system-wide dry-run flag) returns the advisory and opens +no intent; and the apt handlers reference pkgops **lazily** (`getExportedValue` +inside the closure, not `mutation_handler`'s eager `force`) so `operations()` and +`capabilities` still build with pkgops absent. `apt.configure` is nullary +(whole-system set = update/upgrade/dist_upgrade/configure). 68 hermetic tests drive +real pkgops through its own faked seams; proven non-vacuous in CI (the CI also +gained `libjansson-dev` + `pkgops` in the sibling build). Hermetic-only proof, no +new VM run. **Slice 4 was the last slice; the arc is functionally complete.** + --- ## 6. Verification ladder (contract §9) and what needs the VM diff --git a/docs/roadmap.md b/docs/roadmap.md index 34a1ac2..267cd1e 100644 --- a/docs/roadmap.md +++ b/docs/roadmap.md @@ -296,10 +296,18 @@ Priority: **[U]** urgent, **[N]** next, **[L]** later. (pkgexec #3, runix #67, with the VM-only lifecycle driver in runix-audit-broker #4). The verification ladder (contract → helper/API → fixture tests → the disposable-VM gate: dpkg locking, maintainer scripts, interrupted transactions, - package ownership) passed. **Remaining: `pkgops`**, the unprivileged R-facing - layer that previews, obtains the receipt, invokes pkgexec, verifies native state, - and closes the audit outcome. **← current arc.** -8. apt mutation implementation; then gaps 4–6 incrementally. + package ownership) passed. **`pkgops` (the unprivileged R issuer) — done + (2026-08-18):** nine `apt__preview()` + nine `apt_()` that preview, + obtain the receipt, invoke pkgexec, verify native state, and close the audit + outcome; the disposable-VM Part B proof passed (polkit 23/23, §7 gates 68/68). + **`rctl apt.*` (slice 4) — done (2026-08-20, rctl #12, 0.0.1.8):** the arc is + exposed on the CLI (`rctl apt install nginx --json`, always machine mode), + completing the boundary end to end. **← arc functionally complete.** +8. **A0 release of the apt arc.** The read-only stack's A0-dev packaging + (`a0-packaging-plan.md`) does not yet cover `pkgexec` (the privileged `.deb`, + still untagged/unpublished) or `pkgops`; scope their packaging and distribution + — CRAN vs a proper signed apt repo is an **open decision**, and a lot more local + testing is wanted first. Then gaps 4–6 incrementally. ## Validation: the workflow that must be materially better