feat(auth): add actual auth create-token for non-interactive auth#801
Open
benw5483 wants to merge 5 commits into
Open
feat(auth): add actual auth create-token for non-interactive auth#801benw5483 wants to merge 5 commits into
actual auth create-token for non-interactive auth#801benw5483 wants to merge 5 commits into
Conversation
…e tokens Add a scoped personal access token (PAT) path so CI jobs and autonomous agents can authenticate without a browser. `actual auth create-token` reuses the existing `actual login` session: it loads the stored credentials, refreshes the access token if needed, calls the authenticated issuance endpoint, and prints the raw `actl_pat_...` once. Storage is the OS keychain via the `keyring` crate, with an XChaCha20-Poly1305 + Argon2id encrypted-file fallback for headless Linux / CI that has no Secret Service daemon. Non-interactive callers resolve a token from `ACTUAL_TOKEN`, then the keychain, then the encrypted file. The raw value is never written to a log or `Debug` output. `actual auth` keeps its existing runner-auth check when invoked with no subcommand, so the change is backward compatible. Proof of concept: the issuance endpoint is developed against the documented contract and repointed with `--api-url` / `ACTUAL_API_URL` once the server side ships. Generated by the operator's software factory. On behalf of: @benw5483 Co-Authored-By: Actual AI Factory Bot <factory-bot@actual.ai.invalid> Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Point `create-token` at the real issuance route and make the crate build on headless / CI Linux, then bring the new modules to full line coverage. The issuance endpoint is `POST /api/oauth/tokens` (not `/api/auth/tokens`), and `--scopes` now shows the enforced colon vocabulary (`adr:query`, `adr:review`, `mcp:invoke`) that the rest of the login flow already uses. The Linux keychain backend moves from the Secret Service feature to the dbus-free kernel keyutils backend (`linux-native`). The Secret Service backend links `libdbus` at build time, so the CLI failed to compile on any Linux host without `libdbus-1-dev` — the real headless-storage finding, now documented in `docs/AGENT_AUTH.md`. keyutils needs no system library and no daemon, so it builds everywhere and works headless; macOS and Windows keep their native keychains. Add unit coverage for the keychain backends (through an injectable in-memory keystore), the encrypted-file error paths, and the token-refresh and result-printing branches. Generated by the operator's software factory. On behalf of: @benw5483 Co-Authored-By: Actual AI Factory Bot <factory-bot@actual.ai.invalid> Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Two CI follow-ups on the create-token work. - The AEAD salt and nonce are now returned from `random_bytes::<N>()` rather than filling a caller-owned `[0u8; N]` in place. Behaviour is identical (still an `OsRng`-filled salt and nonce), but the random value no longer flows out of a zero-initialized binding, which clears a false-positive `rust/hard-coded-cryptographic-value` CodeQL alert on the salt. - Add a test that routes `actual auth create-token` through the `auth` command dispatch, returning `NotLoggedIn`, so `src/cli/commands/auth.rs` is back at full line coverage. Generated by the operator's software factory. On behalf of: @benw5483 Co-Authored-By: Actual AI Factory Bot <factory-bot@actual.ai.invalid> Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
`create-token` minted the PAT server-side and only then ran the local store; a store error bailed via `?` before the token or its id were ever printed, so the raw token was lost. The most likely store failure is a headless Linux box with no `ACTUAL_TOKEN_PASSPHRASE` and no OS keychain — exactly the target environment — so every failed attempt burned a live, unrevocable token the user could neither capture nor revoke. Close the gap from both ends: - Add `token_store::precheck`, a non-writing probe that confirms a backend is available, and call it BEFORE minting. The predictable headless / no-passphrase case now fails cleanly with no token minted at all. - On any residual post-mint store failure (a full disk, a keychain lock race the precheck cannot foresee), still surface the token and its id so the user can capture and revoke it, then propagate the error. A minted token is never silently dropped. Route `create-token` output through injected writers so the stdout=secret / stderr=chrome split — a load-bearing capture contract — is unit-testable. New tests cover: the pre-mint precheck refusal, the store-fails-after-mint recovery, and that stdout carries only the token. Generated by the operator's software factory. On behalf of: @benw5483 Co-Authored-By: Actual AI Factory Bot <factory-bot@actual.ai.invalid> Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The store-failure revision added `token_store::precheck` (keyring/auto arms) and `keyring_available`, and routed `create-token` output through injected writers with `?` error propagation. Those new lines were not fully exercised, so the 100%-per-file coverage gate failed on `token_store.rs` (97.16%) and `create_token.rs` (95.93%). Add test-only coverage; no production code changes: - `precheck` under keyring and auto modes, both available and unavailable, via the existing in-memory keyring mock — extended with a fail-on-build toggle so `keyring_available`'s entry-init-failure leg is reachable without OS trickery. - `print_unstored_token`'s no-id branch (a mint whose response carries no id). - a failing-writer sweep that drives every `writeln!` `?` error path in `print_result` and `print_unstored_token`, asserting a broken output stream surfaces as an error rather than a false success. Both files are now at 100% line coverage. Generated by the operator's software factory. On behalf of: @benw5483 Co-Authored-By: Actual AI Factory Bot <factory-bot@actual.ai.invalid> Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
8 tasks
f1b811f to
92954b0
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Adds
actual auth create-token, a scoped personal access token (PAT) path so CI jobs and autonomous agents can authenticate without a browser. It builds on the existingactual loginsession, so it doesn't add a second login flow.actl_pat_…once.keyringcrate) with an XChaCha20-Poly1305 + Argon2id encrypted-file fallback for headless Linux / CI where no OS keychain is available.ACTUAL_TOKEN, then the keychain, then the encrypted file.actual authwith no subcommand keeps its existing runner-auth check, so this is backward compatible.Headless-storage finding (the question this POC answers)
Does the keychain library degrade gracefully on a headless Linux box / CI runner with no desktop keyring?
The finding turned out sharper than we'd have guessed from a runtime check. The
keyringcrate's Secret Service backend links the systemlibdbusat build time throughpkg-config, so a host withoutlibdbus-1-devcan't compile the CLI at all. On stock Linux CI runners every job went red onThe system library dbus-1 required by crate libdbus-sys was not found. A missing runtime daemon is recoverable. A binary that never builds is not.The fix is a Linux backend with no build-time system dependency. This CLI uses
linux-native, the kernel keyutils keyring, reached through raw syscalls: nolibdbus, nopkg-config, no D-Bus daemon. It compiles on any Linux, including a bare CI container, and stores secrets headless. macOS and Windows keep their native keychains.Runtime degradation stays explicit. In the default
automode a keychain error routes to the encrypted-file store whenACTUAL_TOKEN_PASSPHRASEis set, and otherwise fails loudly rather than writing a weaker store. keyutils is session / persistent-keyring scoped, so for durable non-interactive use the recommended paths areACTUAL_TOKEN(CI) and the encrypted-file fallback (headless Linux).Library chosen:
keyringv3 —linux-native(keyutils) on Linux,apple-native/windows-nativeelsewhere. Fallback: XChaCha20-Poly1305 AEAD, key derived by Argon2id from a passphrase, file written0600. Without a passphrase the fallback is refused rather than writing anything weaker.Security model for agents
Two rules, documented in
--helpanddocs/AGENT_AUTH.md:--name <agent>): never share the human's interactive login. One PAT per agent makes every action attributable and lets a single agent be revoked on its own.ACTUAL_TOKEN, never in a prompt, shell history, command-line argument, or log. This guards against the agent-specific prompt-injection exfiltration path, where untrusted input an agent reads can instruct it to leak any secret in its context.The raw token is printed once to stdout (for capture) and is never written to a log or
Debugoutput. TheDebugimpls redact it, and the encrypted file holds no plaintext.Test plan
cargo fmt --checkandcargo clippy -- -D warningsare clean.actual authstill runs the runner-auth check.End-to-end run against a mock issuance endpoint (secret masked)
Follow-ups
Revision — never orphan an unrevocable token on store failure
A self-review found the PAT was minted server-side before the local store ran, so a store error bailed via
?before the token or its id were printed — losing a live, unrevocable token. The likeliest trigger is the target env itself (headless Linux, noACTUAL_TOKEN_PASSPHRASE, no OS keychain). This revision closes the gap from both ends:token_store::precheckis a non-writing probe that confirms a backend is available;create-tokennow calls it before minting, so the predictable headless / no-passphrase case fails with no token minted at all.Audit matrix