Commit Graph

80 Commits

Author SHA1 Message Date
michael
4ec69119f6 Merge branch 'feature/phase-12-recovery-backend'
# Conflicts:
#	docs/plans/2026-04-26-authforge-implementation.md
2026-04-27 10:05:59 -07:00
michael
91731150d5 docs: phase 12 backend closeout notes
Flips Phase 12 status to  Code complete; bumps progress to 12/19 (63%).
Closeout block summarizes the 8 implementation tasks, lists all new
artifacts, calls out the 80-test count (was 60), and flags the Phase 14
deferred verifications (libargon2-dev compile + pamtester smoke).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-27 09:53:09 -07:00
michael
982b9403d0 feat(cli): authforgectl recovery list and revoke
* RecoveryCmd::List no longer takes a user — lists all active codes
  across users, matching the daemon's ListRecoveryCodes signature.
* RecoveryCmd::Revoke <user> calls RevokeRecoveryCode; exits 1 with
  a stderr message when the user has no active code.
* bus.rs gains list_recovery_codes() / revoke_recovery_code(user).
* Two new clap-parser tests cover the new subcommand shapes.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-27 09:51:52 -07:00
michael
56ba4dd924 feat(pam): recovery-code mode with Argon2id verify and pending re-enroll handoff
C-side:
* New mode=recovery argv branch in pam_sm_authenticate. Reads the two-line
  /var/lib/authforge/recovery/<user> file, argon2_verify against PAM_AUTHTOK,
  on match unlinks the file (one-shot) and writes pending(re_enroll=true).
  Always returns PAM_IGNORE on failure paths so a missing/wrong code never
  blocks normal auth.
* Makefile links -largon2 alongside -lpam.

Daemon-side:
* policy_apply::render_profile renders the recovery line first in the auth
  stack with [success=done default=ignore] — successful recovery short-
  circuits the rest, missing/wrong code falls through.
* New policy_apply test asserts the recovery line precedes the default
  backstop.

Doc:
* pam/TESTING.md adds libargon2-dev to the build prereqs and a new
  Smoke test 4 walking through the manual recovery-code flow.

C compile gate (make -C pam) requires libargon2-dev — flagged as a
deferred verification step until a host with the dev package is available.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-27 09:49:44 -07:00
michael
a420aa5f75 feat(daemon): real GenerateRecoveryCode + ListRecoveryCodes + RevokeRecoveryCode
Replace the random-number stub at dbus.rs:132 with state.issue_recovery,
add ListRecoveryCodes (returns Vec<RecoveryCodeSummary>) and RevokeRecoveryCode.
New polkit actions list-recovery / revoke-recovery (auth_admin_keep). Adds
4 D-Bus integration tests; the previously-stub generate-code test now
exercises the real Argon2id-backed write path.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-27 09:39:16 -07:00
michael
42482f2a44 docs: mark Phase 8 done in roadmap + closeout notes 2026-04-27 09:37:22 -07:00
michael
e9efde9077 feat(daemon): wire RecoveryStore into AppState with AUTHFORGE_RECOVERY_DIR override
StorageConfig gains a recovery_dir field; from_env_or_defaults reads
AUTHFORGE_RECOVERY_DIR (default /var/lib/authforge/recovery). AppState
exposes issue/list/revoke methods that Task 6 wires through D-Bus. Test
fixtures in state.rs and dbus.rs updated.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-27 09:35:06 -07:00
michael
517b015996 feat(daemon): RecoveryStore with atomic 0600 writes and one-shot verify-and-consume
Two-line file format (expires_unix\nargon2id-PHC) keeps the C PAM module
parser trivial — no json-c link needed. Atomic temp+rename means a
concurrent reader never sees a half-written file.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-27 09:32:40 -07:00
michael
943d9b01ae Merge branch 'feature/phase-8-gui-keys'
Phase 8: GUI Keys tab. Lists current user's enrolled FIDO2 credentials,
enrolls a new key with a touch-prompt modal driven by the daemon's
EnrollmentFailed signal (success comes from the call return), removes
credentials per row, degrades gracefully to a Retry banner when the
daemon is unreachable, and surfaces transient errors via adw::Toast.

