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

Open
opened 2026-10-31 16:32:20 +00:00 by kayg · 129 comments
Owner

J6 implementation plan: infraternal sign-in <server> (D311/D314)

Child implementation slice of #251. Scope for this job is slice 1: device-code and browser-link sign-in end to end. The existing auth backend already defines the device and API-key primitives; this work wires the CLI and browser approval UI to them.

Flow

  1. infraternal sign-in <server> validates the server origin, chooses a local device label and the D311 automation scopes (hosts:read, terminal:connect, actions:dry-run), then POSTs /auth/device-start with {device,scopes}. The server returns {poll_token,user_code,expires_in:600}. The poll token remains in process memory and is never put in a URL, terminal output, JSON output, logs, or argv.
  2. The CLI shows the browser link https://<server>/?device_code=<user_code> and the user code. D311's QR is the same link and contains only the user code, never the polling token. QR rendering is a follow-on slice pending the library measurement below; slice 1 keeps the link and code usable without a QR dependency.
  3. The link opens the existing tracer app with the code prefilled. The signed-in browser POSTs /auth/device-inspect with {code}, then shows the exact device and requested scopes before offering approval. Inspection requires the browser session; /auth/device-approve requires a fresh, passkey-verified session and consumes the code once. A plain link visit or code alone grants nothing.
  4. The CLI polls /auth/device-redeem with {token:poll_token} until approval, local expiry, interruption, or server failure. The server has no decline endpoint, so closing the browser leaves the approval pending until its 10-minute expiry. Redemption consumes the pending approval and returns the API key once; the server stores only its hash. The CLI stores the key under the user's private XDG config directory, using a 0700 directory, 0600 file, atomic write+rename and fsync; it never prints or logs the bearer value. Pending --json output contains server, device, scopes, user code, verification URL and expiry; completion contains server, device, scopes and stored. Neither includes a bearer key or polling token.
  5. Revocation remains user-bound: /auth/api-list lists device/scopes and a nonsecret id; /auth/api-revoke deletes only a key for the authenticated user and requires a fresh passkey tap. The API key itself is never returned by list. Add coverage that a revoked key no longer authorizes any scope.

Server endpoints and admission

  • /auth/device-start: no browser cookie or Origin required; D311's high-entropy poll capability and one-time user code; 10-minute expiry.
  • /auth/device-inspect: exact configured Origin plus live browser session; returns device and scopes only.
  • /auth/device-approve: exact Origin plus fresh session/passkey; code is single-use.
  • /auth/device-redeem: no Origin or browser cookie; high-entropy poll capability; returns the API key once.
  • /auth/api-list and /auth/api-revoke: exact Origin and browser session; revoke also requires freshness.
  • Existing guards to preserve: server temporal source budget is 10 requests/minute for device-start/device-redeem and 30/minute for other auth actions; auth's aggregate process budget is 60 device and 120 browser actions/minute. Trusted proxy handling is upstream; untrusted forwarding headers never reset the source budget. Auth bodies stay bounded and unlogged.

Exit codes and messages (D303/D304/D314)

  • 0: approved, key persisted; output confirms server/device/scopes without the key.
  • 1: invalid/unusable response or local storage preparation failure before approval; name the step and recovery action.
  • 2: missing/malformed <server> or invalid options; show the exact HTTPS invocation.
  • 3: explicit authorization refusal; say to approve with the registered passkey or ask the account owner. Browser closure is not an explicit refusal and remains pending until expiry.
  • 4: code expired/already used/not found; say to rerun infraternal sign-in <server> and approve the new request.
  • 5: server unreachable/TLS/request failure; name the server and say to check its address, certificate and network.
  • 6: the server may have issued a credential but local persistence failed or the one-time redemption response was uncertain; direct the user to have a server owner inspect and revoke a matching device key before retrying. The current Account screen does not list/revoke CLI keys.
  • 130: Ctrl-C while polling, using the existing CLI interrupt envelope and signal behavior. If approval may already have completed, the JSON notice should tell the user to check for and revoke a matching key before retrying.
  • While pending, say plainly that the browser must open the shown link and approve the named device with a passkey. Error text must name a fix and next step; --json follows the existing envelope and never includes a bearer secret.

Slices and gates

  1. This job — link + code: browser approval screen, exact scope review, fresh passkey approval, CLI start/poll/redeem, private credential persistence, D311 scope enforcement and the failure cases above. Gates: focused Rust auth/CLI tests on the assigned aarch64 VM; Playwright virtual-passkey approval and cancellation/expiry coverage; scripts/verify fast on aarch64; x86_64 static-size row on compute01 with the D243 delta reported.
  2. QR output: after the owner selects a generator based on a measured build-size/runtime comparison, print a terminal QR that encodes only the approval link. Test non-TTY output and ensure neither poll token nor API key enters the QR.
  3. Credential management polish: expose the existing list/revoke path in the account UI and test cross-user refusal, freshness, and immediate authorization loss. Native app passkey sign-in stays a separate D311 lane.

Owner question: QR renderer

No new crypto library is needed for slice 1: it reuses the existing server random-token and SHA-256 capability handling. Before slice 2, measure the maintained QR candidates against a tiny in-tree encoder option for x86_64 static binary delta, build time, render time, memory, correctness vectors and maintenance/licensing fit. Report those numbers and ask kayg to choose; this job will not add a QR dependency or choose a cryptographic primitive.

Owner question: lost one-time redemption response

The existing redeem transaction consumes the approval and stores only the new API-key hash before returning the plaintext key once. If the response is lost, the client cannot recover the key and the current Account screen cannot revoke it. Slice 1 reports this uncertain outcome as D303 partial (6) and tells the user to have a server owner inspect/revoke a matching key. Before changing the server contract, kayg should choose whether to keep manual recovery or measure an acknowledged/idempotent redemption design that can safely recover a lost response; no new endpoint is chosen here.

Threat model notes

Threats include a copied/guessed user code, a leaked terminal/URL, a malicious same-network caller, replay/racing two approvals, polling abuse, token theft from disk, cross-account revocation, log/argv disclosure, a local attacker replacing credential files, and a lost one-time redemption response that may leave an active key without a local copy. The code is short-lived and one-time; approval displays the requested identity/scopes and requires fresh passkey proof; the poll token is high entropy and memory-only; server storage hashes bearer credentials; admission is bounded; revocation is user-scoped and serialized by existing auth transactions; the client writes only into a private directory/file atomically. Tests cover expiry, replay, concurrency, wrong-user access, rate-limit refusal, redaction, file mode and revocation.

# J6 implementation plan: `infraternal sign-in <server>` (D311/D314) Child implementation slice of #251. Scope for this job is slice 1: device-code and browser-link sign-in end to end. The existing auth backend already defines the device and API-key primitives; this work wires the CLI and browser approval UI to them. ## Flow 1. `infraternal sign-in <server>` validates the server origin, chooses a local device label and the D311 automation scopes (`hosts:read`, `terminal:connect`, `actions:dry-run`), then POSTs `/auth/device-start` with `{device,scopes}`. The server returns `{poll_token,user_code,expires_in:600}`. The poll token remains in process memory and is never put in a URL, terminal output, JSON output, logs, or argv. 2. The CLI shows the browser link `https://<server>/?device_code=<user_code>` and the user code. D311's QR is the same link and contains only the user code, never the polling token. QR rendering is a follow-on slice pending the library measurement below; slice 1 keeps the link and code usable without a QR dependency. 3. The link opens the existing tracer app with the code prefilled. The signed-in browser POSTs `/auth/device-inspect` with `{code}`, then shows the exact device and requested scopes before offering approval. Inspection requires the browser session; `/auth/device-approve` requires a fresh, passkey-verified session and consumes the code once. A plain link visit or code alone grants nothing. 4. The CLI polls `/auth/device-redeem` with `{token:poll_token}` until approval, local expiry, interruption, or server failure. The server has no decline endpoint, so closing the browser leaves the approval pending until its 10-minute expiry. Redemption consumes the pending approval and returns the API key once; the server stores only its hash. The CLI stores the key under the user's private XDG config directory, using a 0700 directory, 0600 file, atomic write+rename and fsync; it never prints or logs the bearer value. Pending `--json` output contains server, device, scopes, user code, verification URL and expiry; completion contains server, device, scopes and `stored`. Neither includes a bearer key or polling token. 5. Revocation remains user-bound: `/auth/api-list` lists device/scopes and a nonsecret id; `/auth/api-revoke` deletes only a key for the authenticated user and requires a fresh passkey tap. The API key itself is never returned by list. Add coverage that a revoked key no longer authorizes any scope. ## Server endpoints and admission - `/auth/device-start`: no browser cookie or Origin required; D311's high-entropy poll capability and one-time user code; 10-minute expiry. - `/auth/device-inspect`: exact configured Origin plus live browser session; returns device and scopes only. - `/auth/device-approve`: exact Origin plus fresh session/passkey; code is single-use. - `/auth/device-redeem`: no Origin or browser cookie; high-entropy poll capability; returns the API key once. - `/auth/api-list` and `/auth/api-revoke`: exact Origin and browser session; revoke also requires freshness. - Existing guards to preserve: server temporal source budget is 10 requests/minute for `device-start`/`device-redeem` and 30/minute for other auth actions; auth's aggregate process budget is 60 device and 120 browser actions/minute. Trusted proxy handling is upstream; untrusted forwarding headers never reset the source budget. Auth bodies stay bounded and unlogged. ## Exit codes and messages (D303/D304/D314) - 0: approved, key persisted; output confirms server/device/scopes without the key. - 1: invalid/unusable response or local storage preparation failure before approval; name the step and recovery action. - 2: missing/malformed `<server>` or invalid options; show the exact HTTPS invocation. - 3: explicit authorization refusal; say to approve with the registered passkey or ask the account owner. Browser closure is not an explicit refusal and remains pending until expiry. - 4: code expired/already used/not found; say to rerun `infraternal sign-in <server>` and approve the new request. - 5: server unreachable/TLS/request failure; name the server and say to check its address, certificate and network. - 6: the server may have issued a credential but local persistence failed or the one-time redemption response was uncertain; direct the user to have a server owner inspect and revoke a matching device key before retrying. The current Account screen does not list/revoke CLI keys. - 130: Ctrl-C while polling, using the existing CLI interrupt envelope and signal behavior. If approval may already have completed, the JSON notice should tell the user to check for and revoke a matching key before retrying. - While pending, say plainly that the browser must open the shown link and approve the named device with a passkey. Error text must name a fix and next step; `--json` follows the existing envelope and never includes a bearer secret. ## Slices and gates 1. **This job — link + code:** browser approval screen, exact scope review, fresh passkey approval, CLI start/poll/redeem, private credential persistence, D311 scope enforcement and the failure cases above. Gates: focused Rust auth/CLI tests on the assigned aarch64 VM; Playwright virtual-passkey approval and cancellation/expiry coverage; `scripts/verify fast` on aarch64; x86_64 static-size row on compute01 with the D243 delta reported. 2. **QR output:** after the owner selects a generator based on a measured build-size/runtime comparison, print a terminal QR that encodes only the approval link. Test non-TTY output and ensure neither poll token nor API key enters the QR. 3. **Credential management polish:** expose the existing list/revoke path in the account UI and test cross-user refusal, freshness, and immediate authorization loss. Native app passkey sign-in stays a separate D311 lane. ## Owner question: QR renderer No new crypto library is needed for slice 1: it reuses the existing server random-token and SHA-256 capability handling. Before slice 2, measure the maintained QR candidates against a tiny in-tree encoder option for x86_64 static binary delta, build time, render time, memory, correctness vectors and maintenance/licensing fit. Report those numbers and ask kayg to choose; this job will not add a QR dependency or choose a cryptographic primitive. ## Owner question: lost one-time redemption response The existing redeem transaction consumes the approval and stores only the new API-key hash before returning the plaintext key once. If the response is lost, the client cannot recover the key and the current Account screen cannot revoke it. Slice 1 reports this uncertain outcome as D303 partial (6) and tells the user to have a server owner inspect/revoke a matching key. Before changing the server contract, kayg should choose whether to keep manual recovery or measure an acknowledged/idempotent redemption design that can safely recover a lost response; no new endpoint is chosen here. ## Threat model notes Threats include a copied/guessed user code, a leaked terminal/URL, a malicious same-network caller, replay/racing two approvals, polling abuse, token theft from disk, cross-account revocation, log/argv disclosure, a local attacker replacing credential files, and a lost one-time redemption response that may leave an active key without a local copy. The code is short-lived and one-time; approval displays the requested identity/scopes and requires fresh passkey proof; the poll token is high entropy and memory-only; server storage hashes bearer credentials; admission is bounded; revocation is user-scoped and serialized by existing auth transactions; the client writes only into a private directory/file atomically. Tests cover expiry, replay, concurrency, wrong-user access, rate-limit refusal, redaction, file mode and revocation.
Author
Owner

Start: implementing on branch signin-d311, based on origin/dev at 42ab330358eb06d47acade8d0f2c35b9225af74b. The worktree already contains the CLI positional-argument test and browser approval test from the earlier run; auth/runtime implementation is not yet present. Plan is in the issue body.

Start: implementing on branch `signin-d311`, based on `origin/dev` at `42ab330358eb06d47acade8d0f2c35b9225af74b`. The worktree already contains the CLI positional-argument test and browser approval test from the earlier run; auth/runtime implementation is not yet present. Plan is in the issue body.
Author
Owner

Finding — CLI polling must stay below the source limiter: runtime.rs::trusted_ingress caps /auth/device-start and /auth/device-redeem together at 10 requests/minute per source (versus 30 for other auth routes), in addition to the auth-wide 60 device / 120 browser budget (auth/http.rs::ingress). I will poll every 10 seconds and honor Retry-After on 429, leaving margin for the start request and concurrent sign-ins. Evidence: source admission test runtime.rs around the device-start/device-redeem cases; aggregate budget test auth/http.rs::review_invalid_device_polling_exhausts_browser_budget.

Finding — CLI polling must stay below the source limiter: `runtime.rs::trusted_ingress` caps `/auth/device-start` and `/auth/device-redeem` together at 10 requests/minute per source (versus 30 for other auth routes), in addition to the auth-wide 60 device / 120 browser budget (`auth/http.rs::ingress`). I will poll every 10 seconds and honor `Retry-After` on 429, leaving margin for the start request and concurrent sign-ins. Evidence: source admission test `runtime.rs` around the `device-start`/`device-redeem` cases; aggregate budget test `auth/http.rs::review_invalid_device_polling_exhausts_browser_budget`.
Author
Owner

Finding — the prior D311 research in #251 selected option A: browser device-code approval with passkey; it records that D27 already has the server device API and that CLI transport/storage are the missing pieces. That same review said no new transport crate is assumed and the size expectation is 0–64 KiB, subject to matched static measurements. D314 confirms the public command name and its eventual link/code/QR output. I am keeping this slice on the existing HTTPS/auth stack and measuring its actual D243 delta; QR stays a distinct measured follow-on so this slice can establish the link/code ceremony first.

Finding — the prior D311 research in #251 selected option A: browser device-code approval with passkey; it records that D27 already has the server device API and that CLI transport/storage are the missing pieces. That same review said no new transport crate is assumed and the size expectation is 0–64 KiB, subject to matched static measurements. D314 confirms the public command name and its eventual link/code/QR output. I am keeping this slice on the existing HTTPS/auth stack and measuring its actual D243 delta; QR stays a distinct measured follow-on so this slice can establish the link/code ceremony first.
Author
Owner

Finding — the current origin/dev D243 reference is exact: 21,431,472 bytes x86_64 and 19,605,896 bytes aarch64 (docs/PROCESS.md §7; scripts/verify.d/rust.sh). I will calculate the branch delta against these measured caps, report it per architecture, and keep the release size gate exact.

Finding — the current origin/dev D243 reference is exact: 21,431,472 bytes x86_64 and 19,605,896 bytes aarch64 (`docs/PROCESS.md` §7; `scripts/verify.d/rust.sh`). I will calculate the branch delta against these measured caps, report it per architecture, and keep the release size gate exact.
Author
Owner

Finding: the ARM browser run built the production tracer and started the test server, but Playwright could not launch Chromium because the host runtime is missing the required GUI libraries (libglib-2.0.so.0, libnss3.so, libX11.so.6, and others). The spec failed before reaching its expected UI assertion, so this is an environment failure rather than a product finding. I will rerun it inside the repository's Nix dev shell.

Finding: the ARM browser run built the production tracer and started the test server, but Playwright could not launch Chromium because the host runtime is missing the required GUI libraries (`libglib-2.0.so.0`, `libnss3.so`, `libX11.so.6`, and others). The spec failed before reaching its expected UI assertion, so this is an environment failure rather than a product finding. I will rerun it inside the repository's Nix dev shell.
Author
Owner

Plan amendment — the current auth API has no device-approval decline or cancel endpoint. /auth/device-redeem returns the same unauthorized response while approval is pending and when a code is expired or absent, so the CLI cannot distinguish refusal from pending. Closing the browser leaves the approval pending until its 10-minute expiry; the CLI can be stopped with Ctrl-C or will exit 4 on its local deadline. This slice will not invent a server route. The browser will show actionable expiry guidance when inspection reports the code unavailable, and the CLI will tell the user to rerun sign-in after its deadline.

Evidence: crates/infraternal-server/src/auth/http.rs dispatches device-inspect, device-approve, and device-redeem only; crates/infraternal-server/src/auth/devices.rs::device_redeem consumes only approved rows and otherwise returns Error::Denied.

Plan amendment — the current auth API has no device-approval decline or cancel endpoint. `/auth/device-redeem` returns the same unauthorized response while approval is pending and when a code is expired or absent, so the CLI cannot distinguish refusal from pending. Closing the browser leaves the approval pending until its 10-minute expiry; the CLI can be stopped with Ctrl-C or will exit 4 on its local deadline. This slice will not invent a server route. The browser will show actionable expiry guidance when inspection reports the code unavailable, and the CLI will tell the user to rerun sign-in after its deadline. Evidence: `crates/infraternal-server/src/auth/http.rs` dispatches `device-inspect`, `device-approve`, and `device-redeem` only; `crates/infraternal-server/src/auth/devices.rs::device_redeem` consumes only approved rows and otherwise returns `Error::Denied`.
Author
Owner

Finding — credential writes can lose completed sign-ins. On infraternal-dev-02-std, simultaneous_credential_stores_preserve_each_server failed after 16 concurrent writers; the final file held 1 server instead of all 16. Evidence: /home/claude/.cache/codex-scratch/signin-d311/evidence/rust-concurrent-red.log. I will serialize the read/modify/atomic-write sequence with the existing nix::fcntl::Flock facility.

Finding — the initial D303 mapping regression failed on ARM because an expired code returned 3 (not allowed) instead of 4 (not found). Evidence: /home/claude/.cache/codex-scratch/signin-d311/evidence/rust-exit-red.log. TLS, throttling, and post-approval storage cases are asserted in the same test and will be aligned to codes 5, 1, and 6.

Finding — credential writes can lose completed sign-ins. On `infraternal-dev-02-std`, `simultaneous_credential_stores_preserve_each_server` failed after 16 concurrent writers; the final file held 1 server instead of all 16. Evidence: `/home/claude/.cache/codex-scratch/signin-d311/evidence/rust-concurrent-red.log`. I will serialize the read/modify/atomic-write sequence with the existing `nix::fcntl::Flock` facility. Finding — the initial D303 mapping regression failed on ARM because an expired code returned 3 (`not allowed`) instead of 4 (`not found`). Evidence: `/home/claude/.cache/codex-scratch/signin-d311/evidence/rust-exit-red.log`. TLS, throttling, and post-approval storage cases are asserted in the same test and will be aligned to codes 5, 1, and 6.
Author
Owner

Finding — the existing Account settings panel only offers sign-out; it does not yet list or revoke CLI device credentials. The server already has user-bound /auth/api-list and fresh-passkey /auth/api-revoke routes, but the account UI is a later management slice in this plan. Therefore exit 6 must say the newly approved server key remains active and ask the account owner to revoke this device through account controls before retrying; it must not imply that the current browser UI already exposes that action. Evidence: packages/ui/src/settings-panels.ts::renderAccount and crates/infraternal-server/src/auth/devices.rs::api_list / api_revoke.

Finding — the existing Account settings panel only offers sign-out; it does not yet list or revoke CLI device credentials. The server already has user-bound `/auth/api-list` and fresh-passkey `/auth/api-revoke` routes, but the account UI is a later management slice in this plan. Therefore exit 6 must say the newly approved server key remains active and ask the account owner to revoke this device through account controls before retrying; it must not imply that the current browser UI already exposes that action. Evidence: `packages/ui/src/settings-panels.ts::renderAccount` and `crates/infraternal-server/src/auth/devices.rs::api_list` / `api_revoke`.
Author
Owner

Finding — the sign-in action output schema requires server, device, scopes, and stored, but the command first emits pending data with user_code, verification_url, and expires_in_seconds; its failure envelope also has empty data. On the aarch64 VM, cargo test -j3 -p infraternal --bin infraternal --features remote sign_in_output_schema_covers_pending_and_completed_data -- --nocapture fails at server is state-dependent and must remain optional. The generated contract cannot describe the command's actual progress result. I’m updating its fields to be optional across these result states.

Finding — the sign-in action output schema requires `server`, `device`, `scopes`, and `stored`, but the command first emits pending data with `user_code`, `verification_url`, and `expires_in_seconds`; its failure envelope also has empty data. On the aarch64 VM, `cargo test -j3 -p infraternal --bin infraternal --features remote sign_in_output_schema_covers_pending_and_completed_data -- --nocapture` fails at `server is state-dependent and must remain optional`. The generated contract cannot describe the command's actual progress result. I’m updating its fields to be optional across these result states.
Author
Owner

Process correction: while posting the schema finding, shell interpolation triggered an unintended cargo test attempt on the control host. It stopped before compiling infraternal because the host lacked the cc linker; it is not verification evidence. The attempt created about 965 KiB of ignored target output. The shell rejected my cleanup command, so I left that generated directory in place. All follow-up builds and tests are being run on the assigned VMs.

Process correction: while posting the schema finding, shell interpolation triggered an unintended cargo test attempt on the control host. It stopped before compiling infraternal because the host lacked the cc linker; it is not verification evidence. The attempt created about 965 KiB of ignored target output. The shell rejected my cleanup command, so I left that generated directory in place. All follow-up builds and tests are being run on the assigned VMs.
Author
Owner

Finding — one-time redemption can leave an active key without a local copy. auth/devices.rs::device_redeem deletes the approval and inserts the API-key hash in one transaction, then returns the plaintext key only in that HTTP response. If the response is lost or unusable, the client currently maps the failure as a normal network/server error (D303 code 5 or 1), so rerunning sign-in can leave an unreachable key active. In JSON mode, the shared SIGINT handler also terminates immediately with code 130 during this window. For this slice I will classify uncertain redeem outcomes as partial (D303 code 6) and tell the user to have a server owner inspect/revoke a matching device key before retrying. I will not add an endpoint or change the established one-time redeem protocol here. Owner question for a later protocol review: retain one-time redemption and accept manual recovery, or measure an acknowledged/idempotent redemption design that can recover a lost response safely.

Finding — one-time redemption can leave an active key without a local copy. `auth/devices.rs::device_redeem` deletes the approval and inserts the API-key hash in one transaction, then returns the plaintext key only in that HTTP response. If the response is lost or unusable, the client currently maps the failure as a normal network/server error (D303 code 5 or 1), so rerunning sign-in can leave an unreachable key active. In JSON mode, the shared SIGINT handler also terminates immediately with code 130 during this window. For this slice I will classify uncertain redeem outcomes as partial (D303 code 6) and tell the user to have a server owner inspect/revoke a matching device key before retrying. I will not add an endpoint or change the established one-time redeem protocol here. Owner question for a later protocol review: retain one-time redemption and accept manual recovery, or measure an acknowledged/idempotent redemption design that can recover a lost response safely.
Author
Owner

The ARM red regression confirms the missing D303 partial branch: cargo test -j3 -p infraternal --bin infraternal --features remote uncertain_device_redemption_uses_the_partial_exit_code -- --nocapture fails at compile time because Failure::RedemptionUncertain is not defined. The related mapping test also covers uncertain request/body/timeout failures and server 5xx responses. I’m implementing this client-side classification and actionable exit-6 guidance without changing the server endpoint contract.

The ARM red regression confirms the missing D303 partial branch: `cargo test -j3 -p infraternal --bin infraternal --features remote uncertain_device_redemption_uses_the_partial_exit_code -- --nocapture` fails at compile time because `Failure::RedemptionUncertain` is not defined. The related mapping test also covers uncertain request/body/timeout failures and server 5xx responses. I’m implementing this client-side classification and actionable exit-6 guidance without changing the server endpoint contract.
Author
Owner

Finding: one-time device redemption can succeed at the server while the client loses or cannot parse the response. In the current client, request-send and whole-request timeout failures map to D303 exit 5, response-body failure maps to bad_response/exit 1, 5xx maps to server_unavailable/exit 5, and malformed 200 responses map to bad_response/exit 1. These outcomes can leave the approved device key active while the local credential is absent. Evidence: the new ARM regression currently fails to compile with E0599 for the missing Failure::RedemptionUncertain; the relevant mapping branches are in redeem_once, post_json, post_json_with_timeout, and map_http_failure. I am adding D303 exit 6 handling for ambiguous redemption outcomes and guidance to inspect and revoke a matching key before retrying. DNS/connect/TLS failures before sending remain ordinary unreachable failures. The server protocol and cryptographic/library choices are unchanged in this slice.

Finding: one-time device redemption can succeed at the server while the client loses or cannot parse the response. In the current client, request-send and whole-request timeout failures map to D303 exit 5, response-body failure maps to `bad_response`/exit 1, 5xx maps to `server_unavailable`/exit 5, and malformed 200 responses map to `bad_response`/exit 1. These outcomes can leave the approved device key active while the local credential is absent. Evidence: the new ARM regression currently fails to compile with E0599 for the missing `Failure::RedemptionUncertain`; the relevant mapping branches are in `redeem_once`, `post_json`, `post_json_with_timeout`, and `map_http_failure`. I am adding D303 exit 6 handling for ambiguous redemption outcomes and guidance to inspect and revoke a matching key before retrying. DNS/connect/TLS failures before sending remain ordinary unreachable failures. The server protocol and cryptographic/library choices are unchanged in this slice.
Author
Owner

Finding: the JSON Ctrl-C envelope for sign-in used only the generic “Stopped before it finished.” and gave no recovery step, even though the one-time redemption may have completed just before interruption. Evidence: the ARM regression cli::output::tests::interrupted_sign_in_warns_that_the_server_key_may_need_revocation fails on the message assertion against that current envelope. I am tailoring the sign-in notice to warn that the server may have issued a key and to check/revoke a matching device key, while retaining the existing D303 SIGINT re-raise and status 130.

Finding: the JSON Ctrl-C envelope for `sign-in` used only the generic “Stopped before it finished.” and gave no recovery step, even though the one-time redemption may have completed just before interruption. Evidence: the ARM regression `cli::output::tests::interrupted_sign_in_warns_that_the_server_key_may_need_revocation` fails on the message assertion against that current envelope. I am tailoring the sign-in notice to warn that the server may have issued a key and to check/revoke a matching device key, while retaining the existing D303 SIGINT re-raise and status 130.
Author
Owner

The interrupt finding is fixed. On ARM, the regression passed after the change: test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 66 filtered out; finished in 0.00s. The test checks the sign-in-specific recovery warning; the existing D303 exit table still asserts SIGINT status 130. The x86 D243 row is now running detached on compute01; log: /home/claude/.cache/codex-scratch/j6-x86-d243.log.

The interrupt finding is fixed. On ARM, the regression passed after the change: `test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 66 filtered out; finished in 0.00s`. The test checks the sign-in-specific recovery warning; the existing D303 exit table still asserts SIGINT status 130. The x86 D243 row is now running detached on compute01; log: `/home/claude/.cache/codex-scratch/j6-x86-d243.log`.
Author
Owner

Snapshot: signin-d311 now points to ac22dbf98 (branch was pushed so the WIP is recoverable). Slice 1 implementation is present. ARM evidence so far: 11 sign_in::tests pass and the targeted JSON Ctrl-C regression passes; ARM cargo fmt --all --check passes. The browser suite and server scope/freshness/revocation test are queued behind all three occupied ARM build slots. The compute01 D243 x86 static build is queued behind all four occupied x86 slots; its log is /home/claude/.cache/codex-scratch/j6-x86-d243.log. These are interim results, not the final gate. Next: complete the auth and browser tests/screenshots, measure both static rows, then run scripts/verify fast on ARM.

Snapshot: `signin-d311` now points to `ac22dbf98` (branch was pushed so the WIP is recoverable). Slice 1 implementation is present. ARM evidence so far: 11 `sign_in::tests` pass and the targeted JSON Ctrl-C regression passes; ARM `cargo fmt --all --check` passes. The browser suite and server scope/freshness/revocation test are queued behind all three occupied ARM build slots. The compute01 D243 x86 static build is queued behind all four occupied x86 slots; its log is `/home/claude/.cache/codex-scratch/j6-x86-d243.log`. These are interim results, not the final gate. Next: complete the auth and browser tests/screenshots, measure both static rows, then run `scripts/verify fast` on ARM.
Author
Owner

Finding: the initial production Playwright run built the tracer but all five browser cases failed before launch because the VM lacks Chromium runtime libraries (libglib-2.0.so.0, libnss3.so, libX11.so.6, libgbm.so.1, among others). No test interaction ran and no screenshot was produced. I am retrying with the Nix-packaged Chromium wrapper and a temporary config outside the checkout; the tracer production build and repository test file remain unchanged.

Finding: the initial production Playwright run built the tracer but all five browser cases failed before launch because the VM lacks Chromium runtime libraries (`libglib-2.0.so.0`, `libnss3.so`, `libX11.so.6`, `libgbm.so.1`, among others). No test interaction ran and no screenshot was produced. I am retrying with the Nix-packaged Chromium wrapper and a temporary config outside the checkout; the tracer production build and repository test file remain unchanged.
Author
Owner

Finding — the browser retry still failed before reaching the UI assertions. All five specs stopped at Playwright launch because its managed Chromium shell could not find host libraries including libglib-2.0.so.0, libnss3.so, and libX11.so.6; the evidence is /home/claude/.cache/codex-scratch/signin-d311/evidence/browser-nix.log. Running inside the repository Nix dev shell alone did not select a Nix Chromium executable. I am checking the Nix Chromium package and will pass it explicitly to Playwright, keeping the override and screenshots outside the checkout.

Finding — the browser retry still failed before reaching the UI assertions. All five specs stopped at Playwright launch because its managed Chromium shell could not find host libraries including libglib-2.0.so.0, libnss3.so, and libX11.so.6; the evidence is /home/claude/.cache/codex-scratch/signin-d311/evidence/browser-nix.log. Running inside the repository Nix dev shell alone did not select a Nix Chromium executable. I am checking the Nix Chromium package and will pass it explicitly to Playwright, keeping the override and screenshots outside the checkout.
Author
Owner

Finding — redemption timeout classification (D303): post_json_with_timeout currently applies one deadline around DNS lookup, TCP connect, TLS, request send, response headers, and body read. map_redemption_network_failure maps every NetworkFailure::Timeout to partial exit 6. A timeout before Hyper begins sending /auth/device-redeem therefore reports a possible issued credential even though the request could not have reached the server; D303 calls for exit 5 when the server is unreachable. I’ll add a regression for the pre-send and post-send cases, then track whether the request may have started before classifying the deadline. Timeouts from send through response remain partial exit 6.

Finding — redemption timeout classification (D303): post_json_with_timeout currently applies one deadline around DNS lookup, TCP connect, TLS, request send, response headers, and body read. map_redemption_network_failure maps every NetworkFailure::Timeout to partial exit 6. A timeout before Hyper begins sending /auth/device-redeem therefore reports a possible issued credential even though the request could not have reached the server; D303 calls for exit 5 when the server is unreachable. I’ll add a regression for the pre-send and post-send cases, then track whether the request may have started before classifying the deadline. Timeouts from send through response remain partial exit 6.
Author
Owner

Finding — redemption timeout classification (D303): post_json_with_timeout currently applies one deadline around DNS lookup, TCP connect, TLS, request send, response headers, and body read. map_redemption_network_failure maps every NetworkFailure::Timeout to partial exit 6. A timeout before Hyper begins sending /auth/device-redeem therefore reports a possible issued credential even though the request could not have reached the server; D303 calls for exit 5 when the server is unreachable. I’ll add a regression for the pre-send and post-send cases, then track whether the request may have started before classifying the deadline. Timeouts from send through response remain partial exit 6.

Finding — redemption timeout classification (D303): `post_json_with_timeout` currently applies one deadline around DNS lookup, TCP connect, TLS, request send, response headers, and body read. `map_redemption_network_failure` maps every `NetworkFailure::Timeout` to partial exit 6. A timeout before Hyper begins sending `/auth/device-redeem` therefore reports a possible issued credential even though the request could not have reached the server; D303 calls for exit 5 when the server is unreachable. I’ll add a regression for the pre-send and post-send cases, then track whether the request may have started before classifying the deadline. Timeouts from send through response remain partial exit 6.
Author
Owner

