Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
121 changes: 121 additions & 0 deletions docs/a0-apt-arc-addendum.md
Original file line number Diff line number Diff line change
@@ -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.
8 changes: 8 additions & 0 deletions docs/a0-packaging-plan.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
17 changes: 16 additions & 1 deletion docs/pkgops-implementation-plan.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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.<verb>-preview` + nine `apt.<verb>`) 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
Expand Down
16 changes: 12 additions & 4 deletions docs/roadmap.md
Original file line number Diff line number Diff line change
Expand Up @@ -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_<verb>_preview()` + nine `apt_<verb>()` 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

Expand Down
Loading