8 tasks, 4 unit tests (error classifier).
2026-04-27 09:32:37 -07:00
michael
27dec5ab4a docs(gui): smoke-test recipe and CI gate 2026-04-27 09:26:17 -07:00
michael
14b473a48a feat(gui): wire adw::ToastOverlay for error surfacing 2026-04-27 09:25:50 -07:00
michael
45ac398730 feat(daemon): recovery code generation and Argon2id hashing
Pure-logic module: generate 8-digit codes, Argon2id PHC hash with OS RNG salt,
verify via constant-time PasswordVerifier. Dead-code allow until Task 4 wires
the store on top.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-27 09:25:30 -07:00
michael
3fbd872a9c feat(gui): TouchDialog modal racing EnrollOwn against EnrollmentFailed signal
Adds enroll_dialog::present which opens an adw::AlertDialog with a spinner
and races the EnrollOwn call against the EnrollmentFailed signal stream:
- Call returns Ok(_cred) -> close dialog, fire on_success refresh callback.
- Call returns Err -> swap dialog body to the user-message form.
- Signal arrives first -> swap to the signal payload (faster than waiting
  for the call's typed Err to traverse the bus).

KeysPage::start_enroll wires the activated row to present(), passing the
parent window for modal anchoring and a refresh closure as on_success.

Plan deviation: AlertDialog is libadwaita v1_5-gated, so gui/Cargo.toml
enables that feature. Targets Ubuntu 24.04+ (libadwaita 1.5).
2026-04-27 09:24:50 -07:00
michael
402addca27 chore(daemon): add argon2 0.5 for recovery code hashing
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-27 09:23:20 -07:00
michael
eb208579e3 refactor(daemon): centralize username path-segment sanitizer
Extract the inline path-traversal check from PendingStore into a shared
storage::safe_user::join_user_segment helper. RecoveryStore (next) reuses it.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-27 09:22:23 -07:00
michael
f11047fbcb feat(gui): list enrolled credentials with per-row remove + enroll button 2026-04-27 09:21:45 -07:00
michael
5d6b7ceeb0 feat(gui): KeysPage scaffold + tokio runtime in main, gtk::Box layout
KeysPage::new spawns the connect-and-render flow on glib::MainContext::
spawn_local. On success it shows a placeholder PreferencesPage (Task 5
fills in the list); on failure it shows StatusPage with a Retry button
that re-runs the connect attempt.

main.rs installs the multi-thread tokio runtime guard before app.run()
so zbus's tokio futures execute correctly when polled by glib.

Layout deviation from plan: ToolbarView is libadwaita v1_4-gated; the
project sticks with gtk::Box vertical for portability (commit c6a5e94
established this pattern).
2026-04-27 09:20:31 -07:00
michael
de369ef390 docs(plan): step-level plans for Phase 12 backend and Phase 9 + Phase 12 GUI
Phase 12 backend lane is unblocked by Phase 6+7 and runs independently of
Phase 8. Phase 9 + Phase 12 GUI lane gates on Phase 8 landing.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-27 09:14:57 -07:00
michael
d07d3469fe feat(gui): D-Bus client wrapper around AuthForge interface 2026-04-27 09:13:14 -07:00
michael
d7e6d562dd feat(gui): error helper to classify zbus errors and produce user messages 2026-04-27 09:11:06 -07:00
michael
8d6b80276e chore(gui): add zbus + tokio + futures-util deps for D-Bus client 2026-04-27 09:08:02 -07:00
michael
a17f70c4ac docs(plan): fix Phase 8 zbus runtime path (no glib feature exists)
zbus 4.4 does not expose a glib feature; my earlier draft was wrong about
that. Switch the plan to use the workspace tokio config and install a
multi-thread tokio runtime in main.rs, with the runtime guard kept alive for
the lifetime of app.run(). glib::MainContext::spawn_local still drives the
widget-touching closures; tokio's reactor wakes them.

Also: hoist futures-util into the Task 1 dep bundle so Task 6 doesn't need
a separate Cargo.toml edit.
2026-04-27 09:05:58 -07:00
michael
5294ef61ce docs: add Phase 8 GUI Keys tab implementation plan
Single-lane, sequential plan to land the authforge GUI Keys tab — empty-state
banner with Retry, list of enrolled credentials with per-row remove, modal
TouchDialog driving EnrollOwn against the Phase 3 EnrollmentSucceeded /
EnrollmentFailed signals. zbus uses the glib runtime feature (not the
workspace tokio config), so all async runs on the GTK main thread via
glib::MainContext::spawn_local — no tokio dep in the GUI process.

Eight tasks, ~1 day of work. TDD applies to error.rs (pure logic, 4 tests);
widget code ships with the manual smoke recipe at gui/TESTING.md as the
acceptance gate.
2026-04-27 08:55:04 -07:00
michael
481f7f8543 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).
2026-04-27 08:48:34 -07:00
michael
5668ba1f69 docs: mark Phases 4, 5, 15, 16 done in roadmap 2026-04-27 08:42:19 -07:00
michael
7eae081dd7 Merge branch 'gnome-integration-lane' 2026-04-27 08:41:15 -07:00
michael
61f89abae0 Merge branch 'ansible-lane' 2026-04-27 08:41:10 -07:00
michael
5c94319bf0 feat(daemon): policy apply via pam-auth-update + lockout simulator (Phase 4+5)
Lands Lane 1 of the Phase 4+5+8+15+16 parallel cycle. Phase 4 + Phase 5 must
land together because both modify the SetPolicy code path.