Browser verification update: the production tracer Playwright retry on infraternal-dev-02-std, with the Nix Chromium executable selected explicitly, passed all five device-approval specs (5 passed, 26.4s). The run captured the configured light/dark desktop, phone, and small-phone screenshots; those six screenshots are attached.

Browser verification update: the production tracer Playwright retry on infraternal-dev-02-std, with the Nix Chromium executable selected explicitly, passed all five device-approval specs (5 passed, 26.4s). The run captured the configured light/dark desktop, phone, and small-phone screenshots; those six screenshots are attached.
Author
Owner

Blocked on assigned VM capacity: the focused timeout regression is queued before cargo starts. Evidence in /home/claude/.cache/codex-scratch/signin-d311/evidence/redemption-timeout-red.log: “build-slot: [19:29:58] less than 25 GB free on infraternal-dev-02-std; waiting (tell the orchestrator if this lasts)...”. I am checking this job's own target and will not remove another job's cache. The x86 D243 row remains queued behind occupied compute01 slots.

Blocked on assigned VM capacity: the focused timeout regression is queued before cargo starts. Evidence in /home/claude/.cache/codex-scratch/signin-d311/evidence/redemption-timeout-red.log: “build-slot: [19:29:58] less than 25 GB free on infraternal-dev-02-std; waiting (tell the orchestrator if this lasts)...”. I am checking this job's own target and will not remove another job's cache. The x86 D243 row remains queued behind occupied compute01 slots.
Author
Owner

The timeout regression reproduced on infraternal-dev-02-std before the fix. redemption_timeout_before_request_send_is_unreachable failed with left: 6, right: 5 (0 passed, 1 failed, exit 101), confirming that the current redemption mapper reports a pre-send timeout as partial. Evidence: /home/claude/.cache/codex-scratch/signin-d311/evidence/redemption-timeout-red.log.

The timeout regression reproduced on infraternal-dev-02-std before the fix. `redemption_timeout_before_request_send_is_unreachable` failed with `left: 6`, `right: 5` (0 passed, 1 failed, exit 101), confirming that the current redemption mapper reports a pre-send timeout as partial. Evidence: /home/claude/.cache/codex-scratch/signin-d311/evidence/redemption-timeout-red.log.
Author
Owner

The timeout classification fix is green on ARM. The new regression checks both request phases; all 12 sign_in::tests pass, and cargo fmt --all --check passed in the same slot run. Evidence: /home/claude/.cache/codex-scratch/signin-d311/evidence/redemption-timeout-green.log. I am running touched-crate Clippy next, then I will commit and run the final gate on the pushed branch head.

The timeout classification fix is green on ARM. The new regression checks both request phases; all 12 `sign_in::tests` pass, and `cargo fmt --all --check` passed in the same slot run. Evidence: /home/claude/.cache/codex-scratch/signin-d311/evidence/redemption-timeout-green.log. I am running touched-crate Clippy next, then I will commit and run the final gate on the pushed branch head.
Author
Owner

The first touched-crate Clippy pass exits 101 with eight pedantic findings on this branch: two match-to-let-else suggestions, two local Unix-trait imports after statements, one unchecked metadata-length cast, the HTTP transport and CLI dispatcher over 100 lines, and duplicate Connect mapping arms. Evidence: /home/claude/.cache/codex-scratch/signin-d311/evidence/sign-in-clippy.log. I am correcting these in the sign-in and dispatch code, including extracting the TLS connection stage and sign-in handler, then I will rerun Clippy and the tests. No lint suppression is planned.

The first touched-crate Clippy pass exits 101 with eight pedantic findings on this branch: two match-to-let-else suggestions, two local Unix-trait imports after statements, one unchecked metadata-length cast, the HTTP transport and CLI dispatcher over 100 lines, and duplicate Connect mapping arms. Evidence: /home/claude/.cache/codex-scratch/signin-d311/evidence/sign-in-clippy.log. I am correcting these in the sign-in and dispatch code, including extracting the TLS connection stage and sign-in handler, then I will rerun Clippy and the tests. No lint suppression is planned.
Author
Owner

After correcting the initial lint set, touched-crate Clippy now reports one remaining finding: dispatch_action is 101 lines against the 100-line pedantic limit. Evidence: /home/claude/.cache/codex-scratch/signin-d311/evidence/sign-in-clippy-final.log. I am moving the adjacent node-component branch into its own handler and will rerun the sequence.

After correcting the initial lint set, touched-crate Clippy now reports one remaining finding: `dispatch_action` is 101 lines against the 100-line pedantic limit. Evidence: /home/claude/.cache/codex-scratch/signin-d311/evidence/sign-in-clippy-final.log. I am moving the adjacent node-component branch into its own handler and will rerun the sequence.
Author
Owner

The timeout fix and touched-code checks are green on infraternal-dev-02-std. The ARM run reports cargo fmt --all --check passing, touched-crate Clippy passing with -D warnings, and all 12 sign_in::tests passing, including both request-phase exit codes. Evidence: /home/claude/.cache/codex-scratch/signin-d311/evidence/sign-in-clippy-retry3.log (the prior transfer-path retry logs are retained separately). The five production tracer Playwright specs passed and the six light/dark desktop and phone screenshots are attached above. I am committing this correction now; the branch-head fast gate and final compute01 size measurement remain.

The timeout fix and touched-code checks are green on infraternal-dev-02-std. The ARM run reports `cargo fmt --all --check` passing, touched-crate Clippy passing with `-D warnings`, and all 12 `sign_in::tests` passing, including both request-phase exit codes. Evidence: /home/claude/.cache/codex-scratch/signin-d311/evidence/sign-in-clippy-retry3.log (the prior transfer-path retry logs are retained separately). The five production tracer Playwright specs passed and the six light/dark desktop and phone screenshots are attached above. I am committing this correction now; the branch-head fast gate and final compute01 size measurement remain.
Author
Owner

Timeout classification and auth Clippy repairs are committed and pushed as 0e003f4960 on signin-d311. The ARM scratch worktree is reset to this exact head, and branch-head scripts/verify fast is now queued through build-slot. Log: /home/claude/.cache/codex-scratch/signin-d311/evidence/verify-fast.log. The compute01 static-size run will be repeated against this head after its earlier queue entry completes.

Timeout classification and auth Clippy repairs are committed and pushed as 0e003f4960006dc63d794d231fbe94997e427761 on signin-d311. The ARM scratch worktree is reset to this exact head, and branch-head `scripts/verify fast` is now queued through build-slot. Log: /home/claude/.cache/codex-scratch/signin-d311/evidence/verify-fast.log. The compute01 static-size run will be repeated against this head after its earlier queue entry completes.
Author
Owner

Finding — the first branch-head fast gate failed in the workspace test row before the replay-size review test reached its assertions. fixture_state('rr257') reported an AF_UNIX path of 109 bytes. The remote runner omitted the documented short INFRATERAL_TEST_STATE_ROOT, so the worktree/target-derived fallback exceeded Linux sun_path. Evidence: /home/claude/.cache/codex-scratch/signin-d311/evidence/verify-fast.log. I will rerun the final gate with a short per-job state root.

Finding — the first branch-head fast gate failed in the workspace test row before the replay-size review test reached its assertions. `fixture_state('rr257')` reported an AF_UNIX path of 109 bytes. The remote runner omitted the documented short `INFRATERAL_TEST_STATE_ROOT`, so the worktree/target-derived fallback exceeded Linux `sun_path`. Evidence: /home/claude/.cache/codex-scratch/signin-d311/evidence/verify-fast.log. I will rerun the final gate with a short per-job state root.
Author
Owner

The corrected branch-head gate is still waiting at build-slot's disk guard: ARM has 16 GB free (<25 GB), and this job's only target directory is 34 GB. The first gate logs and screenshot evidence remain intact. I will cancel only this queued runner, remove only this job target, and relaunch with the short test-state root; no other target or evidence files will be touched.

The corrected branch-head gate is still waiting at build-slot's disk guard: ARM has 16 GB free (<25 GB), and this job's only target directory is 34 GB. The first gate logs and screenshot evidence remain intact. I will cancel only this queued runner, remove only this job target, and relaunch with the short test-state root; no other target or evidence files will be touched.
Author
Owner

Progress: I stopped only the queued ARM runner and removed only this job's target after build-slot reported 16 GB free (<25 GB). The corrected branch-head scripts/verify fast retry is now running at 0e003f4960006dc63d794d231fbe94997e427761 on infraternal-dev-02-std with both short test-state roots set to /home/claude/.cache/j6 and TRACER_TEST_PORT=4631. It has entered the full Rust test rows. Log: /home/claude/.cache/codex-scratch/signin-d311/evidence/verify-fast-final.log; exit file: /home/claude/.cache/codex-scratch/signin-d311/evidence/verify-fast-final.exit.

The x86 static-size run remains queued on compute01 behind all four build slots. Its worktree remains untouched at the pre-fix feature copy until that row exits; log: /home/claude/.cache/codex-scratch/j6-x86-d243.log.

Progress: I stopped only the queued ARM runner and removed only this job's target after `build-slot` reported 16 GB free (<25 GB). The corrected branch-head `scripts/verify fast` retry is now running at `0e003f4960006dc63d794d231fbe94997e427761` on infraternal-dev-02-std with both short test-state roots set to `/home/claude/.cache/j6` and `TRACER_TEST_PORT=4631`. It has entered the full Rust test rows. Log: `/home/claude/.cache/codex-scratch/signin-d311/evidence/verify-fast-final.log`; exit file: `/home/claude/.cache/codex-scratch/signin-d311/evidence/verify-fast-final.exit`. The x86 static-size run remains queued on compute01 behind all four build slots. Its worktree remains untouched at the pre-fix feature copy until that row exits; log: `/home/claude/.cache/codex-scratch/j6-x86-d243.log`.
Author
Owner

Compute01 capacity update: the x86 static-size runner remains inside build-slot, and lslocks shows all four /tmp/infraternal-build-slots/slot.* locks held. Several holders are running other full scripts/verify fast gates. I have left those jobs untouched. The J6 queue log remains /home/claude/.cache/codex-scratch/j6-x86-d243.log; the pre-fix feature worktree stays unchanged until the queued row exits.

Compute01 capacity update: the x86 static-size runner remains inside `build-slot`, and `lslocks` shows all four `/tmp/infraternal-build-slots/slot.*` locks held. Several holders are running other full `scripts/verify fast` gates. I have left those jobs untouched. The J6 queue log remains `/home/claude/.cache/codex-scratch/j6-x86-d243.log`; the pre-fix feature worktree stays unchanged until the queued row exits.
Author
Owner

Gate finding: the corrected ARM scripts/verify fast run failed one unchanged infraternal shim integration test while running its 24-test shim batch. ring_wrap_snapshot_repaints_real_vim panicked at crates/infraternal/tests/shim.rs:276 because the 5-second socket read returned WouldBlock; 23 tests passed. The test file is byte-for-byte identical to origin/dev, and the J6 diff contains no shim or test changes. I am checking reproduction after the current gate exits before classifying this as load-related. Evidence: /home/claude/.cache/codex-scratch/signin-d311/evidence/verify-fast-final.log.

Gate finding: the corrected ARM `scripts/verify fast` run failed one unchanged `infraternal` shim integration test while running its 24-test shim batch. `ring_wrap_snapshot_repaints_real_vim` panicked at `crates/infraternal/tests/shim.rs:276` because the 5-second socket read returned `WouldBlock`; 23 tests passed. The test file is byte-for-byte identical to `origin/dev`, and the J6 diff contains no shim or test changes. I am checking reproduction after the current gate exits before classifying this as load-related. Evidence: `/home/claude/.cache/codex-scratch/signin-d311/evidence/verify-fast-final.log`.
Author
Owner

ARM scripts/verify fast attempt exited 1. Summary from the gate log:

| test                      | FAIL (101)   |
| static builds             | FAIL (1)     |

The test row records the shim WouldBlock described above. The static-build row ends with Python OSError: [Errno 8] Exec format error: '/home/claude/.cache/codex-scratch/signin-d311/target/infraternal-openssl-avx512-gcm-test'. I am tracing that helper and will retry the shim test after the gate process has exited. Full log: /home/claude/.cache/codex-scratch/signin-d311/evidence/verify-fast-final.log; exit file contains 1.

ARM `scripts/verify fast` attempt exited 1. Summary from the gate log: ``` | test | FAIL (101) | | static builds | FAIL (1) | ``` The test row records the shim `WouldBlock` described above. The static-build row ends with Python `OSError: [Errno 8] Exec format error: '/home/claude/.cache/codex-scratch/signin-d311/target/infraternal-openssl-avx512-gcm-test'`. I am tracing that helper and will retry the shim test after the gate process has exited. Full log: `/home/claude/.cache/codex-scratch/signin-d311/evidence/verify-fast-final.log`; exit file contains `1`.
Author
Owner

Static-row root cause confirmed: the ARM runner is aarch64 (uname -m), while scripts/test-openssl-avx512-gcm.py explicitly compiles infraternal-openssl-avx512-gcm-test for x86_64-linux-musl and then executes it directly. readelf -h confirms the failed file is Advanced Micro Devices X86-64. scripts/verify.d/rust.sh, the helper, and its C source are identical to origin/dev (git diff --quiet origin/dev...HEAD), so this is present on the unmodified base as well. The x86-specific helper needs a native x86 runner; the requested compute01 size row will run there. I have not changed the unrelated gate scripts.

Static-row root cause confirmed: the ARM runner is aarch64 (`uname -m`), while `scripts/test-openssl-avx512-gcm.py` explicitly compiles `infraternal-openssl-avx512-gcm-test` for `x86_64-linux-musl` and then executes it directly. `readelf -h` confirms the failed file is `Advanced Micro Devices X86-64`. `scripts/verify.d/rust.sh`, the helper, and its C source are identical to `origin/dev` (`git diff --quiet origin/dev...HEAD`), so this is present on the unmodified base as well. The x86-specific helper needs a native x86 runner; the requested compute01 size row will run there. I have not changed the unrelated gate scripts.
Author
Owner

Correction to my previous gate comments: I compared origin/dev...HEAD, which only shows changes since the merge base, and mistakenly described those files as identical to current origin/dev. Current origin/dev is 57491e918c91fbf44dd5b318da168e1b4ef40310, while J6 is based on 42ab330358eb06d47acade8d0f2c35b9225af74b. Current dev includes a shim test change that resolves the editor path before entering the login shell, and updates scripts/verify.d/rust.sh to skip the x86 crypto helper on ARM. The reported ARM static failure came from running the older gate source on aarch64; it is addressed upstream. The isolated shim run is still on the old branch. After it exits, I will rebase J6 onto 57491e9, push the updated branch, and rerun verification. I retract the earlier claim that those failures were present on current unmodified origin/dev.

Correction to my previous gate comments: I compared `origin/dev...HEAD`, which only shows changes since the merge base, and mistakenly described those files as identical to current `origin/dev`. Current `origin/dev` is `57491e918c91fbf44dd5b318da168e1b4ef40310`, while J6 is based on `42ab330358eb06d47acade8d0f2c35b9225af74b`. Current dev includes a shim test change that resolves the editor path before entering the login shell, and updates `scripts/verify.d/rust.sh` to skip the x86 crypto helper on ARM. The reported ARM static failure came from running the older gate source on aarch64; it is addressed upstream. The isolated shim run is still on the old branch. After it exits, I will rebase J6 onto `57491e9`, push the updated branch, and rerun verification. I retract the earlier claim that those failures were present on current unmodified `origin/dev`.
Author
Owner

Base correction and rebase: after confirming origin/dev had advanced, I rebased signin-d311 from 42ab330358eb06d47acade8d0f2c35b9225af74b onto current origin/dev 57491e918c91fbf44dd5b318da168e1b4ef40310. The pushed head is now aab3560f1 (force-with-lease on this job branch only). The updated base includes the ARM guard for the x86 AVX-512 helper and the shim editor-path fix. The previous ARM gate and isolated shim retry were on the old base; I am updating the ARM worktree and will rerun scripts/verify fast there.

Base correction and rebase: after confirming `origin/dev` had advanced, I rebased `signin-d311` from `42ab330358eb06d47acade8d0f2c35b9225af74b` onto current `origin/dev` `57491e918c91fbf44dd5b318da168e1b4ef40310`. The pushed head is now `aab3560f1` (force-with-lease on this job branch only). The updated base includes the ARM guard for the x86 AVX-512 helper and the shim editor-path fix. The previous ARM gate and isolated shim retry were on the old base; I am updating the ARM worktree and will rerun `scripts/verify fast` there.
Author
Owner

Shim finding update: after rebasing onto current origin/dev and running inside the pinned flake shell, the ARM fast gate's shim integration batch passed 24/24, including ring_wrap_snapshot_repaints_real_vim. The old-base isolated retry had reproduced the timeout at shim.rs:276; that failure is absent on the updated base. Current full gate is still running; evidence: /home/claude/.cache/codex-scratch/signin-d311/evidence/verify-fast-rebased2.log.

Shim finding update: after rebasing onto current `origin/dev` and running inside the pinned flake shell, the ARM fast gate's shim integration batch passed 24/24, including `ring_wrap_snapshot_repaints_real_vim`. The old-base isolated retry had reproduced the timeout at `shim.rs:276`; that failure is absent on the updated base. Current full gate is still running; evidence: `/home/claude/.cache/codex-scratch/signin-d311/evidence/verify-fast-rebased2.log`.
Author
Owner

Gate finding: on the rebased ARM run, infraternal-plugin-ssh's streaming::decoded_image_and_hash_advance_before_the_transfer_tail_is_released failed while the install integration binary ran (92 passed, 1 failed, 2 ignored). The assertion reports hashed=true staged_bytes=Ok(552579) at crates/plugins/ssh/tests/install/streaming.rs:119. J6 changes no crates/plugins/ssh files. I will rerun this test in isolation after the current gate exits and record whether it reproduces. Evidence: /home/claude/.cache/codex-scratch/signin-d311/evidence/verify-fast-rebased2.log.

Gate finding: on the rebased ARM run, `infraternal-plugin-ssh`'s `streaming::decoded_image_and_hash_advance_before_the_transfer_tail_is_released` failed while the `install` integration binary ran (92 passed, 1 failed, 2 ignored). The assertion reports `hashed=true staged_bytes=Ok(552579)` at `crates/plugins/ssh/tests/install/streaming.rs:119`. J6 changes no `crates/plugins/ssh` files. I will rerun this test in isolation after the current gate exits and record whether it reproduces. Evidence: `/home/claude/.cache/codex-scratch/signin-d311/evidence/verify-fast-rebased2.log`.
Author
Owner

Baseline proof: I reset the ARM verification worktree to the exact current origin/dev head 57491e918c91fbf44dd5b318da168e1b4ef40310 and reran only streaming::decoded_image_and_hash_advance_before_the_transfer_tail_is_released under the same pinned Nix shell, short state roots, and warm target. It failed identically: exit 101, 10.10 s, hashed=true staged_bytes=Ok(552579). Evidence: /home/claude/.cache/codex-scratch/signin-d311/evidence/ssh-stream-origin-dev.log and .exit (101). The J6 run reports the same single test failure; the other fast-gate rows pass, and ARM correctly skips x86 static builds. I restored the ARM worktree to pushed head aab3560f1.

Baseline proof: I reset the ARM verification worktree to the exact current `origin/dev` head `57491e918c91fbf44dd5b318da168e1b4ef40310` and reran only `streaming::decoded_image_and_hash_advance_before_the_transfer_tail_is_released` under the same pinned Nix shell, short state roots, and warm target. It failed identically: exit 101, 10.10 s, `hashed=true staged_bytes=Ok(552579)`. Evidence: `/home/claude/.cache/codex-scratch/signin-d311/evidence/ssh-stream-origin-dev.log` and `.exit` (`101`). The J6 run reports the same single test failure; the other fast-gate rows pass, and ARM correctly skips x86 static builds. I restored the ARM worktree to pushed head `aab3560f1`.
Author
Owner

Compute01 D243 update: the pre-rebase row finished before I updated the VM worktree. It measured 21,522,384 bytes against the 21,431,472-byte x86 cap (delta: +90912 bytes), so it is over cap and above the 64 KiB stop/report threshold. The worktree is now clean at pushed head aab3560f1; the final row is queued through build-slot under the current flake shell. Log: /home/claude/.cache/codex-scratch/j6-x86-d243-final.log. I will report its exact head measurement and stop implementation changes if it remains above 64 KiB.

Compute01 D243 update: the pre-rebase row finished before I updated the VM worktree. It measured 21,522,384 bytes against the 21,431,472-byte x86 cap (`delta: +90912 bytes`), so it is over cap and above the 64 KiB stop/report threshold. The worktree is now clean at pushed head `aab3560f1`; the final row is queued through `build-slot` under the current flake shell. Log: `/home/claude/.cache/codex-scratch/j6-x86-d243-final.log`. I will report its exact head measurement and stop implementation changes if it remains above 64 KiB.
Author
Owner

J6 finish

Branch signin-d311 is pushed at aab3560f1f60dfc024d83fb7f9262b1968849e32, rebased onto current origin/dev 57491e918c91fbf44dd5b318da168e1b4ef40310.

Slice 1 implements infraternal sign-in <server> with a browser link and one-time device code, passkey-required approval, polling and redemption, private atomic credential storage, and actionable D303 outcomes. Poll tokens and API keys stay out of argv, logs, terminal output, and JSON. It reuses the existing user-scoped key-list/revoke endpoints. QR output and account UI key-management polish remain follow-on work; the QR renderer stays an owner choice pending the planned size/runtime measurements.

Auth review of the final timeout-classification change found no actionable issue. The production tracer Playwright suite passed 5/5, with six light/dark desktop and phone screenshots attached above. The focused ARM run passed all 12 sign_in::tests; branch-head Clippy and formatting passed.

ARM scripts/verify fast ran on the rebased head. Its sole failing row is the SSH streaming test below; I reran that test on exact unmodified origin/dev 57491e918 under the same shell and it failed identically (exit 101, 10.10 s, hashed=true staged_bytes=Ok(552579)). All other rows passed; the ARM static-build row is intentionally skipped by the updated gate.

Gate output, verbatim:

+---------------------------+--------------+
| Rust gate                 | Result       |
+---------------------------+--------------+
| fmt                       | PASS         |
| VT native fmt             | PASS         |
| CLI plugin compile-out    | PASS         |
| clippy                    | PASS         |
| SSH runtime test helper   | PASS         |
| real-binary fixture paths | PASS         |
| test                      | FAIL (101)   |
| SSH inode reuse fixture   | PASS         |
| shim process-per-user     | PASS         |
| cargo-deny                | PASS         |
| gate architecture         | PASS         |
| static builds             | SKIP (x86)   |
+---------------------------+--------------+

Compute01 static-size row at this head:

/home/claude/worktrees/signin-d311/target/x86_64-unknown-linux-musl/release/infraternal: ELF 64-bit LSB pie executable, x86-64, version 1 (SYSV), static-pie linked, stripped
PT_INTERP: absent; size: 21522480 bytes; cap: 21431472 bytes; delta: +91008 bytes
binary exceeds the 21431472-byte size budget: 21522480 bytes

The +91,008-byte D243 delta exceeds the 64 KiB stop/report threshold. I stopped implementation changes here; owner direction is needed on the size budget or a size-reduction follow-up. The ARM and compute01 build targets and temporary runners are cleaned up; evidence logs and issue screenshots are retained. Issue remains open.

## J6 finish Branch `signin-d311` is pushed at `aab3560f1f60dfc024d83fb7f9262b1968849e32`, rebased onto current `origin/dev` `57491e918c91fbf44dd5b318da168e1b4ef40310`. Slice 1 implements `infraternal sign-in <server>` with a browser link and one-time device code, passkey-required approval, polling and redemption, private atomic credential storage, and actionable D303 outcomes. Poll tokens and API keys stay out of argv, logs, terminal output, and JSON. It reuses the existing user-scoped key-list/revoke endpoints. QR output and account UI key-management polish remain follow-on work; the QR renderer stays an owner choice pending the planned size/runtime measurements. Auth review of the final timeout-classification change found no actionable issue. The production tracer Playwright suite passed 5/5, with six light/dark desktop and phone screenshots attached above. The focused ARM run passed all 12 `sign_in::tests`; branch-head Clippy and formatting passed. ARM `scripts/verify fast` ran on the rebased head. Its sole failing row is the SSH streaming test below; I reran that test on exact unmodified `origin/dev` `57491e918` under the same shell and it failed identically (exit 101, 10.10 s, `hashed=true staged_bytes=Ok(552579)`). All other rows passed; the ARM static-build row is intentionally skipped by the updated gate. Gate output, verbatim: ``` +---------------------------+--------------+ | Rust gate | Result | +---------------------------+--------------+ | fmt | PASS | | VT native fmt | PASS | | CLI plugin compile-out | PASS | | clippy | PASS | | SSH runtime test helper | PASS | | real-binary fixture paths | PASS | | test | FAIL (101) | | SSH inode reuse fixture | PASS | | shim process-per-user | PASS | | cargo-deny | PASS | | gate architecture | PASS | | static builds | SKIP (x86) | +---------------------------+--------------+ ``` Compute01 static-size row at this head: ``` /home/claude/worktrees/signin-d311/target/x86_64-unknown-linux-musl/release/infraternal: ELF 64-bit LSB pie executable, x86-64, version 1 (SYSV), static-pie linked, stripped PT_INTERP: absent; size: 21522480 bytes; cap: 21431472 bytes; delta: +91008 bytes binary exceeds the 21431472-byte size budget: 21522480 bytes ``` The +91,008-byte D243 delta exceeds the 64 KiB stop/report threshold. I stopped implementation changes here; owner direction is needed on the size budget or a size-reduction follow-up. The ARM and compute01 build targets and temporary runners are cleaned up; evidence logs and issue screenshots are retained. Issue remains open.
Author
Owner

Starting independent security review on branch review-signin-269, based at target aab3560f1 and comparing with origin/dev 57491e918. I am reviewing the fixed slice-1 snapshot while J36 reduces the branch; tests/findings will be recorded on the review branch. No auth implementation edits will be made to signin-d311.

Starting independent security review on branch `review-signin-269`, based at target `aab3560f1` and comparing with `origin/dev` `57491e918`. I am reviewing the fixed slice-1 snapshot while J36 reduces the branch; tests/findings will be recorded on the review branch. No auth implementation edits will be made to `signin-d311`.
Author
Owner

Starting J36 size reduction on branch signin-d311 at aab3560f1f60dfc024d83fb7f9262b1968849e32, based on origin/dev 57491e918c91fbf44dd5b318da168e1b4ef40310. I’m reproducing static release sizes on compute01 and dev-02 by architecture, then will identify dependency and symbol deltas before making the smallest safe reduction.

Starting J36 size reduction on branch `signin-d311` at `aab3560f1f60dfc024d83fb7f9262b1968849e32`, based on `origin/dev` `57491e918c91fbf44dd5b318da168e1b4ef40310`. I’m reproducing static release sizes on compute01 and dev-02 by architecture, then will identify dependency and symbol deltas before making the smallest safe reduction.
Author
Owner

Finding — the initial cross-slice Cargo diff shows no common new dependency activation. cli-sessions-d314 and mesh-pins-258-v2 change no Cargo manifests or lockfile; signin-d311 adds only a direct zeroize edge to the already-enabled remote feature, with no crate version added. The existing hyper, OpenSSL, URL and serde_json dependencies were already present on this branch base. This rules out a shared newly-enabled HTTP/TLS/serde/clap crate as the common cause; per-symbol attribution is still running to check codegen and feature-code costs. Evidence: git diff origin/dev...<branch> -- Cargo.toml crates/infraternal/Cargo.toml Cargo.lock on the sessions, mesh-pins and sign-in worktrees.

Finding — the initial cross-slice Cargo diff shows no common new dependency activation. `cli-sessions-d314` and `mesh-pins-258-v2` change no Cargo manifests or lockfile; `signin-d311` adds only a direct `zeroize` edge to the already-enabled `remote` feature, with no crate version added. The existing `hyper`, OpenSSL, URL and serde_json dependencies were already present on this branch base. This rules out a shared newly-enabled HTTP/TLS/serde/clap crate as the common cause; per-symbol attribution is still running to check codegen and feature-code costs. Evidence: `git diff origin/dev...<branch> -- Cargo.toml crates/infraternal/Cargo.toml Cargo.lock` on the sessions, mesh-pins and sign-in worktrees.
Author
Owner

P1 — The approval page never displays the CLI's one-time user code, so the person cannot confirm that the link belongs to the request they started.

Evidence: apps/tracer/src/device-approval.on-demand.ts:54-75 receives code but renders only the device, scopes, warning and approval button; the value is used later for device-approve at line 101. The production tracer Playwright suite on infraternal-dev-02-std ran 7 approval specs: 6 passed and the new regression approval page shows the code so a person can match it with the CLI failed at line 108 because AbCdEfGhIjKlMnOp was not visible. The six dark/light desktop and phone screenshots attached here show the same missing comparison.

Attack: anyone can create a request with a plausible device label, keep its 256-bit poll token, and send the valid browser link to a person signed in to the legitimate server. The page shows the attacker-chosen label and scopes, then offers fresh passkey approval, but no code to compare with the CLI's terminal. If approved, the attacker redeems the request and receives the victim's scoped API key. Fresh passkey verification does not stop this device-code phishing path.

The regression is committed on review-signin-269 as df544521fde090e1472fe4acf87df326aaef3603. J36's size reduction must preserve both the CLI-to-page code comparison and this regression test.

P1 — The approval page never displays the CLI's one-time user code, so the person cannot confirm that the link belongs to the request they started. Evidence: `apps/tracer/src/device-approval.on-demand.ts:54-75` receives `code` but renders only the device, scopes, warning and approval button; the value is used later for `device-approve` at line 101. The production tracer Playwright suite on infraternal-dev-02-std ran 7 approval specs: 6 passed and the new regression `approval page shows the code so a person can match it with the CLI` failed at line 108 because `AbCdEfGhIjKlMnOp` was not visible. The six dark/light desktop and phone screenshots attached here show the same missing comparison. Attack: anyone can create a request with a plausible device label, keep its 256-bit poll token, and send the valid browser link to a person signed in to the legitimate server. The page shows the attacker-chosen label and scopes, then offers fresh passkey approval, but no code to compare with the CLI's terminal. If approved, the attacker redeems the request and receives the victim's scoped API key. Fresh passkey verification does not stop this device-code phishing path. The regression is committed on `review-signin-269` as `df544521fde090e1472fe4acf87df326aaef3603`. J36's size reduction must preserve both the CLI-to-page code comparison and this regression test.
Author
Owner

J36 progress: ARM64 static release base/head measured through build-slot at 19,601,832 B and 19,679,960 B (+78,128 B). The nm diff attributes +60,460 B to code symbols (+53,500 B added, +7,064 B net size changes) and +7,924 B to read-only data. Largest added code symbols are sign_in::run_flow (16,796 B), sign_in::post_json_with_timeout (12,332 B), and CredentialFile deserialization (5,340 B). The sign-in code duplicates the TLS/Hyper path already in remote join; no new HTTP/TLS dependency was added. I am testing a shared byte-based transport that preserves the existing TLS and bounded-body policy. The dev-02 test queue hit build-slot's disk guard: 10 GB free, J36 target 3.4 GB; I canceled that waiting process and will run the required gate on compute01. ARM remeasurement still needs free capacity.

J36 progress: ARM64 static release base/head measured through build-slot at 19,601,832 B and 19,679,960 B (+78,128 B). The nm diff attributes +60,460 B to code symbols (+53,500 B added, +7,064 B net size changes) and +7,924 B to read-only data. Largest added code symbols are sign_in::run_flow (16,796 B), sign_in::post_json_with_timeout (12,332 B), and CredentialFile deserialization (5,340 B). The sign-in code duplicates the TLS/Hyper path already in remote join; no new HTTP/TLS dependency was added. I am testing a shared byte-based transport that preserves the existing TLS and bounded-body policy. The dev-02 test queue hit build-slot's disk guard: 10 GB free, J36 target 3.4 GB; I canceled that waiting process and will run the required gate on compute01. ARM remeasurement still needs free capacity.
Author
Owner

Cross-slice cause check: the manifests show no common dependency edge. cli-sessions-d314 and mesh-pins-258-v2 change no Cargo manifests or lockfile; D311 adds only a direct zeroize edge under the existing remote feature. Neither diff adds a clap Parser derive, and mesh-pins does not touch the CLI dispatch path. ARM nm attribution for D311 shows +48,056 B of infraternal::sign_in code and +9,980 B under serde_json / +6,632 B under serde_core; it adds no code symbols under clap, webauthn, openssl, or tokio_openssl. So type-specific JSON code is a recurring component, but there is no single shared dependency or client path explaining all three deltas. The sign-in-specific cost is its new flow and duplicate outbound TLS/Hyper path.

Cross-slice cause check: the manifests show no common dependency edge. `cli-sessions-d314` and `mesh-pins-258-v2` change no Cargo manifests or lockfile; D311 adds only a direct `zeroize` edge under the existing `remote` feature. Neither diff adds a clap `Parser` derive, and mesh-pins does not touch the CLI dispatch path. ARM nm attribution for D311 shows +48,056 B of `infraternal::sign_in` code and +9,980 B under `serde_json` / +6,632 B under `serde_core`; it adds no code symbols under `clap`, `webauthn`, `openssl`, or `tokio_openssl`. So type-specific JSON code is a recurring component, but there is no single shared dependency or client path explaining all three deltas. The sign-in-specific cost is its new flow and duplicate outbound TLS/Hyper path.
Author
Owner

