docs: consolidate roadmap with status legend and per-phase closeout notes

Cleans up scattered status markers into a single legend (Done / Code complete
/ Spec'd / Bundled), backfills v0.1.0-phase1 tag reference for Phase 1, marks
the table consistently for what's actually shipping vs. what needs the Phase
14 VM smoke. Updates the lane diagram to show what's done vs. in flight, and
adds a 'lessons learned from this session's parallelism' section capturing
when subagents-in-worktrees actually work and when they slip on sandbox-blocked
verification commands.

Adds closeout notes for Phases 3, 4+5, 6, 7, 15, 16 alongside the existing
Phase 1 and Phase 2 sections — what shipped, plan deviations, and any wire-
breaking signature changes (SetPolicy gained a force flag in Phase 4+5).
This commit is contained in:
michael
2026-04-27 08:48:34 -07:00
parent 5668ba1f69
commit 481f7f8543

View File

@@ -16,30 +16,39 @@
## Phase Roadmap
### Status legend
-**Done** — code on `main`, unit/integration tests green, no Phase 14 VM smoke required (data/packaging/refactor phases that don't talk to real hardware or root-owned files).
-**Code complete** — code on `main`, unit/integration tests green; one or more acceptance gates require real hardware or `/usr/share/...` write access and are deferred to the Phase 14 PPA-build VM smoke.
- **Spec'd** — high-level scope in this doc; needs a step-level expansion plan (re-invoke `superpowers:writing-plans`) before execution.
- **Bundled** — landed together with another phase because they share a code path that can't be split atomically.
### Roadmap
| Phase | Goal | Approx. Effort | Status |
|---|---|---|---|
| 0 | Repo, workspace, CI, packaging skeleton | 23 days | ✅ **Done** (2026-04-26) |
| 1 | Daemon: D-Bus interface stub, systemd unit, polkit rules | 34 days | ✅ **Code complete** (2026-04-27); smoke gates deferred |
| 0 | Repo, workspace, CI, packaging skeleton | 23 days | ✅ **Done** (2026-04-26, tag `v0.1.0-scaffolding`) |
| 1 | Daemon: D-Bus interface stub, systemd unit, polkit rules | 34 days | ✅ **Code complete** (2026-04-27, tag `v0.1.0-phase1`) |
| 2 | Daemon: storage layer (policy.d parser, pending flags, user db) | 34 days | ✅ **Done** (2026-04-27, tag `v0.2.0-storage`) |
| 3 | Daemon: FIDO2 enrollment backend via `ctap-hid-fido2` | 57 days | ✅ **Code complete** (2026-04-27, tag `v0.3.0-multi-lane`); real-HW smoke deferred |
| 4 | Daemon: policy apply via pam-auth-update wrapper | 4 days | ✅ **Done** (2026-04-27, tag `v0.4.0-parallel`) |
| 5 | Daemon: lockout simulator | 3 days | ✅ **Done** (bundled with Phase 4) |
| 6 | PAM module: `pam_authforge_pending.so` (C) | 3 days | ✅ **Code complete** (2026-04-27); pamtester smoke recipe in pam/TESTING.md |
| 7 | CLI: `authforgectl` | 4 days | ✅ **Code complete** (2026-04-27); 14 subcommands wired |
| 8 | GUI: app shell + Security Keys tab | 68 days | Spec'd |
| 9 | GUI: Policy tab + lockout-warning UX | 4 days | Spec'd |
| 10 | First-login flow: autostart entry + fullscreen modal | 4 days | Spec'd |
| 11 | TOTP support (PAM module + GUI tab) — feature flag | 5 days | Spec'd |
| 12 | Recovery flow (codes + emergency unlock) | 4 days | Spec'd |
| 13 | Debian packaging finalization (postinst, debconf, purge) | 4 days | Spec'd |
| 14 | Launchpad PPA build setup | 2 days | Spec'd |
| 15 | `authforge-gnome-integration` (Users panel shortcut) | 3 days | ✅ **Done** (2026-04-27, parallel subagent) |
| 16 | Ansible role `dangerousthings.authforge` | 2 days | ✅ **Done** (2026-04-27, parallel subagent) |
| 17 | Integration test harness (Multipass / LXD VM) | 4 days | Spec'd |
| 18 | User docs + onboarding site | 3 days | Spec'd |
| 3 | Daemon: FIDO2 enrollment backend via `ctap-hid-fido2` | 57 days | ✅ **Code complete** (2026-04-27, tag `v0.3.0-multi-lane`) real-HW smoke deferred |
| 4 | Daemon: policy apply via pam-auth-update wrapper | 4 days | ✅ **Code complete** (2026-04-27, tag `v0.4.0-parallel`) — root-write smoke deferred |
| 5 | Daemon: lockout simulator | 3 days | ✅ **Done** **Bundled** with Phase 4 |
| 6 | PAM module: `pam_authforge_pending.so` (C) | 3 days | ✅ **Code complete** (2026-04-27) `pamtester` smoke recipe in `pam/TESTING.md` |
| 7 | CLI: `authforgectl` | 4 days | ✅ **Done** (2026-04-27) 14 subcommands |
| 8 | GUI: app shell + Security Keys tab | 68 days | **Spec'd** — next single-lane bundle (see § Parallel Execution Lanes) |
| 9 | GUI: Policy tab + lockout-warning UX | 4 days | **Spec'd** — opens after Phase 8 |
| 10 | First-login flow: autostart entry + fullscreen modal | 4 days | **Spec'd** — needs Phase 6 ✓ + Phase 8 |
| 11 | TOTP support (PAM module + GUI tab) — feature flag | 5 days | **Spec'd** — opens now (needs Phase 2 ✓ + Phase 4 ✓) |
| 12 | Recovery flow (codes + emergency unlock) | 4 days | **Spec'd** — opens now (needs Phase 6 ✓) |
| 13 | Debian packaging finalization (postinst, debconf, purge) | 4 days | **Spec'd** — sequential tail; needs all binaries built |
| 14 | Launchpad PPA build setup | 2 days | **Spec'd** — sequential tail after 13; **VM smoke gate for all "Code complete" phases** |
| 15 | `authforge-gnome-integration` (Users panel shortcut) | 3 days | ✅ **Done** (2026-04-27) — parallel subagent (gnome lane) |
| 16 | Ansible role `dangerousthings.authforge` | 2 days | ✅ **Done** (2026-04-27) — parallel subagent (ansible lane) |
| 17 | Integration test harness (Multipass / LXD VM) | 4 days | **Spec'd** — sequential tail |
| 18 | User docs + onboarding site | 3 days | **Spec'd** — anytime slot |
| **R** | **v1.0 release** | 1 day | — |
Total rough estimate: ~14 weeks of focused work for v1.0. Subsequent phases (Debian packaging, KDE port, RPM) are scoped in the design doc.
**Progress: 11 of 19 phases code-complete (58%).** Total original estimate was ~14 weeks of focused work for v1.0. Subsequent phases (Debian packaging, KDE port, RPM) are scoped in the design doc.
---
@@ -56,29 +65,29 @@ The phase numbering is a dependency-respecting ordering, **not** a serial schedu
┌──────────┬───────────┼───────────┬──────────┐
│ │ │ │ │
Phase 3 Phase 4+5 Phase 6 Phase 7 (Phase 15: anytime)
FIDO2 policy+ PAM C CLI (Phase 18: anytime)
backend lockout module authforgectl
Phase 3 Phase 4+5 Phase 6 Phase 7 Phase 15
FIDO2 policy+ PAM C CLI GNOME shim
backend lockout module authforgectl (parallel agent)
│ │ │ │
↓ ↓ ↓ ↓
└──────┬───┘ │ │
↓ │ │
┌───────────┐ │ │
Phase 8 Phase 9 (Phase 12 Phase 16
GUI keys GUI policy recovery) Ansible
│ │ │
└──────────┼────────────┘
↓ │
Phase 10 │
first-login │
│ │
└────────────┬───────────┘
Phase 8 Phase 9 Phase 12 Phase 16
GUI keys GUI policy recovery Ansible (parallel agent)
(next) (after 5) (open)
│ │
└──────────┼────────────┘
Phase 10
first-login (needs 6 + 8)
Phase 13
deb finalization
deb finalization (needs every binary)
Phase 14
Launchpad PPA
Launchpad PPA (VM smoke gate for "Code complete" phases)
Phase 17
integration tests
@@ -90,47 +99,26 @@ The phase numbering is a dependency-respecting ordering, **not** a serial schedu
v1.0 release
```
### Parallelizable now (post-Phase 2)
### What's parallel-safe right now (post-Phase 4+5)
Four lanes touch disjoint file trees and can land in any order:
Three lanes touch disjoint file trees and can land in any order:
| Lane | Phases | Touches | Why parallel-safe |
|---|---|---|---|
| **A: FIDO2 backend** | 3 | `daemon/src/fido/`, small `dbus.rs` hook for signals | Self-contained; only consumes the storage façade from Phase 2. |
| **B: Policy + lockout** | 4 + 5 (must land together) | `daemon/src/policy_apply/`, `daemon/src/lockout/`, `dbus.rs` SetPolicy path | 5 gates 4 in the same call chain, so they go as one unit, but the unit doesn't touch FIDO2, PAM, or CLI. |
| **C: PAM module** | 6 | `pam/pam_authforge_pending.c` + `Makefile` | Pure C against the JSON-pending-flag contract Phase 2 already writes. Zero overlap with Rust code. |
| **D: CLI** | 7 | `cli/src/` | The D-Bus contract was settled in Phase 1; CLI is a thin client. |
### Parallelizable in second wave (after first wave's lanes land)
- **Phase 8** (GUI Keys tab) — opens after Lane A (Phase 3) lands. Needs real enrollment for the demo.
- **Phase 9** (GUI Policy tab) — opens after Lane B (Phase 5) lands. Needs the lockout simulator for the warning banner.
- **Phase 11** (TOTP, feature-flagged) — opens after Lane B. Independent of Phases 8/9.
- **Phase 12** (Recovery flow) — opens after Lane C (Phase 6). Independent of GUI lanes.
- **Phase 16** (Ansible role) — opens after Lane D (Phase 7). Independent of everything else.
### Third wave (single dependency)
- **Phase 10** (first-login flow) — needs Phase 6 (PAM backstop) + Phase 8 (GUI fullscreen). Cannot parallelize earlier.
### Always-anytime slots
- **Phase 15** (`authforge-gnome-integration` shim) — pure data deb; can be built whenever there's idle capacity.
- **Phase 18** (user docs) — best authored alongside their phase, but writing can start whenever a feature stabilizes.
| **GUI** | 8 (then 9, after 5 ✓) | `gui/src/` | The D-Bus contract is fully wired; GUI is a pure consumer. Needs `libgtk-4-dev` + `libadwaita-1-dev` on the box. |
| **Recovery** | 12 | `daemon/src/recovery.rs`, `pam/pam_authforge_pending.c` (recovery hook), small `dbus.rs` `GenerateRecoveryCode` body | The PAM module already has a `recovery_code_matches` stub returning 0; Phase 12 makes it real. Daemon side is brand-new file. |
| **TOTP** | 11 | `daemon/src/totp/`, `gui/src/views/totp.rs` (when 8 lands), `debian/control` Recommends | Feature-flagged at build time; only the daemon-side bits open right now. GUI tab waits on Phase 8. |
### Sequential tail (no parallelism wins)
- **Phase 13** (deb finalization) needs all binaries built.
- **Phase 14** (PPA) needs Phase 13.
- **Phase 17** (integration test harness) needs end-to-end binaries from at least one PPA build.
- **R** (release tag) is last.
Phase 10 (first-login) needs both Phase 6 ✓ and Phase 8. Phase 13 (deb finalization) needs every binary built. Phase 14 (PPA) chains off 13 — and is the **VM smoke gate** for everything currently in "Code complete" status. Phase 17 (integration tests) chains off 14. Phase 18 (user docs) and R (release) follow.
### Practical notes for executing this in parallel
### Lessons learned from this session's parallelism
- Subagents in `git worktree` is the canonical isolation mechanism, but Phase 1 hit a harness `cwd` quirk that broke worktree-based subagents. Re-test before relying on it; otherwise batch independent lanes in a single session.
- Phase 4 + Phase 5 must land in the same change because both modify the SetPolicy path; splitting them creates a window where SetPolicy applies without a lockout pre-check.
- Phase 8 (GUI) requires `libgtk-4-dev` + `libadwaita-1-dev` on the dev box. Treat it as its own physical build environment.
- Phase 6 (PAM C) requires `libpam0g-dev`. Build is a one-line `make -C pam`; no Rust toolchain involvement.
- **Subagents in `isolation: "worktree"` work** when permissions cover `cd <worktree> && …`, `git -C <worktree> …`, and broad `cargo *` / `make *`. Two subagents (Phase 15 gnome + Phase 16 Ansible) finished cleanly in 7 minutes wall-clock for ~250 lines of YAML/desktop config combined.
- **They don't work** when the harness sandbox blocks `desktop-file-validate` / `dpkg-checkbuilddeps` / `ansible-lint` — the Phase 15+16 agents both reported these as "skipped, hand-checked." Verification slips through to manual review of the diff. Acceptable for data/config phases; risky for code phases without strong test coverage.
- **Phase 4+5 had to land together** because both modify the SetPolicy code path; splitting them creates a window where SetPolicy applies without a lockout pre-check.
- **Worktrees share `target/` if and only if you don't isolate** via `CARGO_TARGET_DIR`. We didn't need to — each `git worktree add` produces its own working tree, so cargo target dirs are per-worktree by default. Concurrent cargo runs across worktrees would still race on the global `~/.cargo/registry` cargo lock, but in practice that's a brief synchronization, not a hang.
---
@@ -180,6 +168,68 @@ Phase 2 expanded to step granularity at [2026-04-27-phase-2-storage.md](2026-04-
- `set_policy_replaces_state` test renamed to `set_policy_persists_stacks` and asserts only `merged.stacks == pol.stacks``p2p_pair` seeds a `[storage]` block in `00-test.conf`, so the merged read is by design a superset of what `SetPolicy` wrote to `50-local.conf`. The new assertion captures the actual user-visible behavior.
- `.gitignore` extended to ignore `.claude/` (Phase 1 left a stale agent worktree under `.claude/worktrees/`; ignoring prevents accidental submodule-style commits).
### Phase 3 + 6 + 7 closeout notes (2026-04-27)
Bundled multi-lane execution against the plan at [2026-04-27-phase-3-6-7-multi-lane.md](2026-04-27-phase-3-6-7-multi-lane.md). Tagged `v0.3.0-multi-lane`. 12 commits (1 plan + 11 implementation).
**Phase 3 — FIDO2 enrollment backend:**
- `daemon/src/fido/format.rs``PamU2fCred` -> `kh,pk,es256,+presence` with `hex::encode`; `CoseType` matching COSE alg `-7`/`-8`.
- `daemon/src/fido/authenticator.rs``Authenticator` trait with `discover()` and `make_credential(rp_id, user, pin)`. `AuthnError::{NoDevice, Cancelled, PinRequired, Backend}`.
- `daemon/src/fido/mock.rs``MockAuthenticator::with_one_yubikey()` for tests; counter-bumped per call.
- `daemon/src/fido/ctap.rs``CtapAuthenticator` wrapping `ctap-hid-fido2 = "3.5.9"`. Hardware-dependent; compile-clean gate only.
- `AppState::enroll(user, nickname)` replaces the Phase 2 add-credential stub: calls `authn.make_credential`, writes pam_u2f line, records userdb enrollment, returns `Credential` with `hex(keyHandle)` as id.
- D-Bus signals `DeviceFound`, `TouchRequired`, `EnrollmentSucceeded`, `EnrollmentFailed` emitted from `enroll_own` / `enroll_other`.
**Phase 6 — `pam_authforge_pending.so`:**
- Replaces the Phase 0 stub. Reads `PAM_USER`, rejects unsafe usernames, `stat()`s `/var/lib/authforge/pending/<user>`. Present → user-facing message + `PAM_AUTH_ERR`. ENOENT → `PAM_IGNORE`. Other errno → fail closed.
- Builds clean against `libpam0g-dev` with `-Wall -Wextra -Werror -fPIC -O2`. 16KB ELF.
- `pam/TESTING.md` + `pam/test/authforge.pamd` document the manual `pamtester` smoke recipe.
**Phase 7 — `authforgectl` CLI:**
- 14 subcommands wired through D-Bus: `status`, `enroll`, `list`, `remove`, `policy {show,set,apply,validate}`, `pending {set,clear,list}`, `recovery {generate,list}`. Global `--json` flag.
- `cli/src/bus.rs` — owned-proxy wrapper via `zbus::Proxy::new_owned`.
- 5 clap-parser tests cover subcommand definition validity, status parse, policy-set with method list, enroll with user+nickname, `--json` global.
**Plan deviations:**
- `HidInfo` in ctap-hid-fido2 3.5.9 has `pid`, `vid`, `product_string`, `info`, `param` — no `serial_number` / `path`. Used `product_string` for the device name and `HidParam::Path`/`VidPid` for the path label.
- `FidoKeyHidFactory` and `LibCfg` live at the crate root, not under `fidokey::`.
- `fido/mod.rs` carries `#![allow(dead_code)]` until Phase 8 GUI consumes the `DeviceFound` signal — `discover()` and `DiscoveredDevice` fields are wired via the trait but not yet called from non-test code.
### Phase 4 + 5 closeout notes (2026-04-27)
Tagged `v0.4.0-parallel`. Phase 4 + Phase 5 land together because both modify the SetPolicy code path; splitting would create a window where SetPolicy applies without a lockout pre-check.
**Phase 4 — Policy apply via pam-auth-update:**
- `daemon/src/policy_apply.rs``PolicyApplier::new(profile_path, pam_auth_update)`. Renders the `pam-configs/authforge` profile (`Default: yes` when any stack requires fido2; `pam_u2f.so` + `pam_authforge_pending.so` chain when fido2 required, only the pending backstop otherwise) and runs `pam-auth-update --package`.
- **Stash-and-restore on failure:** prior profile contents are read before write, restored if `pam-auth-update` fails, and `pam-auth-update` is re-run to put the system back into its previous PAM state.
- 4 unit tests including a real-process rollback test against a failing `/bin/sh` shim.
**Phase 5 — Lockout simulator:**
- `daemon/src/lockout.rs` — pure `simulate(&Policy, &[UserEnrollment]) -> Vec<Violation>`. Iterates Required stacks, flags users with no enrolled credential of any required method.
- 5 unit tests: optional-mode skipped, required-with-unenrolled flagged, any-method-satisfies, empty registry, multi-stack violation count.
- `AppState::set_policy(p, force)` always runs the simulator first; if violations and `!force`, returns `PolicyApplyResult { applied: false, violations }` without writing or invoking pam-auth-update. Otherwise persists via `PolicyStore::save` and invokes `PolicyApplier::apply`.
**Wire-protocol breaking change (pre-alpha, OK):** `SetPolicy(p)``SetPolicy(p, force: bool)`. CLI's `policy set` runs with `force=false` and surfaces violations as a non-zero exit + stderr list pointing the user at `policy apply --force-i-know-what-im-doing`.
**StorageConfig grew two fields:** `pam_profile_path` and `pam_auth_update`, env-var driven (`AUTHFORGE_PAMCONF_PATH`, `AUTHFORGE_PAM_AUTH_UPDATE`). Tests inject a no-op `/bin/sh` shim into a tempdir.
### Phase 15 + 16 closeout notes (2026-04-27)
Both delivered by parallel subagents in `git worktree` isolation while Lane 1 (Phase 4+5) ran in the main session. ~7 minutes wall-clock for both, vs. ~15+ if I'd done them sequentially.
**Phase 15 — `authforge-gnome-integration`:**
- `gnome-integration/io.dangerousthings.AuthForge.UsersPanel.desktop``.desktop` overlay tagged `X-GNOME-Settings-Panel=user-accounts`, `NoDisplay=true`, `OnlyShowIn=GNOME;`. Adds "Configure security…" inside `gnome-control-center` Users panel.
- `debian/control` paragraph for the new binary deb (`Architecture: all`, `Depends: authforge-gui, gnome-control-center`).
- `debian/rules` install line for `debian/authforge-gnome-integration/usr/share/applications/`.
- `gnome-integration/README.md` documents the GNOME 46/47 settings-panel-overlay mechanism and notes JS-extension support is deferred until a stable cross-version mechanism exists.
**Phase 16 — Ansible role `dangerousthings.authforge`:**
- Full role at `ansible-role/`: meta, defaults, handlers, tasks, templates, README, examples/playbook.yml.
- Adds the PPA, installs `authforge-daemon authforge-pam authforge-cli` (gated `authforge-gui`), templates `policy.conf.j2` to `/etc/authforge/policy.d/90-fleet.conf`, runs `authforgectl pending set` for users in `authforge_pending_users`.
- Single restart handler for `authforge-daemon.service`.
- All 5 YAML files validated via `yaml.safe_load`. `ansible-lint` skipped — not installed in the harness sandbox.
- **Subagent-initiated divergences (kept):** added `authforge_ppa` override variable so internal-mirror users don't fork the role; pre-creates `/etc/authforge/policy.d/` defensively (idempotent); standard `ansible_managed` template header.
---
# Phase 0: Repository Scaffolding (FULLY DETAILED)