infraternal sign-in <server> (D311) #268

Open
opened 2026-10-31 16:16:36 +00:00 by kayg · 3 comments
Owner

J6 implementation plan — D311 / D314

Base for this plan: origin/dev at 42ab330358eb06d47acade8d0f2c35b9225af74b. This issue is the implementation record for infraternal sign-in <server>; D311's laptop flow uses a link, user code and terminal QR, with a ten-minute, single-use approval and a revocable credential scoped to automation.

Flow

  1. infraternal sign-in <server> validates an HTTPS server origin, starts a device approval, and shows a same-server browser link, the user code and a QR encoding that link. The QR/link only pre-fills the code; seeing the code alone cannot poll or redeem it.
  2. The browser opens the approval route, signs in with a passkey if needed, shows the exact device name and requested scopes, and requires a fresh passkey tap for approval. The browser sends the user code to inspect and approve; approval is single-use.
  3. The CLI polls with the separate high-entropy polling capability. It receives the API key once after approval, then writes it to the per-server client credential store. Polling never prints the polling capability or API key.
  4. Native apps keep the D311 direct-passkey path as a later slice; the web/browser path remains the authority for approving a CLI credential.

Server API and admission

The server already exposes POST /auth/{action} with a 32 KiB body limit and Cache-Control: no-store; no new crypto or server transport is proposed for slice 1.

Action Authorization and role Existing bound
device-start Unauthenticated CLI request; requests an allowlisted, exact scope set 96-bit user code and separate 256-bit poll capability; expires in 600 seconds; only hashes are stored
device-inspect Exact configured Origin plus browser session cookie; returns device and scopes Browser ingress partition
device-approve Exact Origin, browser session, and fresh passkey session; consumes the code once Browser ingress partition
device-redeem Poll capability only; returns API key once CLI ingress partition; one-use database transaction
api-list / api-revoke Exact Origin and browser session; revocation also needs a fresh passkey List returns nonsecret ID/device/scopes; bearer keys are never returned

The auth router has a 60-second aggregate split of 120 browser actions and 60 CLI device actions. The runtime adds per-source ceilings of 30 browser and 10 device actions per minute; forwarded addresses count only behind configured trusted proxies. The client will poll no faster than the device-source budget permits, honor Retry-After, and stop at the server's expiry. Wrong origins fail before spending browser capacity.

Client storage, revocation, and output

The browser's account-storage.ts stores account-scoped UI cache in web storage, not CLI authority. Store the returned API key separately by normalized server origin under $XDG_CONFIG_HOME/infraternal/credentials.json (fallback ~/.config/infraternal/credentials.json): create/check the directory as owner-only 0700, credential file 0600, reject symlinks or unsafe ownership/modes, and write atomically. Never put credentials in argv, environment variables, --json, URLs, diagnostics, or logs. The server retains only credential hashes. Revoke a credential through api-list / api-revoke after a fresh browser passkey; wire the account UI for listing and revocation as a follow-up slice because those routes are not currently used by the web app.

CLI behavior (D303, D314)

--json follows the existing versioned envelope and never includes the bearer credential. Exit mapping: 0 approved and stored; 1 unexpected I/O, HTTP, or response failure; 2 invalid command or server argument; 3 approval or scope refused; 4 code expired/unknown; 5 server unavailable; 6 approval completed but local persistence failed; 130 interrupted. Errors include the fix and next step, e.g. “Use an HTTPS server URL”, “Approval expired; run infraternal sign-in <server> again”, or “Credential was approved but could not be saved; fix permissions on the infraternal config directory and sign in again.” No secret appears in those messages.

Slices and gates

  1. Device-code + link end-to-end: CLI command, browser approval route using existing passkey auth and API endpoints, exact scopes/device confirmation, secure token persistence, and server-backed tests for success, wrong/expired/replayed codes, origin and rate-limit refusal, interruption, unsafe file paths/permissions, and secret redaction. Add a real-browser approval check and attach screenshots from the production build. Run scripts/verify fast and the focused auth/integration gate on infraternal-dev-02-std (aarch64).
  2. Terminal QR rendering: only after the owner resolves the encoder question below; verify the encoded URL/code and scan it from captured terminal output. Include the QR change in the x86 static-size row and report the D243 delta.
  3. Account credential listing/revocation UI: wire api-list / api-revoke, require a fresh passkey for revocation, test that only nonsecret IDs are displayed and revoked credentials fail, then attach production screenshots.
  4. Native-app direct passkey sign-in remains a separate lane.

Owner question — QR encoder/library

No QR encoder exists in origin/dev. Candidates to measure before adding a dependency are qrcodegen 1.8.0 (MIT, focused encoder) and qrcode 0.14.1 (MIT OR Apache-2.0; default image/SVG/pic features can be disabled for terminal-only use). Measure linked static-binary delta against D243's x86 row, per-call RSS and encode p50/p99 for the actual approval URL, and scan reliability from terminal output at common cell/font sizes. Do not select or add either dependency in this job without the owner's choice. Slice 1 can deliver link and code while the QR remains gated on that answer. No new cryptographic library is proposed; current server capabilities and passkey verification are reused.