- daemon/src/lockout.rs — pure simulate(new_policy, registry) -> Vec<Violation>.
  Iterates Required stacks, flags users with no enrolled credential of any
  required method. 5 unit tests cover: optional-mode skipped, required-with-
  unenrolled flagged, any-method-satisfies, empty registry, multi-stack.
- daemon/src/policy_apply.rs — PolicyApplier renders the pam-configs profile
  (Default: yes when any stack requires fido2; pam_u2f.so + pam_authforge_pending
  when fido2 required, only pam_authforge_pending otherwise) and runs
  pam-auth-update --package. Stash-and-restore on failure: prior profile
  contents are restored and pam-auth-update re-run, so a failed apply leaves
  the system in its previous PAM state. 4 unit tests including a real-process
  rollback test against a failing /bin/sh shim.
- daemon/src/state.rs — AppState::set_policy(p, force) returns
  PolicyApplyResult. Always runs the simulator first; if violations and !force,
  returns { applied: false, violations } without writing. Otherwise persists
  via PolicyStore::save and invokes PolicyApplier::apply. StorageConfig grows
  pam_profile_path + pam_auth_update fields (env-var driven, tests inject a
  no-op /bin/sh shim into a tempdir).
- daemon/src/dbus.rs — SetPolicy signature is now (Policy, bool) -> Result.
  Wire-breaking pre-alpha; CLI updated in this commit.
- cli/src/{bus,commands}.rs — set_policy takes force flag. 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. policy
  apply now actually invokes pam-auth-update via the daemon.

Test count: 42 daemon (was 33; adds 5 lockout + 4 policy_apply). 13 common.
5 cli. cargo clippy --workspace --all-targets -D warnings clean.

Plan deviation: PolicyApplier::from_env() became PolicyApplier::new(profile_path,
pam_auth_update) with the env defaults moved into StorageConfig::from_env_or_defaults.
Cleaner: state owns one source of truth for env-driven path config.
2026-04-27 08:41:00 -07:00
michael
407e71072d ansible: document role variables and ship example playbook
README walks through what the role does, every default, and how to
invoke it from a parent playbook. examples/playbook.yml is a runnable
copy that targets a `workstations` group with a sudo=required+fido2
stack and one pending user.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-27 08:36:29 -07:00
michael
6e47dbd3df packaging: ship authforge-gnome-integration binary package
Declares the new arch-all binary package in debian/control (depending on
authforge-gui + gnome-control-center) and wires its single data file
(gnome-integration/io.dangerousthings.AuthForge.UsersPanel.desktop) into
debian/rules' override_dh_auto_install so it lands in
/usr/share/applications/. The metapackage already Suggests:
authforge-gnome-integration, so no change there.
2026-04-27 08:35:33 -07:00
michael
457db3ced2 ansible: implement install + policy + pending tasks
Adds the apt_repository / apt steps that pull authforge-daemon, -pam,
and -cli (with -gui gated behind authforge_install_gui), the template
step that renders /etc/authforge/policy.d/90-fleet.conf and notifies the
restart handler, and a loop that calls `authforgectl pending set` for
each entry in authforge_pending_users.