Addendum to the ARM attribution: the earlier infraternal::sign_in / serde_* substring counts overlap because generic symbol names contain both the owning crate and type parameters. Non-overlapping nm code-symbol owner buckets are: 53,500 B added, 104 B removed, and +7,064 B net size changes to existing symbols; added bytes by owner are infraternal 32,096 B, serde_core 6,632 B, core 5,868 B, tokio 5,064 B, serde_json 2,360 B, url 1,012 B, then smaller standard-library helpers. The two largest new functions remain sign_in::run_flow (16,796 B) and sign_in::post_json_with_timeout (12,332 B).

Addendum to the ARM attribution: the earlier `infraternal::sign_in` / `serde_*` substring counts overlap because generic symbol names contain both the owning crate and type parameters. Non-overlapping nm code-symbol owner buckets are: 53,500 B added, 104 B removed, and +7,064 B net size changes to existing symbols; added bytes by owner are infraternal 32,096 B, serde_core 6,632 B, core 5,868 B, tokio 5,064 B, serde_json 2,360 B, url 1,012 B, then smaller standard-library helpers. The two largest new functions remain `sign_in::run_flow` (16,796 B) and `sign_in::post_json_with_timeout` (12,332 B).
Author
Owner

J36 WIP checkpoint: pushed signin-d311 at ed46f3644. The change shares the byte-based HTTPS POST path between sign-in and remote join, retaining TLS1.3 peer verification, bounded bodies, zeroization, and D303 request-start classification. The x86 base/head attribution run is still linking its pinned aab3560f1 measurement. Next: build/test the new head, run scripts/verify fast on compute01, and remeasure x86/ARM; dev-02 remains below its disk guard (9.6 GB free at last check).

J36 WIP checkpoint: pushed `signin-d311` at ed46f3644. The change shares the byte-based HTTPS POST path between sign-in and remote join, retaining TLS1.3 peer verification, bounded bodies, zeroization, and D303 request-start classification. The x86 base/head attribution run is still linking its pinned `aab3560f1` measurement. Next: build/test the new head, run `scripts/verify fast` on compute01, and remeasure x86/ARM; dev-02 remains below its disk guard (9.6 GB free at last check).
Author
Owner

P2 — Credential file operations are not bound to the directory inode that was locked.

Evidence: crates/infraternal/src/sign_in.rs:635-648 holds lock_private_directory(directory) and then calls read_credential_file(directory) and replace_file_atomically(&path, ...) by pathname. lock_private_directory opens the directory with O_NOFOLLOW|O_DIRECTORY at line 652, but later read_credential_file applies O_NOFOLLOW only to the leaf credential file at line 687; a symlink in the directory path is still followed. A local writer to a misconfigured/shared XDG_CONFIG_HOME parent can rename the locked directory and replace it with a symlink between those path operations, redirecting the subsequent read/write away from the inode protected by the lock.

The deterministic regression on review-signin-269 is credential_read_refuses_a_private_directory_replaced_after_locking at line 98. On infraternal-dev-02-std it failed because the read accepted the replacement directory; the focused sign-in suite reported 12 passed, 1 failed. The static final-file symlink test still passes, so the uncovered case is directory replacement after the lock is acquired.

Keep read/replace operations relative to the already-open directory handle (or otherwise verify and preserve that inode across the whole transaction), not only the final file component.

P2 — Credential file operations are not bound to the directory inode that was locked. Evidence: `crates/infraternal/src/sign_in.rs:635-648` holds `lock_private_directory(directory)` and then calls `read_credential_file(directory)` and `replace_file_atomically(&path, ...)` by pathname. `lock_private_directory` opens the directory with `O_NOFOLLOW|O_DIRECTORY` at line 652, but later `read_credential_file` applies `O_NOFOLLOW` only to the leaf credential file at line 687; a symlink in the directory path is still followed. A local writer to a misconfigured/shared `XDG_CONFIG_HOME` parent can rename the locked directory and replace it with a symlink between those path operations, redirecting the subsequent read/write away from the inode protected by the lock. The deterministic regression on `review-signin-269` is `credential_read_refuses_a_private_directory_replaced_after_locking` at line 98. On infraternal-dev-02-std it failed because the read accepted the replacement directory; the focused sign-in suite reported 12 passed, 1 failed. The static final-file symlink test still passes, so the uncovered case is directory replacement after the lock is acquired. Keep read/replace operations relative to the already-open directory handle (or otherwise verify and preserve that inode across the whole transaction), not only the final file component.
Author
Owner

P2 residual risk / policy gap — CLI API keys have no expiry.

Evidence: crates/infraternal-server/migrations/0001_auth.sql:10 defines api_keys without an expiry; crates/infraternal-server/src/auth/devices.rs:88-110 inserts the key and authorizes it without a clock or expiry check. Only explicit api_revoke at lines 113-121 removes it. A copied credential therefore remains valid indefinitely until a fresh-passkey revoke. The issue plan defers the Account UI for listing/revoking CLI keys to slice 3, so this slice does not expose that recovery path in the app.

D311 calls the laptop credential revocable but does not set a TTL. I did not add a failing expiry test because that would invent an owner policy. Please record whether indefinite validity is intended, or define a maximum lifetime/rotation rule and make revocation discoverable before wider release. This is a risk to accept or schedule, not an implementation mismatch against an existing TTL requirement.

P2 residual risk / policy gap — CLI API keys have no expiry. Evidence: `crates/infraternal-server/migrations/0001_auth.sql:10` defines `api_keys` without an expiry; `crates/infraternal-server/src/auth/devices.rs:88-110` inserts the key and authorizes it without a clock or expiry check. Only explicit `api_revoke` at lines 113-121 removes it. A copied credential therefore remains valid indefinitely until a fresh-passkey revoke. The issue plan defers the Account UI for listing/revoking CLI keys to slice 3, so this slice does not expose that recovery path in the app. D311 calls the laptop credential revocable but does not set a TTL. I did not add a failing expiry test because that would invent an owner policy. Please record whether indefinite validity is intended, or define a maximum lifetime/rotation rule and make revocation discoverable before wider release. This is a risk to accept or schedule, not an implementation mismatch against an existing TTL requirement.
Author
Owner

J36 update: rustfmt output for the shared outbound HTTPS path is now committed and pushed as bf16c2f31. The pinned x86 base/head attribution build is still compiling under build-slot; log: /home/claude/.cache/codex-scratch/j36-signin/x86_64-unknown-linux-musl/measure.log. Next: finish attribution, then run the focused sign-in and tracer approval suites plus scripts/verify fast on compute01, followed by a candidate-head size measurement. ARM remeasurement remains gated by dev-02 disk capacity. The size change keeps the existing one-time code and poll-token handling intact.

J36 update: rustfmt output for the shared outbound HTTPS path is now committed and pushed as bf16c2f31. The pinned x86 base/head attribution build is still compiling under build-slot; log: /home/claude/.cache/codex-scratch/j36-signin/x86_64-unknown-linux-musl/measure.log. Next: finish attribution, then run the focused sign-in and tracer approval suites plus scripts/verify fast on compute01, followed by a candidate-head size measurement. ARM remeasurement remains gated by dev-02 disk capacity. The size change keeps the existing one-time code and poll-token handling intact.
Author
Owner

Finding: the pinned x86_64 static release build reproduced the D243 base exactly: 21,431,472 B at 57491e918c. The matching aab3560f1 head build is still compiling, so no new candidate delta is claimed yet. Evidence/log: /home/claude/.cache/codex-scratch/j36-signin/x86_64-unknown-linux-musl/measure.log.

Finding: the pinned x86_64 static release build reproduced the D243 base exactly: 21,431,472 B at 57491e918c91fbf44dd5b318da168e1b4ef40310. The matching aab3560f1 head build is still compiling, so no new candidate delta is claimed yet. Evidence/log: /home/claude/.cache/codex-scratch/j36-signin/x86_64-unknown-linux-musl/measure.log.
Author
Owner

J37 complete — verdict: fix first

Reviewed signin-d311 at aab3560f1f60dfc024d83fb7f9262b1968849e32 against origin/dev 57491e918c91fbf44dd5b318da168e1b4ef40310. Review branch review-signin-269 is at e53ab2f7c203144f97413762518eb161dcb7d47f (two test-only commits; no auth implementation edits).

Findings are detailed above: P1 approval page omits the CLI user code (comment 71780); P2 credential operations follow a replaceable directory pathname after locking (comment 71818); P2 policy risk that API keys have no expiry and persist until explicit revocation (comment 71822). The P1 code-comparison failure is a device-code phishing path, so this slice is fix first.

Verification on claude@infraternal-dev-02-std through ~/infraternal-ops/build-slot: the focused sign-in suite had 12 passed and the new directory-identity regression failed; the production tracer Playwright suite had 6 passed and the new approval-code visibility regression failed. The two server auth tests passed 2/2. The six dark/light desktop and phone screenshots are attached to comment 71780. Of the six Rust test-gate failures below, five restart timeouts also fail on exact origin/dev; the remaining failure is the new storage regression. Static builds were skipped on this aarch64 VM.

Verbatim final scripts/verify fast summary (exit status 1):

+---------------------------+--------------+
| Rust gate                 | Result       |
+---------------------------+--------------+
| fmt                       | PASS         |
| VT native fmt             | PASS         |
| CLI plugin compile-out    | PASS         |
| clippy                    | PASS         |
| SSH runtime test helper   | PASS         |
| real-binary fixture paths | PASS         |
| test                      | FAIL (101)   |
| SSH inode reuse fixture   | PASS         |
| shim process-per-user     | PASS         |
| cargo-deny                | PASS         |
| gate architecture         | PASS         |
| static builds             | SKIP (x86)   |
+---------------------------+--------------+

The Rust test phase reported 113 passed; 6 failed; 1 ignored. Full output is retained at /home/kayg/.cache/codex-scratch/review-signin-269/verify-fast-final.remote.log.

J36: preserve the CLI-to-browser code comparison and both regressions: approval page shows the code so a person can match it with the CLI and credential_read_refuses_a_private_directory_replaced_after_locking.

## J37 complete — verdict: fix first Reviewed `signin-d311` at `aab3560f1f60dfc024d83fb7f9262b1968849e32` against `origin/dev` `57491e918c91fbf44dd5b318da168e1b4ef40310`. Review branch `review-signin-269` is at `e53ab2f7c203144f97413762518eb161dcb7d47f` (two test-only commits; no auth implementation edits). Findings are detailed above: P1 approval page omits the CLI user code (comment 71780); P2 credential operations follow a replaceable directory pathname after locking (comment 71818); P2 policy risk that API keys have no expiry and persist until explicit revocation (comment 71822). The P1 code-comparison failure is a device-code phishing path, so this slice is **fix first**. Verification on `claude@infraternal-dev-02-std` through `~/infraternal-ops/build-slot`: the focused sign-in suite had 12 passed and the new directory-identity regression failed; the production tracer Playwright suite had 6 passed and the new approval-code visibility regression failed. The two server auth tests passed 2/2. The six dark/light desktop and phone screenshots are attached to comment 71780. Of the six Rust test-gate failures below, five restart timeouts also fail on exact `origin/dev`; the remaining failure is the new storage regression. Static builds were skipped on this aarch64 VM. Verbatim final `scripts/verify fast` summary (exit status 1): ```text +---------------------------+--------------+ | Rust gate | Result | +---------------------------+--------------+ | fmt | PASS | | VT native fmt | PASS | | CLI plugin compile-out | PASS | | clippy | PASS | | SSH runtime test helper | PASS | | real-binary fixture paths | PASS | | test | FAIL (101) | | SSH inode reuse fixture | PASS | | shim process-per-user | PASS | | cargo-deny | PASS | | gate architecture | PASS | | static builds | SKIP (x86) | +---------------------------+--------------+ ``` The Rust test phase reported `113 passed; 6 failed; 1 ignored`. Full output is retained at `/home/kayg/.cache/codex-scratch/review-signin-269/verify-fast-final.remote.log`. J36: preserve the CLI-to-browser code comparison and both regressions: `approval page shows the code so a person can match it with the CLI` and `credential_read_refuses_a_private_directory_replaced_after_locking`.
Author
Owner

J37 P1 red result: the production tracer regression failed as expected on dev-02 because the approval page did not render the exact CLI code; Playwright reports getByText("AbCdEfGhIjKlMnOp", { exact: true }) not found. The same test now also asserts the passkey button stays disabled until the person checks the code-match box. Log: /home/claude/.cache/codex-scratch/j36-signin/arm-p1-red.log.

J37 P1 red result: the production tracer regression failed as expected on dev-02 because the approval page did not render the exact CLI code; Playwright reports `getByText("AbCdEfGhIjKlMnOp", { exact: true })` not found. The same test now also asserts the passkey button stays disabled until the person checks the code-match box. Log: /home/claude/.cache/codex-scratch/j36-signin/arm-p1-red.log.
Author
Owner

J36 P2 failing-first reproduction: on infraternal-dev-02-std (aarch64), the J37 regression credential_read_refuses_a_private_directory_replaced_after_locking failed against bf16c2f31 with 0 passed and 1 failed at “credential reads must stay bound to the directory inode that was locked” (exit 101). Log: /home/claude/.cache/codex-scratch/j36-signin/evidence/p2-red.log. I am implementing directory-fd-relative reads and atomic replacement now; the J37 test is in the signin worktree and will be included with the fix.

J36 P2 failing-first reproduction: on `infraternal-dev-02-std` (aarch64), the J37 regression `credential_read_refuses_a_private_directory_replaced_after_locking` failed against `bf16c2f31` with 0 passed and 1 failed at “credential reads must stay bound to the directory inode that was locked” (exit 101). Log: `/home/claude/.cache/codex-scratch/j36-signin/evidence/p2-red.log`. I am implementing directory-fd-relative reads and atomic replacement now; the J37 test is in the signin worktree and will be included with the fix.
Author
Owner

J36 continuation: resuming on branch signin-d311 from bf16c2f31, based on origin/dev 57491e918c91fbf44dd5b318da168e1b4ef40310. I am preserving both J37 regressions and will keep the approval confirmation and directory-fd repairs in separate commits from the size work. I will not change API-key lifetime because that remains an owner policy question.

J36 continuation: resuming on branch `signin-d311` from `bf16c2f31`, based on `origin/dev` `57491e918c91fbf44dd5b318da168e1b4ef40310`. I am preserving both J37 regressions and will keep the approval confirmation and directory-fd repairs in separate commits from the size work. I will not change API-key lifetime because that remains an owner policy question.
Author
Owner

J37 P1 fix: 1e66039c0 shows the exact one-time CLI code on the approval page and keeps Approve disabled until the person checks that the terminal code matches. The click handler also refuses to start passkey approval without that confirmation. The preserved production tracer approval spec passed 7/7 on infraternal-dev-02 through build-slot (36.7s), including code visibility, confirmation state, cancellation, and a successful virtual-passkey approval. Log: /home/claude/.cache/codex-scratch/j36-signin/evidence/playwright-p1-green2.log. I will attach fresh production screenshots after the requested compute01 run.

J37 P1 fix: `1e66039c0` shows the exact one-time CLI code on the approval page and keeps Approve disabled until the person checks that the terminal code matches. The click handler also refuses to start passkey approval without that confirmation. The preserved production tracer approval spec passed 7/7 on infraternal-dev-02 through build-slot (36.7s), including code visibility, confirmation state, cancellation, and a successful virtual-passkey approval. Log: `/home/claude/.cache/codex-scratch/j36-signin/evidence/playwright-p1-green2.log`. I will attach fresh production screenshots after the requested compute01 run.
Author
Owner

J37 P2 directory-swap fix: 27b406a19 carries the locked directory descriptor through the credential transaction. It verifies the opened directory owner, 0700 mode, and inode identity; reads and temporary-file creation use openat with O_NOFOLLOW, replacement uses renameat, and sync uses the same descriptor. The preserved J37 regression now also uses a private-mode replacement and checks that both a redirected read and write are refused without changing the replacement file. Fresh focused run on infraternal-dev-02 through build-slot: 1 passed, 0 failed; log /home/claude/.cache/codex-scratch/j36-signin/evidence/p2-strengthened.log. I did not change API-key lifetime; its TTL remains an owner policy question.

J37 P2 directory-swap fix: `27b406a19` carries the locked directory descriptor through the credential transaction. It verifies the opened directory owner, 0700 mode, and inode identity; reads and temporary-file creation use `openat` with `O_NOFOLLOW`, replacement uses `renameat`, and sync uses the same descriptor. The preserved J37 regression now also uses a private-mode replacement and checks that both a redirected read and write are refused without changing the replacement file. Fresh focused run on infraternal-dev-02 through build-slot: 1 passed, 0 failed; log `/home/claude/.cache/codex-scratch/j36-signin/evidence/p2-strengthened.log`. I did not change API-key lifetime; its TTL remains an owner policy question.
Author
Owner

J36 size-reduction progress on signin-d311 @ 27b406a19.

The static aarch64 release artifact measured 19,666,104 B at the current head versus 19,601,832 B at base 57491e918: +64,272 B. That is 60,208 B over the current D243 cap (19,605,896 B), or 1,264 B below the 64 KiB stop. The pre-refactor aab3560f1 artifact measured 19,679,960 B, so the shared-transport head is 13,856 B smaller on aarch64. Per-crate and symbol attribution is running now.

Cross-slice scan so far: D311 adds no new package to Cargo.lock (the direct zeroize edge points to an already locked crate); D314 uses ArgMatches rather than a new Clap derive; mesh-pins changes no CLI code. I have not found a shared new Clap, HTTP/TLS, or WebAuthn dependency cause. I will include the linked-symbol evidence in the final breakdown.

Logs and artifacts are under /home/claude/.cache/codex-scratch/j36-signin/aarch64-unknown-linux-musl; the x86 current-head static build is queued through build-slot, with its log at /home/claude/.cache/codex-scratch/j36-signin/x86_64-unknown-linux-musl/build-head.log. Next: finish both-arch attribution, run the sign-in tests and tracer approval spec, then scripts/verify fast on compute01.

J36 size-reduction progress on `signin-d311` @ `27b406a19`. The static aarch64 release artifact measured 19,666,104 B at the current head versus 19,601,832 B at base `57491e918`: +64,272 B. That is 60,208 B over the current D243 cap (19,605,896 B), or 1,264 B below the 64 KiB stop. The pre-refactor `aab3560f1` artifact measured 19,679,960 B, so the shared-transport head is 13,856 B smaller on aarch64. Per-crate and symbol attribution is running now. Cross-slice scan so far: D311 adds no new package to `Cargo.lock` (the direct `zeroize` edge points to an already locked crate); D314 uses `ArgMatches` rather than a new Clap derive; mesh-pins changes no CLI code. I have not found a shared new Clap, HTTP/TLS, or WebAuthn dependency cause. I will include the linked-symbol evidence in the final breakdown. Logs and artifacts are under `/home/claude/.cache/codex-scratch/j36-signin/aarch64-unknown-linux-musl`; the x86 current-head static build is queued through build-slot, with its log at `/home/claude/.cache/codex-scratch/j36-signin/x86_64-unknown-linux-musl/build-head.log`. Next: finish both-arch attribution, run the sign-in tests and tracer approval spec, then `scripts/verify fast` on compute01.
Author
Owner

J36 security-fix results for J37 re-review. Branch signin-d311 is now at 189f41716.

P1, device-code phishing: 1e66039c0 displays the CLI user code prominently and disables passkey approval until the person checks that it matches the terminal. The tracer approval spec passed 7/7 on a production tracer build. This includes J37's exact-code/confirmation regression and the existing check that the code is removed from the URL before auth status is queried. Six dark/light desktop and phone screenshots are under /home/claude/.cache/codex-scratch/j36-signin/evidence/approval-final; I will attach them with the final size/gate update.

P2, credential-directory replacement: 27b406a19 holds and verifies the private directory fd through the locked read/replace section, with openat/renameat, O_NOFOLLOW, ownership/mode checks, and directory sync. Sign-in tests passed 13/13, including J37's directory-replacement test asserting both reads and writes stay out of a replacement directory.

API-key lifetime is unchanged. The tracer's strict typecheck initially exposed a narrowing error in the exact-scope validation; 189f41716 fixes it by validating a local binding without changing the allowlist check. bun run check now passes.

Final two-architecture size attribution and scripts/verify fast on compute01 are still pending; I will post those results separately.

J36 security-fix results for J37 re-review. Branch `signin-d311` is now at `189f41716`. **P1, device-code phishing:** `1e66039c0` displays the CLI user code prominently and disables passkey approval until the person checks that it matches the terminal. The tracer approval spec passed **7/7** on a production tracer build. This includes J37's exact-code/confirmation regression and the existing check that the code is removed from the URL before auth status is queried. Six dark/light desktop and phone screenshots are under `/home/claude/.cache/codex-scratch/j36-signin/evidence/approval-final`; I will attach them with the final size/gate update. **P2, credential-directory replacement:** `27b406a19` holds and verifies the private directory fd through the locked read/replace section, with `openat`/`renameat`, `O_NOFOLLOW`, ownership/mode checks, and directory sync. Sign-in tests passed **13/13**, including J37's directory-replacement test asserting both reads and writes stay out of a replacement directory. API-key lifetime is unchanged. The tracer's strict typecheck initially exposed a narrowing error in the exact-scope validation; `189f41716` fixes it by validating a local binding without changing the allowlist check. `bun run check` now passes. Final two-architecture size attribution and `scripts/verify fast` on compute01 are still pending; I will post those results separately.
Author
Owner

J36 progress: the x86 static head build remains queued on compute01 at position 9 behind four occupied slots (PID 1025693; log /home/claude/.cache/codex-scratch/j36-signin/x86_64-unknown-linux-musl/build-head.log). I have kept the queued worktree unchanged. The Rust changes in the tested head are at 27b406a19; 189f41716 only adjusts tracer TypeScript. Once the x86 build completes, I will run the matched ARM base/head build and the requested gates on compute01.

J36 progress: the x86 static head build remains queued on compute01 at position 9 behind four occupied slots (PID 1025693; log /home/claude/.cache/codex-scratch/j36-signin/x86_64-unknown-linux-musl/build-head.log). I have kept the queued worktree unchanged. The Rust changes in the tested head are at 27b406a19; 189f41716 only adjusts tracer TypeScript. Once the x86 build completes, I will run the matched ARM base/head build and the requested gates on compute01.
Author
Owner

J36 measurement-wrapper correction: the first x86 attempt acquired slot 4/4 and then failed before compilation with env: cargo: No such file or directory. The prepared nested bash -lc reset the Nix dev-shell PATH. I canceled my queued ARM/test commands before checkout, replaced both measurement wrappers with nix develop -c bash -c, and requeued x86, ARM, then the focused tests in that order. Corrected logs: /home/claude/.cache/codex-scratch/j36-signin/x86_64-unknown-linux-musl/build-head-corrected.log and /home/claude/.cache/codex-scratch/j36-signin/aarch64-unknown-linux-musl/build-base-head-corrected.log.

J36 measurement-wrapper correction: the first x86 attempt acquired slot 4/4 and then failed before compilation with `env: cargo: No such file or directory`. The prepared nested `bash -lc` reset the Nix dev-shell PATH. I canceled my queued ARM/test commands before checkout, replaced both measurement wrappers with `nix develop -c bash -c`, and requeued x86, ARM, then the focused tests in that order. Corrected logs: `/home/claude/.cache/codex-scratch/j36-signin/x86_64-unknown-linux-musl/build-head-corrected.log` and `/home/claude/.cache/codex-scratch/j36-signin/aarch64-unknown-linux-musl/build-base-head-corrected.log`.
Author
Owner

Measurement correction: build-slot grants a new FIFO ticket as soon as any one of four slots opens, so my ARM checkout began while the x86 build was still running in the same worktree. The x86 artifact measured 21,440,296 B but lacked the Approve this device sign-in and device_code markers; it is invalid and is not counted. A concurrently queued test also ran against base and reported 0 tests, so that is invalid too. I canceled the still-waiting test wrapper. ARM is currently building the matched base then head; I will wait for it to finish and restore head before a standalone, marker-checked x86 build and fresh tests.

Measurement correction: build-slot grants a new FIFO ticket as soon as any one of four slots opens, so my ARM checkout began while the x86 build was still running in the same worktree. The x86 artifact measured 21,440,296 B but lacked the `Approve this device sign-in` and `device_code` markers; it is invalid and is not counted. A concurrently queued test also ran against base and reported 0 tests, so that is invalid too. I canceled the still-waiting test wrapper. ARM is currently building the matched base then head; I will wait for it to finish and restore head before a standalone, marker-checked x86 build and fresh tests.
Author
Owner

ARM measurement tool correction: the compute01 aarch64 base build finished and produced a valid static AArch64 ELF (file reports ARM aarch64), but host strip cannot process this cross-architecture file (Unable to recognise the architecture). The unstripped base artifact is retained. I am using llvm-strip/llvm-nm from the Nix LLVM package to finish base attribution and build/measure the head; no source changed.

ARM measurement tool correction: the compute01 aarch64 base build finished and produced a valid static AArch64 ELF (`file` reports ARM aarch64), but host `strip` cannot process this cross-architecture file (`Unable to recognise the architecture`). The unstripped base artifact is retained. I am using `llvm-strip`/`llvm-nm` from the Nix LLVM package to finish base attribution and build/measure the head; no source changed.
Author
Owner

J36 continuation checkpoint: signin-d311 is pushed at 189f41716; the working tree is clean. J37's P1 and P2 fixes and their regressions are preserved, and API-key lifetime is unchanged. The corrected compute01 ARM64 base/head size build is queued through build-slot at position 13; its log is /home/claude/.cache/codex-scratch/j36-signin/aarch64-unknown-linux-musl/build-head-llvm.log. I am leaving the shared remote worktree untouched until it completes, then I will run the marker-checked x86 build and the focused sign-in, tracer approval, and fast gates in sequence. The earlier x86 candidate is invalid because its strings do not verify the sign-in checkout; it will not be used as a size result.

J36 continuation checkpoint: `signin-d311` is pushed at `189f41716`; the working tree is clean. J37's P1 and P2 fixes and their regressions are preserved, and API-key lifetime is unchanged. The corrected compute01 ARM64 base/head size build is queued through build-slot at position 13; its log is `/home/claude/.cache/codex-scratch/j36-signin/aarch64-unknown-linux-musl/build-head-llvm.log`. I am leaving the shared remote worktree untouched until it completes, then I will run the marker-checked x86 build and the focused sign-in, tracer approval, and fast gates in sequence. The earlier x86 candidate is invalid because its strings do not verify the sign-in checkout; it will not be used as a size result.
Author
Owner

Matched ARM64 size finding on signin-d311 @ 189f41716, built on compute01 with the pinned static release recipe and checked as static AArch64 ELF:

Target Base 57491e918 Head 189f41716 Delta D243 cap Head vs cap
aarch64-unknown-linux-musl 19,604,576 B 19,665,808 B +61,232 B 19,605,896 B +59,912 B

The delta is below the 64 KiB stop by 4,304 B, but above the 16 KiB target. nm -C -S --size-sort diff code-symbol net by largest owners: infraternal +35,140 B (+48,780 / −13,640), core +10,816 B, serde_json +3,308 B, nix +1,876 B, and url +848 B; other roots are smaller. Largest additions are sign_in::run_flow 17,360 B, shared remote::post_json 15,180 B, CredentialFile deserialization 5,340 B, CLI dispatch 4,356 B, and cli_plugin 3,744 B. This is code-symbol attribution; the stripped artifact delta is measured separately above. Evidence log and symbol files: /home/claude/.cache/codex-scratch/j36-signin/aarch64-unknown-linux-musl/build-head-llvm.log and base-compute01.nm / head-189f41716.nm in that directory. The marker-checked matched x86 build is queued next; no cap changes are made.

Matched ARM64 size finding on `signin-d311` @ `189f41716`, built on compute01 with the pinned static release recipe and checked as static AArch64 ELF: | Target | Base `57491e918` | Head `189f41716` | Delta | D243 cap | Head vs cap | | --- | ---: | ---: | ---: | ---: | ---: | | aarch64-unknown-linux-musl | 19,604,576 B | 19,665,808 B | +61,232 B | 19,605,896 B | +59,912 B | The delta is below the 64 KiB stop by 4,304 B, but above the 16 KiB target. `nm -C -S --size-sort` diff code-symbol net by largest owners: `infraternal` +35,140 B (+48,780 / −13,640), `core` +10,816 B, `serde_json` +3,308 B, `nix` +1,876 B, and `url` +848 B; other roots are smaller. Largest additions are `sign_in::run_flow` 17,360 B, shared `remote::post_json` 15,180 B, `CredentialFile` deserialization 5,340 B, CLI dispatch 4,356 B, and `cli_plugin` 3,744 B. This is code-symbol attribution; the stripped artifact delta is measured separately above. Evidence log and symbol files: `/home/claude/.cache/codex-scratch/j36-signin/aarch64-unknown-linux-musl/build-head-llvm.log` and `base-compute01.nm` / `head-189f41716.nm` in that directory. The marker-checked matched x86 build is queued next; no cap changes are made.
Author
Owner

J36 long-running checkpoint: the exact compute01 AArch64 base/head build completed with static stripped sizes 19,604,576 B / 19,665,808 B (+61,232 B); symbol attribution is in my prior finding comment. The serial marker-checked x86 base/head job is still queued at build-slot ticket 14 after ~35 minutes; its log is /home/claude/.cache/codex-scratch/j36-signin/x86_64-unknown-linux-musl/build-base-head-safe.log. The earlier head-189f41716 x86 artifact was from the checkout race and has no sign-in markers, so it remains excluded. Worktree was restored to 189f41716 before the queued x86 job; no source changed.

J36 long-running checkpoint: the exact compute01 AArch64 base/head build completed with static stripped sizes 19,604,576 B / 19,665,808 B (+61,232 B); symbol attribution is in my prior finding comment. The serial marker-checked x86 base/head job is still queued at build-slot ticket 14 after ~35 minutes; its log is `/home/claude/.cache/codex-scratch/j36-signin/x86_64-unknown-linux-musl/build-base-head-safe.log`. The earlier `head-189f41716` x86 artifact was from the checkout race and has no sign-in markers, so it remains excluded. Worktree was restored to `189f41716` before the queued x86 job; no source changed.
Author
Owner

J36 matched static release sizes and symbol attribution for signin-d311 @ 189f41716, built sequentially on compute01 from base 57491e918 with scripts/static-cargo and stripped as static ELF:

Target Measured base Head Head − base D243 cap Head − cap
x86_64-unknown-linux-musl 21,440,296 B 21,522,280 B +81,984 B 21,431,472 B +90,808 B
aarch64-unknown-linux-musl 19,604,576 B 19,665,808 B +61,232 B 19,605,896 B +59,912 B

The x86 matched delta is 16,448 B above the 64 KiB stop; against D243's recorded cap the overage is 25,272 B. The freshly rebuilt unmodified base itself is 8,824 B above that cap under this compute01 toolchain, so the static-size gate is expected to fail on the pinned base too. ARM64 is below the 64 KiB stop by 4,304 B and remains above the 16 KiB target. Both head artifacts passed the sign-in marker check; file reports static PIE on x86_64 and statically linked AArch64.

nm -C -S --size-sort code-symbol diff, grouped by root (net / added / removed): x86_64 infraternal +45,929 / 62,566 / 16,637 B, core +14,275 / 15,953 / 1,678 B, serde_json +4,159 B, nix +1,758 B, url +974 B, hyper −581 B; all other roots together contribute +874 B. Net code-symbol change is +67,388 B; the stripped image has another +14,596 B in non-code sections. AArch64 roots: infraternal +35,140 / 48,780 / 13,640 B, core +10,816 B, serde_json +3,308 B, nix +1,876 B, url +848 B, hyper −476 B; remaining roots net +724 B. Net code-symbol change is +52,236 B, with +8,996 B in non-code sections.

Largest x86 additions are sign_in::run_flow 24,201 B, shared remote::post_json 17,589 B, credential-file deserialization 7,939 B, dispatch_selected_action 5,493 B, remote::join 5,193 B, and cli_plugin 4,552 B. ARM64 counterparts are 17,360 B, 15,180 B, 5,340 B, 4,356 B, 3,284 B, and 3,744 B. The focused CLI flow, safe credential parsing/storage, and action dispatch dominate the remaining delta; the directory-fd protection stays. Per the >64 KiB stop, I am stopping further size edits for owner direction.

Cross-slice check remains negative: D311 adds no new package to Cargo.lock (only a direct edge to already-locked zeroize); D314 uses existing ArgMatches rather than a new Clap derive; mesh pins changes no CLI dispatch. The current symbol diff adds no Clap, WebAuthn, OpenSSL, or Tokio/OpenSSL dependency code path. Thus there is no shared newly enabled dependency explaining all three growths. Receipts and nm files are under /home/claude/.cache/codex-scratch/j36-signin/{x86_64-unknown-linux-musl,aarch64-unknown-linux-musl}. Focused sign-in, tracer approval, and scripts/verify fast on compute01 are still running/queued.