Threat notes

The user code and QR may be observed, so neither grants authority; only a fresh passkey-authenticated browser approval binds the exact device and scopes. The separate 256-bit poll capability is never shown and only redeems once. HTTPS is mandatory; client HTTP must not forward capabilities across origins/redirects. The code and poll capability are hashed server-side, requests/responses avoid body logging, and API key storage uses owner-only permissions and atomic replacement. A CLI timeout leaves an unapproved row that expires; Ctrl-C stops polling and does not print the poll capability.

## J6 implementation plan — D311 / D314 Base for this plan: `origin/dev` at `42ab330358eb06d47acade8d0f2c35b9225af74b`. This issue is the implementation record for `infraternal sign-in <server>`; D311's laptop flow uses a link, user code and terminal QR, with a ten-minute, single-use approval and a revocable credential scoped to automation. ### Flow 1. `infraternal sign-in <server>` validates an HTTPS server origin, starts a device approval, and shows a same-server browser link, the user code and a QR encoding that link. The QR/link only pre-fills the code; seeing the code alone cannot poll or redeem it. 2. The browser opens the approval route, signs in with a passkey if needed, shows the exact device name and requested scopes, and requires a fresh passkey tap for approval. The browser sends the user code to inspect and approve; approval is single-use. 3. The CLI polls with the separate high-entropy polling capability. It receives the API key once after approval, then writes it to the per-server client credential store. Polling never prints the polling capability or API key. 4. Native apps keep the D311 direct-passkey path as a later slice; the web/browser path remains the authority for approving a CLI credential. ### Server API and admission The server already exposes POST `/auth/{action}` with a 32 KiB body limit and `Cache-Control: no-store`; no new crypto or server transport is proposed for slice 1. | Action | Authorization and role | Existing bound | | --- | --- | --- | | `device-start` | Unauthenticated CLI request; requests an allowlisted, exact scope set | 96-bit user code and separate 256-bit poll capability; expires in 600 seconds; only hashes are stored | | `device-inspect` | Exact configured `Origin` plus browser session cookie; returns device and scopes | Browser ingress partition | | `device-approve` | Exact `Origin`, browser session, and fresh passkey session; consumes the code once | Browser ingress partition | | `device-redeem` | Poll capability only; returns API key once | CLI ingress partition; one-use database transaction | | `api-list` / `api-revoke` | Exact `Origin` and browser session; revocation also needs a fresh passkey | List returns nonsecret ID/device/scopes; bearer keys are never returned | The auth router has a 60-second aggregate split of 120 browser actions and 60 CLI device actions. The runtime adds per-source ceilings of 30 browser and 10 device actions per minute; forwarded addresses count only behind configured trusted proxies. The client will poll no faster than the device-source budget permits, honor `Retry-After`, and stop at the server's expiry. Wrong origins fail before spending browser capacity. ### Client storage, revocation, and output The browser's `account-storage.ts` stores account-scoped UI cache in web storage, not CLI authority. Store the returned API key separately by normalized server origin under `$XDG_CONFIG_HOME/infraternal/credentials.json` (fallback `~/.config/infraternal/credentials.json`): create/check the directory as owner-only `0700`, credential file `0600`, reject symlinks or unsafe ownership/modes, and write atomically. Never put credentials in argv, environment variables, `--json`, URLs, diagnostics, or logs. The server retains only credential hashes. Revoke a credential through `api-list` / `api-revoke` after a fresh browser passkey; wire the account UI for listing and revocation as a follow-up slice because those routes are not currently used by the web app. ### CLI behavior (D303, D314) `--json` follows the existing versioned envelope and never includes the bearer credential. Exit mapping: 0 approved and stored; 1 unexpected I/O, HTTP, or response failure; 2 invalid command or server argument; 3 approval or scope refused; 4 code expired/unknown; 5 server unavailable; 6 approval completed but local persistence failed; 130 interrupted. Errors include the fix and next step, e.g. “Use an HTTPS server URL”, “Approval expired; run `infraternal sign-in <server>` again”, or “Credential was approved but could not be saved; fix permissions on the infraternal config directory and sign in again.” No secret appears in those messages. ### Slices and gates 1. Device-code + link end-to-end: CLI command, browser approval route using existing passkey auth and API endpoints, exact scopes/device confirmation, secure token persistence, and server-backed tests for success, wrong/expired/replayed codes, origin and rate-limit refusal, interruption, unsafe file paths/permissions, and secret redaction. Add a real-browser approval check and attach screenshots from the production build. Run `scripts/verify fast` and the focused auth/integration gate on `infraternal-dev-02-std` (aarch64). 2. Terminal QR rendering: only after the owner resolves the encoder question below; verify the encoded URL/code and scan it from captured terminal output. Include the QR change in the x86 static-size row and report the D243 delta. 3. Account credential listing/revocation UI: wire `api-list` / `api-revoke`, require a fresh passkey for revocation, test that only nonsecret IDs are displayed and revoked credentials fail, then attach production screenshots. 4. Native-app direct passkey sign-in remains a separate lane. ### Owner question — QR encoder/library No QR encoder exists in `origin/dev`. Candidates to measure before adding a dependency are `qrcodegen` 1.8.0 (MIT, focused encoder) and `qrcode` 0.14.1 (MIT OR Apache-2.0; default image/SVG/pic features can be disabled for terminal-only use). Measure linked static-binary delta against D243's x86 row, per-call RSS and encode p50/p99 for the actual approval URL, and scan reliability from terminal output at common cell/font sizes. Do not select or add either dependency in this job without the owner's choice. Slice 1 can deliver link and code while the QR remains gated on that answer. No new cryptographic library is proposed; current server capabilities and passkey verification are reused. ### Threat notes The user code and QR may be observed, so neither grants authority; only a fresh passkey-authenticated browser approval binds the exact device and scopes. The separate 256-bit poll capability is never shown and only redeems once. HTTPS is mandatory; client HTTP must not forward capabilities across origins/redirects. The code and poll capability are hashed server-side, requests/responses avoid body logging, and API key storage uses owner-only permissions and atomic replacement. A CLI timeout leaves an unapproved row that expires; Ctrl-C stops polling and does not print the poll capability.
Author
Owner

