# Phase 13: Debian Packaging Finalization — Implementation Plan > **For Claude:** REQUIRED SUB-SKILL: Use `superpowers:executing-plans` to implement this plan task-by-task. **Goal:** Turn the Phase 0 packaging skeleton into apt-installable, lintian-clean, piuparts-passing debs that survive install → reinstall → remove → purge cycles cleanly. Wire `pam-auth-update --package` registration into the install/remove hooks and seed an initial policy from a debconf prompt at first install. **Architecture:** - **PAM profile lifecycle.** `authforge-pam.postinst` runs `pam-auth-update --package` on `configure`; `authforge-pam.prerm` runs `pam-auth-update --package --remove authforge` on `remove`/`deconfigure`. The deb ships a baseline pam-configs profile (`/usr/share/pam-configs/authforge`) that only enforces the pending-flag backstop — `Default: no`, no required methods. The daemon overwrites this file at runtime via `policy_apply::apply` once an admin sets a real policy. This guarantees a freshly-installed system never locks anyone out, and `pam-auth-update --remove` reverses cleanly. - **Purge purges data.** `authforge-daemon.postrm` removes `/etc/authforge/` and `/var/lib/authforge/` only on `purge`. `remove` (without purge) leaves them, matching standard Debian policy expectations. - **Debconf-seeded policy.** The metapackage ships `debian/authforge.config` (debconf prompt: None / Optional-everywhere / Required-for-sudo) and `debian/authforge.templates` (the prompt definitions). The `authforge-daemon.postinst` reads the debconf answer on first install only (`[ -z "$2" ]`) and writes `/etc/authforge/policy.d/00-debconf.conf` accordingly. Subsequent upgrades leave the policy alone — admin-edited config wins. - **Test matrix.** Targeting Ubuntu 24.04 (libadwaita 1.5+ requirement rules out 22.04). Manual smoke recipe documented in `debian/PIUPARTS.md`. CI gets a `dpkg-buildpackage -us -uc -b` job and a `lintian -i` job; piuparts stays manual until Phase 14 PPA build. **Tech Stack:** - `debhelper-compat = 13` (already pinned in `debian/control`). - `pam-auth-update` from `libpam-runtime` (already a Depends of authforge-pam). - `dh_installdebconf` from debhelper for debconf wiring. - `lintian`, `piuparts` for QA — installed locally, not in CI sandbox. - `dpkg-buildpackage -us -uc -b` (no GPG signing, binary-only build). **Reference:** - Master plan: [2026-04-26-authforge-implementation.md](2026-04-26-authforge-implementation.md) § Phase 13 (line 1390). - Existing scaffolding: [debian/control](../../debian/control), [debian/rules](../../debian/rules), [debian/authforge-daemon.postinst](../../debian/authforge-daemon.postinst). - PAM profile ground-truth: `/usr/share/pam-configs/` on any Debian/Ubuntu host with `libpam-modules` installed (e.g. `/usr/share/pam-configs/unix`, `/usr/share/pam-configs/cap`). - Debconf manual: `man 7 debconf-devel` (in package `debconf-doc`). - debhelper(7), dh_installpam(1), dh_installdebconf(1). --- ## Conventions - One logical change per commit. Conventional prefixes scoped to `pkg`. - Always commit with `--no-gpg-sign`. - TDD does not apply directly to shell hooks. Each task instead verifies via `dpkg-buildpackage` + `lintian -i` + (where possible) script linting (`shellcheck`). - The phase produces no Rust code — `cargo test --workspace` continues to pass on every commit (it's a no-op for these changes), and CI's existing test stage is unaffected. - After every commit: rebuild with `dpkg-buildpackage -us -uc -b` from a clean tree (or `dh clean && dpkg-buildpackage -us -uc -b`) to confirm the package still builds. --- ## Task 1: Baseline pam-configs profile **Files:** - Create: `debian/authforge.pam-config` - Modify: `debian/rules` — add an install line for the new file. **Background:** `pam-auth-update --package` consults `/usr/share/pam-configs/` for the profile to enable. We need to ship a baseline file so the install hook in [Task 2](#task-2-authforge-pampostinst--register-via-pam-auth-update) has something to enable. The daemon's `policy_apply::render_profile` will overwrite this file at runtime once an admin sets a real policy; the baseline only enforces the pending-flag backstop and otherwise does nothing. **Step 1: Author the baseline profile** ```text # debian/authforge.pam-config Name: AuthForge first-login backstop Default: no Priority: 256 Auth-Type: Primary Auth: [success=ok default=die] pam_authforge_pending.so Auth-Initial: [success=ok default=die] pam_authforge_pending.so Account-Type: Primary Account: sufficient pam_unix.so Account-Initial: sufficient pam_unix.so ``` The `Priority: 256` puts AuthForge before stock primary auth (pam_unix is 192) so the pending-flag backstop runs first. `Default: no` means the profile is shipped but disabled by default — admins opt in via the debconf prompt or a later policy-set. (`Auth-Initial` is required for the unconfigured-pam-runtime path; `Account-Type: Primary` + `Account: sufficient pam_unix.so` is a no-op pass-through that satisfies pam-auth-update's requirement that every primary profile have an Account block.) **Step 2: Install via debian/rules** In `debian/rules` `override_dh_auto_install`, after the `# PAM` comment block, add: ```makefile # Baseline pam-configs profile for pam-auth-update; daemon overwrites # /usr/share/pam-configs/authforge at runtime when a real policy lands. install -D -m 0644 debian/authforge.pam-config \ debian/authforge-pam/usr/share/pam-configs/authforge ``` **Step 3: Build to verify** Run: `dh clean && dpkg-buildpackage -us -uc -b 2>&1 | tail -20` Expected: no errors. The new file lands in `debian/authforge-pam/usr/share/pam-configs/authforge` (visible via `find debian/authforge-pam/`). **Step 4: Commit** ```bash git add debian/authforge.pam-config debian/rules git commit --no-gpg-sign -m "feat(pkg): ship baseline pam-configs profile (Default: no, pending-flag only)" ``` --- ## Task 2: `authforge-pam.postinst` — register via `pam-auth-update` **Files:** - Create: `debian/authforge-pam.postinst` **Step 1: Write the postinst** ```sh #!/bin/sh # debian/authforge-pam.postinst # Registers /usr/share/pam-configs/authforge via pam-auth-update on first # install and reconfigure events. The profile ships Default: no, so this # is a no-op for fresh installs — the metapackage's debconf flow flips it # on if the admin opted in. Idempotent: safe to call repeatedly. set -e case "$1" in configure) if [ -x /usr/sbin/pam-auth-update ]; then pam-auth-update --package fi ;; abort-upgrade|abort-remove|abort-deconfigure) ;; *) echo "postinst called with unknown argument \`$1'" >&2 exit 1 ;; esac #DEBHELPER# exit 0 ``` **Step 2: Make executable + lint** ```bash chmod +x debian/authforge-pam.postinst shellcheck debian/authforge-pam.postinst ``` Expected: shellcheck clean (or skip the run with a one-line note in the commit message if `shellcheck` isn't installed in the dev sandbox). **Step 3: Build to verify** Run: `dh clean && dpkg-buildpackage -us -uc -b 2>&1 | tail -10` Expected: clean. **Step 4: Commit** ```bash git add debian/authforge-pam.postinst git commit --no-gpg-sign -m "feat(pkg): authforge-pam postinst calls pam-auth-update --package" ``` --- ## Task 3: `authforge-pam.prerm` — deregister cleanly **Files:** - Create: `debian/authforge-pam.prerm` **Background:** Removing the package without `pam-auth-update --remove` would leave `/etc/pam.d/common-*` referencing a profile whose backing files are about to disappear, which would break authentication for everyone on the box. The `--remove authforge` invocation tells pam-auth-update to take our profile out of the rendered stack first. **Step 1: Write the prerm** ```sh #!/bin/sh # debian/authforge-pam.prerm # Deregisters the authforge profile from /etc/pam.d/common-* before # the .so disappears. Ordering matters: prerm runs before any files are # removed, so /usr/share/pam-configs/authforge is still present here for # pam-auth-update to inspect. set -e case "$1" in remove|deconfigure) if [ -x /usr/sbin/pam-auth-update ]; then pam-auth-update --package --remove authforge fi ;; upgrade|failed-upgrade) ;; *) echo "prerm called with unknown argument \`$1'" >&2 exit 1 ;; esac #DEBHELPER# exit 0 ``` **Step 2: Make executable + lint + build** ```bash chmod +x debian/authforge-pam.prerm shellcheck debian/authforge-pam.prerm dh clean && dpkg-buildpackage -us -uc -b 2>&1 | tail -10 ``` Expected: clean. **Step 3: Commit** ```bash git add debian/authforge-pam.prerm git commit --no-gpg-sign -m "feat(pkg): authforge-pam prerm calls pam-auth-update --remove on uninstall" ``` --- ## Task 4: `authforge-daemon.postrm` — purge cleanup **Files:** - Create: `debian/authforge-daemon.postrm` **Background:** On `purge` (not plain `remove`) we delete `/etc/authforge/` (policy.d directory; admin-edited config) and `/var/lib/authforge/` (pending flags, recovery codes, TOTP secrets, userdb). Plain `remove` leaves these so an upgrade-via-remove-then-install round-trip preserves admin state. **Step 1: Write the postrm** ```sh #!/bin/sh # debian/authforge-daemon.postrm # Cleans up state directories on purge. /etc/authforge/ holds policy # config; /var/lib/authforge/ holds runtime state (pending flags, # recovery hashes, TOTP secrets, userdb). Both are intentionally # preserved on plain remove — admin state survives a remove/install # round-trip, which matches Debian Policy §6.6 "Removing the package". set -e case "$1" in purge) rm -rf /etc/authforge rm -rf /var/lib/authforge ;; remove|upgrade|failed-upgrade|abort-install|abort-upgrade|disappear) ;; *) echo "postrm called with unknown argument \`$1'" >&2 exit 1 ;; esac #DEBHELPER# exit 0 ``` **Step 2: Make executable + lint + build** ```bash chmod +x debian/authforge-daemon.postrm shellcheck debian/authforge-daemon.postrm dh clean && dpkg-buildpackage -us -uc -b 2>&1 | tail -10 ``` Expected: clean. **Step 3: Commit** ```bash git add debian/authforge-daemon.postrm git commit --no-gpg-sign -m "feat(pkg): authforge-daemon postrm purges /etc/authforge and /var/lib/authforge" ``` --- ## Task 5: Debconf templates + config script **Files:** - Create: `debian/authforge.templates` - Create: `debian/authforge.config` - Modify: `debian/control` — add `debconf | debconf-2.0` to the metapackage's `Pre-Depends`. **Background:** Debconf prompts run *before* postinst, so the answer is available when the postinst writes the seed policy in [Task 6](#task-6-postinst-seeds-policy-from-debconf-answer). The prompt is on the metapackage (`authforge`), not `authforge-daemon`, because the metapackage is what users `apt install` — installing `authforge-daemon` directly is an advanced path that skips the prompt and gets the safe Default-no profile. **Step 1: Author the templates** ```text # debian/authforge.templates Template: authforge/initial-policy Type: select Default: None Choices: None, Optional everywhere, Required for sudo Description: Initial AuthForge policy: AuthForge can write a starter policy at install time. Choose: . None — install only, no MFA enforcement (admins configure later). Optional everywhere — every PAM stack offers FIDO2/TOTP but doesn't require it. Required for sudo — sudo prompts for FIDO2 or recovery code in addition to the password. . You can change this any time later via the AuthForge GUI or `authforgectl policy set`. ``` **Step 2: Author the config script** ```sh #!/bin/sh # debian/authforge.config # Asks the admin for an initial policy. Postinst (Task 6) reads the # answer and writes /etc/authforge/policy.d/00-debconf.conf on first # install only. Re-runs (e.g. `dpkg-reconfigure authforge`) re-prompt # but do not overwrite an existing seed file unless the admin chose to. set -e . /usr/share/debconf/confmodule db_input medium authforge/initial-policy || true db_go || true exit 0 ``` **Step 3: Make config executable + add Pre-Depends** ```bash chmod +x debian/authforge.config ``` In `debian/control`, the `Package: authforge` paragraph, add a `Pre-Depends:` line: ``` Pre-Depends: debconf (>= 0.5) | debconf-2.0 ``` (Place it between `Architecture: any` and the existing `Depends:` line.) **Step 4: Lint + build** ```bash shellcheck debian/authforge.config dh clean && dpkg-buildpackage -us -uc -b 2>&1 | tail -10 ``` Expected: clean. The build output now mentions `dh_installdebconf` consuming the templates. **Step 5: Commit** ```bash git add debian/authforge.templates debian/authforge.config debian/control git commit --no-gpg-sign -m "feat(pkg): debconf prompt for initial AuthForge policy on metapackage install" ``` --- ## Task 6: Postinst seeds policy from debconf answer **Files:** - Create: `debian/authforge.postinst` **Background:** The metapackage `authforge` has no binaries of its own, but it gets a postinst because that's the only place we can read the debconf answer with the metapackage's own `confmodule` context. The postinst writes `/etc/authforge/policy.d/00-debconf.conf` only on first install (`[ -z "$2" ]` — no previously-configured version) and only if the admin picked something other than "None". Subsequent upgrades and reconfigures leave any existing file alone — admin edits win. **Step 1: Author the postinst** ```sh #!/bin/sh # debian/authforge.postinst # Reads the debconf answer and writes /etc/authforge/policy.d/00-debconf.conf # exactly once, on first install. Subsequent upgrades/reconfigures don't # touch the file — admin edits via authforgectl/GUI win. set -e . /usr/share/debconf/confmodule POLICY_DIR=/etc/authforge/policy.d SEED_FILE=$POLICY_DIR/00-debconf.conf case "$1" in configure) # Only seed on first install: $2 is empty when no previous version # was configured. Skip if the seed file already exists (admin may # have written one manually before apt configured the metapackage). if [ -z "$2" ] && [ ! -e "$SEED_FILE" ]; then db_get authforge/initial-policy || true mkdir -p "$POLICY_DIR" case "$RET" in "None") : # no seed — leave dir empty ;; "Optional everywhere") cat > "$SEED_FILE" <<'EOF' # /etc/authforge/policy.d/00-debconf.conf # Seeded by debconf on first install. Edit via `authforgectl policy set` # or the AuthForge GUI; admin edits override on next daemon reload. [stacks.gdm-password] mode = "optional" methods = ["fido2"] [stacks.sudo] mode = "optional" methods = ["fido2", "totp"] [stacks.sshd] mode = "optional" methods = ["fido2"] EOF ;; "Required for sudo") cat > "$SEED_FILE" <<'EOF' # /etc/authforge/policy.d/00-debconf.conf # Seeded by debconf on first install. Edit via `authforgectl policy set` # or the AuthForge GUI; admin edits override on next daemon reload. [stacks.sudo] mode = "required" methods = ["fido2", "totp"] EOF ;; esac fi ;; abort-upgrade|abort-remove|abort-deconfigure) ;; *) echo "postinst called with unknown argument \`$1'" >&2 exit 1 ;; esac #DEBHELPER# exit 0 ``` **Step 2: Make executable + lint + build** ```bash chmod +x debian/authforge.postinst shellcheck debian/authforge.postinst dh clean && dpkg-buildpackage -us -uc -b 2>&1 | tail -10 ``` Expected: clean. **Step 3: Verify the seed file format matches `daemon/src/storage/policy.rs`** The policy parser is in `daemon/src/storage/policy.rs`. Skim it (look for the TOML deserialize struct) to confirm the seed file's shape — `[stacks.]` tables with `mode` (string) + `methods` (array). If the parser shape diverges (e.g. expects `[]` directly, or a different field for methods), update the seed snippet before committing. **Step 4: Commit** ```bash git add debian/authforge.postinst git commit --no-gpg-sign -m "feat(pkg): metapackage postinst seeds /etc/authforge/policy.d/00-debconf.conf from debconf answer" ``` --- ## Task 7: lintian sweep + overrides **Files:** - Possibly: `debian/source/lintian-overrides` - Possibly: `debian/.lintian-overrides` (per-binary-package overrides) **Background:** lintian flags packaging deviations. Some are real bugs we'll fix; others are intentional (e.g., embedded-library for vendored Rust crates) and need explicit overrides with rationale comments. **Step 1: Build + run lintian** ```bash dh clean && dpkg-buildpackage -us -uc -b lintian -i ../authforge_*.deb ../authforge-*.deb 2>&1 | tee /tmp/lintian.txt ``` `-i` includes verbose explanations. Capture the output to `/tmp/lintian.txt` for review. **Step 2: Categorize + fix or override** For each `W:` (warning) or `E:` (error) lintian reports: - **Real bug** → fix at the source (e.g. missing manpage → write one; postinst-uses-tmp → use `mktemp`). - **Intentional deviation** → add to a `lintian-overrides` file with a comment explaining why. Common cases for this project: - `embedded-library` on the daemon binary — Rust statically links its deps; this is normal. - `extended-description-is-probably-too-short` if any package's Description field is one-line — fine for now, possibly extend later. - `binary-without-manpage` — defer manpages to Phase 18 (user docs); add an override with `manpages-deferred-to-phase-18`. - `executable-not-elf-or-script` on the GTK desktop file installer scripts (n/a here). Override file format example: ```text # debian/source/lintian-overrides # Rust binaries statically link the cargo dependency tree; this is # the standard Cargo-built Debian package shape, not an embedded # library bug. Same exception used by upstream rustc, cargo, ripgrep, # fd-find, etc. authforge-daemon: embedded-library authforge-cli: embedded-library authforge-gui: embedded-library # Manpages are deferred to Phase 18 (user docs) — we want to ship 0.1.0 # without them rather than block on docs. Listed in master plan. authforge-cli: binary-without-manpage usr/bin/authforgectl authforge-gui: binary-without-manpage usr/bin/authforge authforge-daemon: binary-without-manpage usr/sbin/authforged ``` (The exact set of overrides depends on what lintian actually reports — start with the categorized list from Step 1 and add only what's needed.) **Step 3: Re-run lintian to confirm zero W/E remain** ```bash dh clean && dpkg-buildpackage -us -uc -b lintian ../authforge_*.deb ../authforge-*.deb ``` Expected: no output (silent = clean) or only `I:` info-level notes. **Step 4: Commit** ```bash git add debian/source/lintian-overrides debian/*.lintian-overrides # plus any source-level fixes you made git commit --no-gpg-sign -m "chore(pkg): lintian overrides for embedded-library + deferred-manpage; fix any real warnings" ``` (If you only added overrides and made no real fixes, the commit message can drop the "fix any real warnings" suffix.) --- ## Task 8: piuparts test recipe **Files:** - Create: `debian/PIUPARTS.md` **Background:** piuparts (Package Installation, Upgrading, and Removal Testing Suite) runs install → upgrade → remove → purge → reinstall in a clean chroot. It's heavyweight (downloads a full Ubuntu base) and requires root, so it stays manual until the Phase 14 PPA build picks it up automatically. **Step 1: Author the recipe** ```markdown # Testing the AuthForge debs with piuparts piuparts simulates a clean Debian/Ubuntu install in a chroot, runs the install/upgrade/remove/purge cycle, and verifies no files leak. This is the strongest manual gate before the Phase 14 PPA build picks up automated VM-based testing. ## Target **Ubuntu 24.04 only** for v1. The GUI (`authforge-gui`) hard-requires libadwaita 1.5+, which Ubuntu 22.04 does not ship. Older targets would need a 1.5 backport PPA — not worth the matrix complexity for v1. ## Prerequisites ```bash sudo apt install piuparts debootstrap ``` piuparts pulls a fresh `noble` (24.04) chroot on every run; first run takes ~5 minutes to download base, subsequent runs use the cache at `/var/cache/piuparts/`. ## Run From the project root, after a successful `dpkg-buildpackage -us -uc -b`: ```bash sudo piuparts \ --distribution=noble \ --mirror "http://archive.ubuntu.com/ubuntu noble main universe" \ --arch amd64 \ --warn-on-leftovers-after-purge \ ../authforge_*.deb \ ../authforge-*.deb ``` The `--mirror` line includes `universe` because `libpam-google-authenticator` (Recommends of `authforge-daemon`) lives there. Without it piuparts can't resolve the recommends and the test fails before installing AuthForge. ## What piuparts exercises 1. Install all debs together (metapackage pulls in all binary packages). 2. Verify the daemon's systemd unit enables + starts under systemd-in-chroot. 3. Verify `pam-auth-update --package` registered the AuthForge profile (look for `authforge` in `/etc/pam.d/common-auth`). 4. Reinstall (0.1.0 over 0.1.0) — postinst runs again, must be idempotent. 5. Remove — `pam-auth-update --remove authforge` runs in prerm; the binaries disappear; `/etc/authforge/` and `/var/lib/authforge/` survive. 6. Purge — postrm wipes `/etc/authforge/` and `/var/lib/authforge/`; piuparts diffs the chroot against its initial snapshot and reports leftovers. ## Pass criteria - [ ] piuparts exit code 0. - [ ] No `FAIL` lines in the output. - [ ] Any `WARN: leftover` lines refer only to systemd/journald state (logs, runtime files in `/run/`) — not files in `/etc/authforge/`, `/var/lib/authforge/`, or `/usr/share/pam-configs/`. - [ ] `pam-auth-update --list` (run inside the chroot mid-test, if you pause it) shows `authforge` after install and absent after remove — piuparts surfaces this implicitly via its `/etc/pam.d/` diff. ## Known surface gaps - piuparts can't exercise the GUI or the FIDO2 enrollment flow — those need a real desktop session, deferred to Phase 14 VM smoke (`gui/TESTING.md`, `gui/FIRSTRUN-TESTING.md`). - The debconf prompt is non-interactive in piuparts (uses defaults). To test the "Required for sudo" branch end-to-end, run a manual install with `DEBCONF_PRIORITY=critical` and check that `/etc/authforge/policy.d/00-debconf.conf` was written. ``` **Step 2: Commit** ```bash git add debian/PIUPARTS.md git commit --no-gpg-sign -m "docs(pkg): piuparts test recipe — Ubuntu 24.04, install/remove/purge cycle" ``` --- ## Task 9: Master-plan closeout note **Files:** - Modify: `docs/plans/2026-04-26-authforge-implementation.md` **Step 1: Flip Phase 13 to ✅ Code complete** The roadmap row at line 43 currently reads: ``` | 13 | Debian packaging finalization (postinst, debconf, purge) | 4 days | **Spec'd** — sequential tail; needs all binaries built | ``` Change to: ``` | 13 | Debian packaging finalization (postinst, debconf, purge) | 4 days | ✅ **Code complete** (yyyy-mm-dd) — `piuparts` smoke deferred to Phase 14 | ``` (Replace `yyyy-mm-dd` with the date you commit Task 9.) **Step 2: Append a closeout note** After the existing Phase 11 closeout (currently around line 316): ```markdown ### Phase 13 closeout notes (yyyy-mm-dd) Landed via [2026-04-27-phase-13-deb-packaging.md](2026-04-27-phase-13-deb-packaging.md). 9 commits on `main` (no worktree — all changes are in `debian/` and don't touch the workspace's Rust crates). **Done:** - `debian/authforge.pam-config` — baseline `pam-auth-update` profile, `Default: no`, pending-flag-only. Daemon's `policy_apply::render_profile` overwrites this file at runtime once a real policy is set. - `debian/authforge-pam.{postinst,prerm}` — registers via `pam-auth-update --package` on `configure`; deregisters via `--remove authforge` on `remove`/`deconfigure`. Idempotent. - `debian/authforge-daemon.postrm` — purges `/etc/authforge/` + `/var/lib/authforge/` on `purge`; preserves them on plain `remove` (Debian Policy §6.6). - `debian/authforge.{templates,config}` — debconf prompt: `None` / `Optional everywhere` / `Required for sudo`. Default `None`. `Pre-Depends: debconf` added to the metapackage. - `debian/authforge.postinst` — reads the debconf answer; writes `/etc/authforge/policy.d/00-debconf.conf` exactly once on first install. Subsequent upgrades and admin edits win. - `debian/source/lintian-overrides` + per-binary `lintian-overrides` files — zero W/E from `lintian -i` on the built debs. _(fill in: any real warnings fixed at the source)_ - `debian/PIUPARTS.md` — manual smoke recipe for Ubuntu 24.04. Install → reinstall → remove → purge cycle documented; piuparts run is the Phase 14 gate. **Plan deviations:** _(fill in)_. **Test count delta:** 0 (Phase 13 ships no Rust code). Workspace test count remains 112. **Deferred until Phase 14 PPA + VM smoke:** - `piuparts` actually run on a clean Ubuntu 24.04 chroot. - `pam-auth-update --package` actually mutating `/etc/pam.d/common-*` on a real system. - The "Required for sudo" debconf branch tested end-to-end (sudo prompts for FIDO2 + password). - `dpkg-buildpackage -us -uc -b` producing five debs that install on a fresh VM. ``` **Step 3: Commit** ```bash git add docs/plans/2026-04-26-authforge-implementation.md git commit --no-gpg-sign -m "docs: phase 13 closeout notes" ``` --- ## Phase 13 Acceptance Gate Verify before declaring the phase done: - [ ] `dh clean && dpkg-buildpackage -us -uc -b` exits 0; produces 5 binary `.deb` files in `..` (parent dir of project): `authforge_*.deb`, `authforge-cli_*.deb`, `authforge-daemon_*.deb`, `authforge-gui_*.deb`, `authforge-pam_*.deb` (plus the optional `authforge-gnome-integration_*.deb`). - [ ] `lintian -i ../authforge_*.deb ../authforge-*.deb` reports zero `W:` / `E:` (only `I:` informational notes allowed). - [ ] `cargo test --workspace` still passes (112 tests; this phase doesn't touch Rust). - [ ] `cargo fmt --all -- --check` clean. - [ ] `shellcheck` on every script in `debian/*.{config,postinst,prerm,postrm}` (or document why it was skipped). - [ ] All four hook scripts (`authforge.config`, `authforge.postinst`, `authforge-pam.postinst`, `authforge-pam.prerm`, `authforge-daemon.postrm`) are mode 0755 / executable. - [ ] `debian/changelog` updated to bump from `(0.1.0-1) UNRELEASED` to `(0.1.0-1) noble; urgency=medium` (Phase 14 will dput this). - [ ] Manual `piuparts` run in `debian/PIUPARTS.md` is documented but **not** required for this phase — Phase 14 owns the actual run. --- ## Risks / known unknowns | Risk | Mitigation | |---|---| | The baseline `debian/authforge.pam-config` profile syntax is rejected by `pam-auth-update --package` in the chroot. | Cross-check against `/usr/share/pam-configs/unix` and `/usr/share/pam-configs/cap` on a 24.04 host before committing — those are the canonical reference profiles. If `Auth-Initial:` isn't matching the pattern, `pam-auth-update --debug` shows the parse error. | | `Pre-Depends: debconf` makes the metapackage unbootstrappable in some Launchpad sbuild configs. | The exact line `debconf (>= 0.5) | debconf-2.0` is the standard Debian boilerplate (see `man 7 debconf-devel`). Any sbuild that rejects it would also reject every other Ubuntu package. If a build does fail on this, fall back to plain `Depends:` — debconf is in `essential`-adjacent territory and is always present at install time anyway. | | `cat > "$SEED_FILE" <<'EOF'` in the metapackage postinst writes a TOML structure that `daemon/src/storage/policy.rs` rejects. | Task 6 Step 3 explicitly calls for sanity-checking the parser shape before committing. If the daemon's TOML schema differs, the parser will fail to load the seed and the daemon will boot with an empty policy — recoverable, but ugly. The cheap prevention is a `cargo run -p authforge-daemon` against a real seed file in a tempdir before final commit. | | `lintian` raises `maintainer-script-empty` on hooks that consist of only a `case` block + `#DEBHELPER#`. | Confirmed false-positive — debhelper expands `#DEBHELPER#` to several lines at build time. If lintian flags it, override with `non-empty-after-debhelper-expansion`. | | `piuparts` can't run inside a sandbox (needs root + chroot). | Documented as Phase 14 gate. CI gets `dpkg-buildpackage` + `lintian` only. | | `dh clean` doesn't fully clean the cargo target dir, slowing rebuilds. | `debian/rules`' `override_dh_auto_clean` already calls `cargo clean`. If Phase 13 reveals stale `target/` artifacts, add an explicit `rm -rf target/cargo-home target/release/build` before each test build. | | GUI hard-requires libadwaita 1.5; piuparts on a 24.04 chroot has it but headless install of `authforge-gui` may fail at `dh_strip` time without a display. | `dh_strip` is a static analysis pass — no display server needed. `apt install authforge-gui` in a headless chroot will install fine; running the binary needs a display, which the piuparts test doesn't do. | | The `Auth-Type: Primary` baseline profile causes `pam-auth-update` to elevate AuthForge above `pam_unix` even with `Default: no`, breaking login on systems where someone re-runs `pam-auth-update` interactively. | `Default: no` keeps the profile out of the rendered stack regardless of priority. The interactive run would offer the profile but leave it unchecked unless the admin actively selects it. Phase 14 smoke verifies this on a real desktop. | --- ## Execution Handoff Plan complete and saved to `docs/plans/2026-04-27-phase-13-deb-packaging.md`. Single-lane, single-agent — no parallelism wins (every task touches `debian/`, and Tasks 6 + 7 + 8 + 9 each depend on prior tasks for verification). **Two execution options:** 1. **Subagent-Driven (this session)** — Dispatch a fresh subagent per task with code review between tasks. Fast iteration, but the main session carries the package-build context. 2. **Parallel Session (separate)** — Open a new session; that session uses `superpowers:executing-plans` to walk the plan top-to-bottom with checkpoint commits. Frees this session for review work. Either way, do **not** start until the dev environment has `lintian` and (preferably) `shellcheck` installed: ```bash sudo apt install lintian shellcheck ``` `piuparts` is **not** required for this phase — it's the Phase 14 gate.