J36 matched static release sizes and symbol attribution for `signin-d311` @ `189f41716`, built sequentially on compute01 from base `57491e918` with `scripts/static-cargo` and stripped as static ELF: | Target | Measured base | Head | Head − base | D243 cap | Head − cap | | --- | ---: | ---: | ---: | ---: | ---: | | x86_64-unknown-linux-musl | 21,440,296 B | 21,522,280 B | +81,984 B | 21,431,472 B | +90,808 B | | aarch64-unknown-linux-musl | 19,604,576 B | 19,665,808 B | +61,232 B | 19,605,896 B | +59,912 B | The x86 matched delta is 16,448 B above the 64 KiB stop; against D243's recorded cap the overage is 25,272 B. The freshly rebuilt unmodified base itself is 8,824 B above that cap under this compute01 toolchain, so the static-size gate is expected to fail on the pinned base too. ARM64 is below the 64 KiB stop by 4,304 B and remains above the 16 KiB target. Both head artifacts passed the sign-in marker check; `file` reports static PIE on x86_64 and statically linked AArch64. `nm -C -S --size-sort` code-symbol diff, grouped by root (net / added / removed): x86_64 `infraternal` +45,929 / 62,566 / 16,637 B, `core` +14,275 / 15,953 / 1,678 B, `serde_json` +4,159 B, `nix` +1,758 B, `url` +974 B, `hyper` −581 B; all other roots together contribute +874 B. Net code-symbol change is +67,388 B; the stripped image has another +14,596 B in non-code sections. AArch64 roots: `infraternal` +35,140 / 48,780 / 13,640 B, `core` +10,816 B, `serde_json` +3,308 B, `nix` +1,876 B, `url` +848 B, `hyper` −476 B; remaining roots net +724 B. Net code-symbol change is +52,236 B, with +8,996 B in non-code sections. Largest x86 additions are `sign_in::run_flow` 24,201 B, shared `remote::post_json` 17,589 B, credential-file deserialization 7,939 B, `dispatch_selected_action` 5,493 B, `remote::join` 5,193 B, and `cli_plugin` 4,552 B. ARM64 counterparts are 17,360 B, 15,180 B, 5,340 B, 4,356 B, 3,284 B, and 3,744 B. The focused CLI flow, safe credential parsing/storage, and action dispatch dominate the remaining delta; the directory-fd protection stays. Per the >64 KiB stop, I am stopping further size edits for owner direction. Cross-slice check remains negative: D311 adds no new package to `Cargo.lock` (only a direct edge to already-locked `zeroize`); D314 uses existing `ArgMatches` rather than a new Clap derive; mesh pins changes no CLI dispatch. The current symbol diff adds no Clap, WebAuthn, OpenSSL, or Tokio/OpenSSL dependency code path. Thus there is no shared newly enabled dependency explaining all three growths. Receipts and nm files are under `/home/claude/.cache/codex-scratch/j36-signin/{x86_64-unknown-linux-musl,aarch64-unknown-linux-musl}`. Focused sign-in, tracer approval, and `scripts/verify fast` on compute01 are still running/queued.
Author
Owner

Compute01 per-finding re-review results on signin-d311 @ 189f41716:

  • P1 device-code phishing: the approval page shows the exact CLI code and requires a checked match before passkey approval. The production tracer approval spec passed 7/7, including approval page shows the code so a person can match it with the CLI and successful fresh virtual-passkey approval.
  • P2 credential-directory replacement: the locked directory fd is retained through read and atomic replacement with openat/renameat, O_NOFOLLOW, and ownership/mode checks. Focused sign-in tests passed 13/13, including credential_read_refuses_a_private_directory_replaced_after_locking.
  • API-key lifetime remains unchanged, as requested.

run-final-gates.sh is still running scripts/verify fast on compute01; I will add its exact summary and exit status after completion.

Compute01 per-finding re-review results on `signin-d311` @ `189f41716`: - **P1 device-code phishing:** the approval page shows the exact CLI code and requires a checked match before passkey approval. The production tracer approval spec passed **7/7**, including `approval page shows the code so a person can match it with the CLI` and successful fresh virtual-passkey approval. - **P2 credential-directory replacement:** the locked directory fd is retained through read and atomic replacement with `openat`/`renameat`, `O_NOFOLLOW`, and ownership/mode checks. Focused sign-in tests passed **13/13**, including `credential_read_refuses_a_private_directory_replaced_after_locking`. - API-key lifetime remains unchanged, as requested. `run-final-gates.sh` is still running `scripts/verify fast` on compute01; I will add its exact summary and exit status after completion.
Author
Owner

J36 progress on #269: committed and pushed the lint cleanup as cd04b72387. It keeps the outbound TLS configuration shared and applies the remaining rustfmt/Clippy fixes; the separate P1 and P2 security commits and J37 regressions remain intact.

The previous compute01 focused sign-in tests and tracer approval spec exited 0. Its scripts/verify fast exited 1 on the pre-cleanup head: fmt/clippy, the long fallback Unix socket path, and the exact D243 static-size checks. The gate process has exited. Next I am fetching cd04b7238 on compute01, rerunning the focused tests, Playwright approval spec, and fast gate via build-slot with a short test-state root, then measuring the pushed head. No API-key lifetime change.

J36 progress on #269: committed and pushed the lint cleanup as cd04b72387d62cab26109747ebe7564334ef8b75. It keeps the outbound TLS configuration shared and applies the remaining rustfmt/Clippy fixes; the separate P1 and P2 security commits and J37 regressions remain intact. The previous compute01 focused sign-in tests and tracer approval spec exited 0. Its scripts/verify fast exited 1 on the pre-cleanup head: fmt/clippy, the long fallback Unix socket path, and the exact D243 static-size checks. The gate process has exited. Next I am fetching cd04b7238 on compute01, rerunning the focused tests, Playwright approval spec, and fast gate via build-slot with a short test-state root, then measuring the pushed head. No API-key lifetime change.
Author
Owner

J36 continuation after the owner decision: D243 caps are removed, so I stopped size-reduction work and retained the already committed shared-transport reduction. Rebased signin-d311 onto origin/dev f4b61cd6c and force-with-lease pushed head d1d0fedad. The P1 approval confirmation and P2 directory-fd fixes remain separate commits; both J37 regression tests from review-signin-269 @ e53ab2f7c are in the branch. API-key lifetime is unchanged. Next I am fetching this head on compute01 and rerunning focused sign-in tests, the production tracer approval spec, and scripts/verify fast through build-slot.

J36 continuation after the owner decision: D243 caps are removed, so I stopped size-reduction work and retained the already committed shared-transport reduction. Rebased signin-d311 onto origin/dev f4b61cd6c and force-with-lease pushed head d1d0fedad. The P1 approval confirmation and P2 directory-fd fixes remain separate commits; both J37 regression tests from review-signin-269 @ e53ab2f7c are in the branch. API-key lifetime is unchanged. Next I am fetching this head on compute01 and rerunning focused sign-in tests, the production tracer approval spec, and scripts/verify fast through build-slot.
Author
Owner

J36 debugging update on #269: the first post-rebase focused Rust run exposed a compile error in the shared-transport cleanup. The extracted helper returns an already-built SslConnector, while the caller still invoked the builder-only .build() method (E0599 at remote.rs:334). I removed that stale call and amended/pushed the cleanup commit as 16b728e212. This does not change TLS policy or the P1/P2 implementations. The old detached-head verify fast run is finishing; I will rerun all requested checks against 16b728e.

J36 debugging update on #269: the first post-rebase focused Rust run exposed a compile error in the shared-transport cleanup. The extracted helper returns an already-built SslConnector, while the caller still invoked the builder-only .build() method (E0599 at remote.rs:334). I removed that stale call and amended/pushed the cleanup commit as 16b728e212a783a34c38e229a7a077dc64bac7f6. This does not change TLS policy or the P1/P2 implementations. The old detached-head verify fast run is finishing; I will rerun all requested checks against 16b728e.
Author
Owner

Compute01 queue update for the rebase verification: head 16b728e212 is queued in build-slot at position 4 behind four occupied slots. The focused sign-in command has not started compiling yet; its queue log is /home/claude/.cache/codex-scratch/j36-signin/sign-in-tests-final.log. The existing single target is /home/claude/worktrees/j36-signin-size/target (28 GB before rerun), with ample free disk. The previous Playwright run passed 7/7; that run was before the one-line Rust connector fix. I am waiting for the requested sequential recheck. No additional size work or API-key lifetime change.

Compute01 queue update for the rebase verification: head 16b728e212a783a34c38e229a7a077dc64bac7f6 is queued in build-slot at position 4 behind four occupied slots. The focused sign-in command has not started compiling yet; its queue log is /home/claude/.cache/codex-scratch/j36-signin/sign-in-tests-final.log. The existing single target is /home/claude/worktrees/j36-signin-size/target (28 GB before rerun), with ample free disk. The previous Playwright run passed 7/7; that run was before the one-line Rust connector fix. I am waiting for the requested sequential recheck. No additional size work or API-key lifetime change.
Author
Owner

J36 rebase verification update on signin-d311 @ 16b728e212a783a34c38e229a7a077dc64bac7f6: the focused sign-in suite passed 13/13 on the rebased head, including the directory-replacement regression. The fresh tracer install/typecheck, approval spec and scripts/verify fast are still queued sequentially on compute01 through build-slot; the current install request is waiting at queue position 8. The remote worktree has one target directory at /home/claude/worktrees/j36-signin-size/target. No more size-reduction work is planned; API-key lifetime remains unchanged.

J36 rebase verification update on `signin-d311` @ `16b728e212a783a34c38e229a7a077dc64bac7f6`: the focused sign-in suite passed **13/13** on the rebased head, including the directory-replacement regression. The fresh tracer install/typecheck, approval spec and `scripts/verify fast` are still queued sequentially on compute01 through build-slot; the current install request is waiting at queue position 8. The remote worktree has one target directory at `/home/claude/worktrees/j36-signin-size/target`. No more size-reduction work is planned; API-key lifetime remains unchanged.
Author
Owner

J36 checkpoint on signin-d311 @ 16b728e212a783a34c38e229a7a077dc64bac7f6: the current-head sign-in suite passed 13/13 and the tracer TypeScript check completed without reported errors. The production approval spec is still waiting in compute01 build-slot (its log reported position 4 at 08:06); scripts/verify fast follows it. Four shared slots remain occupied by other jobs. The one remote target is /home/claude/worktrees/j36-signin-size/target; focused and browser logs are under /home/claude/.cache/codex-scratch/j36-signin/. No size-cap or API-key lifetime changes.

J36 checkpoint on `signin-d311` @ `16b728e212a783a34c38e229a7a077dc64bac7f6`: the current-head sign-in suite passed 13/13 and the tracer TypeScript check completed without reported errors. The production approval spec is still waiting in compute01 build-slot (its log reported position 4 at 08:06); `scripts/verify fast` follows it. Four shared slots remain occupied by other jobs. The one remote target is `/home/claude/worktrees/j36-signin-size/target`; focused and browser logs are under `/home/claude/.cache/codex-scratch/j36-signin/`. No size-cap or API-key lifetime changes.
Author
Owner

J36 runner update on signin-d311 @ 16b728e212a783a34c38e229a7a077dc64bac7f6: the fresh Playwright command acquired a build slot but exited before test collection because apps/tracer/playwright.j36.config.ts was missing (resolveConfigFile error). No approval assertion ran. Sign-in remains 13/13 and tracer typecheck completed. The sequential runner is now queued on scripts/verify fast; after it finishes I will restore the temporary Playwright config, rerun the approval spec on compute01, and capture the six requested production screenshots for #269. Logs remain under /home/claude/.cache/codex-scratch/j36-signin/.

J36 runner update on `signin-d311` @ `16b728e212a783a34c38e229a7a077dc64bac7f6`: the fresh Playwright command acquired a build slot but exited before test collection because `apps/tracer/playwright.j36.config.ts` was missing (`resolveConfigFile` error). No approval assertion ran. Sign-in remains 13/13 and tracer typecheck completed. The sequential runner is now queued on `scripts/verify fast`; after it finishes I will restore the temporary Playwright config, rerun the approval spec on compute01, and capture the six requested production screenshots for #269. Logs remain under `/home/claude/.cache/codex-scratch/j36-signin/`.
Author
Owner

J36 verification harness correction on signin-d311 @ 16b728e212a783a34c38e229a7a077dc64bac7f6: scripts/verify fast hit four agent tests that reject Unix socket paths longer than 107 bytes; the configured scratch root was too long for this worktree. I stopped that run and started a fresh single-slot sequence with short /tmp/j36 and /tmp/j36-agent roots. It is queued at position 9 through build-slot. This run repeats focused sign-in tests, tracer typecheck, the production approval spec (tracked Playwright config, six screenshots), and the fast gate; it removes the single target directory on exit. The API-key lifetime is unchanged.

J36 verification harness correction on `signin-d311` @ `16b728e212a783a34c38e229a7a077dc64bac7f6`: `scripts/verify fast` hit four agent tests that reject Unix socket paths longer than 107 bytes; the configured scratch root was too long for this worktree. I stopped that run and started a fresh single-slot sequence with short `/tmp/j36` and `/tmp/j36-agent` roots. It is queued at position 9 through build-slot. This run repeats focused sign-in tests, tracer typecheck, the production approval spec (tracked Playwright config, six screenshots), and the fast gate; it removes the single target directory on exit. The API-key lifetime is unchanged.
Author
Owner

R2 research started. Read-only research branch research-r2, base f4b61cd6c3f52078cad7573c5367d7a6e75f84f0 (origin/dev); also inspecting origin/signin-d311 for sign-in credentials. No implementation or new engine decision. Evidence and owner questions will follow.

R2 research started. Read-only research branch `research-r2`, base `f4b61cd6c3f52078cad7573c5367d7a6e75f84f0` (origin/dev); also inspecting `origin/signin-d311` for sign-in credentials. No implementation or new engine decision. Evidence and owner questions will follow.
Author
Owner

R2 code finding at origin/dev f4b61cd6c: api_keys has no expiry column, and auth/devices.rs::api_authorize checks only the token hash and exact scope. There is no refresh-token protocol. signin-d311 16b728e adds local private credentials.json storage for those same keys, not a new token class or lifetime. By contrast remote_certificates are exactly 604800 seconds (7 days) and remote_renewal.rs schedules half-life renewal, deferring while the CA is locked. Blanket credential expiry would therefore have very different effects. Full storage/lifetime/recovery inventory follows.

R2 code finding at origin/dev f4b61cd6c: api_keys has no expiry column, and auth/devices.rs::api_authorize checks only the token hash and exact scope. There is no refresh-token protocol. signin-d311 16b728e adds local private credentials.json storage for those same keys, not a new token class or lifetime. By contrast remote_certificates are exactly 604800 seconds (7 days) and remote_renewal.rs schedules half-life renewal, deferring while the CA is locked. Blanket credential expiry would therefore have very different effects. Full storage/lifetime/recovery inventory follows.
Author
Owner

R2: complete credential inventory and what expiry means

Recommendation: do not give every “key” the same expiry. Keep bootstrap/approval/ceremony capabilities short and single-use; automatically renew transport certificates; retain explicit revocation for stable identities and recovery material. For API keys, add visible lifetime/last-use/revocation management before introducing a default deadline. Today an expired API key would require browser approval again; there is no refresh-token flow.

This is a code inventory, not a proposed behavior change. Reviewed origin/dev f4b61cd6c3, plus origin/signin-d311 16b728e212. The latter adds CLI storage and approval UX for the existing API-key type. It changes no API-key lifetime. Production-looking test fixtures and research spikes are not counted as shipped credential systems.

Storage shorthand: DB = the configured server data directory's infraternal.sqlite, opened with private directory/file permissions (0700/0600); this is not whole-database encryption. CA = configured CA directory, default $XDG_DATA_HOME/infraternal/ca, falling back to ~/.local/share/infraternal/ca. Automatically provisioned files are under CA/active; externally provisioned legacy configurations can use explicitly configured private paths. Node data = configured infraternal agent data directory; CLI default $XDG_DATA_HOME/infraternal/agent or ~/.local/share/infraternal/agent; service installs can select their own private state path. Paths here are code defaults, not observations of anyone's deployed secret files. Runtime/storage S1.

User identity, browser sessions and device authorization

Credential/type and purpose Holder, storage and protection Current lifetime / rotation / revocation What expiry means for the user; recommendation
Owner setup token: creates the first owner Local server operator obtains it through private setup-token.sock/local CLI; server retains raw token in process memory and SHA-256 hash in DB setup. Startup uses a private socket rather than treating the old banner format as permission to log secrets. 24 hours, one successful owner registration consumes it; startup before owner creation replaces the hash and invalidates the previous token. No token after an owner exists. Expiry prevents initial enrollment, not an existing owner's access. Restart before owner setup to mint a fresh capability, then reread it locally. Keep one-use + 24 h; expose clear expired-token guidance. [S1,S2]
Passkey/WebAuthn private credential: signs login and fresh-action challenges Private key stays in the authenticator/passkey provider, including its provider-managed sync if used. DB passkeys.credential contains public credential/counter metadata; id, aaguid, prf_input are not private signing keys. No wall-clock expiry. Up to two passkeys in registration logic. Recovery replaces account credentials. I did not find a general normal passkey-delete management endpoint in this snapshot; do not assume a complete revoke UI. A server-imposed deadline means enroll a replacement passkey before it expires, or use the second passkey/recovery. Deleting a server row does not erase copies in a provider. No routine expiry; add safe replacement/revocation and test last-passkey handling. [S3,S5]
WebAuthn PRF output / derived passkey wrapping key: unlocks account encryption key Authenticator produces PRF secret into browser memory; HKDF-derived AES-GCM key stays browser-side. Public PRF input is persisted in passkeys.prf_input; salts/nonces/envelopes in wraps are public parameters/ciphertext. Re-derived on demand; temporary buffers are cleared. No independent expiry timestamp. The server never stores PRF output. Some passkey transports/providers cannot return PRF even when authentication succeeds. “Expiry” cannot repair missing PRF support; use a compatible passkey/provider path or recovery. Changing derivation/input without rewrapping strands encrypted material. Treat as transient secret, not an expiring bearer token; version and migrate envelopes. [S3,S4]
Account DEK and its passkey/recovery envelopes: the account's 32-byte wrapping root Browser generates/unwraps DEK in memory. DB wraps(user_id,kind,key_id,blob) stores AES-256-GCM encrypted DEK copies, one per passkey/recovery recipient. Browser login verifies unwrap then clears its working DEK; no persistent plaintext browser DEK store was found. No TTL. Ciphertext is recipient-bound and versioned. Recovery replaces wraps; this is not a general automatic DEK rotation service. Destroying/expiring the only decryptable envelope can permanently lose access to encrypted material. Rotating DEK requires re-encryption/rewrapping, not changing a timestamp. No age expiry; explicit audited cryptographic migration with recovery verified first. The envelope system exists; it does not prove a complete persistent SSH-secret vault is wired. [S4,S5]
Recovery phrase: alternative account recovery/unwrapping authority 24 English BIP39 words, generated in the browser; holder must keep an offline/private copy. V2 derives separate HKDF siblings for authentication verifier and recovery wrap. DB users.recovery_hash holds an Argon2id verifier; wraps holds recovery ciphertext, never the words. Legacy v1 metadata still has compatibility paths. No expiry. V1-to-v2 signed-in migration is one-shot; do not describe it as an unrestricted rotate-anytime endpoint. Successful recovery replaces passkeys/wraps and revokes sessions/API keys/approvals for that account. Phrase expiry could remove the last recovery route while all passkeys are unavailable. The words cannot renew themselves silently without generating and safely recording a new phrase. No calendar expiry; replace on compromise or derivation migration with an explicit backup-confirmation ceremony. A completed recovery is account-wide credential replacement, not just a new browser session. [S4,S5]
Recovery authentication verifier / recovery wrapping sibling Verifier is transmitted over HTTPS and Argon2id-hashed by the server. Independent wrap sibling stays in browser memory. Domain separation prevents a server-held authentication verifier from unwrapping the DEK. Same generation as the recovery phrase; old verifier/capabilities are invalidated by replacement. No independent TTL. Expire/replace verifier and matching envelope coherently, or valid words can authenticate but fail to decrypt. Keep siblings separate, version them together, never turn the wrap sibling into a bearer credential. [S4,S5]
Pending recovery bearer: completes one delayed recovery request Browser memory; DB recovery.token_hash, ready, expires. Raw high-entropy token is not a persistent plaintext DB value. 48 h bearer lifetime from issue/resume; 24 h delay, waived only by a live unlocked same-account session when creation authorizes it. Resume with verified recovery secret replaces bearer and extends its lifetime while retaining original ready time. Same-account signed-in cancellation; completion consumes it. Expired bearer requires proving words again to resume; it does not make the recovery phrase expire or reset the existing waiting deadline. Keep short-lived bearer, cancellation and preserved-delay resumption. [S5]
Browser session cookie: ordinary UI/WebSocket authority Browser __Host-infraternal; Secure, HttpOnly, SameSite=Strict, Path=/; DB sessions stores hash, user/device/passkey, expiry and freshness. DB 30 days sliding on authorized activity, never resurrecting expired/revoked rows. Cookie is issued with Max-Age=2,592,000 on login/registration. Ordinary session reads do not reissue Set-Cookie, so browser cookie lifetime and DB sliding lifetime are distinct in this snapshot. Logout deletes the session; recovery revokes all account sessions. Expired/revoked DB row or missing browser cookie requires a fresh login. WebSocket authority is checked and cannot live forever merely because the socket is open; terminal PTY persistence is separate from browser authorization. Retain D28; separately decide whether to renew browser cookie age on safe activity to match intended sliding behavior. No claim here that 30-day cookie renewal is already implemented. [S6,S7]
Restricted pending-login cookie and completion transaction ID Same cookie/sessions table with unlocked=0; page gets a domain-separated transaction ID derived from the cookie. Completion ID cannot be used as a session bearer; it binds local unwrap completion/cleanup to that login. 5 minutes, non-sliding until unlock_finish. Then session becomes unlocked with 30-day DB expiry. Expired/incomplete pending rows cannot access terminals. Local unwrap failure/timeout means login again. A delayed cleanup must not revoke a subsequent login. Keep 5 min and exact transaction binding; no separate refresh token. [S3,S6]
Fresh-passkey authority timestamp: permits destructive/fresh actions sessions.fresh server-side, backed by a successful UV-required passkey ceremony. It is not a separate key. 300 seconds; fresh-action path rejects future/stale timestamps. Only fresh/destructive operations stop until another passkey tap; ordinary session can remain valid. Keep separate from session expiry. Extending the browser session should not silently refresh freshness. [S6]
WebAuthn ceremony ID/challenge and browser-binding cookie Server process-local bounded pending maps retain WebAuthn state; browser receives ID/challenge and __Host-infraternal-ceremony, Secure/HttpOnly/Strict, bound by hash to pending state. These are ceremony context, not durable login credentials. 5 minutes, consumed on finish; pending state disappears on restart. Binding cookie Max-Age=300. CA-specific ceremonies also have 300-second, purpose/session/membership bindings. An expired challenge or restarted server means retry the tap/registration, not recover the whole account. Keep 5 min, consume once, bind origin/browser/purpose; do not persist challenge authority across restarts. [S3,S8,S12]
Device-code polling token: CLI redeems a browser-approved request CLI process memory, Zeroizing<String> in signin-d311; DB approvals.token_hash. Never URL/QR/output/disk. 600 seconds, one successful redemption atomically deletes approval and creates API-key hash. Closing approval page does not cancel it; no decline endpoint exists. Expiry means rerun infraternal sign-in <server>. No API key is issued by a code merely expiring. A lost successful redemption response can leave a live API key the CLI never received. Keep 10 min/one-use; consider acknowledged redemption separately; never treat an ambiguous response as proof no key exists. [S9,S10]
Device user code / approval URL: locates request for browser review Displayed to user and in ?device_code=...; DB approvals.code_hash. Lower-authority context: code alone cannot redeem or approve; browser session plus fresh passkey approval is required. Current code is 12 random bytes encoded to 16 base64url characters. Same 10 min deadline as approval; approval binds exact device/scopes and may occur only once. Row lasts until redemption/expiry cleanup. Stale link requires a new sign-in. Keep short lifetime, visible code-match confirmation and exact scope review; do not put poll token/API key in the link. [S9,S10]
API key / CLI/MCP device credential: scoped automation bearer Server stores SHA-256 hash in DB api_keys(token_hash,user_id,device,scopes). signin-d311 stores plaintext bearer in $XDG_CONFIG_HOME/infraternal/credentials.json or ~/.config/infraternal/credentials.json, in 0700 owned directory/0600 owned file, locked fd, no-follow path checks, atomic replacement and fsync. This is permission protection, not at-rest encryption/keychain storage. No current expiry, rotation or refresh protocol. Authorization checks exact scope. /auth/api-revoke deletes only own key and needs fresh passkey; recovery deletes account keys. Current scopes are hosts:read, terminal:connect, actions:dry-run; a key grants no browser session or fresh-tap privilege. Adding hard expiry will make new API authorization fail and require interactive browser sign-in again, including unattended users/scripts. It cannot renew silently today. Deleting a local credential does not revoke server key. Account UI currently lacks complete key management despite existing list/revoke backend. Recommend explicit per-key expiry choice, created/last-used/expires metadata, warnings and replacement overlap; retain an owner-approved no-expiry option for unattended automation until a designed renewal/re-auth flow exists. Do not shorten lifetime in the D311 wiring slice. [S9,S10]
WebSocket resume token: recovers a disconnected viewer connection 32 random bytes to browser memory via Welcome; SHA-256 in DB websocket_connections. Bound to connection/user and still requires live session-cookie authority. 120 seconds after disconnect; active row cannot be concurrently resumed. Successful resume rotates token. Startup marks previously active rows disconnected for bounded resume. Expiry loses connection-resume shortcut, not account credentials or necessarily the long-lived terminal shim. A fresh authenticated attach can recover a retained session under normal session policy. Keep short, rotate on successful claim, retain separate live-session check. [S7]

Machine trust, host access and encrypted authority