The template emits TOML that round-trips through tomllib for both the
empty-stacks default and a populated multi-stack config.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-27 08:35:28 -07:00
michael
e638a2defd gnome-integration: add Users-panel .desktop overlay and README
Adds the data file shipped by the optional authforge-gnome-integration
package: a /usr/share/applications/.desktop overlay tagged with
X-GNOME-Settings-Panel=user-accounts so gnome-control-center surfaces a
"Configure security…" launcher inside each user's detail view. The
README documents why a .desktop overlay was picked over a JS extension
(cross-version stability, packaging simplicity) and how to verify the
integration works in GNOME 46/47.
2026-04-27 08:34:50 -07:00
michael
792dca976d ansible: scaffold dangerousthings.authforge role skeleton
Adds meta/main.yml (Galaxy metadata for Ubuntu 22.04/24.04 under the
Apache-2.0 license), defaults/main.yml covering the policy/storage/
firstrun/pending knobs the tasks layer will consume, and a single
restart-daemon handler. Tasks and template land in the next commit.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-27 08:34:26 -07:00
michael
865e37b6aa docs: mark Phases 3, 6, 7 done in roadmap 2026-04-27 08:30:09 -07:00
michael
22938f657a feat(daemon-fido): Authenticator trait + Mock + Ctap impl + signals (Lane A)
Bundles plan tasks A2 (PamU2fCred encoder), A3 (Authenticator trait), A4
(MockAuthenticator), A5 (CtapAuthenticator wrapping ctap-hid-fido2 3.5.9),
A6 (wire enrollment through AppState::enroll), A7 (DeviceFound /
TouchRequired / EnrollmentSucceeded / EnrollmentFailed D-Bus signals).

- daemon/src/fido/format.rs — PamU2fCred -> 'kh,pk,es256,+presence' with
  hex::encode for the binary blobs and CoseType matching COSE alg -7/-8.
- daemon/src/fido/authenticator.rs — Authenticator trait with discover() and
  make_credential(rp_id, user, pin); AuthnError covers NoDevice / Cancelled /
  PinRequired / Backend.
- daemon/src/fido/mock.rs — MockAuthenticator::with_one_yubikey produces
  deterministic-but-distinct PamU2fCreds (counter-bumped per call).
- daemon/src/fido/ctap.rs — CtapAuthenticator. discover via
  ctap_hid_fido2::get_fidokey_devices(); make_credential via
  FidoKeyHidFactory::create + fk.make_credential. Heuristic error mapping
  to AuthnError variants. Real-hardware path; compile-clean gate only.
- daemon/src/state.rs — AppState::open now takes Arc<dyn Authenticator>.
  New enroll(user, nickname) replaces the Phase 2 add_credential stub: calls
  authn.make_credential, writes pam_u2f line via CredentialsStore::add,
  records enrollment in userdb, returns Credential with hex(keyHandle) as id.
- daemon/src/dbus.rs — enroll_own / enroll_other now emit TouchRequired
  before the call and EnrollmentSucceeded / EnrollmentFailed after. Removed
  the unused stub-credential builder + import baggage.
- daemon/src/main.rs — picks CtapAuthenticator for prod; tests inject Mock.

Test count: 33 daemon tests pass (was 26). Adds 2 fido::format tests, 3
fido::mock tests, 1 state::enroll_writes_real_pam_u2f_line, 1 dbus::
enrollment_succeeded_signal_fires_on_enroll_own. cargo clippy --workspace
--all-targets -D warnings clean. cargo fmt clean.

Plan deviations:
- HidInfo doesn't have a serial_number field in 3.5.9; switched to using
  product_string and HidParam::Path/VidPid for the device path label.
- FidoKeyHidFactory and LibCfg are at the crate root, not under fidokey::.
- fido/mod.rs has #![allow(dead_code)] for now: discover() and DiscoveredDevice
  fields are wired via the trait but only called from tests until Phase 8
  GUI consumes the DeviceFound signal.