Start: branch signin-d311, based on origin/dev at 42ab330358eb06d47acade8d0f2c35b9225af74b. The implementation plan is posted above; source edits will start in the requested worktree.

Start: branch `signin-d311`, based on `origin/dev` at `42ab330358eb06d47acade8d0f2c35b9225af74b`. The implementation plan is posted above; source edits will start in the requested worktree.
Author
Owner

Finding: origin/dev already implements device-start/inspect/approve/redeem and api-list/api-revoke in crates/infraternal-server/src/auth/{devices,http}.rs, with 96-bit user codes, 256-bit poll tokens, ten-minute expiry, fresh-session approval, and the 120/60 aggregate plus 30/10 per-source admission limits (contracts/keywrap.md, runtime.rs). apps/tracer does not call these actions yet, and the root CLI is registered through cli/actions.rs/CliHandler, so slice 1 needs browser approval and command wiring. git grep -i qrcode found no QR encoder in origin/dev; the plan gates that renderer on measured options and owner choice.

Finding: `origin/dev` already implements device-start/inspect/approve/redeem and api-list/api-revoke in `crates/infraternal-server/src/auth/{devices,http}.rs`, with 96-bit user codes, 256-bit poll tokens, ten-minute expiry, fresh-session approval, and the 120/60 aggregate plus 30/10 per-source admission limits (`contracts/keywrap.md`, `runtime.rs`). `apps/tracer` does not call these actions yet, and the root CLI is registered through `cli/actions.rs`/`CliHandler`, so slice 1 needs browser approval and command wiring. `git grep -i qrcode` found no QR encoder in `origin/dev`; the plan gates that renderer on measured options and owner choice.
Author
Owner

Owner-question measurement (not an integration build): on infraternal-dev-02-std (aarch64), pinned scripts/static-cargo, release aarch64-unknown-linux-musl, an isolated 62-byte HTTPS approval-URL probe, medium error correction, 2,048 encodes per run measured: qrcodegen 1.8.0 = 375,456-byte static probe, p50 49,583 ns, p99 73,333 ns, max RSS 6,132 KiB; qrcode 0.14.1 with default image/SVG/pic features disabled = 391,152 bytes, p50 565,337 ns, p99 1,098,841 ns, max RSS 6,104 KiB. In this probe qrcodegen is 15,696 bytes smaller and about 11.4× faster at p50; qrcode uses 28 KiB less max RSS. These are isolated probe binaries, not the final infraternal D243 delta, and scan reliability/integrated x86 size remain to be measured. No library was selected.

Owner-question measurement (not an integration build): on `infraternal-dev-02-std` (aarch64), pinned `scripts/static-cargo`, release `aarch64-unknown-linux-musl`, an isolated 62-byte HTTPS approval-URL probe, medium error correction, 2,048 encodes per run measured: `qrcodegen` 1.8.0 = 375,456-byte static probe, p50 49,583 ns, p99 73,333 ns, max RSS 6,132 KiB; `qrcode` 0.14.1 with default image/SVG/pic features disabled = 391,152 bytes, p50 565,337 ns, p99 1,098,841 ns, max RSS 6,104 KiB. In this probe `qrcodegen` is 15,696 bytes smaller and about 11.4× faster at p50; `qrcode` uses 28 KiB less max RSS. These are isolated probe binaries, not the final infraternal D243 delta, and scan reliability/integrated x86 size remain to be measured. No library was selected.
Sign in to join this conversation.
No labels
launch-qa
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
kayg/infraternal#268
No description provided.