Credential/type and purpose Holder, storage and protection Current lifetime / rotation / revocation What expiry means for the user; recommendation
Machine CA Ed25519 signing key and root certificate: issues remote-host identities Unlocked private seed only in server memory; protected OpenSSL secure heap, root refuses unavailable protection, nonroot warns. Encrypted administrator copies in CA/active/administrators/administrator-<member-hash>.<generation>.json, or configured CA-copy directory. AES-256-GCM binds issuer/member/generation. Public issuer.pem and public DB certificate ledger are not secret keys. Generated root certificate lasts 315,360,000 s (3,650 days). Private key itself has no TTL. Cold server start locks signer; ordinary update/rollback can hand it over in memory. No complete automated root rollover was found. Signing refuses leaves that outlive issuer. Root expiry prevents valid machine authentication/issuance even if the signing key is still present. Swapping root without distributing overlap trust disconnects the fleet. Keep long-lived root for now; build advance warnings, authenticated overlapping root rollout and recovery before enforcing shorter roots. Back up encrypted copies plus account recovery material. [S11,S12]
CA administrator unlock material / membership envelope Browser unwraps DEK using passkey PRF, derives scoped CA key with HKDF-SHA256 over member/generation/salt; sends scoped key with purpose-bound passkey proof over same-origin HTTPS. Server uses temporary zeroizing scoped key to open AES-GCM copy. DB ca_administrators records membership generation, not signing seed/envelope. Unlock challenge 300 s. Scoped derivation has no independent wall-clock expiry; authority requires current membership. Removing membership invalidates its generation/copy and affects custody state; no periodic unlock timeout was found in MemoryCaSource. Cold restart requires admin unlock; in-process signer is not a 5-minute lease. Expiring an unlock ceremony means retry tap. Relocking the signer blocks new enrollment/renewal; existing valid host TLS does not need the signer merely to verify. If left locked, host leaves eventually expire. A removed administrator who previously extracted private material cannot be made to forget it by deleting ciphertext. Keep ceremony short and membership checks strict; do not introduce frequent CA relocking without a documented availability policy and expiry alerts. Compromised CA requires CA rotation, not envelope deletion alone. [S11,S12]
Server remote-listener Ed25519 key and leaf certificate: server side of host mTLS CA/active/remote.key (unencrypted PKCS#8 PEM, private file/dir) and remote.pem; loaded into server memory. Distinct key from the CA signer. Configured legacy paths may differ. Bootstrap gives it the same 3,650-day validity as issuer, not the 7-day host lifetime. No automatic server-listener leaf renewal path found. Expiry breaks host TLS handshakes/reconnections independently of whether the CA is unlocked. Give this its own renewal lifecycle with overlap/reload before shortening lifetime; alert well in advance. Do not claim host half-life renewal covers it. [S11]
Public HTTPS server leaf/key: browser/API and enrollment HTTPS Operator-configured TlsFiles.certificate/private_key PEM paths, or upstream reverse proxy when plain HTTP is explicitly configured behind it. Loaded by OpenSSL listener. No automatic public certificate issuance is wired in current product. Validity is whatever the external issuer/operator supplied. Runtime startup loads config; no product ACME renewal/reload orchestrator was found. Expiry causes browser/CLI TLS rejection and can block enrollment. Operator/proxy renews and applies certificate. Automatically renew with the approved proxy/ACME implementation, show renewal health, keep this trust domain separate from machine CA. [S1,S11]
Node Ed25519 identity private key: durable host possession, signs enrollment/renewal CSR Generated on node; unencrypted base64 PKCS#8 in private Node data/identity.json with host ID, endpoint, CA and current certificate. Private paths checked, publication atomic. Server sees public key/certificate only in remote_certificates, never node private key. No independent TTL and no automatic key rotation. Authorized re-enrollment deliberately retains the same key; certificate renewal retains it too. Host tombstone/certificate revocation controls admission. Expiring/deleting the key would require fresh secure enrollment and can change host identity semantics. An expired certificate does not erase this key. Stable key, explicit compromise rotation/re-enrollment; do not use a cert's 7-day validity as private-key expiry. [S13,S14]
Node client mTLS leaf certificate: remote-host access authority Public cert in identity.json; DB remote_certificates records fingerprint, host, issuer, public key, validity and revocation. Private key as preceding row. Exactly 604,800 s (7 days); server schedules renewal at half-life (~3.5 days) on authenticated control stream. Host signs CSR with retained key; server commits new cert, node validates/fsyncs it, then Ack extends authority. Locked CA defers renewal (hour retry and unlock notification). Host removal/revocation prevents admission; active authority has an expiry guard. Offline node beyond expiry cannot connect to request normal live renewal. User needs an owner-authorized fresh join/re-enrollment with existing retained key; not silent renewal from an expired credential. Locked signer plus prolonged admin absence can strand hosts. Keep 7 days/half-life, add visible remaining-validity/locked-CA alerts and clear authenticated re-enrollment recovery; no grace acceptance of expired mTLS. [S13,S14]
Host join/enrollment token: authorizes one bound host/mode enrollment High-entropy raw token passes private stdin/descriptor invitation; DB remote_join_tokens stores SHA-256 digest, host/mode, timestamps/consumption only. Invitation also carries public endpoint/pins, not CA private material. 900 s (15 min), one-use transaction; replacement invalidates predecessor; revoked host cannot use it. Expiry blocks this enrollment attempt; it does not disconnect an already enrolled host. Issue a fresh owner-authorized invitation and retry. Keep 15 min/one-use/host+mode binding; never long-lived reusable setup credentials. [S13]
Mesh WireGuard X25519 static key: node-local encrypted mesh identity Generated on node, transient Zeroizing key buffer, installed through netlink; private key remains in kernel WireGuard interface memory, not SQLite or node JSON. DB mesh tables and node checkpoint/interface ownership files contain public keys only. Retained interface can survive infraternal agent process restart; deleting interface/reboot loses kernel key and requires fresh setup. No scheduled static-key expiry found. Coordinator library supports rotation with at least 120 s overlap plus peer delivery acknowledgements, but no production plugin call to rotate_key was found, so that is a capability, not a shipped automatic rotation loop. WireGuard's own traffic/session rekey is separate. Revocation/policy updates remove access when delivered. Hard local static-key expiry could cut off mesh traffic while server/control path is unavailable, undermining offline retained policy. No autonomous calendar cutoff; implement coordinated overlap/ack rotation and fallback reachability before scheduling static-key replacement. On compromise remove/revoke, and report delivery/offline limits truthfully. [S15]
Dedicated server SSH Ed25519 identity (D183): passwordless host bootstrap/login credential server_ssh_key DB singleton holds public key and opaque administrator-wrapped envelope; StoredSshKey holds plaintext only while unlocked. Scoped helper receives zeroizing PEM through stdin, not a persistent private-key file. Vault/unlock are explicit adapter interfaces; this source proves custody primitives, not every production vault integration. No expiry or automatic rotation; cold source locked, explicit lock has generation protection against in-flight unwrap. Update handoff can preserve unlocked material in memory. Replacing key requires updating target authorized_keys/trust first. Expiry would break new bootstrap/SSH authentication, not automatically terminate established SSH sessions. Stable key with explicit coordinated rotation and alternate host access; do not set a short TTL before distribution exists. [S16]
User-supplied SSH private key, key passphrase and SSH password Browser-unwrapped or one-attempt input transferred over authenticated socket. Rust Secret zeroizes on drop. Private key enters temporary scoped ssh-agent through ssh-add -; password/passphrase served once through private askpass socket after peer executable/parent checks. No plaintext key written to disk; private temporary helper directory and sockets cleaned. A persistent per-host encrypted-secret table was not found on these refs; D23 is not proof such a vault is complete. One-use auth override has 60 s acceptance deadline; unused secrets clear on expiry/drop and helpers retire after authentication. Actual SSH key/password lifetime/revocation belongs to target/issuer; no product-wide persistent expiry. Expired one-attempt input means ask again; completed authenticated connection can continue. Target password rotation or revoked key breaks next authentication. Keep operation-scoped plaintext; store only encrypted credentials if/when vault is wired; let issuer validity drive SSH certificate deadlines. [S17]
SSH host-key pins, known-host CA keys and revocation lists: public server identity trust Public keys in app-managed known-host store or validated system OpenSSH known-host/trust paths. They are public trust anchors, not bearer secrets. Ambient IdentityFile/client certificate/SSH_AUTH_SOCK are intentionally excluded from D183 auth configuration. Plain host keys have no expiry; SSH certificates carry issuer validity; revocation follows configured lists/trust changes. Confirmation requires authorized owner and exact observed key. Expiring a pin does not renew the server's identity; autoaccepting its replacement enables impersonation. Preserve strict pin checks, explicit verified replacement and issuer validity; never silently erase expired/old trust to reconnect. [S17]

Release and external credentials, plus requested types that do not exist yet

Credential/type Holder/storage/protection Current lifetime and user impact; recommendation
Release Ed25519 signing key apps/release-signer expects Worker secret SIGNING_KEY, provisioned from owner's password manager. Runtime imports nonextractable WebCrypto signing key. Compiled public release/allowed_signers is verification trust, not private material. Code documents custody intent; no deployed secret value/config was inspected. No key-age expiry in verifier. Signed checksum manifests use OpenSSH signature namespace infraternal-release. Rotation must update accepted public trust in deployed binaries before retiring old signer. Losing/expiring key blocks new releases/updates, not execution of existing binaries. Overlap trust rollout, offline backup and compromise procedure; do not make old signatures invalid solely by a calendar deadline without an update/recovery plan. [S18]
Release signing caller bearer Worker secret CALLER_TOKEN; pipeline environment RELEASE_CALLER_TOKEN; shared bearer authorizes /sign. Code hashes comparisons; secret never belongs in argv/repo. No app-enforced expiry; replacing Worker secret invalidates old caller immediately. Expiry/revocation stops signing until pipeline updated; already signed releases unaffected. Scoped CI custody, periodic coordinated replacement and immediate compromise revocation; choose operational cadence separately from product API keys. [S18]
Release storage/provider API bearer Pipeline environment CLOUDFLARE_API_TOKEN, used by scripts/release/publish for object storage API; curl config supplied over stdin. Token provisioning/protection outside pipeline is operator-managed, not a product DB row. Worker BUILDS is a platform binding, not a user-exported storage secret in code. Lifetime/revocation managed by external provider. Expiry blocks uploads/release publication, not running installations. Least privilege to release storage, provider expiry alerts and coordinated CI renewal. No secret values read. [S18]
Notification URL if it embeds access authority Optional signer NOTIFY_URL binding; could be just a public endpoint or include an opaque topic/token. Code does not define a distinct notification-key schema. No application TTL; notification is best effort. Expiry breaks release notifications only. Treat as secret if authority-bearing; rotate through deployment configuration without coupling it to signing-key lifetime. [S18]
DNS-01 provider token Not implemented/persisted in current product. J20 uses a local stub provider in research. Future Traefik/ACME integration needs a scoped DNS credential and approved custody on the issuing node/controller. A release-storage API token is not evidence of a DNS token. Provider TTL/revocation would stop new issuance/renewal, while issued certificates work until their own expiry. Use narrow zone/record permissions, private file delivery when supported, advance provider/certificate warnings and independent replacement. Storage path and default TTL remain a design decision; do not invent one. [S19]
ACME account private key, issued service private keys/certificates, optional EAB key Research/future integration, not present product custody on these refs. A Traefik resolver would normally retain account/certificate material in private acme.json; external issuer paths could differ. EAB exists only if chosen CA requires it. Account authorization and cert validity are distinct; provider expiry does not invalidate an already issued certificate. Persist/backup account material, renew leaves automatically with overlap, never use one blanket “ACME key expiry.” Exact policy awaits proxy/CA decision. [S19]
Relay tickets / relay shared secrets No runtime credential implementation found. crates/plugins/relay/src/lib.rs explicitly declares CLI actions before relay runtime. Existing terminal relay.rs is flow-control/replay code, not a ticket issuer. No current storage/lifetime/revocation to report. When implemented, short-lived audience/node/session-bound tickets with current authorization checks; ask owner then. Do not claim these keys already exist. [S20]
Capability tokens Existing bearer capabilities are setup, join, sessions, recovery, device polling, API and resume tokens, individually inventoried above. Hello capability bits and plugin action capabilities are feature/permission declarations, not additional secret tokens. Server-path transport proofs are authenticated-connection authority, not persisted user API keys. Apply each token's real lifetime, rather than adding a fictional generic capability TTL. Keep audiences/purposes separate. [S2,S6,S7,S9,S13,S20]
TLS/QUIC/WireGuard traffic keys and TLS session tickets Crypto library/kernel connection state, distinct from persisted node/CA/static keys. No app-level durable session-ticket secret store or management API found. QUIC explicitly disables early data (enable_early_data=false). Library/protocol handshakes and rekey rules govern them; no user-configured product expiry policy. Keep protocol key management inside chosen libraries and preserve independent application authority checks; do not equate TLS resumption with continued host authorization. [S14,S15]
OIDC client secrets/refresh tokens, mesh-provider setup/API keys, enterprise license keys, push credentials, desktop/VNC/RDP passwords/tickets No production credential store/issuer for these was found in the reviewed code. Some are architectural plans/research contracts; current mesh-SSH identity evidence is not proof an external provider bearer is stored. VNC/Proxmox research discusses console tickets but is not a shipped credential implementation. Not current product keys. Define custody/lifetimes per integration when implemented; external IdP/provider expiry semantics should not be guessed. Keep out of today's API-key policy decision. [S19,S20]

R2 code sources

All product links pin the reviewed dev commit. D311 links pin its separate branch head. No credentials or deployed secret files were read.

Research verification and handoff

Read-only branch research-r2, base/head f4b61cd6c3f52078cad7573c5367d7a6e75f84f0; no repository files changed, no commit, push, deployment or new measurement. Reports live in issue comments and control-host evidence directory /home/kayg/.local/state/infraternal-orchestrator/r2/. Referenced branch signin-d311 was read at 16b728e212a783a34c38e229a7a077dc64bac7f6.

A VM scripts/verify fast request was submitted through compute01 build-slot, but the gate itself never started. Verbatim scheduler output:

build-slot: [08:50:06] all 4 slots busy; queue position 9, waiting...

The scheduler printed UTC (14:20:06 IST). I canceled my queued wrapper before handoff; no R2 target directory/build/server remains. No passing fast gate is claimed; repository verification remains outstanding. Evidence here is code/source inspection and J20's cited measurements, not new runtime verification. Reports were checked for consistent table columns and source pinning.

Owner questions, recommendation first

  1. API keys: recommend explicit optional expiry and revocation/last-use UI first, with no silent lifetime change in D311. Should interactive personal CLI keys have a default hard lifetime (for example 90 days, proposed and unmeasured), while explicitly designated unattended credentials may be non-expiring? Or do you want every key to force browser reapproval at a deadline? Today there is no refresh credential, so the latter deliberately interrupts automation. A separate renewal protocol would be a new security design.
  2. Browser sessions: recommend retaining D28's 30-day sliding policy. Should cookie renewal be implemented to match DB sliding age, or should the browser require re-login 30 days after cookie issuance even if the DB row slid? Current source implements those differently.
  3. Machine trust: recommend retaining 7-day host leaves and long-lived roots while adding renewal/unlock alarms and tested rollover/re-enrollment. Is an administrator being required after a cold restart acceptable when a locked CA can eventually prevent host renewal? What offline-host duration should the re-enrollment UX support? Shortening the server listener/root lifetime must wait for their own renewal paths.
  4. Recovery/identity: recommend no calendar expiry for passkeys, BIP39 words, DEK envelopes, node identity keys or WireGuard static identities. Do you want compromise-driven replacement plus explicit rotation controls, rather than periodic forced replacement that can strand offline hosts or destroy the last account recovery route? Routine protocol traffic-key rekey already belongs to the underlying transports.
## R2: complete credential inventory and what expiry means **Recommendation: do not give every “key” the same expiry.** Keep bootstrap/approval/ceremony capabilities short and single-use; automatically renew transport certificates; retain explicit revocation for stable identities and recovery material. For API keys, add visible lifetime/last-use/revocation management before introducing a default deadline. Today an expired API key would require browser approval again; there is no refresh-token flow. This is a code inventory, not a proposed behavior change. Reviewed `origin/dev` **f4b61cd6c3f52078cad7573c5367d7a6e75f84f0**, plus `origin/signin-d311` **16b728e212a783a34c38e229a7a077dc64bac7f6**. The latter adds CLI storage and approval UX for the **existing API-key type**. It changes no API-key lifetime. Production-looking test fixtures and research spikes are not counted as shipped credential systems. Storage shorthand: **DB** = the configured server data directory's `infraternal.sqlite`, opened with private directory/file permissions (0700/0600); this is not whole-database encryption. **CA** = configured CA directory, default `$XDG_DATA_HOME/infraternal/ca`, falling back to `~/.local/share/infraternal/ca`. Automatically provisioned files are under `CA/active`; externally provisioned legacy configurations can use explicitly configured private paths. **Node data** = configured infraternal agent data directory; CLI default `$XDG_DATA_HOME/infraternal/agent` or `~/.local/share/infraternal/agent`; service installs can select their own private state path. Paths here are code defaults, not observations of anyone's deployed secret files. [Runtime/storage S1](#r2-code-sources). ### User identity, browser sessions and device authorization | Credential/type and purpose | Holder, storage and protection | Current lifetime / rotation / revocation | What expiry means for the user; recommendation | |---|---|---|---| | **Owner setup token**: creates the first owner | Local server operator obtains it through private `setup-token.sock`/local CLI; server retains raw token in process memory and SHA-256 hash in DB `setup`. Startup uses a private socket rather than treating the old banner format as permission to log secrets. | **24 hours**, one successful owner registration consumes it; startup before owner creation replaces the hash and invalidates the previous token. No token after an owner exists. | Expiry prevents initial enrollment, not an existing owner's access. Restart before owner setup to mint a fresh capability, then reread it locally. **Keep one-use + 24 h; expose clear expired-token guidance.** [S1,S2] | | **Passkey/WebAuthn private credential**: signs login and fresh-action challenges | Private key stays in the authenticator/passkey provider, including its provider-managed sync if used. DB `passkeys.credential` contains public credential/counter metadata; `id`, `aaguid`, `prf_input` are not private signing keys. | **No wall-clock expiry.** Up to two passkeys in registration logic. Recovery replaces account credentials. I did not find a general normal passkey-delete management endpoint in this snapshot; do not assume a complete revoke UI. | A server-imposed deadline means enroll a replacement passkey before it expires, or use the second passkey/recovery. Deleting a server row does not erase copies in a provider. **No routine expiry; add safe replacement/revocation and test last-passkey handling.** [S3,S5] | | **WebAuthn PRF output / derived passkey wrapping key**: unlocks account encryption key | Authenticator produces PRF secret into browser memory; HKDF-derived AES-GCM key stays browser-side. Public PRF input is persisted in `passkeys.prf_input`; salts/nonces/envelopes in `wraps` are public parameters/ciphertext. | Re-derived on demand; temporary buffers are cleared. No independent expiry timestamp. The server never stores PRF output. Some passkey transports/providers cannot return PRF even when authentication succeeds. | “Expiry” cannot repair missing PRF support; use a compatible passkey/provider path or recovery. Changing derivation/input without rewrapping strands encrypted material. **Treat as transient secret, not an expiring bearer token; version and migrate envelopes.** [S3,S4] | | **Account DEK and its passkey/recovery envelopes**: the account's 32-byte wrapping root | Browser generates/unwraps DEK in memory. DB `wraps(user_id,kind,key_id,blob)` stores AES-256-GCM encrypted DEK copies, one per passkey/recovery recipient. Browser login verifies unwrap then clears its working DEK; no persistent plaintext browser DEK store was found. | **No TTL.** Ciphertext is recipient-bound and versioned. Recovery replaces wraps; this is not a general automatic DEK rotation service. | Destroying/expiring the only decryptable envelope can permanently lose access to encrypted material. Rotating DEK requires re-encryption/rewrapping, not changing a timestamp. **No age expiry; explicit audited cryptographic migration with recovery verified first.** The envelope system exists; it does not prove a complete persistent SSH-secret vault is wired. [S4,S5] | | **Recovery phrase**: alternative account recovery/unwrapping authority | **24 English BIP39 words**, generated in the browser; holder must keep an offline/private copy. V2 derives separate HKDF siblings for authentication verifier and recovery wrap. DB `users.recovery_hash` holds an Argon2id verifier; `wraps` holds recovery ciphertext, never the words. Legacy v1 metadata still has compatibility paths. | **No expiry.** V1-to-v2 signed-in migration is one-shot; do not describe it as an unrestricted rotate-anytime endpoint. Successful recovery replaces passkeys/wraps and revokes sessions/API keys/approvals for that account. | Phrase expiry could remove the last recovery route while all passkeys are unavailable. The words cannot renew themselves silently without generating and safely recording a new phrase. **No calendar expiry; replace on compromise or derivation migration with an explicit backup-confirmation ceremony.** A completed recovery is account-wide credential replacement, not just a new browser session. [S4,S5] | | **Recovery authentication verifier / recovery wrapping sibling** | Verifier is transmitted over HTTPS and Argon2id-hashed by the server. Independent wrap sibling stays in browser memory. Domain separation prevents a server-held authentication verifier from unwrapping the DEK. | Same generation as the recovery phrase; old verifier/capabilities are invalidated by replacement. No independent TTL. | Expire/replace verifier and matching envelope coherently, or valid words can authenticate but fail to decrypt. **Keep siblings separate, version them together, never turn the wrap sibling into a bearer credential.** [S4,S5] | | **Pending recovery bearer**: completes one delayed recovery request | Browser memory; DB `recovery.token_hash`, `ready`, `expires`. Raw high-entropy token is not a persistent plaintext DB value. | **48 h bearer lifetime** from issue/resume; **24 h delay**, waived only by a live unlocked same-account session when creation authorizes it. Resume with verified recovery secret replaces bearer and extends its lifetime while retaining original ready time. Same-account signed-in cancellation; completion consumes it. | Expired bearer requires proving words again to resume; it does not make the recovery phrase expire or reset the existing waiting deadline. **Keep short-lived bearer, cancellation and preserved-delay resumption.** [S5] | | **Browser session cookie**: ordinary UI/WebSocket authority | Browser `__Host-infraternal`; Secure, HttpOnly, SameSite=Strict, Path=/; DB `sessions` stores hash, user/device/passkey, expiry and freshness. | DB **30 days sliding** on authorized activity, never resurrecting expired/revoked rows. Cookie is issued with **Max-Age=2,592,000** on login/registration. Ordinary session reads do **not** reissue Set-Cookie, so browser cookie lifetime and DB sliding lifetime are distinct in this snapshot. Logout deletes the session; recovery revokes all account sessions. | Expired/revoked DB row or missing browser cookie requires a fresh login. WebSocket authority is checked and cannot live forever merely because the socket is open; terminal PTY persistence is separate from browser authorization. **Retain D28; separately decide whether to renew browser cookie age on safe activity to match intended sliding behavior.** No claim here that 30-day cookie renewal is already implemented. [S6,S7] | | **Restricted pending-login cookie and completion transaction ID** | Same cookie/`sessions` table with `unlocked=0`; page gets a domain-separated transaction ID derived from the cookie. Completion ID cannot be used as a session bearer; it binds local unwrap completion/cleanup to that login. | **5 minutes, non-sliding** until `unlock_finish`. Then session becomes unlocked with 30-day DB expiry. Expired/incomplete pending rows cannot access terminals. | Local unwrap failure/timeout means login again. A delayed cleanup must not revoke a subsequent login. **Keep 5 min and exact transaction binding; no separate refresh token.** [S3,S6] | | **Fresh-passkey authority timestamp**: permits destructive/fresh actions | `sessions.fresh` server-side, backed by a successful UV-required passkey ceremony. It is not a separate key. | **300 seconds**; fresh-action path rejects future/stale timestamps. | Only fresh/destructive operations stop until another passkey tap; ordinary session can remain valid. **Keep separate from session expiry.** Extending the browser session should not silently refresh freshness. [S6] | | **WebAuthn ceremony ID/challenge and browser-binding cookie** | Server process-local bounded pending maps retain WebAuthn state; browser receives ID/challenge and `__Host-infraternal-ceremony`, Secure/HttpOnly/Strict, bound by hash to pending state. These are ceremony context, not durable login credentials. | **5 minutes**, consumed on finish; pending state disappears on restart. Binding cookie Max-Age=300. CA-specific ceremonies also have 300-second, purpose/session/membership bindings. | An expired challenge or restarted server means retry the tap/registration, not recover the whole account. **Keep 5 min, consume once, bind origin/browser/purpose; do not persist challenge authority across restarts.** [S3,S8,S12] | | **Device-code polling token**: CLI redeems a browser-approved request | CLI process memory, `Zeroizing<String>` in `signin-d311`; DB `approvals.token_hash`. Never URL/QR/output/disk. | **600 seconds**, one successful redemption atomically deletes approval and creates API-key hash. Closing approval page does not cancel it; no decline endpoint exists. | Expiry means rerun `infraternal sign-in <server>`. No API key is issued by a code merely expiring. A lost successful redemption response can leave a live API key the CLI never received. **Keep 10 min/one-use; consider acknowledged redemption separately; never treat an ambiguous response as proof no key exists.** [S9,S10] | | **Device user code / approval URL**: locates request for browser review | Displayed to user and in `?device_code=...`; DB `approvals.code_hash`. Lower-authority context: code alone cannot redeem or approve; browser session plus fresh passkey approval is required. Current code is 12 random bytes encoded to 16 base64url characters. | Same **10 min** deadline as approval; approval binds exact device/scopes and may occur only once. Row lasts until redemption/expiry cleanup. | Stale link requires a new sign-in. **Keep short lifetime, visible code-match confirmation and exact scope review; do not put poll token/API key in the link.** [S9,S10] | | **API key / CLI/MCP device credential**: scoped automation bearer | Server stores SHA-256 hash in DB `api_keys(token_hash,user_id,device,scopes)`. `signin-d311` stores plaintext bearer in `$XDG_CONFIG_HOME/infraternal/credentials.json` or `~/.config/infraternal/credentials.json`, in 0700 owned directory/0600 owned file, locked fd, no-follow path checks, atomic replacement and fsync. This is permission protection, **not at-rest encryption/keychain storage**. | **No current expiry, rotation or refresh protocol.** Authorization checks exact scope. `/auth/api-revoke` deletes only own key and needs fresh passkey; recovery deletes account keys. Current scopes are `hosts:read`, `terminal:connect`, `actions:dry-run`; a key grants no browser session or fresh-tap privilege. | Adding hard expiry will make new API authorization fail and require interactive browser sign-in again, including unattended users/scripts. It cannot renew silently today. Deleting a local credential does not revoke server key. Account UI currently lacks complete key management despite existing list/revoke backend. **Recommend explicit per-key expiry choice, created/last-used/expires metadata, warnings and replacement overlap; retain an owner-approved no-expiry option for unattended automation until a designed renewal/re-auth flow exists. Do not shorten lifetime in the D311 wiring slice.** [S9,S10] | | **WebSocket resume token**: recovers a disconnected viewer connection | 32 random bytes to browser memory via Welcome; SHA-256 in DB `websocket_connections`. Bound to connection/user and still requires live session-cookie authority. | **120 seconds after disconnect**; active row cannot be concurrently resumed. Successful resume rotates token. Startup marks previously active rows disconnected for bounded resume. | Expiry loses connection-resume shortcut, not account credentials or necessarily the long-lived terminal shim. A fresh authenticated attach can recover a retained session under normal session policy. **Keep short, rotate on successful claim, retain separate live-session check.** [S7] | ### Machine trust, host access and encrypted authority | Credential/type and purpose | Holder, storage and protection | Current lifetime / rotation / revocation | What expiry means for the user; recommendation | |---|---|---|---| | **Machine CA Ed25519 signing key and root certificate**: issues remote-host identities | Unlocked private seed only in server memory; protected OpenSSL secure heap, root refuses unavailable protection, nonroot warns. Encrypted administrator copies in `CA/active/administrators/administrator-<member-hash>.<generation>.json`, or configured CA-copy directory. AES-256-GCM binds issuer/member/generation. Public `issuer.pem` and public DB certificate ledger are not secret keys. | Generated root certificate lasts **315,360,000 s (3,650 days)**. Private key itself has no TTL. Cold server start locks signer; ordinary update/rollback can hand it over in memory. No complete automated root rollover was found. Signing refuses leaves that outlive issuer. | Root expiry prevents valid machine authentication/issuance even if the signing key is still present. Swapping root without distributing overlap trust disconnects the fleet. **Keep long-lived root for now; build advance warnings, authenticated overlapping root rollout and recovery before enforcing shorter roots. Back up encrypted copies plus account recovery material.** [S11,S12] | | **CA administrator unlock material / membership envelope** | Browser unwraps DEK using passkey PRF, derives scoped CA key with HKDF-SHA256 over member/generation/salt; sends scoped key with purpose-bound passkey proof over same-origin HTTPS. Server uses temporary zeroizing scoped key to open AES-GCM copy. DB `ca_administrators` records membership generation, not signing seed/envelope. | Unlock challenge **300 s**. Scoped derivation has no independent wall-clock expiry; authority requires current membership. Removing membership invalidates its generation/copy and affects custody state; no periodic unlock timeout was found in `MemoryCaSource`. Cold restart requires admin unlock; in-process signer is not a 5-minute lease. | Expiring an unlock ceremony means retry tap. Relocking the signer blocks new enrollment/renewal; existing valid host TLS does not need the signer merely to verify. If left locked, host leaves eventually expire. A removed administrator who previously extracted private material cannot be made to forget it by deleting ciphertext. **Keep ceremony short and membership checks strict; do not introduce frequent CA relocking without a documented availability policy and expiry alerts. Compromised CA requires CA rotation, not envelope deletion alone.** [S11,S12] | | **Server remote-listener Ed25519 key and leaf certificate**: server side of host mTLS | `CA/active/remote.key` (unencrypted PKCS#8 PEM, private file/dir) and `remote.pem`; loaded into server memory. Distinct key from the CA signer. Configured legacy paths may differ. | Bootstrap gives it the **same 3,650-day validity** as issuer, not the 7-day host lifetime. No automatic server-listener leaf renewal path found. | Expiry breaks host TLS handshakes/reconnections independently of whether the CA is unlocked. **Give this its own renewal lifecycle with overlap/reload before shortening lifetime; alert well in advance. Do not claim host half-life renewal covers it.** [S11] | | **Public HTTPS server leaf/key**: browser/API and enrollment HTTPS | Operator-configured `TlsFiles.certificate/private_key` PEM paths, or upstream reverse proxy when plain HTTP is explicitly configured behind it. Loaded by OpenSSL listener. No automatic public certificate issuance is wired in current product. | Validity is whatever the external issuer/operator supplied. Runtime startup loads config; no product ACME renewal/reload orchestrator was found. | Expiry causes browser/CLI TLS rejection and can block enrollment. Operator/proxy renews and applies certificate. **Automatically renew with the approved proxy/ACME implementation, show renewal health, keep this trust domain separate from machine CA.** [S1,S11] | | **Node Ed25519 identity private key**: durable host possession, signs enrollment/renewal CSR | Generated on node; unencrypted base64 PKCS#8 in private `Node data/identity.json` with host ID, endpoint, CA and current certificate. Private paths checked, publication atomic. Server sees public key/certificate only in `remote_certificates`, never node private key. | **No independent TTL and no automatic key rotation.** Authorized re-enrollment deliberately retains the same key; certificate renewal retains it too. Host tombstone/certificate revocation controls admission. | Expiring/deleting the key would require fresh secure enrollment and can change host identity semantics. An expired certificate does not erase this key. **Stable key, explicit compromise rotation/re-enrollment; do not use a cert's 7-day validity as private-key expiry.** [S13,S14] | | **Node client mTLS leaf certificate**: remote-host access authority | Public cert in `identity.json`; DB `remote_certificates` records fingerprint, host, issuer, public key, validity and revocation. Private key as preceding row. | **Exactly 604,800 s (7 days)**; server schedules renewal at half-life (~3.5 days) on authenticated control stream. Host signs CSR with retained key; server commits new cert, node validates/fsyncs it, then Ack extends authority. Locked CA defers renewal (hour retry and unlock notification). Host removal/revocation prevents admission; active authority has an expiry guard. | Offline node beyond expiry cannot connect to request normal live renewal. User needs an owner-authorized fresh join/re-enrollment with existing retained key; not silent renewal from an expired credential. Locked signer plus prolonged admin absence can strand hosts. **Keep 7 days/half-life, add visible remaining-validity/locked-CA alerts and clear authenticated re-enrollment recovery; no grace acceptance of expired mTLS.** [S13,S14] | | **Host join/enrollment token**: authorizes one bound host/mode enrollment | High-entropy raw token passes private stdin/descriptor invitation; DB `remote_join_tokens` stores SHA-256 digest, host/mode, timestamps/consumption only. Invitation also carries public endpoint/pins, not CA private material. | **900 s (15 min)**, one-use transaction; replacement invalidates predecessor; revoked host cannot use it. | Expiry blocks this enrollment attempt; it does not disconnect an already enrolled host. Issue a fresh owner-authorized invitation and retry. **Keep 15 min/one-use/host+mode binding; never long-lived reusable setup credentials.** [S13] | | **Mesh WireGuard X25519 static key**: node-local encrypted mesh identity | Generated on node, transient `Zeroizing` key buffer, installed through netlink; private key remains in **kernel WireGuard interface memory**, not SQLite or node JSON. DB mesh tables and node checkpoint/interface ownership files contain public keys only. Retained interface can survive infraternal agent process restart; deleting interface/reboot loses kernel key and requires fresh setup. | **No scheduled static-key expiry found.** Coordinator library supports rotation with at least **120 s overlap plus peer delivery acknowledgements**, but no production plugin call to `rotate_key` was found, so that is a capability, not a shipped automatic rotation loop. WireGuard's own traffic/session rekey is separate. Revocation/policy updates remove access when delivered. | Hard local static-key expiry could cut off mesh traffic while server/control path is unavailable, undermining offline retained policy. **No autonomous calendar cutoff; implement coordinated overlap/ack rotation and fallback reachability before scheduling static-key replacement. On compromise remove/revoke, and report delivery/offline limits truthfully.** [S15] | | **Dedicated server SSH Ed25519 identity (D183)**: passwordless host bootstrap/login credential | `server_ssh_key` DB singleton holds public key and opaque administrator-wrapped envelope; `StoredSshKey` holds plaintext only while unlocked. Scoped helper receives zeroizing PEM through stdin, not a persistent private-key file. Vault/unlock are explicit adapter interfaces; this source proves custody primitives, not every production vault integration. | **No expiry or automatic rotation**; cold source locked, explicit lock has generation protection against in-flight unwrap. Update handoff can preserve unlocked material in memory. | Replacing key requires updating target `authorized_keys`/trust first. Expiry would break new bootstrap/SSH authentication, not automatically terminate established SSH sessions. **Stable key with explicit coordinated rotation and alternate host access; do not set a short TTL before distribution exists.** [S16] | | **User-supplied SSH private key, key passphrase and SSH password** | Browser-unwrapped or one-attempt input transferred over authenticated socket. Rust `Secret` zeroizes on drop. Private key enters temporary scoped `ssh-agent` through `ssh-add -`; password/passphrase served once through private askpass socket after peer executable/parent checks. No plaintext key written to disk; private temporary helper directory and sockets cleaned. **A persistent per-host encrypted-secret table was not found on these refs; D23 is not proof such a vault is complete.** | One-use auth override has **60 s** acceptance deadline; unused secrets clear on expiry/drop and helpers retire after authentication. Actual SSH key/password lifetime/revocation belongs to target/issuer; no product-wide persistent expiry. | Expired one-attempt input means ask again; completed authenticated connection can continue. Target password rotation or revoked key breaks next authentication. **Keep operation-scoped plaintext; store only encrypted credentials if/when vault is wired; let issuer validity drive SSH certificate deadlines.** [S17] | | **SSH host-key pins, known-host CA keys and revocation lists**: public server identity trust | Public keys in app-managed known-host store or validated system OpenSSH known-host/trust paths. They are public trust anchors, not bearer secrets. Ambient `IdentityFile`/client certificate/SSH_AUTH_SOCK are intentionally excluded from D183 auth configuration. | Plain host keys have **no expiry**; SSH certificates carry issuer validity; revocation follows configured lists/trust changes. Confirmation requires authorized owner and exact observed key. | Expiring a pin does not renew the server's identity; autoaccepting its replacement enables impersonation. **Preserve strict pin checks, explicit verified replacement and issuer validity; never silently erase expired/old trust to reconnect.** [S17] | ### Release and external credentials, plus requested types that do not exist yet | Credential/type | Holder/storage/protection | Current lifetime and user impact; recommendation | |---|---|---| | **Release Ed25519 signing key** | `apps/release-signer` expects Worker secret `SIGNING_KEY`, provisioned from owner's password manager. Runtime imports nonextractable WebCrypto signing key. Compiled public `release/allowed_signers` is verification trust, not private material. Code documents custody intent; no deployed secret value/config was inspected. | **No key-age expiry in verifier.** Signed checksum manifests use OpenSSH signature namespace `infraternal-release`. Rotation must update accepted public trust in deployed binaries before retiring old signer. Losing/expiring key blocks new releases/updates, not execution of existing binaries. **Overlap trust rollout, offline backup and compromise procedure; do not make old signatures invalid solely by a calendar deadline without an update/recovery plan.** [S18] | | **Release signing caller bearer** | Worker secret `CALLER_TOKEN`; pipeline environment `RELEASE_CALLER_TOKEN`; shared bearer authorizes `/sign`. Code hashes comparisons; secret never belongs in argv/repo. | **No app-enforced expiry**; replacing Worker secret invalidates old caller immediately. Expiry/revocation stops signing until pipeline updated; already signed releases unaffected. **Scoped CI custody, periodic coordinated replacement and immediate compromise revocation; choose operational cadence separately from product API keys.** [S18] | | **Release storage/provider API bearer** | Pipeline environment `CLOUDFLARE_API_TOKEN`, used by `scripts/release/publish` for object storage API; curl config supplied over stdin. Token provisioning/protection outside pipeline is operator-managed, not a product DB row. Worker `BUILDS` is a platform binding, not a user-exported storage secret in code. | Lifetime/revocation managed by external provider. Expiry blocks uploads/release publication, not running installations. **Least privilege to release storage, provider expiry alerts and coordinated CI renewal.** No secret values read. [S18] | | **Notification URL if it embeds access authority** | Optional signer `NOTIFY_URL` binding; could be just a public endpoint or include an opaque topic/token. Code does not define a distinct notification-key schema. | No application TTL; notification is best effort. Expiry breaks release notifications only. **Treat as secret if authority-bearing; rotate through deployment configuration without coupling it to signing-key lifetime.** [S18] | | **DNS-01 provider token** | **Not implemented/persisted in current product.** J20 uses a local stub provider in research. Future Traefik/ACME integration needs a scoped DNS credential and approved custody on the issuing node/controller. A release-storage API token is not evidence of a DNS token. | Provider TTL/revocation would stop new issuance/renewal, while issued certificates work until their own expiry. **Use narrow zone/record permissions, private file delivery when supported, advance provider/certificate warnings and independent replacement.** Storage path and default TTL remain a design decision; do not invent one. [S19] | | **ACME account private key, issued service private keys/certificates, optional EAB key** | **Research/future integration**, not present product custody on these refs. A Traefik resolver would normally retain account/certificate material in private `acme.json`; external issuer paths could differ. EAB exists only if chosen CA requires it. | Account authorization and cert validity are distinct; provider expiry does not invalidate an already issued certificate. **Persist/backup account material, renew leaves automatically with overlap, never use one blanket “ACME key expiry.”** Exact policy awaits proxy/CA decision. [S19] | | **Relay tickets / relay shared secrets** | **No runtime credential implementation found.** `crates/plugins/relay/src/lib.rs` explicitly declares CLI actions before relay runtime. Existing terminal `relay.rs` is flow-control/replay code, not a ticket issuer. | No current storage/lifetime/revocation to report. **When implemented, short-lived audience/node/session-bound tickets with current authorization checks; ask owner then.** Do not claim these keys already exist. [S20] | | **Capability tokens** | Existing bearer capabilities are **setup, join, sessions, recovery, device polling, API and resume tokens**, individually inventoried above. `Hello` capability bits and plugin action capabilities are feature/permission declarations, not additional secret tokens. Server-path transport proofs are authenticated-connection authority, not persisted user API keys. | Apply each token's real lifetime, rather than adding a fictional generic capability TTL. **Keep audiences/purposes separate.** [S2,S6,S7,S9,S13,S20] | | **TLS/QUIC/WireGuard traffic keys and TLS session tickets** | Crypto library/kernel connection state, distinct from persisted node/CA/static keys. No app-level durable session-ticket secret store or management API found. QUIC explicitly disables early data (`enable_early_data=false`). | Library/protocol handshakes and rekey rules govern them; no user-configured product expiry policy. **Keep protocol key management inside chosen libraries and preserve independent application authority checks; do not equate TLS resumption with continued host authorization.** [S14,S15] | | **OIDC client secrets/refresh tokens, mesh-provider setup/API keys, enterprise license keys, push credentials, desktop/VNC/RDP passwords/tickets** | No production credential store/issuer for these was found in the reviewed code. Some are architectural plans/research contracts; current mesh-SSH identity evidence is not proof an external provider bearer is stored. VNC/Proxmox research discusses console tickets but is not a shipped credential implementation. | **Not current product keys.** Define custody/lifetimes per integration when implemented; external IdP/provider expiry semantics should not be guessed. **Keep out of today's API-key policy decision.** [S19,S20] | ### R2 code sources All product links pin the reviewed dev commit. D311 links pin its separate branch head. No credentials or deployed secret files were read. * **S1:** [runtime DB permissions, TLS files, setup socket](https://git.kayg.org/kayg/infraternal/src/commit/f4b61cd6c3f52078cad7573c5367d7a6e75f84f0/crates/infraternal-server/src/runtime.rs), [core auth schema](https://git.kayg.org/kayg/infraternal/src/commit/f4b61cd6c3f52078cad7573c5367d7a6e75f84f0/crates/infraternal-server/migrations/0001_auth.sql). * **S2:** [setup expiry/consumption](https://git.kayg.org/kayg/infraternal/src/commit/f4b61cd6c3f52078cad7573c5367d7a6e75f84f0/crates/infraternal-server/src/auth/setup.rs). * **S3:** [WebAuthn lifecycle](https://git.kayg.org/kayg/infraternal/src/commit/f4b61cd6c3f52078cad7573c5367d7a6e75f84f0/crates/infraternal-server/src/auth/webauthn.rs), [ceremony constants](https://git.kayg.org/kayg/infraternal/src/commit/f4b61cd6c3f52078cad7573c5367d7a6e75f84f0/crates/infraternal-server/src/auth/mod.rs). * **S4:** [browser BIP39/PRF/HKDF/AES-GCM](https://git.kayg.org/kayg/infraternal/src/commit/f4b61cd6c3f52078cad7573c5367d7a6e75f84f0/apps/tracer/src/auth.ts), [server ciphertext envelopes](https://git.kayg.org/kayg/infraternal/src/commit/f4b61cd6c3f52078cad7573c5367d7a6e75f84f0/crates/infraternal-server/src/auth/wraps.rs), [Argon2 verifier](https://git.kayg.org/kayg/infraternal/src/commit/f4b61cd6c3f52078cad7573c5367d7a6e75f84f0/crates/infraternal-server/src/auth/crypto.rs). * **S5:** [recovery delay, resume and account credential reset](https://git.kayg.org/kayg/infraternal/src/commit/f4b61cd6c3f52078cad7573c5367d7a6e75f84f0/crates/infraternal-server/src/auth/recovery.rs), [one-shot legacy recovery migration](https://git.kayg.org/kayg/infraternal/src/commit/f4b61cd6c3f52078cad7573c5367d7a6e75f84f0/crates/infraternal-server/src/auth/recovery_rotation.rs). * **S6:** [session sliding, pending authority and completion ID](https://git.kayg.org/kayg/infraternal/src/commit/f4b61cd6c3f52078cad7573c5367d7a6e75f84f0/crates/infraternal-server/src/auth/sessions.rs), [HTTP cookie attributes/issuance](https://git.kayg.org/kayg/infraternal/src/commit/f4b61cd6c3f52078cad7573c5367d7a6e75f84f0/crates/infraternal-server/src/auth/http.rs). * **S7:** [WebSocket authority and resume lifecycle](https://git.kayg.org/kayg/infraternal/src/commit/f4b61cd6c3f52078cad7573c5367d7a6e75f84f0/crates/infraternal-server/src/websocket.rs), [resume schema](https://git.kayg.org/kayg/infraternal/src/commit/f4b61cd6c3f52078cad7573c5367d7a6e75f84f0/crates/infraternal-server/migrations/0003_websocket_resume.sql). * **S8:** [browser ceremony-binding cookie](https://git.kayg.org/kayg/infraternal/src/commit/f4b61cd6c3f52078cad7573c5367d7a6e75f84f0/crates/infraternal-server/src/auth/binding.rs). * **S9:** [device codes, redemption, API authorization/revoke/list](https://git.kayg.org/kayg/infraternal/src/commit/f4b61cd6c3f52078cad7573c5367d7a6e75f84f0/crates/infraternal-server/src/auth/devices.rs). * **S10:** [D311 exact CLI storage/Zeroizing/atomic fd-bound persistence](https://git.kayg.org/kayg/infraternal/src/commit/16b728e212a783a34c38e229a7a077dc64bac7f6/crates/infraternal/src/sign_in.rs), [known UI/protocol gaps and branch findings](https://git.kayg.org/kayg/infraternal/issues/269). * **S11:** [CA and remote-listener ten-year bootstrap](https://git.kayg.org/kayg/infraternal/src/commit/f4b61cd6c3f52078cad7573c5367d7a6e75f84f0/crates/infraternal-server/src/ca_bootstrap.rs), [state paths/private files](https://git.kayg.org/kayg/infraternal/src/commit/f4b61cd6c3f52078cad7573c5367d7a6e75f84f0/crates/infraternal-server/src/remote_startup.rs), [locked memory signer/update handoff](https://git.kayg.org/kayg/infraternal/src/commit/f4b61cd6c3f52078cad7573c5367d7a6e75f84f0/crates/infraternal-server/src/ca_memory.rs). * **S12:** [encrypted administrator copies](https://git.kayg.org/kayg/infraternal/src/commit/f4b61cd6c3f52078cad7573c5367d7a6e75f84f0/crates/infraternal-server/src/ca_copies.rs), [AES-GCM seed envelope](https://git.kayg.org/kayg/infraternal/src/commit/f4b61cd6c3f52078cad7573c5367d7a6e75f84f0/crates/infraternal-server/src/ca/wrapping.rs), [membership/unlock](https://git.kayg.org/kayg/infraternal/src/commit/f4b61cd6c3f52078cad7573c5367d7a6e75f84f0/crates/infraternal-server/src/ca_administration.rs), [browser scoped HKDF](https://git.kayg.org/kayg/infraternal/src/commit/f4b61cd6c3f52078cad7573c5367d7a6e75f84f0/apps/tracer/src/canvas/addhost/authority.on-demand.ts). * **S13:** [join token transaction](https://git.kayg.org/kayg/infraternal/src/commit/f4b61cd6c3f52078cad7573c5367d7a6e75f84f0/crates/infraternal-server/src/enrollment.rs), [remote identity schema](https://git.kayg.org/kayg/infraternal/src/commit/f4b61cd6c3f52078cad7573c5367d7a6e75f84f0/crates/infraternal-server/migrations/0015_remote_identity.sql), [node identity persistence/re-enrollment](https://git.kayg.org/kayg/infraternal/src/commit/f4b61cd6c3f52078cad7573c5367d7a6e75f84f0/crates/infraternal/src/remote.rs). * **S14:** [half-life renewal scheduler](https://git.kayg.org/kayg/infraternal/src/commit/f4b61cd6c3f52078cad7573c5367d7a6e75f84f0/crates/infraternal-server/src/remote_renewal.rs), [node CSR/cert validation and expiry refusal](https://git.kayg.org/kayg/infraternal/src/commit/f4b61cd6c3f52078cad7573c5367d7a6e75f84f0/crates/plugins/remote/src/remote_renewal.rs), [active host authority](https://git.kayg.org/kayg/infraternal/src/commit/f4b61cd6c3f52078cad7573c5367d7a6e75f84f0/crates/infraternal-server/src/remote_authority.rs), [QUIC TLS policy](https://git.kayg.org/kayg/infraternal/src/commit/f4b61cd6c3f52078cad7573c5367d7a6e75f84f0/crates/plugins/remote/src/remote_quic.rs). * **S15:** [kernel key custody](https://git.kayg.org/kayg/infraternal/src/commit/f4b61cd6c3f52078cad7573c5367d7a6e75f84f0/crates/plugins/mesh/src/wireguard.rs), [node retained interface identity](https://git.kayg.org/kayg/infraternal/src/commit/f4b61cd6c3f52078cad7573c5367d7a6e75f84f0/crates/plugins/mesh/src/node.rs), [rotation overlap/acks/offline rules](https://git.kayg.org/kayg/infraternal/src/commit/f4b61cd6c3f52078cad7573c5367d7a6e75f84f0/crates/infraternal-mesh/src/coordinator.rs). * **S16:** [dedicated SSH key custody](https://git.kayg.org/kayg/infraternal/src/commit/f4b61cd6c3f52078cad7573c5367d7a6e75f84f0/crates/infraternal-server/src/ssh_key.rs). * **S17:** [one-use/60-second SSH override](https://git.kayg.org/kayg/infraternal/src/commit/f4b61cd6c3f52078cad7573c5367d7a6e75f84f0/crates/plugins/ssh/src/auth_once.rs), [temporary helper custody](https://git.kayg.org/kayg/infraternal/src/commit/f4b61cd6c3f52078cad7573c5367d7a6e75f84f0/crates/plugins/ssh/src/credentials.rs), [trust checks](https://git.kayg.org/kayg/infraternal/src/commit/f4b61cd6c3f52078cad7573c5367d7a6e75f84f0/crates/plugins/ssh/src/trust.rs), [public SSH config/ambient identity exclusion](https://git.kayg.org/kayg/infraternal/src/commit/f4b61cd6c3f52078cad7573c5367d7a6e75f84f0/crates/plugins/ssh/src/config.rs). * **S18:** [release signing Worker and bindings](https://git.kayg.org/kayg/infraternal/src/commit/f4b61cd6c3f52078cad7573c5367d7a6e75f84f0/apps/release-signer/src/index.js), [compiled signature verifier](https://git.kayg.org/kayg/infraternal/src/commit/f4b61cd6c3f52078cad7573c5367d7a6e75f84f0/crates/infraternal-server/src/release_verify.rs), [publish token usage](https://git.kayg.org/kayg/infraternal/src/commit/f4b61cd6c3f52078cad7573c5367d7a6e75f84f0/scripts/release/publish), [private curl configuration](https://git.kayg.org/kayg/infraternal/src/commit/f4b61cd6c3f52078cad7573c5367d7a6e75f84f0/scripts/release/common). * **S19:** [J20 DNS-01 research and limits](https://git.kayg.org/kayg/infraternal/issues/261#issuecomment-71101), [future Traefik account storage](https://doc.traefik.io/traefik/v3.7/reference/install-configuration/tls/certificate-resolvers/acme/). Current product source searches for `acme`, `DNS-01`, `instant-acme` in `crates`/tracer find no implemented issuer/provider store. * **S20:** [relay declaration without runtime](https://git.kayg.org/kayg/infraternal/src/commit/f4b61cd6c3f52078cad7573c5367d7a6e75f84f0/crates/plugins/relay/src/lib.rs), [protocol feature bits/resume fields](https://git.kayg.org/kayg/infraternal/src/commit/f4b61cd6c3f52078cad7573c5367d7a6e75f84f0/crates/infraternal-protocol/src/lib.rs), [future desktop integration research](https://git.kayg.org/kayg/infraternal/src/commit/f4b61cd6c3f52078cad7573c5367d7a6e75f84f0/research/vnc/DESIGN.md). ### Research verification and handoff Read-only branch `research-r2`, base/head `f4b61cd6c3f52078cad7573c5367d7a6e75f84f0`; no repository files changed, no commit, push, deployment or new measurement. Reports live in issue comments and control-host evidence directory `/home/kayg/.local/state/infraternal-orchestrator/r2/`. Referenced branch `signin-d311` was read at `16b728e212a783a34c38e229a7a077dc64bac7f6`. A VM `scripts/verify fast` request was submitted through compute01 `build-slot`, but the gate itself never started. Verbatim scheduler output: ```text build-slot: [08:50:06] all 4 slots busy; queue position 9, waiting... ``` The scheduler printed UTC (14:20:06 IST). I canceled my queued wrapper before handoff; no R2 target directory/build/server remains. **No passing fast gate is claimed; repository verification remains outstanding.** Evidence here is code/source inspection and J20's cited measurements, not new runtime verification. Reports were checked for consistent table columns and source pinning. ### Owner questions, recommendation first 1. **API keys: recommend explicit optional expiry and revocation/last-use UI first, with no silent lifetime change in D311.** Should interactive personal CLI keys have a default hard lifetime (for example 90 days, proposed and unmeasured), while explicitly designated unattended credentials may be non-expiring? Or do you want every key to force browser reapproval at a deadline? Today there is no refresh credential, so the latter deliberately interrupts automation. A separate renewal protocol would be a new security design. 2. **Browser sessions: recommend retaining D28's 30-day sliding policy.** Should cookie renewal be implemented to match DB sliding age, or should the browser require re-login 30 days after cookie issuance even if the DB row slid? Current source implements those differently. 3. **Machine trust: recommend retaining 7-day host leaves and long-lived roots while adding renewal/unlock alarms and tested rollover/re-enrollment.** Is an administrator being required after a cold restart acceptable when a locked CA can eventually prevent host renewal? What offline-host duration should the re-enrollment UX support? Shortening the server listener/root lifetime must wait for their own renewal paths. 4. **Recovery/identity: recommend no calendar expiry for passkeys, BIP39 words, DEK envelopes, node identity keys or WireGuard static identities.** Do you want compromise-driven replacement plus explicit rotation controls, rather than periodic forced replacement that can strand offline hosts or destroy the last account recovery route? Routine protocol traffic-key rekey already belongs to the underlying transports.
Author
Owner

J36 verification correction on signin-d311 @ 16b728e212a783a34c38e229a7a077dc64bac7f6: the first post-rebase scripts/verify fast run failed seven restart fixture tests with PermissionDenied. The fixture helper explicitly keeps executable fixtures under trusted $HOME ancestry, while that run set INFRATERNAL_TEST_STATE_ROOT=/tmp/j36; I will rerun with short job roots under /home/claude/.cache/codex-scratch. The previous Playwright attempt exited 127 because its nested Chromium shell could not find Bun. I canceled the duplicate queued command after confirming it used the same shell; the retry will provide both Bun and Chromium through build-slot. The old static-size checks still reject both artifacts, but D243's caps were removed by the owner; I am not changing the gate. No source or API-key lifetime changes.

J36 verification correction on `signin-d311` @ `16b728e212a783a34c38e229a7a077dc64bac7f6`: the first post-rebase `scripts/verify fast` run failed seven restart fixture tests with `PermissionDenied`. The fixture helper explicitly keeps executable fixtures under trusted `$HOME` ancestry, while that run set `INFRATERNAL_TEST_STATE_ROOT=/tmp/j36`; I will rerun with short job roots under `/home/claude/.cache/codex-scratch`. The previous Playwright attempt exited 127 because its nested Chromium shell could not find Bun. I canceled the duplicate queued command after confirming it used the same shell; the retry will provide both Bun and Chromium through build-slot. The old static-size checks still reject both artifacts, but D243's caps were removed by the owner; I am not changing the gate. No source or API-key lifetime changes.
Author
Owner

J36 checkpoint on signin-d311 @ c72ac8f266663b63b7bc56fd181eb943e99063d2: the corrected single build-slot runner is queued at position 12 while all four compute01 slots are occupied. Once admitted it will run the 13 focused sign-in tests, the production tracer approval spec, and scripts/verify fast sequentially. The browser check uses a temporary config to pass the Nix Chromium executable explicitly; the tracked Playwright config does not consume TRACER_TEST_BROWSER. Fast-gate state roots are now under /home/claude/.cache/codex-scratch, preserving the restart fixtures' trusted-$HOME path requirement. The prior queued run was canceled before compilation. No source changes since the pushed head; target/state cleanup runs after the checks. Logs are under /home/claude/.cache/codex-scratch/j36-signin/.

J36 checkpoint on `signin-d311` @ `c72ac8f266663b63b7bc56fd181eb943e99063d2`: the corrected single build-slot runner is queued at position 12 while all four compute01 slots are occupied. Once admitted it will run the 13 focused sign-in tests, the production tracer approval spec, and `scripts/verify fast` sequentially. The browser check uses a temporary config to pass the Nix Chromium executable explicitly; the tracked Playwright config does not consume `TRACER_TEST_BROWSER`. Fast-gate state roots are now under `/home/claude/.cache/codex-scratch`, preserving the restart fixtures' trusted-`$HOME` path requirement. The prior queued run was canceled before compilation. No source changes since the pushed head; target/state cleanup runs after the checks. Logs are under `/home/claude/.cache/codex-scratch/j36-signin/`.
Author
Owner

J36 queue checkpoint on signin-d311 @ c72ac8f266663b63b7bc56fd181eb943e99063d2: the single compute01 build-slot ticket is still waiting behind occupied slots (last scheduler output: position 12). No compilation has started and no source changed. The queued runner is preserved under /home/claude/.cache/codex-scratch/j36-signin/run-final-followup-launcher.sh; it will write per-step logs and clean this job's target/state directories on exit.

J36 queue checkpoint on `signin-d311` @ `c72ac8f266663b63b7bc56fd181eb943e99063d2`: the single compute01 build-slot ticket is still waiting behind occupied slots (last scheduler output: position 12). No compilation has started and no source changed. The queued runner is preserved under `/home/claude/.cache/codex-scratch/j36-signin/run-final-followup-launcher.sh`; it will write per-step logs and clean this job's target/state directories on exit.
Author
Owner

J36 queue advance on signin-d311 @ c72ac8f266663b63b7bc56fd181eb943e99063d2: the single compute01 build-slot ticket moved from reported position 12 to current position 7 (six live tickets ahead; all four slots are still occupied). No compilation has started. The branch and source tree are unchanged; the queued runner and per-step logs remain under /home/claude/.cache/codex-scratch/j36-signin/.

J36 queue advance on `signin-d311` @ `c72ac8f266663b63b7bc56fd181eb943e99063d2`: the single compute01 build-slot ticket moved from reported position 12 to current position 7 (six live tickets ahead; all four slots are still occupied). No compilation has started. The branch and source tree are unchanged; the queued runner and per-step logs remain under `/home/claude/.cache/codex-scratch/j36-signin/`.
Author
Owner

J36 queue update on signin-d311 @ c72ac8f266663b63b7bc56fd181eb943e99063d2: the live compute01 build-slot ticket is now 3rd among 10 FIFO waiters (the runner previously printed position 12). The VM has 126 GB free. No verification command has started and no source changed; the single queued runner remains at /home/claude/.cache/codex-scratch/j36-signin/run-final-followup-launcher.sh.

J36 queue update on `signin-d311` @ `c72ac8f266663b63b7bc56fd181eb943e99063d2`: the live compute01 build-slot ticket is now 3rd among 10 FIFO waiters (the runner previously printed position 12). The VM has 126 GB free. No verification command has started and no source changed; the single queued runner remains at `/home/claude/.cache/codex-scratch/j36-signin/run-final-followup-launcher.sh`.
Author
Owner

J36 verification started on compute01 via build-slot: admitted to slot 1/4 at 09:50:53 (scheduler clock). The focused sign-in tests are compiling now. The same slot will run the tracer device-approval spec and scripts/verify fast afterward; per-step logs and screenshots are under /home/claude/.cache/codex-scratch/j36-signin/. Branch remains c72ac8f266663b63b7bc56fd181eb943e99063d2.

J36 verification started on compute01 via build-slot: admitted to slot 1/4 at 09:50:53 (scheduler clock). The focused sign-in tests are compiling now. The same slot will run the tracer device-approval spec and `scripts/verify fast` afterward; per-step logs and screenshots are under `/home/claude/.cache/codex-scratch/j36-signin/`. Branch remains `c72ac8f266663b63b7bc56fd181eb943e99063d2`.
Author
Owner

J36 per-finding verification checkpoint on c72ac8f266663b63b7bc56fd181eb943e99063d2:

  • P2 credential directory replacement: credential_read_refuses_a_private_directory_replaced_after_locking passed; the focused sign-in suite is 13/13.
  • P1 user-code binding: the CLI regression approval_link_carries_only_the_browser_user_code passed in the same 13/13 suite. The browser regression has not yet executed: all 7 Playwright cases failed at launch because the configured /nix/store/ini1ifgvcsvw48x4mxawrv9jvbdwn70p-chromium-155.0.8059.79/bin/chromium is absent. I found the installed Playwright Chromium at /home/claude/.cache/ms-playwright/chromium-1228/chrome-linux64/chrome and will rerun the approval spec after the active fast gate finishes.

verify fast is still running; its current output has no failures.

J36 per-finding verification checkpoint on `c72ac8f266663b63b7bc56fd181eb943e99063d2`: - **P2 credential directory replacement:** `credential_read_refuses_a_private_directory_replaced_after_locking` passed; the focused sign-in suite is 13/13. - **P1 user-code binding:** the CLI regression `approval_link_carries_only_the_browser_user_code` passed in the same 13/13 suite. The browser regression has not yet executed: all 7 Playwright cases failed at launch because the configured `/nix/store/ini1ifgvcsvw48x4mxawrv9jvbdwn70p-chromium-155.0.8059.79/bin/chromium` is absent. I found the installed Playwright Chromium at `/home/claude/.cache/ms-playwright/chromium-1228/chrome-linux64/chrome` and will rerun the approval spec after the active fast gate finishes. `verify fast` is still running; its current output has no failures.
Author
Owner

Owner decisions (kayg, 2026-11-01), machine trust: (Q10) a host whose 7-day mTLS certificate expired may self-renew for up to 90 days after expiry by proving possession of its registered node identity key, if not revoked — no manual re-enrollment after long server outages. (Q11, pending the owner seeing the key table) proposed: automatic 7-day rotation of node identity and WireGuard static keys with overlap, only while online, immediate on compromise. Browser sessions: keep 30-day sliding window and fix the cookie/DB renewal mismatch (agreed).

**Owner decisions (kayg, 2026-11-01), machine trust:** (Q10) a host whose 7-day mTLS certificate expired may **self-renew for up to 90 days after expiry** by proving possession of its registered node identity key, if not revoked — no manual re-enrollment after long server outages. (Q11, pending the owner seeing the key table) proposed: automatic 7-day rotation of node identity and WireGuard static keys with overlap, only while online, immediate on compromise. Browser sessions: keep 30-day sliding window and fix the cookie/DB renewal mismatch (agreed).
Author
Owner

Owner decisions (kayg, 2026-11-01), key lifetimes — final:

Key Decision
WireGuard session keys unchanged (WireGuard rekeys every ~2 min)
WireGuard static key rotate automatically every 7 days, with overlap (new key distributed via coordinator, old and new both accepted until peers acknowledge; ≥120 s), only while the host is online and reaches the coordinator; immediate rotation on compromise
Node identity key (Ed25519) rotate automatically every 7 days, new key signed by the old, with overlap; only while online; immediate on compromise
Host mTLS certificate 7 days, renewed at half-life (unchanged) + self-renewal up to 90 days after expiry by proving possession of the registered identity key, if not revoked
Machine CA root keep 10-year root; add expiry/locked-CA warnings and a tested overlapping rollover later
Server listener key rotate together with the CA rollover work
Host join token 15 min, single use (unchanged)
Browser sessions 30-day sliding window; fix cookie vs DB renewal mismatch
Owner on "overlap": agreed. Next: an implementation plan for the machine-trust slice.
**Owner decisions (kayg, 2026-11-01), key lifetimes — final:** | Key | Decision | |---|---| | WireGuard session keys | unchanged (WireGuard rekeys every ~2 min) | | WireGuard static key | **rotate automatically every 7 days**, with overlap (new key distributed via coordinator, old and new both accepted until peers acknowledge; ≥120 s), only while the host is online and reaches the coordinator; immediate rotation on compromise | | Node identity key (Ed25519) | **rotate automatically every 7 days**, new key signed by the old, with overlap; only while online; immediate on compromise | | Host mTLS certificate | 7 days, renewed at half-life (unchanged) **+ self-renewal up to 90 days after expiry** by proving possession of the registered identity key, if not revoked | | Machine CA root | keep 10-year root; add expiry/locked-CA warnings and a tested overlapping rollover later | | Server listener key | rotate together with the CA rollover work | | Host join token | 15 min, single use (unchanged) | | Browser sessions | 30-day sliding window; fix cookie vs DB renewal mismatch | Owner on "overlap": agreed. Next: an implementation plan for the machine-trust slice.
Author
Owner

J36 scripts/verify fast result on compute01, branch head c72ac8f266663b63b7bc56fd181eb943e99063d2: the Rust gate summary reports fmt, VT native fmt, CLI plugin compile-out, clippy, SSH runtime helper, real-binary fixture paths, tests, SSH inode reuse fixture, shim process-per-user, cargo-deny, and gate architecture all PASS; static builds FAIL (1). The built stripped static x86 image is 21,527,792 bytes versus the checked-in 21,443,824-byte cap (+83,968 bytes), with no PT_INTERP. The owner removed D243 size caps, so this is the known stale gate row; I left scripts/verify unchanged. scripts/verify fast exit: 1.

The focused sign-in suite passed 13/13, including P2 directory replacement and the CLI user-code link regression. The P1 browser spec is queued for a retry with the Nix Playwright Chromium path; its first attempt never launched because the configured Chromium path was absent.

J36 `scripts/verify fast` result on compute01, branch head `c72ac8f266663b63b7bc56fd181eb943e99063d2`: the Rust gate summary reports fmt, VT native fmt, CLI plugin compile-out, clippy, SSH runtime helper, real-binary fixture paths, tests, SSH inode reuse fixture, shim process-per-user, cargo-deny, and gate architecture all PASS; static builds FAIL (1). The built stripped static x86 image is 21,527,792 bytes versus the checked-in 21,443,824-byte cap (+83,968 bytes), with no PT_INTERP. The owner removed D243 size caps, so this is the known stale gate row; I left `scripts/verify` unchanged. `scripts/verify fast` exit: 1. The focused sign-in suite passed 13/13, including P2 directory replacement and the CLI user-code link regression. The P1 browser spec is queued for a retry with the Nix Playwright Chromium path; its first attempt never launched because the configured Chromium path was absent.
Author
Owner

J36 progress on signin-d311 @ c72ac8f266663b63b7bc56fd181eb943e99063d2: the retry uses the existing Nix Playwright Chromium wrapper and is queued on compute01 through build-slot. Its live ticket is position 6 of 16; no browser process or screenshot result yet. The failed fast-gate runner cleaned its target/ and test state roots, and the worktree remains clean. No source changed.

J36 progress on `signin-d311` @ `c72ac8f266663b63b7bc56fd181eb943e99063d2`: the retry uses the existing Nix Playwright Chromium wrapper and is queued on compute01 through build-slot. Its live ticket is position 6 of 16; no browser process or screenshot result yet. The failed fast-gate runner cleaned its `target/` and test state roots, and the worktree remains clean. No source changed.
Author
Owner

J36 P1 browser retry update: the first Nix store path was absent; the second retry also found its unrooted playwright-chromium output and derivation had been garbage collected before launch. The persistent Playwright cache binary is present. I verified the VM’s working runtime setup from its Chromium test helper and changed only the scratch runner to set the Nix-provided library path and check ldd before launch. This third browser attempt is queued on compute01 at position 7; no repository source changed.

J36 P1 browser retry update: the first Nix store path was absent; the second retry also found its unrooted `playwright-chromium` output and derivation had been garbage collected before launch. The persistent Playwright cache binary is present. I verified the VM’s working runtime setup from its Chromium test helper and changed only the scratch runner to set the Nix-provided library path and check `ldd` before launch. This third browser attempt is queued on compute01 at position 7; no repository source changed.
Author
Owner

J36 verification queue update on signin-d311 @ c72ac8f266663b63b7bc56fd181eb943e99063d2: the corrected Playwright run has been waiting at position 2 on compute01 for about 28 minutes. It remains a single build-slot ticket, with no browser/test process started. The tested code and worktree are unchanged.

J36 verification queue update on `signin-d311` @ `c72ac8f266663b63b7bc56fd181eb943e99063d2`: the corrected Playwright run has been waiting at position 2 on compute01 for about 28 minutes. It remains a single build-slot ticket, with no browser/test process started. The tested code and worktree are unchanged.
Author
Owner

J36 browser-retry update on signin-d311 @ c72ac8f266663b63b7bc56fd181eb943e99063d2:

  • The current-head focused sign-in suite is recorded as 13/13, including credential_read_refuses_a_private_directory_replaced_after_locking and approval_link_carries_only_the_browser_user_code.
  • scripts/verify fast completed; all listed Rust rows pass except the stale static-size check. The owner removed the D243 caps, and I left the gate unchanged. The x86 image is 21,527,792 B vs. the obsolete 21,443,824 B cap (+83,968 B); exit 1.
  • The latest approval attempt exited before test collection because the cached Chromium lacked runtime libraries. I added the missing Nix library paths and verified both cached Chromium binaries with ldd.
  • I added the repo-required ±0.5 px Playwright alignment measurements for the approval code, match confirmation, and passkey button. Next I am rerunning the approval spec on compute01 via build-slot at DPR 2; its production-build screenshots will be saved under /home/claude/.cache/codex-scratch/j36-signin/evidence/approval-final-retry2.

No product source changed since c72ac8f; the alignment assertion is currently an uncommitted test-only addition and will be committed after its browser run.

J36 browser-retry update on `signin-d311` @ `c72ac8f266663b63b7bc56fd181eb943e99063d2`: - The current-head focused sign-in suite is recorded as 13/13, including `credential_read_refuses_a_private_directory_replaced_after_locking` and `approval_link_carries_only_the_browser_user_code`. - `scripts/verify fast` completed; all listed Rust rows pass except the stale static-size check. The owner removed the D243 caps, and I left the gate unchanged. The x86 image is 21,527,792 B vs. the obsolete 21,443,824 B cap (+83,968 B); exit 1. - The latest approval attempt exited before test collection because the cached Chromium lacked runtime libraries. I added the missing Nix library paths and verified both cached Chromium binaries with `ldd`. - I added the repo-required ±0.5 px Playwright alignment measurements for the approval code, match confirmation, and passkey button. Next I am rerunning the approval spec on compute01 via build-slot at DPR 2; its production-build screenshots will be saved under `/home/claude/.cache/codex-scratch/j36-signin/evidence/approval-final-retry2`. No product source changed since `c72ac8f`; the alignment assertion is currently an uncommitted test-only addition and will be committed after its browser run.
Author
Owner

J36 queue update: the corrected Playwright runner for test/device-approval.spec.ts is queued on compute01 via build-slot at position 21. The cached Chromium runtime check now passes with the VM's Nix library paths. No browser test has started yet; the run captures six production-build screenshots at DPR 2 and checks the approval code/confirmation alignment within 0.5 px.

J36 queue update: the corrected Playwright runner for `test/device-approval.spec.ts` is queued on compute01 via build-slot at position 21. The cached Chromium runtime check now passes with the VM's Nix library paths. No browser test has started yet; the run captures six production-build screenshots at DPR 2 and checks the approval code/confirmation alignment within 0.5 px.
Author
Owner

J36 crash-safety checkpoint: test-only alignment assertions are pushed at 2579ac125 on signin-d311. The approval spec checks code/label/hint/confirmation left edges and checkbox/passkey icon center lines within 0.5 px at each captured viewport. This is a WIP checkpoint until the queued browser run verifies it. The production source remains unchanged since c72ac8f; the browser run remains queued on compute01.

J36 crash-safety checkpoint: test-only alignment assertions are pushed at `2579ac125` on `signin-d311`. The approval spec checks code/label/hint/confirmation left edges and checkbox/passkey icon center lines within 0.5 px at each captured viewport. This is a WIP checkpoint until the queued browser run verifies it. The production source remains unchanged since `c72ac8f`; the browser run remains queued on compute01.
Author
Owner

J36 checkpoint: the queued compute01 browser runner is still alive at build-slot position 14 after about 29 minutes. Its dependency preflight passed; no browser test has started and no assertion has failed. It will run test/device-approval.spec.ts at DPR 2 and save screenshots under /home/claude/.cache/codex-scratch/j36-signin/evidence/approval-final-retry3. Branch head remains 2579ac125; the pushed alignment assertions are still test-only.

J36 checkpoint: the queued compute01 browser runner is still alive at build-slot position 14 after about 29 minutes. Its dependency preflight passed; no browser test has started and no assertion has failed. It will run `test/device-approval.spec.ts` at DPR 2 and save screenshots under `/home/claude/.cache/codex-scratch/j36-signin/evidence/approval-final-retry3`. Branch head remains `2579ac125`; the pushed alignment assertions are still test-only.
Author
Owner

J77 round-2 independent review started. Target signin-d311 pushed head 2579ac125e8f4aec0fab33c6c33cb809b5a26175; current origin/dev base 0067e1255407f0f3ed0f1d6c5e5caf51667e00a0. Review checkout: detached infraternal-j77.

Plan (about 35 minutes): read all issue history and J37/J6/J36 findings; inspect the repair diff, sign-in transport/storage and existing issuance/scope/revoke guards; check merge compatibility; post numbered findings or APPROVE. Tests only to confirm a concrete finding, on a VM, blocking through completion. No implementation changes and no broad gate rerun for this review.

J77 round-2 independent review started. Target `signin-d311` pushed head `2579ac125e8f4aec0fab33c6c33cb809b5a26175`; current `origin/dev` base `0067e1255407f0f3ed0f1d6c5e5caf51667e00a0`. Review checkout: detached `infraternal-j77`. Plan (about 35 minutes): read all issue history and J37/J6/J36 findings; inspect the repair diff, sign-in transport/storage and existing issuance/scope/revoke guards; check merge compatibility; post numbered findings or APPROVE. Tests only to confirm a concrete finding, on a VM, blocking through completion. No implementation changes and no broad gate rerun for this review.
Author
Owner

J77 round 2 — verdict: FIX FIRST (merge conflicts)

Reviewed latest pushed signin-d311 head 2579ac125e8f4aec0fab33c6c33cb809b5a26175 against current origin/dev 0067e1255407f0f3ed0f1d6c5e5caf51667e00a0. Both refs were fetched again immediately before this report and were unchanged. No implementation changes.

  1. P2 — Resolve the two conflicts with current dev before merge. apps/tracer/auth.css:229 and crates/infraternal/src/main.rs:193 (target-head lines). Read-only git merge-tree --write-tree origin/dev origin/signin-d311 exits 1 and reports:
    CONFLICT (content): Merge conflict in apps/tracer/auth.css
    CONFLICT (content): Merge conflict in crates/infraternal/src/main.rs
    
    The CSS conflict overlaps dev's rem-based auth styles with the new device-approval styles. The dispatch conflict overlaps dev's combined CliHandler::NodeComponent | CliHandler::Sessions => run_local_action(...) path with this branch's extracted run_node_component(...) path. Preserve current dev's sessions dispatch and auth sizing while integrating sign-in. Conflicts were reported only, not repaired.

Round-1 disposition:

  • J37 P1, missing user-code comparison: fixed with a regression. apps/tracer/src/device-approval.on-demand.ts:67 renders the exact code using textContent; approval is disabled until the match checkbox is checked, and the click handler independently enforces confirmation. apps/tracer/test/device-approval.spec.ts:139 preserves the exact-code/disabled-until-confirmed regression. J36 records the production approval suite passing 7/7 on dev-02 and attaches six production screenshots. The newest commit adds alignment assertions only; its fresh compute01 browser/screenshot run is still queued in the latest issue update, so I do not claim those new assertions passed.
  • J37 P2, replaceable credential-directory pathname: fixed with a regression. crates/infraternal/src/sign_in.rs:641 retains the locked directory descriptor; reads use openat and replacement uses openat/renameat against that descriptor, with identity, owner, private-mode and sync checks. The preserved regression at sign_in.rs:98 checks that both read and replacement refuse the substituted private directory and leave its file unchanged. J36 records 13/13 focused sign-in tests passing, including this regression, on production-equivalent source at c72ac8f26; the only later commit is the browser alignment test.
  • API-key lifetime: noted, not a defect. Issued CLI keys still never expire. The expiry policy remains an unanswered owner question; this review does not invent a TTL requirement.

The security delta review found no additional actionable defect in the device ceremony, fixed automation scopes, one-time issuance, user-bound revocation, secret-free argv/output/log paths, TLS verification, bounded response handling, uncertain-redemption exit classification, or private atomic credential persistence. Existing sessions_slide_but_fresh_tap_does_not_and_device_keys_are_scoped covers scope refusal, one-time redemption, expiry and immediate authorization loss after revocation. QR and credential-management UI remain the separately declared follow-on slices.

D243 size is report-only. J36's recorded x86 static image is 21,527,792 B (+83,968 B versus the obsolete 21,443,824 B checked-in cap); it is not a review defect. J36's completed fast gate at c72ac8f26 reports all other Rust rows PASS and the stale static-size row FAIL (1), overall exit 1. These are author-recorded results, not a new J77 run.

J77 verification: source and regression inspection plus the deterministic merge-tree check above. No tests run: there was no runtime finding needing confirmation. No code changed, gate not applicable. No J77 build, test, browser, or background run remains pending. Issue stays open.

## J77 round 2 — verdict: FIX FIRST (merge conflicts) Reviewed latest pushed `signin-d311` head `2579ac125e8f4aec0fab33c6c33cb809b5a26175` against current `origin/dev` `0067e1255407f0f3ed0f1d6c5e5caf51667e00a0`. Both refs were fetched again immediately before this report and were unchanged. No implementation changes. 1. **P2 — Resolve the two conflicts with current dev before merge.** `apps/tracer/auth.css:229` and `crates/infraternal/src/main.rs:193` (target-head lines). Read-only `git merge-tree --write-tree origin/dev origin/signin-d311` exits **1** and reports: ```text CONFLICT (content): Merge conflict in apps/tracer/auth.css CONFLICT (content): Merge conflict in crates/infraternal/src/main.rs ``` The CSS conflict overlaps dev's rem-based auth styles with the new device-approval styles. The dispatch conflict overlaps dev's combined `CliHandler::NodeComponent | CliHandler::Sessions => run_local_action(...)` path with this branch's extracted `run_node_component(...)` path. Preserve current dev's sessions dispatch and auth sizing while integrating sign-in. Conflicts were reported only, not repaired. Round-1 disposition: - **J37 P1, missing user-code comparison: fixed with a regression.** `apps/tracer/src/device-approval.on-demand.ts:67` renders the exact code using `textContent`; approval is disabled until the match checkbox is checked, and the click handler independently enforces confirmation. `apps/tracer/test/device-approval.spec.ts:139` preserves the exact-code/disabled-until-confirmed regression. J36 records the production approval suite passing 7/7 on dev-02 and attaches six production screenshots. The newest commit adds alignment assertions only; its fresh compute01 browser/screenshot run is still queued in the latest issue update, so I do not claim those new assertions passed. - **J37 P2, replaceable credential-directory pathname: fixed with a regression.** `crates/infraternal/src/sign_in.rs:641` retains the locked directory descriptor; reads use `openat` and replacement uses `openat`/`renameat` against that descriptor, with identity, owner, private-mode and sync checks. The preserved regression at `sign_in.rs:98` checks that both read and replacement refuse the substituted private directory and leave its file unchanged. J36 records 13/13 focused sign-in tests passing, including this regression, on production-equivalent source at `c72ac8f26`; the only later commit is the browser alignment test. - **API-key lifetime: noted, not a defect.** Issued CLI keys still never expire. The expiry policy remains an unanswered owner question; this review does not invent a TTL requirement. The security delta review found no additional actionable defect in the device ceremony, fixed automation scopes, one-time issuance, user-bound revocation, secret-free argv/output/log paths, TLS verification, bounded response handling, uncertain-redemption exit classification, or private atomic credential persistence. Existing `sessions_slide_but_fresh_tap_does_not_and_device_keys_are_scoped` covers scope refusal, one-time redemption, expiry and immediate authorization loss after revocation. QR and credential-management UI remain the separately declared follow-on slices. D243 size is report-only. J36's recorded x86 static image is 21,527,792 B (+83,968 B versus the obsolete 21,443,824 B checked-in cap); it is not a review defect. J36's completed fast gate at `c72ac8f26` reports all other Rust rows PASS and the stale static-size row FAIL (1), overall exit 1. These are author-recorded results, not a new J77 run. J77 verification: source and regression inspection plus the deterministic merge-tree check above. No tests run: there was no runtime finding needing confirmation. **No code changed, gate not applicable.** No J77 build, test, browser, or background run remains pending. Issue stays open.
Author
Owner

Owner decision (kayg, 2026-11-02): CLI sign-in API keys (D311) expire after 30 days, renewed silently while the CLI is online and in use. Each authenticated use within the window slides it; a key unused for 30 days stops working and needs infraternal sign-in again; revocation stays immediate. To be recorded as a D311 amendment and in docs/keys.md.

**Owner decision (kayg, 2026-11-02): CLI sign-in API keys (D311) expire after 30 days, renewed silently while the CLI is online and in use.** Each authenticated use within the window slides it; a key unused for 30 days stops working and needs `infraternal sign-in` again; revocation stays immediate. To be recorded as a D311 amendment and in docs/keys.md.
Author
Owner

J90 start: branch apikey-30d-269, base 2b351d5f3 (origin/dev). Plan: (1) add failing deterministic expiry/sliding/hourly-write/no-oracle/revocation/upgrade and CLI diagnostic tests; (2) add per-key migration and conditional renewal, preserve deployment grace for existing keys; (3) separate docs/D311 amendment commit; (4) run targeted tests and scripts/verify fast, report static sizes, push branch. Estimate: 100 minutes including VM builds/gate. Both VM queues checked: all slots free; choose dev-02 (210 GB free vs compute01 81 GB). No foundational changes.

J90 start: branch apikey-30d-269, base 2b351d5f3 (origin/dev). Plan: (1) add failing deterministic expiry/sliding/hourly-write/no-oracle/revocation/upgrade and CLI diagnostic tests; (2) add per-key migration and conditional renewal, preserve deployment grace for existing keys; (3) separate docs/D311 amendment commit; (4) run targeted tests and scripts/verify fast, report static sizes, push branch. Estimate: 100 minutes including VM builds/gate. Both VM queues checked: all slots free; choose dev-02 (210 GB free vs compute01 81 GB). No foundational changes.
Author
Owner

Finding/scope blocker: rg api_authorize crates shows only auth/devices.rs and auth/tests.rs; rg credentials.json crates/infraternal/src shows only sign_in.rs storage/tests. No production API-key client or authenticated request path exists on origin/dev 2b351d5f3. Implementing a new authenticated CLI transport/automation path exceeds J90 scope (estimate >1 hour extra). Asked orchestrator whether to leave CLI diagnostic blocked or target an existing client branch; continuing independent server/tests/docs work.

Finding/scope blocker: rg api_authorize crates shows only auth/devices.rs and auth/tests.rs; rg credentials.json crates/infraternal/src shows only sign_in.rs storage/tests. No production API-key client or authenticated request path exists on origin/dev 2b351d5f3. Implementing a new authenticated CLI transport/automation path exceeds J90 scope (estimate >1 hour extra). Asked orchestrator whether to leave CLI diagnostic blocked or target an existing client branch; continuing independent server/tests/docs work.
Author
Owner

Owner scope clarification accepted: finish server/tests/docs, add a small tested CLI expiry diagnostic helper only, no automation wiring. Follow-up opened: #303, CLI: use sign-in API keys for remote actions (D311). docs/keys.md will explicitly say no CLI command uses stored sign-in keys yet.

Owner scope clarification accepted: finish server/tests/docs, add a small tested CLI expiry diagnostic helper only, no automation wiring. Follow-up opened: #303, CLI: use sign-in API keys for remote actions (D311). docs/keys.md will explicitly say no CLI command uses stored sign-in keys yet.
Author
Owner

Owner addition: configurable server setting api_key_lifetime_days, default 30, range 1–365, read at authorization so shortening affects existing keys. No UI. Revised plan: persist last renewal and expiry, apply current lifetime on checks; add owner/fresh-passkey protected auth security-settings read/update API with validation and tests, since existing settings APIs cover plugin switches only. Revised estimate: 140 minutes total including full gate. Follow-up #303 tracks future CLI key consumers.

Owner addition: configurable server setting api_key_lifetime_days, default 30, range 1–365, read at authorization so shortening affects existing keys. No UI. Revised plan: persist last renewal and expiry, apply current lifetime on checks; add owner/fresh-passkey protected auth security-settings read/update API with validation and tests, since existing settings APIs cover plugin switches only. Revised estimate: 140 minutes total including full gate. Follow-up #303 tracks future CLI key consumers.
Author
Owner

Tests-first evidence on dev-02: cargo test --locked -p infraternal-server --lib api_key_ ran 6 tests, all failed on the unchanged implementation (RED_EXIT=101). Boundary test accepted the expired key; upgrade/sliding tests found no expires column; settings API tests returned 400 because endpoints do not exist. Log: ~/.cache/codex-scratch/j90/red.log. Implementing the migration, dynamic lifetime checks, hourly conditional renewal and protected settings endpoints now.

Tests-first evidence on dev-02: cargo test --locked -p infraternal-server --lib api_key_ ran 6 tests, all failed on the unchanged implementation (RED_EXIT=101). Boundary test accepted the expired key; upgrade/sliding tests found no expires column; settings API tests returned 400 because endpoints do not exist. Log: ~/.cache/codex-scratch/j90/red.log. Implementing the migration, dynamic lifetime checks, hourly conditional renewal and protected settings endpoints now.
Author
Owner

Targeted checks pass on dev-02: 7 API-key server tests plus 1 CLI diagnostic test, GREEN_EXIT=0 (~/.cache/codex-scratch/j90/green2.log). Coverage: strict expiry boundary/same Denied as unknown, hourly renewal with a real SQLite write-count trigger, concurrent uses renewing once, denied scopes not renewing, immediate revocation, pre-upgrade keys backfilled once at migration, settings default/range/current-policy shortening and owner/fresh-passkey/origin authorization. One test fixture was corrected to send only the foreign Origin (the shared helper appends headers). Full gate and base/head static-size comparison next. One target directory: /home/claude/worktrees/j90/target.

Targeted checks pass on dev-02: 7 API-key server tests plus 1 CLI diagnostic test, GREEN_EXIT=0 (~/.cache/codex-scratch/j90/green2.log). Coverage: strict expiry boundary/same Denied as unknown, hourly renewal with a real SQLite write-count trigger, concurrent uses renewing once, denied scopes not renewing, immediate revocation, pre-upgrade keys backfilled once at migration, settings default/range/current-policy shortening and owner/fresh-passkey/origin authorization. One test fixture was corrected to send only the foreign Origin (the shared helper appends headers). Full gate and base/head static-size comparison next. One target directory: /home/claude/worktrees/j90/target.
Author
Owner

Crash-safety checkpoint pushed: apikey-30d-269 at fd116ac9c. This is a provisional snapshot, to be split into the required code and docs commits after validation. Targeted 7 server tests + 1 CLI helper test pass. Baseline static builds passed: x86_64 22035888 B; aarch64 20127168 B (base 2b351d5f3). Full scripts/verify fast is running on dev-02: formatting and CLI compile-out passed, clippy in progress. Logs: /home/claude/.cache/codex-scratch/j90/{red,cli-red,green2,baseline,verify,validation}.log. Exactly one target retained while running: /home/claude/worktrees/j90/target.

Crash-safety checkpoint pushed: apikey-30d-269 at fd116ac9c. This is a provisional snapshot, to be split into the required code and docs commits after validation. Targeted 7 server tests + 1 CLI helper test pass. Baseline static builds passed: x86_64 22035888 B; aarch64 20127168 B (base 2b351d5f3). Full scripts/verify fast is running on dev-02: formatting and CLI compile-out passed, clippy in progress. Logs: /home/claude/.cache/codex-scratch/j90/{red,cli-red,green2,baseline,verify,validation}.log. Exactly one target retained while running: /home/claude/worktrees/j90/target.
Author
Owner

Full gate finding: server suite had 708 passed, 2 failed, 54 ignored. Both failures are J90 migration-count expectations in remote_migration_tests.rs: canvas_layout_migration_upgrades_production_v27_and_fresh_schema_in_order expected versions 1..32 but got 1..33 (line 103); hostname_migration_upgrades_release6_copy_without_changing_canvas_or_checksums expected 32 rows but got 33 (line 266). All seven new API-key tests passed in the full workspace feature set. Updating only those now-stale migration expectations; the current gate continues through static validation before the repair rerun.

Full gate finding: server suite had 708 passed, 2 failed, 54 ignored. Both failures are J90 migration-count expectations in remote_migration_tests.rs: canvas_layout_migration_upgrades_production_v27_and_fresh_schema_in_order expected versions 1..32 but got 1..33 (line 103); hostname_migration_upgrades_release6_copy_without_changing_canvas_or_checksums expected 32 rows but got 33 (line 266). All seven new API-key tests passed in the full workspace feature set. Updating only those now-stale migration expectations; the current gate continues through static validation before the repair rerun.
Author
Owner

Checkpoint pushed at 772482890: repaired explicit migration-chain expectations from 32 to 33, preserving ordered versions, old checksums and route/canvas data assertions. No production implementation changes after targeted/full-workspace API-key tests passed. First gate has passed clippy, SSH fixtures, shared-process tests, cargo-deny and gate architecture; static builds are running. Its test row is red from the two now-repaired expectations. A fresh full gate will run after the current run completes. Target remains /home/claude/worktrees/j90/target; logs remain ~/.cache/codex-scratch/j90/.

Checkpoint pushed at 772482890: repaired explicit migration-chain expectations from 32 to 33, preserving ordered versions, old checksums and route/canvas data assertions. No production implementation changes after targeted/full-workspace API-key tests passed. First gate has passed clippy, SSH fixtures, shared-process tests, cargo-deny and gate architecture; static builds are running. Its test row is red from the two now-repaired expectations. A fresh full gate will run after the current run completes. Target remains /home/claude/worktrees/j90/target; logs remain ~/.cache/codex-scratch/j90/.
Author
Owner

First full gate completed on dev-02: every Rust row passed except test (the two migration-count assertions repaired in 772482890). Both static images passed: x86_64 22051024 B (+15136 vs base 22035888); arm64 20134320 B (+7152 vs base 20127168). Rerun moved to compute01 after checking both queues: dev-02 was full, compute01 had a free slot. Cancelled the queued dev-02 rerun before it started. Dev-02 target is being deleted, not retained; the active target for the new full gate is /home/claude/worktrees/j90/target on compute01. Logs there: ~/.cache/codex-scratch/j90/{verify2,validation2}.log. First-run evidence remains on dev-02.

First full gate completed on dev-02: every Rust row passed except test (the two migration-count assertions repaired in 772482890). Both static images passed: x86_64 22051024 B (+15136 vs base 22035888); arm64 20134320 B (+7152 vs base 20127168). Rerun moved to compute01 after checking both queues: dev-02 was full, compute01 had a free slot. Cancelled the queued dev-02 rerun before it started. Dev-02 target is being deleted, not retained; the active target for the new full gate is /home/claude/worktrees/j90/target on compute01. Logs there: ~/.cache/codex-scratch/j90/{verify2,validation2}.log. First-run evidence remains on dev-02.
Author
Owner

J94 coordination with J90: J94 will add the scoped action adapter and audit. On origin/dev, api_authorize returns the person id, while api_list already uses the SHA-256 token hash as the key's non-bearer identifier. J90's apikey-30d-269 changes the same authorization method for sliding expiry. I plan to preserve J90's expiry/renewal path and derive the audit key id from the same token hash, without duplicating expiry logic. J94 also extends the accepted device scopes; the current origin/apikey-30d-269 diff does not touch that validator. Please note any planned API return-shape change so I can integrate without reworking expiry.

J94 coordination with J90: J94 will add the scoped action adapter and audit. On `origin/dev`, `api_authorize` returns the person id, while `api_list` already uses the SHA-256 token hash as the key's non-bearer identifier. J90's `apikey-30d-269` changes the same authorization method for sliding expiry. I plan to preserve J90's expiry/renewal path and derive the audit key id from the same token hash, without duplicating expiry logic. J94 also extends the accepted device scopes; the current `origin/apikey-30d-269` diff does not touch that validator. Please note any planned API return-shape change so I can integrate without reworking expiry.
Author
Owner

Progress checkpoint pushed at the current apikey-30d-269 head: only a documentation source-footer correction since 772482890. The compute01 full rerun passed fmt, CLI compile-out, clippy and fixture checks; workspace tests are compiling. No implementation changes after the tested API-key behavior. Dev-02 target removal completed; only compute01 /home/claude/worktrees/j90/target remains. Current logs: compute01 ~/.cache/codex-scratch/j90/{verify2,validation2}.log. Final code/docs commit split and push follow validation.

Progress checkpoint pushed at the current apikey-30d-269 head: only a documentation source-footer correction since 772482890. The compute01 full rerun passed fmt, CLI compile-out, clippy and fixture checks; workspace tests are compiling. No implementation changes after the tested API-key behavior. Dev-02 target removal completed; only compute01 /home/claude/worktrees/j90/target remains. Current logs: compute01 ~/.cache/codex-scratch/j90/{verify2,validation2}.log. Final code/docs commit split and push follow validation.
Author
Owner

Validation progress, no source changes since pushed checkpoint 45f6e76a5: the compute01 rerun passed formatting, CLI feature checks, clippy, workspace tests, SSH inode fixture, shared-process tests, dependency audit and gate architecture. Both previously failing migration expectations now pass, as do all 7 API-key tests. The x86 static image passed; arm64 static build is running, then frontend checks. Full gate is still pending, not claimed complete. Only active target: compute01 /home/claude/worktrees/j90/target. Logs: ~/.cache/codex-scratch/j90/{verify2,validation2}.log.

Validation progress, no source changes since pushed checkpoint 45f6e76a5: the compute01 rerun passed formatting, CLI feature checks, clippy, workspace tests, SSH inode fixture, shared-process tests, dependency audit and gate architecture. Both previously failing migration expectations now pass, as do all 7 API-key tests. The x86 static image passed; arm64 static build is running, then frontend checks. Full gate is still pending, not claimed complete. Only active target: compute01 /home/claude/worktrees/j90/target. Logs: ~/.cache/codex-scratch/j90/{verify2,validation2}.log.
Author
Owner

J90 finished and pushed. No merge or deployment.

  • Branch: apikey-30d-269, base 2b351d5f3bb1aebed97b70e8d4460e8979a9c19e.
  • Code: c51954831769ccc46ed8ec04b38afa47002c35e5.
  • Separate docs commit and final head: d8ec67b0e4a8974cdfd9ba585dbf1481449a9356.
  • Final tree matches the checkpoint tree; SHA-256 comparisons of all 12 changed files matched the VM-tested sources. Remote head verified.

Implemented per-key renewal/expiry storage, deployment-time grace for existing keys, current-policy expiry checks, identical expired/unknown denial, exact scopes, at-most-hourly conditional renewal and immediate revocation. Added persisted api_key_lifetime_days, default 30, integer range 1–365, read at authorization so shortening affects existing keys on their next use. POST /auth/security-settings reads the field for a live owner; POST /auth/security-settings-update validates and saves it with fresh owner authority and pinned Origin. Settings UI is a later job.

The CLI change is only the tested one-line invalid/expired-key diagnostic and D303 exit 3. No CLI command consumes stored sign-in keys and no production route calls api_authorize; remote actions remain #303. The docs say this plainly. D311 records the owner's 2026-11-02 amendment; docs/keys.md and the auth API contract describe default/range, renewal and upgrade grace.

Tests-first evidence: initial 6 API-key tests failed on the old implementation; final targeted run passed 7 server tests plus 1 CLI helper test. The full workspace run passed the same expiry tests and both repaired migration-chain expectations. Server suite: 710 passed; 0 failed; 54 ignored. The ignored measurements/soaks are existing tests, not J90 additions.

Same-host D243 attribution on dev-02, against this branch's exact base:

Target Base bytes J90 bytes Delta
x86_64 musl 22035888 22051024 +15136
arm64 musl 20127168 20134320 +7152

The final compute01 gate built x86_64 at 22054608 B and arm64 at 20128176 B. These are different-host builds; the per-slice deltas above use matching base/head builds on dev-02. Both final images passed static linkage, trust and metadata checks. No auth latency claim is made for a path without a production caller. SQLite trigger tests verify no per-request writes and exactly one renewal for concurrent uses.

scripts/verify fast on compute01, gate summary verbatim:

+---------------------------+--------------+
| Rust gate                 | Result       |
+---------------------------+--------------+
| fmt                       | PASS         |
| VT native fmt             | PASS         |
| CLI plugin compile-out    | PASS         |
| clippy                    | PASS         |
| SSH runtime test helper   | PASS         |
| real-binary fixture paths | PASS         |
| test                      | PASS         |
| SSH inode reuse fixture   | PASS         |
| shim process-per-user     | PASS         |
| cargo-deny                | PASS         |
| gate architecture         | PASS         |
| static builds             | PASS         |
+---------------------------+--------------+

Final protocol test output and gate exit, verbatim:

 39 pass
 0 fail
 59970 expect() calls
Ran 39 tests across 10 files. [6.07s]
VERIFY_EXIT=0

The full gate also passed frontend compilation, production build and transfer budgets. Critical gzip: 310908/327680 B; deferred gzip: 61341/61440 B.

How to test:

  1. From the signed-in owner browser, POST {} to /auth/security-settings; a new deployment returns api_key_lifetime_days: 30.
  2. With a fresh passkey session, POST {"api_key_lifetime_days":1} to /auth/security-settings-update; 0, 366, non-integers and unknown fields are rejected. Another account or a stale session cannot change it.
  3. Run cargo test --locked -p infraternal-server --lib api_key_ for deterministic expiry/sliding/write-limit/revocation/upgrade tests. CLI remote-action testing remains #303.

Cleanup complete: both J90 VM target directories removed, no J90 servers retained. Evidence logs remain under ~/.cache/codex-scratch/j90/ on the respective VMs and on the control host. No blockers or owner decisions remain for this scope. Independent review remains with the orchestrator; this job does not close issues.

J90 finished and pushed. No merge or deployment. - Branch: `apikey-30d-269`, base `2b351d5f3bb1aebed97b70e8d4460e8979a9c19e`. - Code: `c51954831769ccc46ed8ec04b38afa47002c35e5`. - Separate docs commit and final head: `d8ec67b0e4a8974cdfd9ba585dbf1481449a9356`. - Final tree matches the checkpoint tree; SHA-256 comparisons of all 12 changed files matched the VM-tested sources. Remote head verified. Implemented per-key renewal/expiry storage, deployment-time grace for existing keys, current-policy expiry checks, identical expired/unknown denial, exact scopes, at-most-hourly conditional renewal and immediate revocation. Added persisted `api_key_lifetime_days`, default 30, integer range 1–365, read at authorization so shortening affects existing keys on their next use. POST `/auth/security-settings` reads the field for a live owner; POST `/auth/security-settings-update` validates and saves it with fresh owner authority and pinned Origin. Settings UI is a later job. The CLI change is only the tested one-line invalid/expired-key diagnostic and D303 exit 3. No CLI command consumes stored sign-in keys and no production route calls `api_authorize`; remote actions remain #303. The docs say this plainly. D311 records the owner's 2026-11-02 amendment; docs/keys.md and the auth API contract describe default/range, renewal and upgrade grace. Tests-first evidence: initial 6 API-key tests failed on the old implementation; final targeted run passed 7 server tests plus 1 CLI helper test. The full workspace run passed the same expiry tests and both repaired migration-chain expectations. Server suite: `710 passed; 0 failed; 54 ignored`. The ignored measurements/soaks are existing tests, not J90 additions. Same-host D243 attribution on dev-02, against this branch's exact base: | Target | Base bytes | J90 bytes | Delta | | --- | ---: | ---: | ---: | | x86_64 musl | 22035888 | 22051024 | +15136 | | arm64 musl | 20127168 | 20134320 | +7152 | The final compute01 gate built x86_64 at 22054608 B and arm64 at 20128176 B. These are different-host builds; the per-slice deltas above use matching base/head builds on dev-02. Both final images passed static linkage, trust and metadata checks. No auth latency claim is made for a path without a production caller. SQLite trigger tests verify no per-request writes and exactly one renewal for concurrent uses. `scripts/verify fast` on compute01, gate summary verbatim: ```text +---------------------------+--------------+ | Rust gate | Result | +---------------------------+--------------+ | fmt | PASS | | VT native fmt | PASS | | CLI plugin compile-out | PASS | | clippy | PASS | | SSH runtime test helper | PASS | | real-binary fixture paths | PASS | | test | PASS | | SSH inode reuse fixture | PASS | | shim process-per-user | PASS | | cargo-deny | PASS | | gate architecture | PASS | | static builds | PASS | +---------------------------+--------------+ ``` Final protocol test output and gate exit, verbatim: ```text 39 pass 0 fail 59970 expect() calls Ran 39 tests across 10 files. [6.07s] VERIFY_EXIT=0 ``` The full gate also passed frontend compilation, production build and transfer budgets. Critical gzip: 310908/327680 B; deferred gzip: 61341/61440 B. How to test: 1. From the signed-in owner browser, POST `{}` to `/auth/security-settings`; a new deployment returns `api_key_lifetime_days: 30`. 2. With a fresh passkey session, POST `{"api_key_lifetime_days":1}` to `/auth/security-settings-update`; 0, 366, non-integers and unknown fields are rejected. Another account or a stale session cannot change it. 3. Run `cargo test --locked -p infraternal-server --lib api_key_` for deterministic expiry/sliding/write-limit/revocation/upgrade tests. CLI remote-action testing remains #303. Cleanup complete: both J90 VM target directories removed, no J90 servers retained. Evidence logs remain under `~/.cache/codex-scratch/j90/` on the respective VMs and on the control host. No blockers or owner decisions remain for this scope. Independent review remains with the orchestrator; this job does not close issues.
Author
Owner

J97 independent auth review, round 1, started. Target apikey-30d-269 at d8ec67b0e4; current origin/dev be20430b2e. Review checkout: /home/kayg/Developer/Worktrees/infraternal-j97 (detached). Plan (about 20 minutes): inspect D311/D303 and J90 report 74429; trace authorization, renewal, policy changes, migration and secret handling; confirm concrete findings only on a VM; post numbered verdict. No code changes or full gate planned for this review.

J97 independent auth review, round 1, started. Target apikey-30d-269 at d8ec67b0e4a8974cdfd9ba585dbf1481449a9356; current origin/dev be20430b2e0dc4f591aa98a036f1066cf5dd5f55. Review checkout: /home/kayg/Developer/Worktrees/infraternal-j97 (detached). Plan (about 20 minutes): inspect D311/D303 and J90 report 74429; trace authorization, renewal, policy changes, migration and secret handling; confirm concrete findings only on a VM; post numbered verdict. No code changes or full gate planned for this review.
Author
Owner

J97 independent auth review, round 1: REQUEST CHANGES.

Reviewed apikey-30d-269 at d8ec67b0e4a8974cdfd9ba585dbf1481449a9356 against current origin/dev be20430b2e0dc4f591aa98a036f1066cf5dd5f55, D311/D303, #269 and J90 report 74429.

  1. P1 — A lifetime increase resurrects expired credentials. crates/infraternal-server/src/auth/devices.rs:108 (also :121 and :127). Authorization uses only renewed_at + current_policy; it never checks the stored expires and never makes expiry terminal. Exact-query SQLite reproduction on dev-02: a key last renewed at T is denied at T+31 days with the default 30-day setting, then accepted at the same server time after setting 365. A key denied at T+2 days after shortening to 1 day is likewise accepted after restoring 30 days. Its next successful scope check can renew it. This violates the required no-resurrection behavior and lets a stolen expired key become usable again without sign-in. Preserve terminal expiry across policy increases, including expiry caused by shortening, and cover both sequences.

  2. P1 — Migration version 0033 conflicts with current dev's deployed history. crates/infraternal-server/migrations/0033_api_key_expiry.sql:1. Current origin/dev already contains 0033_remote_join_key_binding.sql and expects versions 1–33. This new, different migration also claims 33. The compact migration generator preserves numeric prefixes and original SHA-384 checksums (build_support/migrations.rs:27,53); the SQLx migrator checks an applied version's checksum and returns VersionMismatch on disagreement. Retaining both files on integration also gives a duplicate version. Renumber the new expiry migration to the next free version and update migration-chain tests before integration; do not replace the existing deployed 33. Evidence: git ls-tree origin/dev, git show origin/dev:crates/infraternal-server/migrations/0033_remote_join_key_binding.sql, and pinned SQLx 0.8 migrator source lines 173–176 on dev-02. No build was needed to establish the duplicate version.

  3. P2 — Wall-clock rollback makes expired keys valid again. crates/infraternal-server/src/auth/devices.rs:103 and :108; clock implementation auth/http.rs:203. The new check samples raw SystemTime, with no monotonic elapsed-time anchor or terminal-expiry state. Exact-query reproduction: deny at T+30 days, move the sampled server clock back one second, and the same key is accepted. The saturating subtraction at :118 only avoids moving renewed_at backward; it does not protect the authorization decision. Use a rollback-safe server clock/expiry policy and add a deterministic rollback regression. No system clock was changed for this review.

  4. P2 — Renewal rechecks use a stale time sample after database waits. crates/infraternal-server/src/auth/devices.rs:121–123. now is captured before the initial pool query and reused through the writer-lock wait and fallback read. A request sampled one second before a 1-day deadline can execute its UPDATE one second after that deadline and still renew for another day; the advertised under-lock live-deadline recheck uses the old time. Exact-query reproduction confirmed renewed_rows=1 and a now-valid key after that expired interval. Resample authoritative time after acquiring the writer and use that sample consistently for expiry, hourly cutoff and renewal; add a controlled lock/pool-wait boundary regression.

Evidence: /home/kayg/.cache/codex-scratch/j97/expiry-repro.py and expiry-repro.log on the control host. The synchronous reproduction ran on infraternal-dev-02-std, using the exact SELECT/UPDATE extracted from the reviewed source and migration 0033, with a synthetic nonsecret hash. It confirms SQL counterexamples, not end-to-end HTTP behavior or latency. Output verbatim:

policy increase: day31 denied=True; after 30->365 accepted=True
shorten then lengthen: day2 denied at 1d=True; accepted again at 30d=True
clock rollback: denied at deadline=True; accepted at deadline-1=True
expiry across await: sampled=deadline-1; resumes=deadline+1; renewed_rows=1; live_after_stale_update=True
confirmed 4 counterexamples using exact authorization/renewal SQL from d8ec67b0e; no bearer credentials used

Other checks: expired/unknown inputs share the same hash-lookup query and Denied error; no timing-class claim beyond that code inspection. Exact scope rejection precedes renewal and the conditional UPDATE bounds concurrent renewal writes. Normal startup tracks migration completion, so repeated Auth construction does not rerun grace. Settings enforce integer 1–365, pinned Origin, fresh owner session and an under-writer owner/session recheck. Changed code binds hashes, does not log keys, and the secret-free CLI diagnostic returns D303 exit 3. No production caller of api_authorize exists in this target, as J90 disclosed; that limits present exposure but does not satisfy the requested auth contract.

No repository code changed, gate not applicable. Per review scope, only the lightweight finding-confirmation reproduction ran; no builds, full gate, pushes or deployments. All runs completed; no background work remains.

J97 independent auth review, round 1: REQUEST CHANGES. Reviewed `apikey-30d-269` at `d8ec67b0e4a8974cdfd9ba585dbf1481449a9356` against current `origin/dev` `be20430b2e0dc4f591aa98a036f1066cf5dd5f55`, D311/D303, #269 and J90 report 74429. 1. **P1 — A lifetime increase resurrects expired credentials.** `crates/infraternal-server/src/auth/devices.rs:108` (also :121 and :127). Authorization uses only `renewed_at + current_policy`; it never checks the stored `expires` and never makes expiry terminal. Exact-query SQLite reproduction on dev-02: a key last renewed at T is denied at T+31 days with the default 30-day setting, then accepted at the same server time after setting 365. A key denied at T+2 days after shortening to 1 day is likewise accepted after restoring 30 days. Its next successful scope check can renew it. This violates the required no-resurrection behavior and lets a stolen expired key become usable again without sign-in. Preserve terminal expiry across policy increases, including expiry caused by shortening, and cover both sequences. 2. **P1 — Migration version 0033 conflicts with current dev's deployed history.** `crates/infraternal-server/migrations/0033_api_key_expiry.sql:1`. Current origin/dev already contains `0033_remote_join_key_binding.sql` and expects versions 1–33. This new, different migration also claims 33. The compact migration generator preserves numeric prefixes and original SHA-384 checksums (`build_support/migrations.rs:27,53`); the SQLx migrator checks an applied version's checksum and returns `VersionMismatch` on disagreement. Retaining both files on integration also gives a duplicate version. Renumber the new expiry migration to the next free version and update migration-chain tests before integration; do not replace the existing deployed 33. Evidence: `git ls-tree origin/dev`, `git show origin/dev:crates/infraternal-server/migrations/0033_remote_join_key_binding.sql`, and pinned SQLx 0.8 migrator source lines 173–176 on dev-02. No build was needed to establish the duplicate version. 3. **P2 — Wall-clock rollback makes expired keys valid again.** `crates/infraternal-server/src/auth/devices.rs:103` and :108; clock implementation `auth/http.rs:203`. The new check samples raw `SystemTime`, with no monotonic elapsed-time anchor or terminal-expiry state. Exact-query reproduction: deny at T+30 days, move the sampled server clock back one second, and the same key is accepted. The saturating subtraction at :118 only avoids moving `renewed_at` backward; it does not protect the authorization decision. Use a rollback-safe server clock/expiry policy and add a deterministic rollback regression. No system clock was changed for this review. 4. **P2 — Renewal rechecks use a stale time sample after database waits.** `crates/infraternal-server/src/auth/devices.rs:121`–123. `now` is captured before the initial pool query and reused through the writer-lock wait and fallback read. A request sampled one second before a 1-day deadline can execute its UPDATE one second after that deadline and still renew for another day; the advertised under-lock live-deadline recheck uses the old time. Exact-query reproduction confirmed `renewed_rows=1` and a now-valid key after that expired interval. Resample authoritative time after acquiring the writer and use that sample consistently for expiry, hourly cutoff and renewal; add a controlled lock/pool-wait boundary regression. Evidence: `/home/kayg/.cache/codex-scratch/j97/expiry-repro.py` and `expiry-repro.log` on the control host. The synchronous reproduction ran on `infraternal-dev-02-std`, using the exact SELECT/UPDATE extracted from the reviewed source and migration 0033, with a synthetic nonsecret hash. It confirms SQL counterexamples, not end-to-end HTTP behavior or latency. Output verbatim: ```text policy increase: day31 denied=True; after 30->365 accepted=True shorten then lengthen: day2 denied at 1d=True; accepted again at 30d=True clock rollback: denied at deadline=True; accepted at deadline-1=True expiry across await: sampled=deadline-1; resumes=deadline+1; renewed_rows=1; live_after_stale_update=True confirmed 4 counterexamples using exact authorization/renewal SQL from d8ec67b0e; no bearer credentials used ``` Other checks: expired/unknown inputs share the same hash-lookup query and Denied error; no timing-class claim beyond that code inspection. Exact scope rejection precedes renewal and the conditional UPDATE bounds concurrent renewal writes. Normal startup tracks migration completion, so repeated Auth construction does not rerun grace. Settings enforce integer 1–365, pinned Origin, fresh owner session and an under-writer owner/session recheck. Changed code binds hashes, does not log keys, and the secret-free CLI diagnostic returns D303 exit 3. No production caller of api_authorize exists in this target, as J90 disclosed; that limits present exposure but does not satisfy the requested auth contract. No repository code changed, gate not applicable. Per review scope, only the lightweight finding-confirmation reproduction ran; no builds, full gate, pushes or deployments. All runs completed; no background work remains.
Author
Owner

J90 repair start: branch apikey-30d-269, head d8ec67b0e4a8974cdfd9ba585dbf1481449a9356, rebasing onto current dev be20430b2e0dc4f591aa98a036f1066cf5dd5f55. Plan: (1) append-only migration renumber and upgrade regression; (2) absolute expiry, permanent tombstones and policy regressions; (3) persisted time high-water and rollback regression; (4) post-lock time sampling and controlled DB-wait regression. One atomic commit per finding. Then focused tests and full fast gate, distinguishing known dev failures. Estimate: 2 hours including VM queue/build time. Reviewer exact-query reproductions read and confirmed against the current source. Both VM queues checked; both currently full. No CLI automation changes.

J90 repair start: branch `apikey-30d-269`, head `d8ec67b0e4a8974cdfd9ba585dbf1481449a9356`, rebasing onto current dev `be20430b2e0dc4f591aa98a036f1066cf5dd5f55`. Plan: (1) append-only migration renumber and upgrade regression; (2) absolute expiry, permanent tombstones and policy regressions; (3) persisted time high-water and rollback regression; (4) post-lock time sampling and controlled DB-wait regression. One atomic commit per finding. Then focused tests and full fast gate, distinguishing known dev failures. Estimate: 2 hours including VM queue/build time. Reviewer exact-query reproductions read and confirmed against the current source. Both VM queues checked; both currently full. No CLI automation changes.
Author
Owner

J97 repair 1 committed/pushed as b987f8982: rebased onto dev be20430b2, preserved deployed 0033, appended API-key policy as 0034, and extended ordered/legacy-upgrade tests. Duplicate-version reproduction failed with [...32,33,33]; renumbered migration test run on dev-02 exited 0. Docs changes followed the dev relocation to docs/user/keys.md. No persisted clock-health implementation exists on this dev tree, so the expiry high-water mark will be a small persisted server-settings value, not new clock infrastructure. Policy regression red run is compiling on compute01 (free slots versus full dev-02); only target is ~/worktrees/j90/target on compute01. Logs: ~/.cache/codex-scratch/j90/repair-*.log.

J97 repair 1 committed/pushed as `b987f8982`: rebased onto dev `be20430b2`, preserved deployed 0033, appended API-key policy as 0034, and extended ordered/legacy-upgrade tests. Duplicate-version reproduction failed with `[...32,33,33]`; renumbered migration test run on dev-02 exited 0. Docs changes followed the dev relocation to `docs/user/keys.md`. No persisted clock-health implementation exists on this dev tree, so the expiry high-water mark will be a small persisted server-settings value, not new clock infrastructure. Policy regression red run is compiling on compute01 (free slots versus full dev-02); only target is `~/worktrees/j90/target` on compute01. Logs: `~/.cache/codex-scratch/j90/repair-*.log`.
Author
Owner

J97 repairs 2 and 3 pushed: 574b16f32 stores absolute deadlines, permanent tombstones and policy clamps; 4560f0f9c persists the server time high-water mark across restart. Evidence: policy red run was 7 passed / 2 failed; after terminal-expiry repair 9 passed / 2 failed (only clock and DB-wait reproductions); after clock repair 10 passed / 1 failed (only DB wait), plus 16 migration tests passed. Reviewer exact-query script was rerun on compute01 and reproduced all four original counterexamples verbatim. Logs: repair-policy-red.log, repair-terminal-green-clock-wait-red.log, repair-clock-green-wait-red.log, j97-exact-query-repro.log under ~/.cache/codex-scratch/j90/ on their respective VMs. Compute01 became full before the clock run acquired a slot, so that queued run was cancelled and its target removed; the only live J90 target is now dev-02 ~/worktrees/j90/target. Final post-lock time-sampling repair is under focused validation with the remote feature enabled. Next: fourth atomic commit, full fast gate, baseline comparison for any unrelated failures.

J97 repairs 2 and 3 pushed: `574b16f32` stores absolute deadlines, permanent tombstones and policy clamps; `4560f0f9c` persists the server time high-water mark across restart. Evidence: policy red run was 7 passed / 2 failed; after terminal-expiry repair 9 passed / 2 failed (only clock and DB-wait reproductions); after clock repair 10 passed / 1 failed (only DB wait), plus 16 migration tests passed. Reviewer exact-query script was rerun on compute01 and reproduced all four original counterexamples verbatim. Logs: `repair-policy-red.log`, `repair-terminal-green-clock-wait-red.log`, `repair-clock-green-wait-red.log`, `j97-exact-query-repro.log` under `~/.cache/codex-scratch/j90/` on their respective VMs. Compute01 became full before the clock run acquired a slot, so that queued run was cancelled and its target removed; the only live J90 target is now dev-02 `~/worktrees/j90/target`. Final post-lock time-sampling repair is under focused validation with the remote feature enabled. Next: fourth atomic commit, full fast gate, baseline comparison for any unrelated failures.
Author
Owner

J94 coordination update: the focused RED tests now confirm the read-only sign-in default and narrow scope allowlist are not implemented on origin/dev. J94 will keep api_authorize(token, scope) -> Result<String> unchanged and derive the audit key id from the existing SHA-256 token hash; no sliding-expiry logic will be duplicated. The production scope allowlist change is isolated to device_start, separate from the api_authorize expiry repair. Please send the final repaired head when available so J94 can revalidate and update its base before the final gate.

J94 coordination update: the focused RED tests now confirm the read-only sign-in default and narrow scope allowlist are not implemented on `origin/dev`. J94 will keep `api_authorize(token, scope) -> Result<String>` unchanged and derive the audit key id from the existing SHA-256 token hash; no sliding-expiry logic will be duplicated. The production scope allowlist change is isolated to `device_start`, separate from the `api_authorize` expiry repair. Please send the final repaired head when available so J94 can revalidate and update its base before the final gate.
Author
Owner

All four repair commits are pushed; head 6883d3c743b1a344ec6d147591d9156de5c0acfb. Final focused validation on dev-02: 11/11 API-key regressions, 21/21 remote-enabled migration tests, 1/1 CLI diagnostic test; all exits 0 (repair-final-focused.log). The VM checkout was fetched/reset to that exact head and was clean. The first full-gate invocation was cancelled: I mistakenly put INFRATERNAL_TEST_STATE_ROOT under /tmp, and six CLI replacement/signature fixtures correctly rejected its writable ancestry with PermissionDenied. Source inspection confirmed that environment error; it is not being classified as a dev failure. The full gate is rerunning with private /home/claude/.j90-state; final log is ~/.cache/codex-scratch/j90/repair-verify-fast-final.log on dev-02. No implementation changes followed the focused pass.

All four repair commits are pushed; head `6883d3c743b1a344ec6d147591d9156de5c0acfb`. Final focused validation on dev-02: 11/11 API-key regressions, 21/21 remote-enabled migration tests, 1/1 CLI diagnostic test; all exits 0 (`repair-final-focused.log`). The VM checkout was fetched/reset to that exact head and was clean. The first full-gate invocation was cancelled: I mistakenly put `INFRATERNAL_TEST_STATE_ROOT` under `/tmp`, and six CLI replacement/signature fixtures correctly rejected its writable ancestry with PermissionDenied. Source inspection confirmed that environment error; it is not being classified as a dev failure. The full gate is rerunning with private `/home/claude/.j90-state`; final log is `~/.cache/codex-scratch/j90/repair-verify-fast-final.log` on dev-02. No implementation changes followed the focused pass.
Author
Owner

J94 coordination request: the action-bound hosts remove approval lasts up to 5 minutes and retains no bearer plaintext. At final passkey completion, the API key must still be live and have hosts:write, with revocation/expiry serialized against the removal transaction. Could J90 expose a transaction-aware authorization helper that accepts the stored token hash, scope and existing SQLite transaction (or tell J94 the intended helper shape)? J94 will not retain the raw key or reimplement the sliding-expiry policy. The audit key id is the existing SHA-256 token hash.

J94 coordination request: the action-bound `hosts remove` approval lasts up to 5 minutes and retains no bearer plaintext. At final passkey completion, the API key must still be live and have `hosts:write`, with revocation/expiry serialized against the removal transaction. Could J90 expose a transaction-aware authorization helper that accepts the stored token hash, scope and existing SQLite transaction (or tell J94 the intended helper shape)? J94 will not retain the raw key or reimplement the sliding-expiry policy. The audit key id is the existing SHA-256 token hash.
Author
Owner

Gate progress at unchanged head 6883d3c74: corrected private fixture-root run has passed fmt, VT fmt, clippy, SSH runtime helper and real-binary fixture paths, and is now in workspace tests. All six prior PermissionDenied cases pass after correcting the runner environment. CLI plugin compile-out has one separate failure: restart::tests::reload_reopens_install_path_when_the_running_inode_was_replaced exceeded its existing 60-second child hang bound (127 passed, 1 failed, 1 ignored). This is not one of the three J96 failures; I will retry the exact test and compare on unmodified dev after the gate finishes. No source change is planned for that unrelated fixture. Gate remains running on dev-02, log ~/.cache/codex-scratch/j90/repair-verify-fast-final.log; only J90 target is ~/worktrees/j90/target there. All code is committed and pushed.

Gate progress at unchanged head `6883d3c74`: corrected private fixture-root run has passed fmt, VT fmt, clippy, SSH runtime helper and real-binary fixture paths, and is now in workspace tests. All six prior PermissionDenied cases pass after correcting the runner environment. CLI plugin compile-out has one separate failure: `restart::tests::reload_reopens_install_path_when_the_running_inode_was_replaced` exceeded its existing 60-second child hang bound (127 passed, 1 failed, 1 ignored). This is not one of the three J96 failures; I will retry the exact test and compare on unmodified dev after the gate finishes. No source change is planned for that unrelated fixture. Gate remains running on dev-02, log `~/.cache/codex-scratch/j90/repair-verify-fast-final.log`; only J90 target is `~/worktrees/j90/target` there. All code is committed and pushed.
Author
Owner

Fast gate finished at 6883d3c74: VERIFY_EXIT=1; TS_EXIT=0. Both static builds pass (x86 22,417,136 bytes, arm 20,450,336 bytes). The Rust test row stopped at the unrelated live-shell revocation fixture timeout, before server tests. The compile-out log also contains a 60-second restart fixture timeout even though that helper reports its row PASS. Both exact timeout cases now pass when retried alone on the same source. Repeated focused results: 11 expiry + 21 migration + 1 CLI helper tests all pass. The two known J96 server cases (enrollment_http status and already_added typed-address selection) still fail in focused execution; unmodified-dev comparison is running from the archived be20430b2 source. Full gate log: dev-02 ~/.cache/codex-scratch/j90/repair-verify-fast-final.log (also copied to control cache). During the gate the shared VM disk reached 1.3 GB free; deleting only J90 incremental and completed debug artifacts restored 63 GB, with diagnostic binaries preserved until follow-up finishes. Current only target: dev-02 ~/worktrees/j90/target; no source changes or pending work on compute01.

Fast gate finished at `6883d3c74`: VERIFY_EXIT=1; TS_EXIT=0. Both static builds pass (x86 22,417,136 bytes, arm 20,450,336 bytes). The Rust test row stopped at the unrelated live-shell revocation fixture timeout, before server tests. The compile-out log also contains a 60-second restart fixture timeout even though that helper reports its row PASS. Both exact timeout cases now pass when retried alone on the same source. Repeated focused results: 11 expiry + 21 migration + 1 CLI helper tests all pass. The two known J96 server cases (enrollment_http status and already_added typed-address selection) still fail in focused execution; unmodified-dev comparison is running from the archived `be20430b2` source. Full gate log: dev-02 `~/.cache/codex-scratch/j90/repair-verify-fast-final.log` (also copied to control cache). During the gate the shared VM disk reached 1.3 GB free; deleting only J90 incremental and completed debug artifacts restored 63 GB, with diagnostic binaries preserved until follow-up finishes. Current only target: dev-02 `~/worktrees/j90/target`; no source changes or pending work on compute01.
Author
Owner

J90 / J97 repairs finished and pushed on apikey-30d-269.

Head: 6883d3c743b1a344ec6d147591d9156de5c0acfb.
Rebased onto dev be20430b2e0dc4f591aa98a036f1066cf5dd5f55.

Four atomic repair commits:

  • b987f8982: preserve deployed migration 0033 and append API-key policy at 0034; retain the upgrade/checksum tests.
  • 574b16f32: absolute expires_at, permanent dead-key tombstones, shortening clamps, and lengthening only on successful renewal.
  • 4560f0f9c: persisted server time high-water mark, including denied checks and restart coverage.
  • 6883d3c74: sample authorization time after acquiring the writer and key row; check expiry before renewal under that transaction.

The reviewer’s exact-query script reproduced all four original counterexamples. The regression tests failed before their corresponding repairs. Final focused verification was rebuilt after clearing the job-local package artifacts: 11 expiry tests, 21 migration tests, and 1 CLI diagnostic test passed; all three exits are 0. Scope, revocation, hourly renewal writes, legacy upgrade grace, policy validation and owner authority remain covered. Migration changes are append-only relative to dev (0034–0036). The published keys page is now docs/user/keys.md following dev’s documentation move. No CLI automation was wired; stored keys remain unused by CLI commands, documented explicitly and tracked in #303.

The full fast gate is red from unrelated fixture timeouts:

  • The compile-out log contains a 60-second timeout in restart::tests::reload_reopens_install_path_when_the_running_inode_was_replaced, although the helper’s summary row reports PASS.
  • The workspace test row stops at agent_daemon::real_binary_rootless_person_revocation_stops_existing_shells with a socket/child timeout, before reaching the server suite.

Both exact fixtures pass when retried alone on this head and on unmodified dev be20430b2. Neither gate failure is one of the three reported J96 failures. Separate focused runs confirm the enrollment HTTP status failure and already_added_breakit::the_typed_address_decides_which_host_is_named_when_two_match fail on both head and dev. The third known failure, migration count 33 versus 32, reproduces on dev; the branch’s updated migration chain passes. No unrelated fixture code was changed.

Gate summary, verbatim:

+---------------------------+--------------+
| Rust gate                 | Result       |
+---------------------------+--------------+
| fmt                       | PASS         |
| VT native fmt             | PASS         |
| CLI plugin compile-out    | PASS         |
| clippy                    | PASS         |
| SSH runtime test helper   | PASS         |
| real-binary fixture paths | PASS         |
| test                      | FAIL (101)   |
| SSH inode reuse fixture   | PASS         |
| shim process-per-user     | PASS         |
| cargo-deny                | PASS         |
| gate architecture         | PASS         |
| static builds             | PASS         |
+---------------------------+--------------+
VERIFY_EXIT=1

The separate TypeScript continuation completed with the following verbatim result:

TS_EXIT=0

Both static images passed linkage and target checks. Native aarch64 key-memory checks passed; foreign x86 runtime probes were skipped. Image sizes: x86_64 22,417,136 bytes; aarch64 20,450,336 bytes. Against the gate’s recorded baselines, the reported deltas are +973,312 and +821,096 bytes. Critical JavaScript gzip is 311,025/327,680 bytes; deferred is 61,272/61,440. Protocol tests: 39 pass, 0 fail.

Evidence:

The control worktree and dev-02 checkout are clean, and the remote branch head matches the SHA above. No J90 run remains pending. Both VM targets are absent; diagnostic binaries, comparison source copy and private test state were removed. Evidence logs are retained. No merge or deployment was performed; issues remain open.

J90 / J97 repairs finished and pushed on `apikey-30d-269`. Head: `6883d3c743b1a344ec6d147591d9156de5c0acfb`. Rebased onto dev `be20430b2e0dc4f591aa98a036f1066cf5dd5f55`. Four atomic repair commits: - `b987f8982`: preserve deployed migration 0033 and append API-key policy at 0034; retain the upgrade/checksum tests. - `574b16f32`: absolute `expires_at`, permanent dead-key tombstones, shortening clamps, and lengthening only on successful renewal. - `4560f0f9c`: persisted server time high-water mark, including denied checks and restart coverage. - `6883d3c74`: sample authorization time after acquiring the writer and key row; check expiry before renewal under that transaction. The reviewer’s exact-query script reproduced all four original counterexamples. The regression tests failed before their corresponding repairs. Final focused verification was rebuilt after clearing the job-local package artifacts: 11 expiry tests, 21 migration tests, and 1 CLI diagnostic test passed; all three exits are 0. Scope, revocation, hourly renewal writes, legacy upgrade grace, policy validation and owner authority remain covered. Migration changes are append-only relative to dev (0034–0036). The published keys page is now `docs/user/keys.md` following dev’s documentation move. No CLI automation was wired; stored keys remain unused by CLI commands, documented explicitly and tracked in #303. The full fast gate is red from unrelated fixture timeouts: - The compile-out log contains a 60-second timeout in `restart::tests::reload_reopens_install_path_when_the_running_inode_was_replaced`, although the helper’s summary row reports PASS. - The workspace test row stops at `agent_daemon::real_binary_rootless_person_revocation_stops_existing_shells` with a socket/child timeout, before reaching the server suite. Both exact fixtures pass when retried alone on this head and on unmodified dev `be20430b2`. Neither gate failure is one of the three reported J96 failures. Separate focused runs confirm the enrollment HTTP status failure and `already_added_breakit::the_typed_address_decides_which_host_is_named_when_two_match` fail on both head and dev. The third known failure, migration count 33 versus 32, reproduces on dev; the branch’s updated migration chain passes. No unrelated fixture code was changed. Gate summary, verbatim: ``` +---------------------------+--------------+ | Rust gate | Result | +---------------------------+--------------+ | fmt | PASS | | VT native fmt | PASS | | CLI plugin compile-out | PASS | | clippy | PASS | | SSH runtime test helper | PASS | | real-binary fixture paths | PASS | | test | FAIL (101) | | SSH inode reuse fixture | PASS | | shim process-per-user | PASS | | cargo-deny | PASS | | gate architecture | PASS | | static builds | PASS | +---------------------------+--------------+ VERIFY_EXIT=1 ``` The separate TypeScript continuation completed with the following verbatim result: ``` TS_EXIT=0 ``` Both static images passed linkage and target checks. Native aarch64 key-memory checks passed; foreign x86 runtime probes were skipped. Image sizes: x86_64 22,417,136 bytes; aarch64 20,450,336 bytes. Against the gate’s recorded baselines, the reported deltas are +973,312 and +821,096 bytes. Critical JavaScript gzip is 311,025/327,680 bytes; deferred is 61,272/61,440. Protocol tests: 39 pass, 0 fail. Evidence: - [Full gate output, verbatim](https://git.kayg.org/attachments/7a8240e8-c43f-414a-a77d-7e1a7e508676) - [Final clean focused rebuild](https://git.kayg.org/attachments/19a80ef8-501e-4aae-ba57-3cc6167286f5) - [Head retries and unmodified-dev comparison](https://git.kayg.org/attachments/99243af9-7d42-486d-a836-29694e04f542) - [Reviewer exact-query reproduction](https://git.kayg.org/attachments/4f058bc7-f572-45a8-968b-5f0247865eb2) The control worktree and dev-02 checkout are clean, and the remote branch head matches the SHA above. No J90 run remains pending. Both VM targets are absent; diagnostic binaries, comparison source copy and private test state were removed. Evidence logs are retained. No merge or deployment was performed; issues remain open.
Author
Owner

J98 independent auth review round 2 started. Target apikey-30d-269 at 6883d3c743b1a344ec6d147591d9156de5c0acfb, review branch review-j98; comparison is current origin/dev. Plan (about 30 minutes): inspect D311 and all four J97 fixes plus remaining auth properties, confirm any suspected finding using a lightweight VM reproduction, then post numbered findings or APPROVE. No code changes planned; no full gate per review scope.

J98 independent auth review round 2 started. Target `apikey-30d-269` at `6883d3c743b1a344ec6d147591d9156de5c0acfb`, review branch `review-j98`; comparison is current origin/dev. Plan (about 30 minutes): inspect D311 and all four J97 fixes plus remaining auth properties, confirm any suspected finding using a lightweight VM reproduction, then post numbered findings or APPROVE. No code changes planned; no full gate per review scope.
Author
Owner

J98 independent auth review, round 2: APPROVE.

Reviewed apikey-30d-269 at 6883d3c743b1a344ec6d147591d9156de5c0acfb against current origin/dev 7089be97953f9d5893d5f8d9c4e0713fb7f3b2f7, D311, owner comments 74285/74294, J97 findings 74442 and J90 repair report 74585. No actionable P0–P3 findings.

Round-1 disposition:

  1. Lifetime resurrection: fixed. crates/infraternal-server/src/auth/devices.rs:120 checks the minimum of the absolute deadline and current-policy deadline. Expiry commits dead=1 before returning Denied (:121–129); renewal requires a live row and an allowed scope (:131–141). auth/api_expiry_tests.rs:286 covers both original policy-increase counterexamples and an expired key without an earlier denied check; :328 covers shortening clamps and extension only at a successful hourly renewal.
  2. Migration collision: fixed. Current dev contains 0001–0033. git diff origin/dev HEAD -- crates/infraternal-server/migrations contains additions only: 0034, 0035, 0036. Deployed migrations, including 0033, are byte-for-byte unchanged. Updated chain and legacy-upgrade tests check versions, preserved checksums/data and one-time upgrade grace. git merge-tree --write-tree origin/dev HEAD exits 0, with no conflicts.
  3. Clock rollback: fixed. auth/devices.rs:183 reads/updates the persisted high-water mark under the same writer transaction, and returns max(wall, recorded). Authorization commits it on unknown-key and expiry denial too. auth/api_expiry_tests.rs:353 covers the original deadline/deadline-minus-one sequence, an unchecked key, and Auth reconstruction across restart.
  4. Stale renewal timestamp: fixed. auth/devices.rs:107–112 acquires the writer and reads the key before sampling the clock. Deadline, hourly eligibility and renewal all use that sample in the same transaction. auth/api_expiry_tests.rs:381 deterministically holds the sole pool connection, polls authorization at deadline-minus-one, advances the injected clock to deadline-plus-one, then releases the connection and requires denial with no extension.

Other properties remain intact: expired and unknown keys return the same Denied variant/message (test :29), with no stronger timing-indistinguishability claim; allowed renewals remain at most hourly and concurrent uses renew once (tests :49, :262); denied scopes do not renew and committed revocation denies subsequent authorization (:76). Settings enforce integer 1–365, reject unknown fields, pin Origin, and require a live owner plus fresh passkey authority for mutation (:151, :203). The writer-side authority recheck uses fresh_session_tx (crates/infraternal-plugin-api/src/lib.rs:224), whose SQL uses max(request time, SQLite current time), and re-reads owner status. Changed SQL binds hashes; the changed paths introduce no bearer-key logging, error disclosure or argv handling. The CLI diagnostic accepts no credential and retains exit 3.

Test evidence: independently inspected the regression source and the attached final clean focused-build log, which identifies this exact head: https://git.kayg.org/attachments/19a80ef8-501e-4aae-ba57-3cc6167286f5 . Author-run results, verbatim:

FINAL_FOCUSED_EXIT=0 FINAL_MIGRATION_EXIT=0 FINAL_CLI_EXIT=0
RUN_EXIT=0

That log records 11 expiry tests, 21 migration tests and 1 CLI diagnostic test passing. These are verified author-run records, not new J98 test executions. Per review scope, no tests were run because no runtime finding needed confirmation. No code changed, gate not applicable. The reported full fast gate remains red (test FAIL (101), VERIFY_EXIT=1); this review does not claim a green gate or reopen the named unrelated fixture failures. No production API-key consumer is present in this target; integration remains #303. No latency claim is made.

Review branch review-j98 remains at the reviewed head. No build, test, browser or background run remains pending. No push, merge or deployment; issue stays open.

J98 independent auth review, round 2: **APPROVE**. Reviewed `apikey-30d-269` at `6883d3c743b1a344ec6d147591d9156de5c0acfb` against current `origin/dev` `7089be97953f9d5893d5f8d9c4e0713fb7f3b2f7`, D311, owner comments 74285/74294, J97 findings 74442 and J90 repair report 74585. No actionable P0–P3 findings. Round-1 disposition: 1. **Lifetime resurrection: fixed.** `crates/infraternal-server/src/auth/devices.rs:120` checks the minimum of the absolute deadline and current-policy deadline. Expiry commits `dead=1` before returning Denied (:121–129); renewal requires a live row and an allowed scope (:131–141). `auth/api_expiry_tests.rs:286` covers both original policy-increase counterexamples and an expired key without an earlier denied check; :328 covers shortening clamps and extension only at a successful hourly renewal. 2. **Migration collision: fixed.** Current dev contains 0001–0033. `git diff origin/dev HEAD -- crates/infraternal-server/migrations` contains additions only: 0034, 0035, 0036. Deployed migrations, including 0033, are byte-for-byte unchanged. Updated chain and legacy-upgrade tests check versions, preserved checksums/data and one-time upgrade grace. `git merge-tree --write-tree origin/dev HEAD` exits 0, with no conflicts. 3. **Clock rollback: fixed.** `auth/devices.rs:183` reads/updates the persisted high-water mark under the same writer transaction, and returns max(wall, recorded). Authorization commits it on unknown-key and expiry denial too. `auth/api_expiry_tests.rs:353` covers the original deadline/deadline-minus-one sequence, an unchecked key, and Auth reconstruction across restart. 4. **Stale renewal timestamp: fixed.** `auth/devices.rs:107–112` acquires the writer and reads the key before sampling the clock. Deadline, hourly eligibility and renewal all use that sample in the same transaction. `auth/api_expiry_tests.rs:381` deterministically holds the sole pool connection, polls authorization at deadline-minus-one, advances the injected clock to deadline-plus-one, then releases the connection and requires denial with no extension. Other properties remain intact: expired and unknown keys return the same Denied variant/message (test :29), with no stronger timing-indistinguishability claim; allowed renewals remain at most hourly and concurrent uses renew once (tests :49, :262); denied scopes do not renew and committed revocation denies subsequent authorization (:76). Settings enforce integer 1–365, reject unknown fields, pin Origin, and require a live owner plus fresh passkey authority for mutation (:151, :203). The writer-side authority recheck uses `fresh_session_tx` (`crates/infraternal-plugin-api/src/lib.rs:224`), whose SQL uses max(request time, SQLite current time), and re-reads owner status. Changed SQL binds hashes; the changed paths introduce no bearer-key logging, error disclosure or argv handling. The CLI diagnostic accepts no credential and retains exit 3. Test evidence: independently inspected the regression source and the attached final clean focused-build log, which identifies this exact head: https://git.kayg.org/attachments/19a80ef8-501e-4aae-ba57-3cc6167286f5 . Author-run results, verbatim: ```text FINAL_FOCUSED_EXIT=0 FINAL_MIGRATION_EXIT=0 FINAL_CLI_EXIT=0 RUN_EXIT=0 ``` That log records 11 expiry tests, 21 migration tests and 1 CLI diagnostic test passing. These are verified author-run records, not new J98 test executions. Per review scope, no tests were run because no runtime finding needed confirmation. No code changed, gate not applicable. The reported full fast gate remains red (`test FAIL (101)`, `VERIFY_EXIT=1`); this review does not claim a green gate or reopen the named unrelated fixture failures. No production API-key consumer is present in this target; integration remains #303. No latency claim is made. Review branch `review-j98` remains at the reviewed head. No build, test, browser or background run remains pending. No push, merge or deployment; issue stays open.
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#269
No description provided.