2026-04-27 08:29:42 -07:00
michael
01df2109d8 chore: add ctap-hid-fido2 + hex deps for Phase 3 (Task A1) 2026-04-27 08:20:15 -07:00
michael
1c6c361d4a docs(pam): smoke-test recipe + test PAM stack file (Task B2) 2026-04-27 08:19:18 -07:00
michael
209167df22 feat(pam): real pending-flag check with path-traversal guard (Task B1)
Replaces the Phase 0 stub. Reads PAM_USER, rejects usernames containing /,
\0, .., or that are . / .. / empty. stat()s /var/lib/authforge/pending/<user>:
present -> emits the design-doc user-facing message + PAM_AUTH_ERR; ENOENT
-> PAM_IGNORE (lets the rest of the stack decide); other errno -> logs and
fails closed (PAM_AUTH_ERR).

Builds clean against libpam0g-dev with -Wall -Wextra -Werror -fPIC -O2.
Resulting .so is a 16KB stripped-with-buildid x86-64 ELF; passes file(1) sanity.
pamtester smoke recipe lands in Task B2 alongside the test PAM stack file.
2026-04-27 08:18:57 -07:00
michael
0697ba1c65 feat(cli): D-Bus client wrapper + full subcommand dispatcher
Lands plan tasks C2 (Daemon proxy wrapping all 9 D-Bus methods via
Proxy::new_owned), C3 (status/list/policy-show), C4 (enroll/remove/policy-
set/apply/validate), and C5 (pending/recovery). Bundled because dispatch
needs the bus wrapper to compile cleanly under -D warnings.

Phase 4/5/12 placeholders in the dispatcher: policy apply re-saves to trigger
PolicyChanged; policy validate is a TOML round-trip; pending list and
recovery list are no-op messages until the corresponding D-Bus methods land.

