infraternal sign-in <server> (D311) #269
Open
opened 2026-10-31 16:32:20 +00:00 by kayg
·
129 comments
No Branch/Tag specified
main
j470b-server-mesh-rejoin
mesh-compat-382
j444b-d381-key-wins-flood
j461b-macos-utun-reader-thread
j475b-remove-host-motion
fix-reconnect-new-link
j481-d380
dev
j479-mesh-soak
fix/j476-host-facts
j475-motion-pill-split
j470-mac-node-join
root-upgrade-d376
j474-review-merge
latency-targets-120hz
nightly-suite-shards
gotatun-pin-j469
macos-utun-reader-thread
fix-flood-echo-stall
wip/j416
motion-fast-ease-out
mesh-addhost-j259
takeover-close-terminals-d368
kbd-instant-followups
fix-trust-key-double-click
nightly-suite-partial-report
appwin-icons-j207
rusqlite-slice1
swipe-content-first-d374
mesh-mtu-381
addhost-instant-open
glass-contrast-d365-mock
glass-contrast-d365
signout-followups-442
fix-startup-toast-red
research-gotatun-matrix-j317
themes-sync-j304
fix-load-flakes
fix-nix-zig-patch
fix-compute01-browser-baseline
ratchet-size-baseline-419
predict-auto-d130
revoke-coverage-d367
nightly-suite-queue-timeout
nightly-browser-suite
fix-rustsec-2026-0459
fonts-ioskeley-obel
log-retention-d375
fix-kbd-no-anim-427
fix-dev-red-specs-2
takeover-proof-305-j205
wip/j410-server-pump
ui-consistency-d372
fix-osc52-clipboard
libghostty-vt-1.0
forget-retired-host-d369
j404-merge-426
fix-setup-lock-race
fix-touch-select-426
revoke-access-report-ui
fix-predictive-echo-specs
fix-selinux-agent-exec
wip/j395-checkpoint2
wip/j395
j391-review-scratch
ui-consistency-j195-deferred-rebased
fix-rustc-paths-421
decisions-wording-341
fix-install-suite-red
fix-dev-rtt-specs
fix-pending-removal-align
fix-privileged-skip
fix-install-suite-404
review-j368
mesh-dns-j254
revoke-access-report-d356b
revoke-privilege-round5
fix-removal-confirm
review-j360
gotatun-rotation-367
deps-refresh-340
review-j354
review-J351
review-j350
addhost-takeover-d359
revoke-report-items-347
revoke-end-sessions-d362
review-j348
size-mesh-ssh-346
size-trim-337
revoke-access-diff-d356
j259-pre-rebase-b06c5a6
j305-ended-at-start-spec
addhost-sudo-consent
fix-layout-real-specs-411
fix-shell-open-411
fix-console-hosts-373
fix-shell-exit
fuzz-host-name-353
fix-host-details-410
macos-filter-j197
research-gotatun-tricks
revoke-p3-j216l
stale-state-pill
revoke-followups-j216k
revoke-progress-j216e
backup/macos01-20261106/lume/j197
backup/macos01-20261106/lume/appwin-icons-j207-stash0
backup/macos01-20261106/claude/stash-0
backup/macos01-20261106/claude/stash-1
backup/macos01-20261106/claude/J80-macos-s1
backup/macos01-20261106/claude/J80
backup/macos01-20261106/claude/J80-slice1-20261102
backup/macos01-20261106/claude/J16b-appwin-on-dev
backup/macos01-20261106/claude/appwin-launch-idle-146
backup/macos01-20261106/claude/J80-s1-run
wip/j259
trust-285-p3c
runtime-lock-j216f
mesh-ssh-p3-j175
tun-proof-p3b
trust-285-p3
terminal-polish-j271
tun-proof-p3
ssh-install-flakes-j216i
fuzz-fixes-j264
trust-285-a
trust-285-b
restart-oversized-fixture-j198
backup/macos01-20261106/lume/appwin-icons-j207
restart-bound-j216h
up-preflight-p3-j198
backup/macos01-20261106/lume/j251
up-preflight-j198
wip/up-preflight-j198-j245-p2
wip/up-preflight-j198-j245-p1
agent-fsync-offloop-j196
wip/up-preflight-j198-j245
settings-offscreen-j231
wip/up-preflight-j198-j226-inspection
wip/j103f
themes-j206
wip/up-preflight-j198-j226
flakes-j216c
backup/macos01-20261106/claude/j197
flakes-j216b
backup/macos01-20261106/claude/j197-red
wip/up-preflight-j198-review
flakes-j216
backup/macos01-20261106/claude/appwin-icons-j207
engineering-j208b
appwin-mock-j209
toast-stack-j165
engineering-j208
decisions-d351
decisions-d350
decisions-d349
appearance-presets-mock-j129
decisions-d348
docs-audit-spec
doc-coverage-baseline-raise
decisions-d347
decisions-d346b
decisions-d346
j88h-profile
ui-consistency-j195-deferred
ui-consistency-j195
engineering-practices-j194
preferred-login-ui-j166
signin-pools-263-j174
macos-node-265-s1
decisions-d345
decisions-d344
proxy-cgroup-j164
verify-quick-fuzz-329
proxy-start-io-j168
timing-flakes-j163
up-263-s1
mcp-events-j147
verify-quick-fuzz-j158
proxy-p3-j159
agent-resource-j157-detect
wip/j103d-final
remote-cancel-hang-j155
proxy-traefik-281-j62d
j149-verify-quick
proxy-traefik-281-j62b
j150-drop-unnamed
wip/j79d-red-r4
wip/j103d-d332
rootless-toast-j142
wip/j103d-head
wip/j103d
j125c-mcp
macos-build-j136
backup/macos01-20261106/claude/j136
wip/j79d-red-cli
wip/j79d-red-server
j121-link-liveness
j118c-tracer-failures
wip/j79c-head
release-desktop-j131
j124c-signed-macos
wip/j79c-red-p3
wip/j79c-red-p23
wip/j79c-red-p22
merge-j128-check
perf-deferred-j128
sidebar-toggle-260
j127-shortcut-changed-dot
j125-mcp
j118-tracer-failures
j126-account-summary
settings-sections-mock-j92
j120-pinch-lock
sidebar-292
backup/macos01-20261106/claude/j120
taste-2026-11-03
row-actions-242-215
backup/macos01-20261106/claude/stash-0-untracked
backup/macos01-20261106/claude/J80-265-s1-run
backup/macos01-20261106/claude/J80-macos-s1-20261102
j112-canvas-commands
ui-no-overshoot-scroll
fix-recovery-restart
backup/macos01-20261106/claude/stash-1-untracked
taste-2026-11-02
size-baseline-ratchet
machine-cards-structure-j108
j109-boot-copy-mockups
fix-dev-red
wip/up-263-s1
j95b-settings-rows
proxy-traefik-281
j95-option-rows
docs-human-j91
docs-keys-palette
fix-flood-1013-270-j38d
mesh-ssh-s1
mesh-ssh-s0
addhost-d291-tunnel
test-stability-load
mesh-relay-220
repro-296
shim-thp-off-24
mesh-pins-258-v2
mesh-dns
signin-d311-merged
docs-pages
ui-size-283
fix-ui-batch-specs
wip/j12b-edge-update
gate-on-arm
fix-rename-sync-293
trust-285-j60b
fix-focus-ring-clip
integ-ui-1101
edge-update-v2
signin-d311
taste-2026-11-01
layout-per-tab-250
ui-approved-splitopt-224
ui-approved-splitopt-224-on-dev
icon-text-centre-282
ui-approved-189-212-216
research-reverse-proxy
transfer-budget-320
review-mesh-pins-258-r2
sidebar-pill-259
review-mesh-relay-220
wip/j12-edge-update-v2
nix-package
status-crash-loop-237
fix-dev-test-failures
fix-fixture-socket-path
wip/mesh-relay-220-pre-dev-advance-rebase
fix-canvas-side-clip
research-head-to-head
frosted-real-256
review-mesh-pins-258
wip/mesh-relay-220-pre-final-rebase
review-layout-per-tab-250-r2
broad-suite-triage
nix-devshell
review-status-crash-loop-237-r3
wip/nix-devshell-pre-p3
taste-frosted-real
wip/code-serve-demo-2026-10-30
wip/code-serve-glass-2026-10-30
wip/code-serve-clusters-2026-10-30
wip/code-merge-release94-dev-2026-10-30
wip/rproxy-delta-scratch-2026-10-30
wip/stale-note-257-staged-2026-10-30
wip/d241-size-staged-2026-10-30
review-broad-suite-207
wip/mesh-pins-258-pre-split
wip/mesh-relay-220-pre-rebase
mesh-measure-220
plan-211-update
review-edge-autoupdate-r3
perf24-reconnect
review-stale-note-r2
release-prod-live
d241-shell-marker
review-mesh-pins-server-258
review-revocation-r3
review-setup-token-r2
review-sync-journal-r3
wip/review-mesh-pins-258-prune-review-1995fee46
frosted-scroll-256
sync-journal-r3
revocation-per-account-wip
review-tokio-asyncfd-0386
review-sync-p3s
wip/stash-8by64-stash0-2026-10-30
mesh-drag-r2
setup-token-data-wip
release-9-4
auth-busy-1013
mesh-view-overlap
sync-p3s
review-auth-busy-wake
review-readd-r8-wip
mesh-drag-bounds
audit-auth-93
app-windows-stream-start
review-authority-cap-r3
app-windows-r7-measure
readd-minimal-r6
readd-minimal-r5
app-windows-r7-soak
readd-minimal-r4
app-windows-r7
app-windows-r6
readd-minimal-r3
readd-minimal-r2
readd-minimal
merge-release8-dev
review-busy-page-liveness-r2
review-busy-page-liveness
mesh-view-trim
review-authority-read-retry-r2
review-remove-uninstall-r2
release-retry-reattach
release-retry
review-remove-uninstall
review-authority-read-retry
app-windows-ship
mesh-autoswitch-p2
mesh-ssh
mesh-relay
mesh-macos-node
screen-and-control
session-labels
review-real-logins-r5
review-real-logins-r4
review-real-logins-r3
shim-memory-r2
review-real-logins-r2
shim-memory
shadow-addhost-instant
shadow-real-logins
shadow-same-session-flood
shadow-prod-perf-fix
hosts-from-server
app-windows-real
app-windows-transport
release-prod-6w
mesh-policy-verify
quiet-close-toast
dev-merge-r4
review-mesh-agent-transport
review-app-windows-transport
settings-roundtrip
review-release-pipeline-r4
mesh-agent-transport
ended-strip-fix
review-audit-ca-update-d244
settings-rows
phone-width
signout-clear
fix-restart-tests
dev-merge-rp
signer-hardening
review-audit-ca-update
terminal-fidelity
review-audit-ca-update-wip
mesh-integrate
review-mesh-agent-transport-515d3712
review-mesh-dataplane-feec1988
mesh-dataplane
signout
research-canvas-node
wip/review-renderer-cost
wip/review-terminal-fidelity
padlock-glyph
review-release-pipeline-2e95b689
phone-polish
sidebar-polish
inbox-generic
review-mesh-dataplane-378d4055
prod-batch
ui-inbox
ui-ca-unlock
ui-machine-sections
review-mesh-dataplane-a5f744fb
tls-session-prod
session-bounds-prod
fix-dead-session
session-bounds
tls-session
palette-polish
shortcuts-d222
research-appmeta
phone-gutter
ui-centre-button
wip/ui-centre-button
wip/tls-session
review-addhost-breakit
ui-platform
renderer-cost
remote-agents
mesh-api
ssh-bootstrap
prod-hotfix2
prod-fixes
review-remote-agents
review-mesh-dataplane
session-life
research-browser
review-mesh-api
research-capture
review-mesh-api-round3
review-mesh-api-round1
gate-stability
review-gate-stability
review-relay-direct
review-mesh-api-findings
proto-sessions
font-picker
proto-addhost
proto-windows
mesh-core
latency-gate
research-bsd
research-discovery
keytray-mods
mock-core
ui-tweaks
fix-motion-sel
fix-motion
integrate2
fix-robust
t-web
t-server
vt-ghostty
fix-snap-echo
t-web-auth
review-vt2
t-agent
lane-protocol
lane-shim
lane-auth
s1-base
s1-contracts
s1-workspace
No results found.
No labels
launch-qa
Milestone
Clear milestone
No items
No milestone
Projects
Clear projects
No items
No project
Assignees
Clear assignees
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
Loading…
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
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
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-startwith{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.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./auth/device-inspectwith{code}, then shows the exact device and requested scopes before offering approval. Inspection requires the browser session;/auth/device-approverequires a fresh, passkey-verified session and consumes the code once. A plain link visit or code alone grants nothing./auth/device-redeemwith{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--jsonoutput contains server, device, scopes, user code, verification URL and expiry; completion contains server, device, scopes andstored. Neither includes a bearer key or polling token./auth/api-listlists device/scopes and a nonsecret id;/auth/api-revokedeletes 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-listand/auth/api-revoke: exact Origin and browser session; revoke also requires freshness.device-start/device-redeemand 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)
<server>or invalid options; show the exact HTTPS invocation.infraternal sign-in <server>and approve the new request.--jsonfollows the existing envelope and never includes a bearer secret.Slices and gates
scripts/verify faston aarch64; x86_64 static-size row on compute01 with the D243 delta reported.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.
Start: implementing on branch
signin-d311, based onorigin/devat42ab330358eb06d47acade8d0f2c35b9225af74b. 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.Finding — CLI polling must stay below the source limiter:
runtime.rs::trusted_ingresscaps/auth/device-startand/auth/device-redeemtogether 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 honorRetry-Afteron 429, leaving margin for the start request and concurrent sign-ins. Evidence: source admission testruntime.rsaround thedevice-start/device-redeemcases; aggregate budget testauth/http.rs::review_invalid_device_polling_exhausts_browser_budget.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 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 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.Plan amendment — the current auth API has no device-approval decline or cancel endpoint.
/auth/device-redeemreturns 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.rsdispatchesdevice-inspect,device-approve, anddevice-redeemonly;crates/infraternal-server/src/auth/devices.rs::device_redeemconsumes only approved rows and otherwise returnsError::Denied.Finding — credential writes can lose completed sign-ins. On
infraternal-dev-02-std,simultaneous_credential_stores_preserve_each_serverfailed 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 existingnix::fcntl::Flockfacility.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 — 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-listand fresh-passkey/auth/api-revokeroutes, 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::renderAccountandcrates/infraternal-server/src/auth/devices.rs::api_list/api_revoke.Finding — the sign-in action output schema requires
server,device,scopes, andstored, but the command first emits pending data withuser_code,verification_url, andexpires_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 -- --nocapturefails atserver 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.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.
Finding — one-time redemption can leave an active key without a local copy.
auth/devices.rs::device_redeemdeletes 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.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 -- --nocapturefails at compile time becauseFailure::RedemptionUncertainis 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.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 toserver_unavailable/exit 5, and malformed 200 responses map tobad_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 missingFailure::RedemptionUncertain; the relevant mapping branches are inredeem_once,post_json,post_json_with_timeout, andmap_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: the JSON Ctrl-C envelope for
sign-inused 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 regressioncli::output::tests::interrupted_sign_in_warns_that_the_server_key_may_need_revocationfails 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.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.Snapshot:
signin-d311now points toac22dbf98(branch was pushed so the WIP is recoverable). Slice 1 implementation is present. ARM evidence so far: 11sign_in::testspass and the targeted JSON Ctrl-C regression passes; ARMcargo fmt --all --checkpasses. 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 runscripts/verify faston ARM.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 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 — 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_timeoutcurrently applies one deadline around DNS lookup, TCP connect, TLS, request send, response headers, and body read.map_redemption_network_failuremaps everyNetworkFailure::Timeoutto partial exit 6. A timeout before Hyper begins sending/auth/device-redeemtherefore 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.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.
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.
The timeout regression reproduced on infraternal-dev-02-std before the fix.
redemption_timeout_before_request_send_is_unreachablefailed withleft: 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 classification fix is green on ARM. The new regression checks both request phases; all 12
sign_in::testspass, andcargo fmt --all --checkpassed 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 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.
After correcting the initial lint set, touched-crate Clippy now reports one remaining finding:
dispatch_actionis 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.The timeout fix and touched-code checks are green on infraternal-dev-02-std. The ARM run reports
cargo fmt --all --checkpassing, touched-crate Clippy passing with-D warnings, and all 12sign_in::testspassing, 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.Timeout classification and auth Clippy repairs are committed and pushed as
0e003f4960on signin-d311. The ARM scratch worktree is reset to this exact head, and branch-headscripts/verify fastis 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.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 shortINFRATERAL_TEST_STATE_ROOT, so the worktree/target-derived fallback exceeded Linuxsun_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.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.
Progress: I stopped only the queued ARM runner and removed only this job's target after
build-slotreported 16 GB free (<25 GB). The corrected branch-headscripts/verify fastretry is now running at0e003f4960006dc63d794d231fbe94997e427761on infraternal-dev-02-std with both short test-state roots set to/home/claude/.cache/j6andTRACER_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.Compute01 capacity update: the x86 static-size runner remains inside
build-slot, andlslocksshows all four/tmp/infraternal-build-slots/slot.*locks held. Several holders are running other fullscripts/verify fastgates. 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.Gate finding: the corrected ARM
scripts/verify fastrun failed one unchangedinfraternalshim integration test while running its 24-test shim batch.ring_wrap_snapshot_repaints_real_vimpanicked atcrates/infraternal/tests/shim.rs:276because the 5-second socket read returnedWouldBlock; 23 tests passed. The test file is byte-for-byte identical toorigin/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.ARM
scripts/verify fastattempt exited 1. Summary from the gate log:The test row records the shim
WouldBlockdescribed above. The static-build row ends with PythonOSError: [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 contains1.Static-row root cause confirmed: the ARM runner is aarch64 (
uname -m), whilescripts/test-openssl-avx512-gcm.pyexplicitly compilesinfraternal-openssl-avx512-gcm-testforx86_64-linux-musland then executes it directly.readelf -hconfirms the failed file isAdvanced Micro Devices X86-64.scripts/verify.d/rust.sh, the helper, and its C source are identical toorigin/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.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 currentorigin/dev. Currentorigin/devis57491e918c91fbf44dd5b318da168e1b4ef40310, while J6 is based on42ab330358eb06d47acade8d0f2c35b9225af74b. Current dev includes a shim test change that resolves the editor path before entering the login shell, and updatesscripts/verify.d/rust.shto 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 onto57491e9, push the updated branch, and rerun verification. I retract the earlier claim that those failures were present on current unmodifiedorigin/dev.Base correction and rebase: after confirming
origin/devhad advanced, I rebasedsignin-d311from42ab330358eb06d47acade8d0f2c35b9225af74bonto currentorigin/dev57491e918c91fbf44dd5b318da168e1b4ef40310. The pushed head is nowaab3560f1(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 rerunscripts/verify fastthere.Shim finding update: after rebasing onto current
origin/devand running inside the pinned flake shell, the ARM fast gate's shim integration batch passed 24/24, includingring_wrap_snapshot_repaints_real_vim. The old-base isolated retry had reproduced the timeout atshim.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.Gate finding: on the rebased ARM run,
infraternal-plugin-ssh'sstreaming::decoded_image_and_hash_advance_before_the_transfer_tail_is_releasedfailed while theinstallintegration binary ran (92 passed, 1 failed, 2 ignored). The assertion reportshashed=true staged_bytes=Ok(552579)atcrates/plugins/ssh/tests/install/streaming.rs:119. J6 changes nocrates/plugins/sshfiles. 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.Baseline proof: I reset the ARM verification worktree to the exact current
origin/devhead57491e918c91fbf44dd5b318da168e1b4ef40310and reran onlystreaming::decoded_image_and_hash_advance_before_the_transfer_tail_is_releasedunder 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.logand.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 headaab3560f1.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 headaab3560f1; the final row is queued throughbuild-slotunder 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.J6 finish
Branch
signin-d311is pushed ataab3560f1f60dfc024d83fb7f9262b1968849e32, rebased onto currentorigin/dev57491e918c91fbf44dd5b318da168e1b4ef40310.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 fastran on the rebased head. Its sole failing row is the SSH streaming test below; I reran that test on exact unmodifiedorigin/dev57491e918under 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:
Compute01 static-size row at this head:
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.
Starting independent security review on branch
review-signin-269, based at targetaab3560f1and comparing withorigin/dev57491e918. 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 tosignin-d311.Starting J36 size reduction on branch
signin-d311ataab3560f1f60dfc024d83fb7f9262b1968849e32, based onorigin/dev57491e918c91fbf44dd5b318da168e1b4ef40310. 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.Finding — the initial cross-slice Cargo diff shows no common new dependency activation.
cli-sessions-d314andmesh-pins-258-v2change no Cargo manifests or lockfile;signin-d311adds only a directzeroizeedge to the already-enabledremotefeature, with no crate version added. The existinghyper, 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.lockon the sessions, mesh-pins and sign-in worktrees.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-75receivescodebut renders only the device, scopes, warning and approval button; the value is used later fordevice-approveat line 101. The production tracer Playwright suite on infraternal-dev-02-std ran 7 approval specs: 6 passed and the new regressionapproval page shows the code so a person can match it with the CLIfailed at line 108 becauseAbCdEfGhIjKlMnOpwas 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-269asdf544521fde090e1472fe4acf87df326aaef3603. J36's size reduction must preserve both the CLI-to-page code comparison and this regression test.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.
Cross-slice cause check: the manifests show no common dependency edge.
cli-sessions-d314andmesh-pins-258-v2change no Cargo manifests or lockfile; D311 adds only a directzeroizeedge under the existingremotefeature. Neither diff adds a clapParserderive, and mesh-pins does not touch the CLI dispatch path. ARM nm attribution for D311 shows +48,056 B ofinfraternal::sign_incode and +9,980 B underserde_json/ +6,632 B underserde_core; it adds no code symbols underclap,webauthn,openssl, ortokio_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.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 remainsign_in::run_flow(16,796 B) andsign_in::post_json_with_timeout(12,332 B).J36 WIP checkpoint: pushed
signin-d311ated46f3644. 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 pinnedaab3560f1measurement. Next: build/test the new head, runscripts/verify faston compute01, and remeasure x86/ARM; dev-02 remains below its disk guard (9.6 GB free at last check).P2 — Credential file operations are not bound to the directory inode that was locked.
Evidence:
crates/infraternal/src/sign_in.rs:635-648holdslock_private_directory(directory)and then callsread_credential_file(directory)andreplace_file_atomically(&path, ...)by pathname.lock_private_directoryopens the directory withO_NOFOLLOW|O_DIRECTORYat line 652, but laterread_credential_fileappliesO_NOFOLLOWonly to the leaf credential file at line 687; a symlink in the directory path is still followed. A local writer to a misconfigured/sharedXDG_CONFIG_HOMEparent 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-269iscredential_read_refuses_a_private_directory_replaced_after_lockingat 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 residual risk / policy gap — CLI API keys have no expiry.
Evidence:
crates/infraternal-server/migrations/0001_auth.sql:10definesapi_keyswithout an expiry;crates/infraternal-server/src/auth/devices.rs:88-110inserts the key and authorizes it without a clock or expiry check. Only explicitapi_revokeat 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.
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.Finding: the pinned x86_64 static release build reproduced the D243 base exactly: 21,431,472 B at
57491e918c. The matchingaab3560f1head 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.J37 complete — verdict: fix first
Reviewed
signin-d311ataab3560f1f60dfc024d83fb7f9262b1968849e32againstorigin/dev57491e918c91fbf44dd5b318da168e1b4ef40310. Review branchreview-signin-269is ate53ab2f7c203144f97413762518eb161dcb7d47f(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-stdthrough~/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 exactorigin/dev; the remaining failure is the new storage regression. Static builds were skipped on this aarch64 VM.Verbatim final
scripts/verify fastsummary (exit status 1):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 CLIandcredential_read_refuses_a_private_directory_replaced_after_locking.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.J36 P2 failing-first reproduction: on
infraternal-dev-02-std(aarch64), the J37 regressioncredential_read_refuses_a_private_directory_replaced_after_lockingfailed againstbf16c2f31with 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 continuation: resuming on branch
signin-d311frombf16c2f31, based onorigin/dev57491e918c91fbf44dd5b318da168e1b4ef40310. 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.J37 P1 fix:
1e66039c0shows 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 P2 directory-swap fix:
27b406a19carries the locked directory descriptor through the credential transaction. It verifies the opened directory owner, 0700 mode, and inode identity; reads and temporary-file creation useopenatwithO_NOFOLLOW, replacement usesrenameat, 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.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-refactoraab3560f1artifact 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 directzeroizeedge points to an already locked crate); D314 usesArgMatchesrather 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, thenscripts/verify faston compute01.J36 security-fix results for J37 re-review. Branch
signin-d311is now at189f41716.P1, device-code phishing:
1e66039c0displays 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:
27b406a19holds and verifies the private directory fd through the locked read/replace section, withopenat/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;
189f41716fixes it by validating a local binding without changing the allowlist check.bun run checknow passes.Final two-architecture size attribution and
scripts/verify faston compute01 are still pending; I will post those results separately.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;189f41716only adjusts tracer TypeScript. Once the x86 build completes, I will run the matched ARM base/head build and the requested gates on compute01.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 nestedbash -lcreset the Nix dev-shell PATH. I canceled my queued ARM/test commands before checkout, replaced both measurement wrappers withnix 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.logand/home/claude/.cache/codex-scratch/j36-signin/aarch64-unknown-linux-musl/build-base-head-corrected.log.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-inanddevice_codemarkers; 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.ARM measurement tool correction: the compute01 aarch64 base build finished and produced a valid static AArch64 ELF (
filereports ARM aarch64), but hoststripcannot process this cross-architecture file (Unable to recognise the architecture). The unstripped base artifact is retained. I am usingllvm-strip/llvm-nmfrom the Nix LLVM package to finish base attribution and build/measure the head; no source changed.J36 continuation checkpoint:
signin-d311is pushed at189f41716; 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.Matched ARM64 size finding on
signin-d311@189f41716, built on compute01 with the pinned static release recipe and checked as static AArch64 ELF:57491e918189f41716The delta is below the 64 KiB stop by 4,304 B, but above the 16 KiB target.
nm -C -S --size-sortdiff 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, andurl+848 B; other roots are smaller. Largest additions aresign_in::run_flow17,360 B, sharedremote::post_json15,180 B,CredentialFiledeserialization 5,340 B, CLI dispatch 4,356 B, andcli_plugin3,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.logandbase-compute01.nm/head-189f41716.nmin that directory. The marker-checked matched x86 build is queued next; no cap changes are made.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 earlierhead-189f41716x86 artifact was from the checkout race and has no sign-in markers, so it remains excluded. Worktree was restored to189f41716before the queued x86 job; no source changed.J36 matched static release sizes and symbol attribution for
signin-d311@189f41716, built sequentially on compute01 from base57491e918withscripts/static-cargoand stripped as static ELF: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;
filereports static PIE on x86_64 and statically linked AArch64.nm -C -S --size-sortcode-symbol diff, grouped by root (net / added / removed): x86_64infraternal+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_flow24,201 B, sharedremote::post_json17,589 B, credential-file deserialization 7,939 B,dispatch_selected_action5,493 B,remote::join5,193 B, andcli_plugin4,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-lockedzeroize); D314 uses existingArgMatchesrather 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, andscripts/verify faston compute01 are still running/queued.Compute01 per-finding re-review results on
signin-d311@189f41716:approval page shows the code so a person can match it with the CLIand successful fresh virtual-passkey approval.openat/renameat,O_NOFOLLOW, and ownership/mode checks. Focused sign-in tests passed 13/13, includingcredential_read_refuses_a_private_directory_replaced_after_locking.run-final-gates.shis still runningscripts/verify faston compute01; I will add its exact summary and exit status after completion.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
cd04b7238on 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 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
f4b61cd6cand force-with-lease pushed headd1d0fedad. 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 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 against16b728e.Compute01 queue update for the rebase verification: head
16b728e212is 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.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 andscripts/verify fastare 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 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 fastfollows 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 runner update on
signin-d311@16b728e212a783a34c38e229a7a077dc64bac7f6: the fresh Playwright command acquired a build slot but exited before test collection becauseapps/tracer/playwright.j36.config.tswas missing (resolveConfigFileerror). No approval assertion ran. Sign-in remains 13/13 and tracer typecheck completed. The sequential runner is now queued onscripts/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 verification harness correction on
signin-d311@16b728e212a783a34c38e229a7a077dc64bac7f6:scripts/verify fasthit 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/j36and/tmp/j36-agentroots. 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.R2 research started. Read-only research branch
research-r2, basef4b61cd6c3f52078cad7573c5367d7a6e75f84f0(origin/dev); also inspectingorigin/signin-d311for sign-in credentials. No implementation or new engine decision. Evidence and owner questions will follow.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-d31116b728eadds 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: 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/devf4b61cd6c3, plusorigin/signin-d31116b728e212. 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 underCA/active; externally provisioned legacy configurations can use explicitly configured private paths. Node data = configured infraternal agent data directory; CLI default$XDG_DATA_HOME/infraternal/agentor~/.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
setup-token.sock/local CLI; server retains raw token in process memory and SHA-256 hash in DBsetup. Startup uses a private socket rather than treating the old banner format as permission to log secrets.passkeys.credentialcontains public credential/counter metadata;id,aaguid,prf_inputare not private signing keys.passkeys.prf_input; salts/nonces/envelopes inwrapsare public parameters/ciphertext.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.users.recovery_hashholds an Argon2id verifier;wrapsholds recovery ciphertext, never the words. Legacy v1 metadata still has compatibility paths.recovery.token_hash,ready,expires. Raw high-entropy token is not a persistent plaintext DB value.__Host-infraternal; Secure, HttpOnly, SameSite=Strict, Path=/; DBsessionsstores hash, user/device/passkey, expiry and freshness.sessionstable withunlocked=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.unlock_finish. Then session becomes unlocked with 30-day DB expiry. Expired/incomplete pending rows cannot access terminals.sessions.freshserver-side, backed by a successful UV-required passkey ceremony. It is not a separate key.__Host-infraternal-ceremony, Secure/HttpOnly/Strict, bound by hash to pending state. These are ceremony context, not durable login credentials.Zeroizing<String>insignin-d311; DBapprovals.token_hash. Never URL/QR/output/disk.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_code=...; DBapprovals.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.api_keys(token_hash,user_id,device,scopes).signin-d311stores plaintext bearer in$XDG_CONFIG_HOME/infraternal/credentials.jsonor~/.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./auth/api-revokedeletes only own key and needs fresh passkey; recovery deletes account keys. Current scopes arehosts:read,terminal:connect,actions:dry-run; a key grants no browser session or fresh-tap privilege.websocket_connections. Bound to connection/user and still requires live session-cookie authority.Machine trust, host access and encrypted authority
CA/active/administrators/administrator-<member-hash>.<generation>.json, or configured CA-copy directory. AES-256-GCM binds issuer/member/generation. Publicissuer.pemand public DB certificate ledger are not secret keys.ca_administratorsrecords membership generation, not signing seed/envelope.MemoryCaSource. Cold restart requires admin unlock; in-process signer is not a 5-minute lease.CA/active/remote.key(unencrypted PKCS#8 PEM, private file/dir) andremote.pem; loaded into server memory. Distinct key from the CA signer. Configured legacy paths may differ.TlsFiles.certificate/private_keyPEM 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.Node data/identity.jsonwith host ID, endpoint, CA and current certificate. Private paths checked, publication atomic. Server sees public key/certificate only inremote_certificates, never node private key.identity.json; DBremote_certificatesrecords fingerprint, host, issuer, public key, validity and revocation. Private key as preceding row.remote_join_tokensstores SHA-256 digest, host/mode, timestamps/consumption only. Invitation also carries public endpoint/pins, not CA private material.Zeroizingkey 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.rotate_keywas 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.server_ssh_keyDB singleton holds public key and opaque administrator-wrapped envelope;StoredSshKeyholds 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.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]Secretzeroizes on drop. Private key enters temporary scopedssh-agentthroughssh-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.IdentityFile/client certificate/SSH_AUTH_SOCK are intentionally excluded from D183 auth configuration.Release and external credentials, plus requested types that do not exist yet
apps/release-signerexpects Worker secretSIGNING_KEY, provisioned from owner's password manager. Runtime imports nonextractable WebCrypto signing key. Compiled publicrelease/allowed_signersis verification trust, not private material. Code documents custody intent; no deployed secret value/config was inspected.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]CALLER_TOKEN; pipeline environmentRELEASE_CALLER_TOKEN; shared bearer authorizes/sign. Code hashes comparisons; secret never belongs in argv/repo.CLOUDFLARE_API_TOKEN, used byscripts/release/publishfor object storage API; curl config supplied over stdin. Token provisioning/protection outside pipeline is operator-managed, not a product DB row. WorkerBUILDSis a platform binding, not a user-exported storage secret in code.NOTIFY_URLbinding; could be just a public endpoint or include an opaque topic/token. Code does not define a distinct notification-key schema.acme.json; external issuer paths could differ. EAB exists only if chosen CA requires it.crates/plugins/relay/src/lib.rsexplicitly declares CLI actions before relay runtime. Existing terminalrelay.rsis flow-control/replay code, not a ticket issuer.Hellocapability 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.enable_early_data=false).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.
acme,DNS-01,instant-acmeincrates/tracer find no implemented issuer/provider store.Research verification and handoff
Read-only branch
research-r2, base/headf4b61cd6c3f52078cad7573c5367d7a6e75f84f0; 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 branchsignin-d311was read at16b728e212a783a34c38e229a7a077dc64bac7f6.A VM
scripts/verify fastrequest was submitted through compute01build-slot, but the gate itself never started. Verbatim scheduler output: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
J36 verification correction on
signin-d311@16b728e212a783a34c38e229a7a077dc64bac7f6: the first post-rebasescripts/verify fastrun failed seven restart fixture tests withPermissionDenied. The fixture helper explicitly keeps executable fixtures under trusted$HOMEancestry, while that run setINFRATERNAL_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 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, andscripts/verify fastsequentially. The browser check uses a temporary config to pass the Nix Chromium executable explicitly; the tracked Playwright config does not consumeTRACER_TEST_BROWSER. Fast-gate state roots are now under/home/claude/.cache/codex-scratch, preserving the restart fixtures' trusted-$HOMEpath 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 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 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 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 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 fastafterward; per-step logs and screenshots are under/home/claude/.cache/codex-scratch/j36-signin/. Branch remainsc72ac8f266663b63b7bc56fd181eb943e99063d2.J36 per-finding verification checkpoint on
c72ac8f266663b63b7bc56fd181eb943e99063d2:credential_read_refuses_a_private_directory_replaced_after_lockingpassed; the focused sign-in suite is 13/13.approval_link_carries_only_the_browser_user_codepassed 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/chromiumis absent. I found the installed Playwright Chromium at/home/claude/.cache/ms-playwright/chromium-1228/chrome-linux64/chromeand will rerun the approval spec after the active fast gate finishes.verify fastis still running; its current output has no failures.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), key lifetimes — final:
J36
scripts/verify fastresult on compute01, branch headc72ac8f266663b63b7bc56fd181eb943e99063d2: 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 leftscripts/verifyunchanged.scripts/verify fastexit: 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 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 itstarget/and test state roots, and the worktree remains clean. No source changed.J36 P1 browser retry update: the first Nix store path was absent; the second retry also found its unrooted
playwright-chromiumoutput 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 checklddbefore launch. This third browser attempt is queued on compute01 at position 7; no repository source changed.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 browser-retry update on
signin-d311@c72ac8f266663b63b7bc56fd181eb943e99063d2:credential_read_refuses_a_private_directory_replaced_after_lockingandapproval_link_carries_only_the_browser_user_code.scripts/verify fastcompleted; 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.ldd./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 queue update: the corrected Playwright runner for
test/device-approval.spec.tsis 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 crash-safety checkpoint: test-only alignment assertions are pushed at
2579ac125onsignin-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 sincec72ac8f; the browser run remains queued on compute01.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.tsat DPR 2 and save screenshots under/home/claude/.cache/codex-scratch/j36-signin/evidence/approval-final-retry3. Branch head remains2579ac125; the pushed alignment assertions are still test-only.J77 round-2 independent review started. Target
signin-d311pushed head2579ac125e8f4aec0fab33c6c33cb809b5a26175; currentorigin/devbase0067e1255407f0f3ed0f1d6c5e5caf51667e00a0. Review checkout: detachedinfraternal-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 — verdict: FIX FIRST (merge conflicts)
Reviewed latest pushed
signin-d311head2579ac125e8f4aec0fab33c6c33cb809b5a26175against currentorigin/dev0067e1255407f0f3ed0f1d6c5e5caf51667e00a0. Both refs were fetched again immediately before this report and were unchanged. No implementation changes.apps/tracer/auth.css:229andcrates/infraternal/src/main.rs:193(target-head lines). Read-onlygit merge-tree --write-tree origin/dev origin/signin-d311exits 1 and reports: The CSS conflict overlaps dev's rem-based auth styles with the new device-approval styles. The dispatch conflict overlaps dev's combinedCliHandler::NodeComponent | CliHandler::Sessions => run_local_action(...)path with this branch's extractedrun_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:
apps/tracer/src/device-approval.on-demand.ts:67renders the exact code usingtextContent; approval is disabled until the match checkbox is checked, and the click handler independently enforces confirmation.apps/tracer/test/device-approval.spec.ts:139preserves 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.crates/infraternal/src/sign_in.rs:641retains the locked directory descriptor; reads useopenatand replacement usesopenat/renameatagainst that descriptor, with identity, owner, private-mode and sync checks. The preserved regression atsign_in.rs:98checks 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 atc72ac8f26; the only later commit is the browser alignment test.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_scopedcovers 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
c72ac8f26reports 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.
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-inagain; revocation stays immediate. To be recorded as a D311 amendment and in docs/keys.md.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.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.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 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.
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.
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.
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 (base2b351d5f3). 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.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.
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/.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.J94 coordination with J90: J94 will add the scoped action adapter and audit. On
origin/dev,api_authorizereturns the person id, whileapi_listalready uses the SHA-256 token hash as the key's non-bearer identifier. J90'sapikey-30d-269changes 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 currentorigin/apikey-30d-269diff does not touch that validator. Please note any planned API return-shape change so I can integrate without reworking expiry.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.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.J90 finished and pushed. No merge or deployment.
apikey-30d-269, base2b351d5f3bb1aebed97b70e8d4460e8979a9c19e.c51954831769ccc46ed8ec04b38afa47002c35e5.d8ec67b0e4a8974cdfd9ba585dbf1481449a9356.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-settingsreads the field for a live owner; POST/auth/security-settings-updatevalidates 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:
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 faston compute01, gate summary verbatim:Final protocol test output and gate exit, verbatim:
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:
{}to/auth/security-settings; a new deployment returnsapi_key_lifetime_days: 30.{"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.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.J97 independent auth review, round 1, started. Target apikey-30d-269 at
d8ec67b0e4; current origin/devbe20430b2e. 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: REQUEST CHANGES.
Reviewed
apikey-30d-269atd8ec67b0e4a8974cdfd9ba585dbf1481449a9356against currentorigin/devbe20430b2e0dc4f591aa98a036f1066cf5dd5f55, D311/D303, #269 and J90 report 74429.P1 — A lifetime increase resurrects expired credentials.
crates/infraternal-server/src/auth/devices.rs:108(also :121 and :127). Authorization uses onlyrenewed_at + current_policy; it never checks the storedexpiresand 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.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 contains0033_remote_join_key_binding.sqland 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 returnsVersionMismatchon 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.P2 — Wall-clock rollback makes expired keys valid again.
crates/infraternal-server/src/auth/devices.rs:103and :108; clock implementationauth/http.rs:203. The new check samples rawSystemTime, 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 movingrenewed_atbackward; 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.P2 — Renewal rechecks use a stale time sample after database waits.
crates/infraternal-server/src/auth/devices.rs:121–123.nowis 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 confirmedrenewed_rows=1and 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.pyandexpiry-repro.logon the control host. The synchronous reproduction ran oninfraternal-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: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.
J90 repair start: branch
apikey-30d-269, headd8ec67b0e4a8974cdfd9ba585dbf1481449a9356, rebasing onto current devbe20430b2e0dc4f591aa98a036f1066cf5dd5f55. 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.J97 repair 1 committed/pushed as
b987f8982: rebased onto devbe20430b2, 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 todocs/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/targeton compute01. Logs:~/.cache/codex-scratch/j90/repair-*.log.J97 repairs 2 and 3 pushed:
574b16f32stores absolute deadlines, permanent tombstones and policy clamps;4560f0f9cpersists 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.logunder~/.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.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 keepapi_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 todevice_start, separate from theapi_authorizeexpiry repair. Please send the final repaired head when available so J94 can revalidate and update its base before the final gate.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 putINFRATERNAL_TEST_STATE_ROOTunder/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.logon dev-02. No implementation changes followed the focused pass.J94 coordination request: the action-bound
hosts removeapproval lasts up to 5 minutes and retains no bearer plaintext. At final passkey completion, the API key must still be live and havehosts: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.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_replacedexceeded 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/targetthere. All code is committed and pushed.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 archivedbe20430b2source. 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.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: absoluteexpires_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.mdfollowing 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:
restart::tests::reload_reopens_install_path_when_the_running_inode_was_replaced, although the helper’s summary row reports PASS.agent_daemon::real_binary_rootless_person_revocation_stops_existing_shellswith 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 andalready_added_breakit::the_typed_address_decides_which_host_is_named_when_two_matchfail 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:
The separate TypeScript continuation completed with the following verbatim result:
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.
J98 independent auth review round 2 started. Target
apikey-30d-269at6883d3c743b1a344ec6d147591d9156de5c0acfb, review branchreview-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: APPROVE.
Reviewed
apikey-30d-269at6883d3c743b1a344ec6d147591d9156de5c0acfbagainst currentorigin/dev7089be97953f9d5893d5f8d9c4e0713fb7f3b2f7, D311, owner comments 74285/74294, J97 findings 74442 and J90 repair report 74585. No actionable P0–P3 findings.Round-1 disposition:
crates/infraternal-server/src/auth/devices.rs:120checks the minimum of the absolute deadline and current-policy deadline. Expiry commitsdead=1before returning Denied (:121–129); renewal requires a live row and an allowed scope (:131–141).auth/api_expiry_tests.rs:286covers 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.git diff origin/dev HEAD -- crates/infraternal-server/migrationscontains 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 HEADexits 0, with no conflicts.auth/devices.rs:183reads/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:353covers the original deadline/deadline-minus-one sequence, an unchecked key, and Auth reconstruction across restart.auth/devices.rs:107–112acquires 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:381deterministically 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:
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-j98remains at the reviewed head. No build, test, browser or background run remains pending. No push, merge or deployment; issue stays open.