5 clap-parser tests pass (was 5; same — parser shape unchanged in this
commit). Workspace clippy clean.
2026-04-27 08:18:17 -07:00
michael
1b223fe4f9 feat(cli): authforgectl subcommand structure with clap derive (Task C1) 2026-04-27 08:16:29 -07:00
michael
6351041a6c docs: expand Phases 3+6+7 to step granularity (multi-lane bundle) 2026-04-27 08:15:20 -07:00
michael
8f4225d136 docs: add Parallel Execution Lanes section to roadmap
Captures which remaining phases are parallel-safe and which must stay
sequential. Four lanes (3 / 4+5 / 6 / 7) can land in any order right after
Phase 2 because they touch disjoint file trees: daemon/src/fido,
daemon/src/policy_apply + lockout, pam/*.c, and cli/src.

Documents practical caveats: Phase 4+5 must land together (shared SetPolicy
path), Phase 6 is the only C lane and needs libpam0g-dev, and the Phase 1
subagent-worktree cwd quirk should be retested before relying on it for
literal parallel-compute execution.
2026-04-27 07:52:30 -07:00
michael
f655d41f5c docs: mark Phase 2 done in roadmap, capture closeout notes 2026-04-27 06:45:11 -07:00
michael
24237cbf8b refactor(daemon): AppState delegates to storage modules; dbus tests use tempdir
Lands plan tasks 2.15 (AppState refactor with StorageConfig + open()) and 2.16
(dbus.rs tests switched to tempdir-backed AppState; storage errors threaded
through D-Bus methods as Failed). Bundled because the AppState surface change
forces dbus.rs adjustments in the same commit.

- daemon/src/state.rs: AppState::open(StorageConfig) replaces with_fixtures().
  StorageConfig.from_env_or_defaults() reads AUTHFORGE_POLICY_DIR /
  _PENDING_DIR / _USERDB env vars (defaults: /etc/authforge/policy.d,
  /var/lib/authforge/pending, /var/lib/authforge/users.db). State delegates
  list/add/remove credentials to CredsPathResolver + CredentialsStore picked
  per-call from current Policy; pending and userdb operate independently.
- daemon/src/main.rs: opens state via env-driven config; reuses cfg.policy_dir
  for the watcher to keep one source of truth.
- daemon/src/dbus.rs: every write method maps StateError to fdo::Error::Failed.
  p2p_pair seeds 00-test.conf with [storage] backend = central pointing into
  the tempdir so credential writes don't try to touch /home/<user>/...
  (alice/bob/carol aren't real accounts in tests).
- Renamed: list_credentials_returns_fixture_for_alice ->
  list_credentials_after_enroll. Removed: with_fixtures().
- .gitignore: add .claude/ so leftover Phase 1 worktree state isn't committed.

Test count: 26/26 daemon tests green (was 17). Common: 13/13. Clippy + fmt clean.
2026-04-27 06:44:07 -07:00
michael
90f7a0f4fc feat(daemon): storage::userdb sqlite cache for enrollment registry 2026-04-27 06:29:36 -07:00
michael
eb3362c81c feat(daemon): storage::credentials parser, CRUD, and per-user vs central path
Lands plan tasks 2.10 (pam_u2f line format parser via CredEntry::from_line/
to_line, credId extraction), 2.11 (CredentialsStore add/remove/list with
idempotent add-by-credId), and 2.12 (CredsPathResolver dispatching central vs
per-user paths). Bundled because the three pieces compose into one storage
boundary.

CredEntry treats post-username chunks as opaque blobs split on ':', preserving
pam_u2f's full record on round-trip. credId = first comma-separated field of a
blob. add() is idempotent on credId match (Phase 3 may decide to refresh
publicKey on re-enroll; out of scope here).

Path resolution: Central -> Storage.central_path verbatim. PerUser -> getpwnam
via nix, with a /home/<user>/... fallback if NSS errors (CI users, distro
quirks); pam_u2f does the real lookup at auth time, so the fallback only
matters for write-on-enroll where the user does exist.

7 tests added; clippy + fmt clean.
2026-04-27 06:29:00 -07:00
michael
dd766e3077 feat(daemon): storage::pending JSON read/write/clear with path-traversal guards 2026-04-27 06:27:30 -07:00
michael
ea70386c2f feat(daemon): storage::policy + PolicyChanged signal + inotify watcher
Lands plan tasks 2.6 (PolicyStore wraps load_from_dir / save_local), 2.7
(PolicyChanged D-Bus signal on the AuthForge interface), and 2.8 (notify-based
inotify watcher in main.rs that emits the signal on any change in the policy.d
directory). Bundled because watcher → emit signal → wraps PolicyStore is one
data flow.

- daemon/src/storage/{mod,policy}.rs — PolicyStore::{load,save,watch}; watch
  returns a (RecommendedWatcher, watch::Receiver) so the caller keeps the
  watcher alive.
- daemon/src/dbus.rs — adds #[zbus(signal)] policy_changed; integration test
  via p2p connection asserts the signal arrives within 2s.
- daemon/src/main.rs — spawns a task that ticks PolicyChanged on every
  rx.changed(), keyed off AUTHFORGE_POLICY_DIR env var (default
  /etc/authforge/policy.d). Watcher leaked via std::mem::forget; daemon
  lifetime = process lifetime.

Test count: 17/17 daemon (was 14) + 13/13 common.
2026-04-27 06:26:49 -07:00
michael
9be8e4d0b3 feat(common): Policy load_from_dir + save_local with last-wins merge
Lands plan tasks 2.3 (single-file load), 2.4 (multi-file merge cases — last-wins,
lex order, non-conf skip, missing-dir default), and 2.5 (save_local preserves
sibling files) as one logical unit.

- load_from_dir: reads *.conf in lex-ascending order, parses TOML, merges via
  last-wins on stack key, storage block, and firstrun block. Missing dir yields
  Policy::default(). Non-.conf entries silently skipped.
- save_local: writes 50-local.conf with toml::to_string_pretty; never reads
  or removes siblings. Created via create_dir_all.
- 6 parse tests added: load_single_file, last_file_wins_on_overlap,
  lex_order_not_filesystem_order, ignores_non_conf_files, missing_dir_yields_default,
  save_local_preserves_sibling_files.
2026-04-27 06:23:49 -07:00
michael
dc2c4b02cb refactor(common): move Policy types from types.rs into policy.rs 2026-04-27 06:22:54 -07:00