Auth: multiple browser tabs break refresh (rotation race / no 401 recovery) #44

Open
opened 2026-09-25 18:08:39 +00:00 by kayg · 41 comments
Owner

Found while diagnosing the #36 browser 401. The browser harness opened a second page in the same BrowserContext, so the two pages shared the refresh cookie. That page's refresh rotated the cookie, and the main page was left with a revoked access token. GET /api/v1/containers then returned 401 and nothing recovered it. The harness has been changed, but real users have the same setup: several infraternal tabs in one browser.

Current behavior:

  • The web client (packages/web/src/lib/auth.svelte.ts) calls POST /api/v1/auth/refresh on load.
  • There is no coordination between tabs, and no refresh-and-retry when a request gets a 401.
  • The contract (docs/contracts.md §1, around lines 92–97) says reuse of any consumed refresh credential revokes every access and refresh credential in the family.

Expected problems:

  1. Tab B's refresh leaves tab A's access token unusable (observed), and tab A never recovers.
  2. Two tabs refreshing at nearly the same time (both loaded, or both waking from sleep) makes one of them a "reuse", so the whole family is revoked and every tab is signed out.

It needs a design that keeps reuse detection secure, for example:

  • serialize refreshes across tabs with the Web Locks API or a BroadcastChannel, and share the result;
  • on a 401, refresh once and retry;
  • decide exactly what rotation does to the access token it replaces;
  • optionally a short server-side grace window that returns the same successor for the just-consumed credential. That needs care, because it weakens reuse detection.

This is an auth surface, so it runs on Sol.

Found while diagnosing the #36 browser 401. The browser harness opened a second page in the same `BrowserContext`, so the two pages shared the refresh cookie. That page's refresh rotated the cookie, and the main page was left with a revoked access token. `GET /api/v1/containers` then returned 401 and nothing recovered it. The harness has been changed, but **real users have the same setup: several infraternal tabs in one browser.** Current behavior: - The web client (`packages/web/src/lib/auth.svelte.ts`) calls `POST /api/v1/auth/refresh` on load. - There is no coordination between tabs, and no refresh-and-retry when a request gets a 401. - The contract (`docs/contracts.md` §1, around lines 92–97) says reuse of any consumed refresh credential revokes every access and refresh credential in the family. Expected problems: 1. Tab B's refresh leaves tab A's access token unusable (observed), and tab A never recovers. 2. Two tabs refreshing at nearly the same time (both loaded, or both waking from sleep) makes one of them a "reuse", so the whole family is revoked and every tab is signed out. It needs a design that keeps reuse detection secure, for example: - serialize refreshes across tabs with the Web Locks API or a BroadcastChannel, and share the result; - on a 401, refresh once and retry; - decide exactly what rotation does to the access token it replaces; - optionally a short server-side grace window that returns the same successor for the just-consumed credential. That needs care, because it weakens reuse detection. This is an auth surface, so it runs on Sol.
Author
Owner

Started issue #44 on branch multi-tab at base d9197837c5. Reading the refresh contract and implementation; next I will reproduce both tab failures before changing behavior.

Started issue #44 on branch multi-tab at base d9197837c5adb20ad0da2a6b8872ed2826a92aee. Reading the refresh contract and implementation; next I will reproduce both tab failures before changing behavior.
Author
Owner

Issue #44 design finding: SqliteStorage::refresh_session marks the old refresh credential used and updates its backing access token to revoked in one immediate transaction. That explains tab A's 401 after tab B refreshes. A second use of the old refresh credential revokes every active access token in its family and audits auth.refresh.reuse.

I updated docs/contracts.md §1.1 before implementation: same-origin web tabs serialize cookie refresh with Web Locks, share the new access token through BroadcastChannel, and an access-authenticated request gets one 401 recovery and retry. The old access token remains revoked on rotation. There is no server grace: cookie plus matching Origin cannot prove that a presenter is another tab rather than another browser holding a stolen cookie. RFC 9700 §4.14.2 requires replay detection for public clients and explains that the server cannot tell which presenter is legitimate after a reused refresh token: https://www.rfc-editor.org/rfc/rfc9700.html#section-4.14.2 .

Issue #44 design finding: `SqliteStorage::refresh_session` marks the old refresh credential used and updates its backing access token to `revoked` in one immediate transaction. That explains tab A's 401 after tab B refreshes. A second use of the old refresh credential revokes every active access token in its family and audits `auth.refresh.reuse`. I updated `docs/contracts.md` §1.1 before implementation: same-origin web tabs serialize cookie refresh with Web Locks, share the new access token through BroadcastChannel, and an access-authenticated request gets one 401 recovery and retry. The old access token remains revoked on rotation. There is no server grace: cookie plus matching Origin cannot prove that a presenter is another tab rather than another browser holding a stolen cookie. RFC 9700 §4.14.2 requires replay detection for public clients and explains that the server cannot tell which presenter is legitimate after a reused refresh token: https://www.rfc-editor.org/rfc/rfc9700.html#section-4.14.2 .
Author
Owner

Red/green evidence for #44: with packages/web/src/lib/api.ts temporarily restored from origin/dev and the new api.test.ts retained, bun run --filter @infraternal/web test -- src/lib/api.test.ts exited 1 (3 failed; setUnauthorizedRecovery is not a function). Restored fixed source; the same focused test exited 0 (3 passed). bun run --filter @infraternal/web check found 0 errors and 0 warnings. The server-level concurrent test uses tokio::join! on one refresh secret and verifies one rotation and one reuse revoke the winner's access credential; this documents why cross-tab serialization is required and preserves the true-reuse security behavior. The browser check now creates multiple pages in one context, deliberately suppresses BroadcastChannel on a stale tab to exercise 401 recovery, then starts two tabs together with delayed refresh HTTP requests to exercise the rotation race.

Red/green evidence for #44: with `packages/web/src/lib/api.ts` temporarily restored from `origin/dev` and the new `api.test.ts` retained, `bun run --filter @infraternal/web test -- src/lib/api.test.ts` exited 1 (3 failed; `setUnauthorizedRecovery is not a function`). Restored fixed source; the same focused test exited 0 (3 passed). `bun run --filter @infraternal/web check` found 0 errors and 0 warnings. The server-level concurrent test uses `tokio::join!` on one refresh secret and verifies one rotation and one reuse revoke the winner's access credential; this documents why cross-tab serialization is required and preserves the true-reuse security behavior. The browser check now creates multiple pages in one context, deliberately suppresses BroadcastChannel on a stale tab to exercise 401 recovery, then starts two tabs together with delayed refresh HTTP requests to exercise the rotation race.
Author
Owner

fast gate at head b3e9617 plus the subsequent access-revocation assertion (the gate read the current working tree):

WARN load5=8.52 exceeds online CPU count=8; timing-sensitive checks may be delayed
PASS fast: migrations are append-only
PASS fast: workspace package dependencies
PASS fast: JavaScript dependencies
PASS fast: build web app
PASS fast: cargo fmt --check
PASS fast: cargo clippy
PASS fast: cargo test
PASS fast: svelte-check
PASS fast: UI package svelte-check
PASS fast: stream package typecheck
PASS fast: Vitest
PASS fast: UI package Vitest
PASS fast: stream package Vitest
PASS fast: OpenAPI and TypeScript client drift
PASS fast: terminal TypeScript
PASS fast: terminal Vitest
`fast` gate at head b3e9617 plus the subsequent access-revocation assertion (the gate read the current working tree): ``` WARN load5=8.52 exceeds online CPU count=8; timing-sensitive checks may be delayed PASS fast: migrations are append-only PASS fast: workspace package dependencies PASS fast: JavaScript dependencies PASS fast: build web app PASS fast: cargo fmt --check PASS fast: cargo clippy PASS fast: cargo test PASS fast: svelte-check PASS fast: UI package svelte-check PASS fast: stream package typecheck PASS fast: Vitest PASS fast: UI package Vitest PASS fast: stream package Vitest PASS fast: OpenAPI and TypeScript client drift PASS fast: terminal TypeScript PASS fast: terminal Vitest ```
Author
Owner

Integration/browser gate scheduling: scripts/verify fast passed. At 20:27 local time, load5 was 8.77 and another job's cargo build --release -p infraternal was still active. The job contract prohibits overlapping full release builds and asks for load5 below 8 before integration/browser, so those gates are waiting; I am polling without touching the other job's process or files.

Integration/browser gate scheduling: `scripts/verify fast` passed. At 20:27 local time, load5 was 8.77 and another job's `cargo build --release -p infraternal` was still active. The job contract prohibits overlapping full release builds and asks for load5 below 8 before integration/browser, so those gates are waiting; I am polling without touching the other job's process or files.
Author
Owner

Real-browser red proof against origin/dev web auth/api/inventory code: I rebuilt the base web app with scripts/build-web, cleaned and rebuilt this worktree's debug server package so the embedded assets were actually replaced, and ran the new Playwright check with two pages in one BrowserContext against a fresh local server. It exited 1 at the intended stale-tab assertion (before any later browser checks):

file:///home/kayg/wt/multi-tab/scripts/browser-check.mjs:1182
      throw new Error("first tab did not recover after the second tab rotated its cookie: no successful container response");
            ^

Error: first tab did not recover after the second tab rotated its cookie: no successful container response
    at checkSharedCookieTabs (file:///home/kayg/wt/multi-tab/scripts/browser-check.mjs:1182:13)
    at async file:///home/kayg/wt/multi-tab/scripts/browser-check.mjs:1753:3

Node.js v22.23.3
Real-browser red proof against `origin/dev` web auth/api/inventory code: I rebuilt the base web app with `scripts/build-web`, cleaned and rebuilt this worktree's debug server package so the embedded assets were actually replaced, and ran the new Playwright check with two pages in one BrowserContext against a fresh local server. It exited 1 at the intended stale-tab assertion (before any later browser checks): ``` file:///home/kayg/wt/multi-tab/scripts/browser-check.mjs:1182 throw new Error("first tab did not recover after the second tab rotated its cookie: no successful container response"); ^ Error: first tab did not recover after the second tab rotated its cookie: no successful container response at checkSharedCookieTabs (file:///home/kayg/wt/multi-tab/scripts/browser-check.mjs:1182:13) at async file:///home/kayg/wt/multi-tab/scripts/browser-check.mjs:1753:3 Node.js v22.23.3 ```
Author
Owner

Focused real-browser green proof: rebuilt the fixed web app with scripts/build-web, cleaned and rebuilt this worktree's debug server package to embed it, and ran the same Playwright check against a fresh local server. The multi-tab check printed:

PASS multi-tab refresh: one stale-access recovery and two simultaneous tabs (1 refresh HTTP requests)

I stopped this targeted debug run after that check; it is not the required production browser gate. The corresponding base-client run exited 1 at the stale-tab assertion (comment above). The production scripts/verify browser gate is still pending the other job's active release build.

Focused real-browser green proof: rebuilt the fixed web app with `scripts/build-web`, cleaned and rebuilt this worktree's debug server package to embed it, and ran the same Playwright check against a fresh local server. The multi-tab check printed: ``` PASS multi-tab refresh: one stale-access recovery and two simultaneous tabs (1 refresh HTTP requests) ``` I stopped this targeted debug run after that check; it is not the required production browser gate. The corresponding base-client run exited 1 at the stale-tab assertion (comment above). The production `scripts/verify browser` gate is still pending the other job's active release build.
Author
Owner

Additional focused real-browser result against the fixed debug server: the check now runs two simultaneous pages both with Web Locks and with navigator.locks disabled to force BroadcastChannel fallback. Both modes completed with a single refresh HTTP request:

PASS multi-tab web-locks: stale-access recovery and simultaneous tabs (1 refresh HTTP requests)
PASS multi-tab broadcast-fallback: stale-access recovery and simultaneous tabs (1 refresh HTTP requests)

This was a targeted debug run stopped after those assertions; production browser gate remains pending.

Additional focused real-browser result against the fixed debug server: the check now runs two simultaneous pages both with Web Locks and with `navigator.locks` disabled to force BroadcastChannel fallback. Both modes completed with a single refresh HTTP request: ``` PASS multi-tab web-locks: stale-access recovery and simultaneous tabs (1 refresh HTTP requests) PASS multi-tab broadcast-fallback: stale-access recovery and simultaneous tabs (1 refresh HTTP requests) ``` This was a targeted debug run stopped after those assertions; production browser gate remains pending.
Author
Owner

Server-level HTTP race test now passes: CARGO_BUILD_JOBS=2 cargo test -p infraternal-server --test auth_http concurrent_cookie_refreshes_detect_reuse_without_browser_serialization -- --nocapture exited 0 with 1 passed. It sends two concurrent refresh requests with the same cookie and exact Origin but distinct idempotency keys. Exactly one rotates; the other is rejected as reuse, and the winner's successor cookie is then rejected because the family was revoked. This is the server behavior preserved while browser tabs serialize refresh.

Server-level HTTP race test now passes: `CARGO_BUILD_JOBS=2 cargo test -p infraternal-server --test auth_http concurrent_cookie_refreshes_detect_reuse_without_browser_serialization -- --nocapture` exited 0 with `1 passed`. It sends two concurrent refresh requests with the same cookie and exact Origin but distinct idempotency keys. Exactly one rotates; the other is rejected as reuse, and the winner's successor cookie is then rejected because the family was revoked. This is the server behavior preserved while browser tabs serialize refresh.
Author
Owner

Final source head 3b0bfbe passed scripts/verify fast (verbatim output):

PASS fast: migrations are append-only
PASS fast: workspace package dependencies
PASS fast: JavaScript dependencies
PASS fast: build web app
PASS fast: cargo fmt --check
PASS fast: cargo clippy
PASS fast: cargo test
PASS fast: svelte-check
PASS fast: UI package svelte-check
PASS fast: stream package typecheck
PASS fast: Vitest
PASS fast: UI package Vitest
PASS fast: stream package Vitest
PASS fast: OpenAPI and TypeScript client drift
PASS fast: terminal TypeScript
PASS fast: terminal Vitest
Final source head 3b0bfbe passed `scripts/verify fast` (verbatim output): ``` PASS fast: migrations are append-only PASS fast: workspace package dependencies PASS fast: JavaScript dependencies PASS fast: build web app PASS fast: cargo fmt --check PASS fast: cargo clippy PASS fast: cargo test PASS fast: svelte-check PASS fast: UI package svelte-check PASS fast: stream package typecheck PASS fast: Vitest PASS fast: UI package Vitest PASS fast: stream package Vitest PASS fast: OpenAPI and TypeScript client drift PASS fast: terminal TypeScript PASS fast: terminal Vitest ```
Author
Owner

Second real-browser red proof against origin/dev web client, using two pages in one BrowserContext and the real server. The focused concurrent mode sent both refresh requests through Playwright's route.fetch() before fulfilling either response, so the browser cookie jar did not advance between them. The check exited 1:

file:///home/kayg/wt/multi-tab/scripts/browser-check.mjs:1129
    throw new Error(`${step}: dashboard heading did not appear: ${JSON.stringify(observed)}`);
          ^

Error: web-locks simultaneous tab 1: dashboard heading did not appear: {"path":"/","headings":["Create your operator account"],"selectedAuthTab":"Set up"}
    at waitForDashboard (file:///home/kayg/wt/multi-tab/scripts/browser-check.mjs:1129:11)
    at runNextTicks (node:internal/process/task_queues:64:5)
    at process.processImmediate (node:internal/timers:452:9)
    at process.callbackTrampoline (node:internal/async_hooks:130:17)
    at async Promise.all (index 0)
    at async checkSharedCookieTabs (file:///home/kayg/wt/multi-tab/scripts/browser-check.mjs:1212:7)
    at async file:///home/kayg/wt/multi-tab/scripts/browser-check.mjs:1765:3

Node.js v22.23.3
Second real-browser red proof against `origin/dev` web client, using two pages in one BrowserContext and the real server. The focused concurrent mode sent both refresh requests through Playwright's `route.fetch()` before fulfilling either response, so the browser cookie jar did not advance between them. The check exited 1: ``` file:///home/kayg/wt/multi-tab/scripts/browser-check.mjs:1129 throw new Error(`${step}: dashboard heading did not appear: ${JSON.stringify(observed)}`); ^ Error: web-locks simultaneous tab 1: dashboard heading did not appear: {"path":"/","headings":["Create your operator account"],"selectedAuthTab":"Set up"} at waitForDashboard (file:///home/kayg/wt/multi-tab/scripts/browser-check.mjs:1129:11) at runNextTicks (node:internal/process/task_queues:64:5) at process.processImmediate (node:internal/timers:452:9) at process.callbackTrampoline (node:internal/async_hooks:130:17) at async Promise.all (index 0) at async checkSharedCookieTabs (file:///home/kayg/wt/multi-tab/scripts/browser-check.mjs:1212:7) at async file:///home/kayg/wt/multi-tab/scripts/browser-check.mjs:1765:3 Node.js v22.23.3 ```
Author
Owner

The strengthened Playwright concurrency check is green against the fixed real debug server after the baseline run failed. It uses route.fetch() and delayed route.fulfill() so both uncoordinated requests can reach the server while the browser still has the original cookie; Playwright documents that route.continue() cannot override Cookie, which is why the earlier delay-only version was insufficient: https://playwright.dev/docs/api/class-route . Targeted output:

PASS multi-tab web-locks: stale-access recovery and simultaneous tabs (1 refresh HTTP requests)
PASS multi-tab broadcast-fallback: stale-access recovery and simultaneous tabs (1 refresh HTTP requests)

The targeted debug run continued into unrelated browser checks afterward; the required production browser gate still waits for the release build slot.

The strengthened Playwright concurrency check is green against the fixed real debug server after the baseline run failed. It uses `route.fetch()` and delayed `route.fulfill()` so both uncoordinated requests can reach the server while the browser still has the original cookie; Playwright documents that `route.continue()` cannot override `Cookie`, which is why the earlier delay-only version was insufficient: https://playwright.dev/docs/api/class-route . Targeted output: ``` PASS multi-tab web-locks: stale-access recovery and simultaneous tabs (1 refresh HTTP requests) PASS multi-tab broadcast-fallback: stale-access recovery and simultaneous tabs (1 refresh HTTP requests) ``` The targeted debug run continued into unrelated browser checks afterward; the required production browser gate still waits for the release build slot.
Author
Owner

At final head 11ff44e, scripts/verify fast passed through Clippy but its full cargo test stage failed in the unchanged engine allocation test while tests ran in parallel. Relevant verbatim output:

FAIL fast: cargo test
---- logs::tests::steady_state_framing_does_not_allocate_per_chunk stdout ----
thread 'logs::tests::steady_state_framing_does_not_allocate_per_chunk' (3640420) panicked at crates/engine/src/logs.rs:349:9:
log framing allocated per chunk after warmup: Stats { allocations: 101, deallocations: 49, reallocations: 11, bytes_allocated: 164440, bytes_deallocated: 3728, bytes_reallocated: 1702 }
test result: FAILED. 14 passed; 1 failed; 1 ignored; 0 measured; 0 filtered out; finished in 0.01s

Focused rerun with one test thread passed:

running 1 test
test logs::tests::steady_state_framing_does_not_allocate_per_chunk ... ok
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 15 filtered out; finished in 0.00s

I will rerun the full fast gate once at the unchanged source head and report its actual result. This is outside the owned auth files, so I am not changing the engine test here.

At final head 11ff44e, `scripts/verify fast` passed through Clippy but its full cargo test stage failed in the unchanged engine allocation test while tests ran in parallel. Relevant verbatim output: ``` FAIL fast: cargo test ---- logs::tests::steady_state_framing_does_not_allocate_per_chunk stdout ---- thread 'logs::tests::steady_state_framing_does_not_allocate_per_chunk' (3640420) panicked at crates/engine/src/logs.rs:349:9: log framing allocated per chunk after warmup: Stats { allocations: 101, deallocations: 49, reallocations: 11, bytes_allocated: 164440, bytes_deallocated: 3728, bytes_reallocated: 1702 } test result: FAILED. 14 passed; 1 failed; 1 ignored; 0 measured; 0 filtered out; finished in 0.01s ``` Focused rerun with one test thread passed: ``` running 1 test test logs::tests::steady_state_framing_does_not_allocate_per_chunk ... ok test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 15 filtered out; finished in 0.00s ``` I will rerun the full fast gate once at the unchanged source head and report its actual result. This is outside the owned auth files, so I am not changing the engine test here.
Author
Owner

Full scripts/verify fast rerun at head 11ff44e passed after the preceding unchanged engine allocation-test failure. Verbatim gate output:

PASS fast: migrations are append-only
PASS fast: workspace package dependencies
PASS fast: JavaScript dependencies
PASS fast: build web app
PASS fast: cargo fmt --check
PASS fast: cargo clippy
PASS fast: cargo test
PASS fast: svelte-check
PASS fast: UI package svelte-check
PASS fast: stream package typecheck
PASS fast: Vitest
PASS fast: UI package Vitest
PASS fast: stream package Vitest
PASS fast: OpenAPI and TypeScript client drift
PASS fast: terminal TypeScript
PASS fast: terminal Vitest
Full `scripts/verify fast` rerun at head 11ff44e passed after the preceding unchanged engine allocation-test failure. Verbatim gate output: ``` PASS fast: migrations are append-only PASS fast: workspace package dependencies PASS fast: JavaScript dependencies PASS fast: build web app PASS fast: cargo fmt --check PASS fast: cargo clippy PASS fast: cargo test PASS fast: svelte-check PASS fast: UI package svelte-check PASS fast: stream package typecheck PASS fast: Vitest PASS fast: UI package Vitest PASS fast: stream package Vitest PASS fast: OpenAPI and TypeScript client drift PASS fast: terminal TypeScript PASS fast: terminal Vitest ```
Author
Owner

Gate blocker after the requested 45-minute polling window (started about 20:26 CEST; now 21:11 CEST): load5 is 7.30, but another worktree still has cargo build --release -p infraternal active (PID 3116828, elapsed 1:53:22). The shared job rule says not to start more than one full release build at a time and forbids signaling processes owned by another job. Both scripts/verify integration and scripts/verify browser begin with their own full release build, so neither gate can start while that process remains active. I have not touched it. Final head is 11ff44e; scripts/verify fast rerun passed and both focused browser regressions passed. Integration and production browser gates remain outstanding.

Gate blocker after the requested 45-minute polling window (started about 20:26 CEST; now 21:11 CEST): load5 is 7.30, but another worktree still has `cargo build --release -p infraternal` active (PID 3116828, elapsed 1:53:22). The shared job rule says not to start more than one full release build at a time and forbids signaling processes owned by another job. Both `scripts/verify integration` and `scripts/verify browser` begin with their own full release build, so neither gate can start while that process remains active. I have not touched it. Final head is 11ff44e; `scripts/verify fast` rerun passed and both focused browser regressions passed. Integration and production browser gates remain outstanding.
Author
Owner

Job handoff at head 11ff44ef5a8218ba5071512757889a894cefa1ca on branch multi-tab (clean worktree; not pushed). The contract and implementation are committed. Both browser regressions were demonstrated red against base web assets in a real browser, and fixed Web Locks and forced BroadcastChannel checks each issued one refresh request for two simultaneous tabs. The same-cookie HTTP concurrency test and focused web tests pass. Final scripts/verify fast rerun passed verbatim as posted above; its earlier unrelated engine allocation-test failure and focused pass are also posted.

integration and production browser gates remain unrun. After the full 45-minute load polling window and extra waiting, the other worktree's cargo build --release -p infraternal is still active (PID 3116828, over 2 hours elapsed). This job's shared rule forbids overlapping full release builds and signaling another job's process. I did not push, close the issue, or start either prohibited overlapping gate. progress.md at the worktree root is excluded from git and holds the continuation instructions.

Job handoff at head `11ff44ef5a8218ba5071512757889a894cefa1ca` on branch `multi-tab` (clean worktree; not pushed). The contract and implementation are committed. Both browser regressions were demonstrated red against base web assets in a real browser, and fixed Web Locks and forced BroadcastChannel checks each issued one refresh request for two simultaneous tabs. The same-cookie HTTP concurrency test and focused web tests pass. Final `scripts/verify fast` rerun passed verbatim as posted above; its earlier unrelated engine allocation-test failure and focused pass are also posted. `integration` and production `browser` gates remain unrun. After the full 45-minute load polling window and extra waiting, the other worktree's `cargo build --release -p infraternal` is still active (PID 3116828, over 2 hours elapsed). This job's shared rule forbids overlapping full release builds and signaling another job's process. I did not push, close the issue, or start either prohibited overlapping gate. `progress.md` at the worktree root is excluded from git and holds the continuation instructions.
Author
Owner

Independent review started on branch multi-tab, head 11ff44ef5a8218ba5071512757889a894cefa1ca, against base d9197837c5adb20ad0da2a6b8872ed2826a92aee. Scope: refresh rotation contract and changed web/server tests. I will report source-level findings and run the requested integration/browser gates after the load threshold, plus regression checks in a throwaway worktree.

Independent review started on branch `multi-tab`, head `11ff44ef5a8218ba5071512757889a894cefa1ca`, against base `d9197837c5adb20ad0da2a6b8872ed2826a92aee`. Scope: refresh rotation contract and changed web/server tests. I will report source-level findings and run the requested integration/browser gates after the load threshold, plus regression checks in a throwaway worktree.
Author
Owner

Source review finding: packages/web/src/lib/auth.svelte.ts:120-148 uses an 80 ms BroadcastChannel leader election. The channel delivery has no 80 ms bound; if both tabs' intent events arrive after their timers, each sees only its own owner id and sends a refresh with the shared cookie. The unchanged server path (crates/server/src/storage/mod.rs:2057-2074) treats the loser as reuse and revokes the family. The browser test exercises ordinary timing but does not delay intent delivery. This blocks the claimed fallback guarantee; the fallback needs a coordinator that can guarantee exclusivity, or must fail closed when it cannot coordinate.

Source review finding: `packages/web/src/lib/auth.svelte.ts:120-148` uses an 80 ms BroadcastChannel leader election. The channel delivery has no 80 ms bound; if both tabs' intent events arrive after their timers, each sees only its own owner id and sends a refresh with the shared cookie. The unchanged server path (`crates/server/src/storage/mod.rs:2057-2074`) treats the loser as reuse and revokes the family. The browser test exercises ordinary timing but does not delay intent delivery. This blocks the claimed fallback guarantee; the fallback needs a coordinator that can guarantee exclusivity, or must fail closed when it cannot coordinate.
Author
Owner

Regression evidence: in the throwaway worktree, I restored the three changed web implementation files from d919783 while retaining the new tests. bun run test src/lib/api.test.ts failed 3/3 (TypeError: setUnauthorizedRecovery is not a function). Both new server concurrency tests still passed (1 passed; 0 failed each), as expected: git diff d919783 11ff44e -- crates/server/src/auth.rs crates/server/src/storage/mod.rs is empty. The server tests characterize the existing no-grace reuse rule; they cannot fail on the base client. The scripts/verify fast log has 16 PASS lines and no FAIL lines. Integration/browser remain pending the load5 < 8 condition.

Regression evidence: in the throwaway worktree, I restored the three changed web implementation files from `d919783` while retaining the new tests. `bun run test src/lib/api.test.ts` failed 3/3 (`TypeError: setUnauthorizedRecovery is not a function`). Both new server concurrency tests still passed (`1 passed; 0 failed` each), as expected: `git diff d919783 11ff44e -- crates/server/src/auth.rs crates/server/src/storage/mod.rs` is empty. The server tests characterize the existing no-grace reuse rule; they cannot fail on the base client. The `scripts/verify fast` log has 16 PASS lines and no FAIL lines. Integration/browser remain pending the load5 < 8 condition.
Author
Owner

Source review finding: packages/web/src/lib/auth.svelte.ts:34-45 accepts any same-origin BroadcastChannel {type: "session", accessToken: <nonempty string>} and sets signedIn = true even after explicit sign-out. setSession() sends the access credential to every listener (:60-65), including an open tab on the signed-out screen; signOut()/clear() has no generation guard against late session messages or refresh responses (:232-253). The HTML Standard scopes the channel by storage key/name and queues delivery, but supplies no sender authentication. The client should gate token adoption to an active matching session or pending refresh, and discard late messages/results after sign-out. A delayed-refresh/sign-out browser check is missing.

Source review finding: `packages/web/src/lib/auth.svelte.ts:34-45` accepts any same-origin BroadcastChannel `{type: "session", accessToken: <nonempty string>}` and sets `signedIn = true` even after explicit sign-out. `setSession()` sends the access credential to every listener (`:60-65`), including an open tab on the signed-out screen; `signOut()`/`clear()` has no generation guard against late session messages or refresh responses (`:232-253`). The HTML Standard scopes the channel by storage key/name and queues delivery, but supplies no sender authentication. The client should gate token adoption to an active matching session or pending refresh, and discard late messages/results after sign-out. A delayed-refresh/sign-out browser check is missing.
Author
Owner

scripts/verify integration completed with exit 0 on 11ff44e, after starting at load5=6.65. Verbatim gate output:

PASS integration: JavaScript dependencies
PASS integration: build web app
PASS integration: build release binary
PASS integration: real HTTP behavior
PASS integration: Docker socket _ping
PASS integration: rootless Podman socket _ping
PASS integration: engine adapters against Docker and Podman
PASS integration: bounded follow:false log snapshots against Docker and rootless Podman
PASS integration: TypeScript stream client against Docker and Podman
PASS integration: stream authorization and CLOSE regressions against Docker
PASS integration: persistent PTY termination preempts blocked send
PASS integration: resource HTTP API against Docker and rootless Podman
PASS integration: compiled CLI and MCP against real server and engines
PASS integration: SSH contract against isolated real sshd
PASS integration: SSH product API and stream against isolated real sshd
PASS integration: remote mTLS infraternal agent enroll, rotate, list, exec and revoke against Docker

Browser gate is pending load5 < 8.

`scripts/verify integration` completed with exit 0 on `11ff44e`, after starting at load5=6.65. Verbatim gate output: ``` PASS integration: JavaScript dependencies PASS integration: build web app PASS integration: build release binary PASS integration: real HTTP behavior PASS integration: Docker socket _ping PASS integration: rootless Podman socket _ping PASS integration: engine adapters against Docker and Podman PASS integration: bounded follow:false log snapshots against Docker and rootless Podman PASS integration: TypeScript stream client against Docker and Podman PASS integration: stream authorization and CLOSE regressions against Docker PASS integration: persistent PTY termination preempts blocked send PASS integration: resource HTTP API against Docker and rootless Podman PASS integration: compiled CLI and MCP against real server and engines PASS integration: SSH contract against isolated real sshd PASS integration: SSH product API and stream against isolated real sshd PASS integration: remote mTLS infraternal agent enroll, rotate, list, exec and revoke against Docker ``` Browser gate is pending load5 < 8.
Author
Owner

scripts/verify browser completed with exit 0 on 11ff44e. Load5 at the prestart poll was 7.98; the script's startup warning sampled 8.18. Verbatim gate output:

WARN load5=8.18 exceeds online CPU count=8; timing-sensitive checks may be delayed
PASS browser: JavaScript dependencies
PASS browser: install Chromium and system dependencies
PASS browser: terminal fixture Playwright
PASS browser: build web app
PASS browser: build release binary
METRIC browser log flood: {"recordsPerSecond":17860,"frames":109,"fps":60,"retained":10000,"bytes":210000,"dropped":49400,"renderedRows":29}
PASS browser: Playwright against release server
PASS browser: terminal production first-load budget

Browser screenshots are under /mnt/hdd/infraternal/jobs/review-44/artifacts/, including 1440×900 and 390×844. The throwaway worktree with base web source is now running the same browser checks to confirm regression failure.

`scripts/verify browser` completed with exit 0 on `11ff44e`. Load5 at the prestart poll was 7.98; the script's startup warning sampled 8.18. Verbatim gate output: ``` WARN load5=8.18 exceeds online CPU count=8; timing-sensitive checks may be delayed PASS browser: JavaScript dependencies PASS browser: install Chromium and system dependencies PASS browser: terminal fixture Playwright PASS browser: build web app PASS browser: build release binary METRIC browser log flood: {"recordsPerSecond":17860,"frames":109,"fps":60,"retained":10000,"bytes":210000,"dropped":49400,"renderedRows":29} PASS browser: Playwright against release server PASS browser: terminal production first-load budget ``` Browser screenshots are under `/mnt/hdd/infraternal/jobs/review-44/artifacts/`, including 1440×900 and 390×844. The throwaway worktree with base web source is now running the same browser checks to confirm regression failure.
Author
Owner

Red-before-green browser evidence in the throwaway worktree: restored packages/web/src/lib/api.ts, auth.svelte.ts, and inventory.ts from d919783 while retaining the new scripts/browser-check.mjs checks. The first baseline scripts/verify browser attempt stopped before the app checks because port 4175 was already occupied (FAIL browser: terminal fixture Playwright); I reran with INFRATERMINAL_TERMINAL_TEST_PORT=47144. That run built the base web app and release binary, then failed at the new stale-tab assertion:

PASS browser: JavaScript dependencies
PASS browser: install Chromium and system dependencies
PASS browser: terminal fixture Playwright
PASS browser: build web app
PASS browser: build release binary
FAIL browser: Playwright against release server
error: first tab did not recover after the second tab rotated its cookie: no successful container response
      at checkSharedCookieTabs (.../scripts/browser-check.mjs:1183:19)

INFRATERNAL_MULTI_TAB_CONCURRENT_ONLY=1 is now running separately so the first failure cannot mask the simultaneous-tabs check.

Red-before-green browser evidence in the throwaway worktree: restored `packages/web/src/lib/api.ts`, `auth.svelte.ts`, and `inventory.ts` from `d919783` while retaining the new `scripts/browser-check.mjs` checks. The first baseline `scripts/verify browser` attempt stopped before the app checks because port 4175 was already occupied (`FAIL browser: terminal fixture Playwright`); I reran with `INFRATERMINAL_TERMINAL_TEST_PORT=47144`. That run built the base web app and release binary, then failed at the new stale-tab assertion: ``` PASS browser: JavaScript dependencies PASS browser: install Chromium and system dependencies PASS browser: terminal fixture Playwright PASS browser: build web app PASS browser: build release binary FAIL browser: Playwright against release server error: first tab did not recover after the second tab rotated its cookie: no successful container response at checkSharedCookieTabs (.../scripts/browser-check.mjs:1183:19) ``` `INFRATERNAL_MULTI_TAB_CONCURRENT_ONLY=1` is now running separately so the first failure cannot mask the simultaneous-tabs check.
Author
Owner

Review verdict: request changes on 11ff44ef5a8218ba5071512757889a894cefa1ca against d9197837c5adb20ad0da2a6b8872ed2826a92aee. Full review: /mnt/hdd/infraternal/jobs/review-44/findings.md.

Blocking findings:

  1. High — fallback refresh can race (packages/web/src/lib/auth.svelte.ts:120-148). The 80 ms BroadcastChannel election has no delivery guarantee. Two tabs can both decide they own refresh, and the unchanged server correctly revokes the family on reuse. Use guaranteed exclusion or fail closed; test delayed intent delivery.
  2. High — signed-out tabs accept session broadcasts (auth.svelte.ts:34-45,60-65,242-253). Any same-origin channel participant can send a nonempty session token; the recipient sets signedIn=true even after sign-out. Broadcasts also disclose tokens to same-origin listeners, including signed-out tabs. Add session binding and a sign-out generation guard against late responses/messages; bound and validate message shapes. See the HTML Standard's queued, storage-key-scoped delivery: https://html.spec.whatwg.org/multipage/web-messaging.html#broadcasting-to-other-browsing-contexts.
  3. Medium — no-BroadcastChannel case leaves stale access tokens (auth.svelte.ts:23-24,120-121,152-165). Web Locks serialize cookie rotations but cannot share the new access token without a channel; a later tab revokes an earlier tab's token. With neither API, concurrent refresh can trigger reuse revocation. Sign-out cannot immediately clear other tabs without the channel. Handle unsupported environments explicitly and test both tabs after recovery.

Lower-severity findings are in findings.md: the generic 401 helper has no key guard for unkeyed mutating calls, and the server concurrency tests are invariants rather than red-before-green regressions. Confirmed: normal rotation revokes the old access token immediately; the server added no grace window; access tokens are not written to persistent browser storage; a keyed request reuses its Idempotency-Key and retries at most once, excluding refresh itself.

Gate evidence: scripts/verify integration exited 0 with 16 PASS lines (quoted in the earlier comment). scripts/verify browser exited 0 with six PASS lines (also quoted earlier), including the two-tab checks. scripts/verify fast printed the following verbatim output; my outer zsh capture wrapper then errored by assigning its read-only status variable, after the gate itself had finished:

WARN load5=10.82 exceeds online CPU count=8; timing-sensitive checks may be delayed
PASS fast: migrations are append-only
PASS fast: workspace package dependencies
PASS fast: JavaScript dependencies
PASS fast: build web app
PASS fast: cargo fmt --check
PASS fast: cargo clippy
PASS fast: cargo test
PASS fast: svelte-check
PASS fast: UI package svelte-check
PASS fast: stream package typecheck
PASS fast: Vitest
PASS fast: UI package Vitest
PASS fast: stream package Vitest
PASS fast: OpenAPI and TypeScript client drift
PASS fast: terminal TypeScript
PASS fast: terminal Vitest

Red-before-green proof in the removed throwaway worktree, retaining the head tests with base web code: API recovery unit tests failed 3/3; the browser stale-tab check failed because the first tab got no successful container response; the separate concurrent-only run failed because simultaneous Web Locks tab 1 showed the setup screen instead of the dashboard. The first base browser attempt stopped on an unrelated port 4175 collision; rerun used port 47144. Both new server concurrency tests passed with base web code (1 passed; 0 failed each), because server refresh code is unchanged. Therefore the requested claim that the server test fails without this client fix is refuted.

The review worktree has no tracked changes. Screenshots and complete gate logs are under /mnt/hdd/infraternal/jobs/review-44/.

**Review verdict: request changes** on `11ff44ef5a8218ba5071512757889a894cefa1ca` against `d9197837c5adb20ad0da2a6b8872ed2826a92aee`. Full review: `/mnt/hdd/infraternal/jobs/review-44/findings.md`. Blocking findings: 1. **High — fallback refresh can race** (`packages/web/src/lib/auth.svelte.ts:120-148`). The 80 ms BroadcastChannel election has no delivery guarantee. Two tabs can both decide they own refresh, and the unchanged server correctly revokes the family on reuse. Use guaranteed exclusion or fail closed; test delayed intent delivery. 2. **High — signed-out tabs accept session broadcasts** (`auth.svelte.ts:34-45,60-65,242-253`). Any same-origin channel participant can send a nonempty `session` token; the recipient sets `signedIn=true` even after sign-out. Broadcasts also disclose tokens to same-origin listeners, including signed-out tabs. Add session binding and a sign-out generation guard against late responses/messages; bound and validate message shapes. See the HTML Standard's queued, storage-key-scoped delivery: https://html.spec.whatwg.org/multipage/web-messaging.html#broadcasting-to-other-browsing-contexts. 3. **Medium — no-BroadcastChannel case leaves stale access tokens** (`auth.svelte.ts:23-24,120-121,152-165`). Web Locks serialize cookie rotations but cannot share the new access token without a channel; a later tab revokes an earlier tab's token. With neither API, concurrent refresh can trigger reuse revocation. Sign-out cannot immediately clear other tabs without the channel. Handle unsupported environments explicitly and test both tabs after recovery. Lower-severity findings are in `findings.md`: the generic 401 helper has no key guard for unkeyed mutating calls, and the server concurrency tests are invariants rather than red-before-green regressions. Confirmed: normal rotation revokes the old access token immediately; the server added no grace window; access tokens are not written to persistent browser storage; a keyed request reuses its Idempotency-Key and retries at most once, excluding refresh itself. Gate evidence: `scripts/verify integration` exited 0 with 16 PASS lines (quoted in the earlier comment). `scripts/verify browser` exited 0 with six PASS lines (also quoted earlier), including the two-tab checks. `scripts/verify fast` printed the following verbatim output; my outer zsh capture wrapper then errored by assigning its read-only `status` variable, after the gate itself had finished: ``` WARN load5=10.82 exceeds online CPU count=8; timing-sensitive checks may be delayed PASS fast: migrations are append-only PASS fast: workspace package dependencies PASS fast: JavaScript dependencies PASS fast: build web app PASS fast: cargo fmt --check PASS fast: cargo clippy PASS fast: cargo test PASS fast: svelte-check PASS fast: UI package svelte-check PASS fast: stream package typecheck PASS fast: Vitest PASS fast: UI package Vitest PASS fast: stream package Vitest PASS fast: OpenAPI and TypeScript client drift PASS fast: terminal TypeScript PASS fast: terminal Vitest ``` Red-before-green proof in the removed throwaway worktree, retaining the head tests with base web code: API recovery unit tests failed 3/3; the browser stale-tab check failed because the first tab got no successful container response; the separate concurrent-only run failed because simultaneous Web Locks tab 1 showed the setup screen instead of the dashboard. The first base browser attempt stopped on an unrelated port 4175 collision; rerun used port 47144. Both new server concurrency tests **passed** with base web code (`1 passed; 0 failed` each), because server refresh code is unchanged. Therefore the requested claim that the server test fails without this client fix is refuted. The review worktree has no tracked changes. Screenshots and complete gate logs are under `/mnt/hdd/infraternal/jobs/review-44/`.
Author
Owner

Started fix-44 on branch multi-tab at base head 11ff44ef5a8218ba5071512757889a894cefa1ca in /home/kayg/wt/multi-tab. Reviewing findings 1–5 and implementing the agreed Web Locks + BroadcastChannel design. Compatibility sources: MDN Web Locks, MDN BroadcastChannel, caniuse Web Locks, caniuse BroadcastChannel. Both APIs support Safari/iOS Safari 15.4+, Chrome and Firefox; Web Locks requires a secure context.

Started fix-44 on branch `multi-tab` at base head `11ff44ef5a8218ba5071512757889a894cefa1ca` in `/home/kayg/wt/multi-tab`. Reviewing findings 1–5 and implementing the agreed Web Locks + BroadcastChannel design. Compatibility sources: [MDN Web Locks](https://developer.mozilla.org/en-US/docs/Web/API/Web_Locks_API), [MDN BroadcastChannel](https://developer.mozilla.org/en-US/docs/Web/API/BroadcastChannel), [caniuse Web Locks](https://caniuse.com/mdn-api_navigator_locks), [caniuse BroadcastChannel](https://caniuse.com/broadcastchannel). Both APIs support Safari/iOS Safari 15.4+, Chrome and Firefox; Web Locks requires a secure context.
Author
Owner

Started fix-44 on branch multi-tab at base head 11ff44ef5a8218ba5071512757889a894cefa1ca in /home/kayg/wt/multi-tab. Reviewing findings 1–5 and implementing the agreed Web Locks + BroadcastChannel design. Compatibility sources: MDN Web Locks, MDN BroadcastChannel, caniuse Web Locks, caniuse BroadcastChannel. Both APIs support Safari/iOS Safari 15.4+, Chrome and Firefox; Web Locks requires a secure context.

Started fix-44 on branch `multi-tab` at base head `11ff44ef5a8218ba5071512757889a894cefa1ca` in `/home/kayg/wt/multi-tab`. Reviewing findings 1–5 and implementing the agreed Web Locks + BroadcastChannel design. Compatibility sources: [MDN Web Locks](https://developer.mozilla.org/en-US/docs/Web/API/Web_Locks_API), [MDN BroadcastChannel](https://developer.mozilla.org/en-US/docs/Web/API/BroadcastChannel), [caniuse Web Locks](https://caniuse.com/mdn-api_navigator_locks), [caniuse BroadcastChannel](https://caniuse.com/broadcastchannel). Both APIs support Safari/iOS Safari 15.4+, Chrome and Firefox; Web Locks requires a secure context.
Author
Owner

Design and finding fixes at 6563cf6:

  • Findings 1 and 3: removed the 80 ms channel election and owner map. Shared mode exists only with both Web Locks and BroadcastChannel; a nonsecret generation marker lets the lock holder detect a completed refresh before its channel event arrives. With either API missing, refresh is per tab and the UI shows one non-blocking notice. Sources: MDN Web Locks, MDN BroadcastChannel, caniuse Web Locks, caniuse BroadcastChannel.
  • Finding 2: a sign-out epoch rejects late responses and messages; session messages require a shaped 72-character credential, a safe integer generation newer than local state, and an active or pending session. Removed the owner map entirely, so it cannot grow from untrusted messages.
  • Finding 4: automatic 401 retry is restricted to GET/HEAD or a request carrying Idempotency-Key, which remains on the retry.
  • Finding 5: server concurrency tests are labelled invariant tests; client and browser checks are the regression proof.
    Targeted evidence: bun run --cwd packages/web check printed svelte-check found 0 errors and 0 warnings; bun run --cwd packages/web test -- src/lib/auth.test.ts src/lib/api.test.ts printed Test Files 2 passed (2) and Tests 15 passed (15) before the additional two sign-out tests; bun run --cwd packages/web test -- src/lib/auth.test.ts printed Test Files 1 passed (1) and Tests 8 passed (8) afterward. Full gates pending.
Design and finding fixes at `6563cf6`: - Findings 1 and 3: removed the 80 ms channel election and owner map. Shared mode exists only with both Web Locks and BroadcastChannel; a nonsecret generation marker lets the lock holder detect a completed refresh before its channel event arrives. With either API missing, refresh is per tab and the UI shows one non-blocking notice. Sources: [MDN Web Locks](https://developer.mozilla.org/en-US/docs/Web/API/Web_Locks_API), [MDN BroadcastChannel](https://developer.mozilla.org/en-US/docs/Web/API/BroadcastChannel), [caniuse Web Locks](https://caniuse.com/mdn-api_navigator_locks), [caniuse BroadcastChannel](https://caniuse.com/broadcastchannel). - Finding 2: a sign-out epoch rejects late responses and messages; session messages require a shaped 72-character credential, a safe integer generation newer than local state, and an active or pending session. Removed the owner map entirely, so it cannot grow from untrusted messages. - Finding 4: automatic 401 retry is restricted to GET/HEAD or a request carrying Idempotency-Key, which remains on the retry. - Finding 5: server concurrency tests are labelled invariant tests; client and browser checks are the regression proof. Targeted evidence: `bun run --cwd packages/web check` printed `svelte-check found 0 errors and 0 warnings`; `bun run --cwd packages/web test -- src/lib/auth.test.ts src/lib/api.test.ts` printed `Test Files 2 passed (2)` and `Tests 15 passed (15)` before the additional two sign-out tests; `bun run --cwd packages/web test -- src/lib/auth.test.ts` printed `Test Files 1 passed (1)` and `Tests 8 passed (8)` afterward. Full gates pending.
Author
Owner

Verification update through bd1694b:

Red-before-green proof: with only packages/web/src/lib/auth.svelte.ts temporarily restored from reviewed head 11ff44e, the new auth.test.ts suite printed verbatim:

Test Files  1 failed (1)
Tests  6 failed | 2 passed (8)
BASELINE_TEST_EXIT=1

The failures included a signed-out tab becoming signed in from a forged channel message, malformed token acceptance, and missing unsupported-browser notice. The source was restored and git status --short was clean. At the fixed head, targeted client tests printed:

Test Files  2 passed (2)
Tests  17 passed (17)

First integration gate (exit 1) failed in the unchanged engine PTY cleanup test:

test pty_session_drop_and_close_terminate_exec_processes_on_all_real_engines ...
thread 'pty_session_drop_and_close_terminate_exec_processes_on_all_real_engines' (4053386) panicked at crates/engine/tests/integration.rs:743:27:
pty close: EngineError { code: Conflict, message: "marker scan failed and no host PID namespace was captured", kind: "exec", engine_code: Some("exec_termination_unconfirmed"), retryable: false, outcome_unknown: true }
FAILED
FAIL integration: engine adapters against Docker and Podman

The exact test rerun with --ignored --exact --nocapture --test-threads=1 printed:

test pty_session_drop_and_close_terminate_exec_processes_on_all_real_engines ... ok
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 10 filtered out; finished in 37.35s

Full integration gate rerun (exit 0) printed:

PASS integration: JavaScript dependencies
PASS integration: build web app
PASS integration: build release binary
PASS integration: real HTTP behavior
PASS integration: Docker socket _ping
PASS integration: rootless Podman socket _ping
PASS integration: engine adapters against Docker and Podman
PASS integration: bounded follow:false log snapshots against Docker and rootless Podman
PASS integration: TypeScript stream client against Docker and Podman
PASS integration: stream authorization and CLOSE regressions against Docker
PASS integration: persistent PTY termination preempts blocked send
PASS integration: resource HTTP API against Docker and rootless Podman
PASS integration: compiled CLI and MCP against real server and engines
PASS integration: SSH contract against isolated real sshd
PASS integration: SSH product API and stream against isolated real sshd
PASS integration: remote mTLS infraternal agent enroll, rotate, list, exec and revoke against Docker

Latest fast gate (exit 0, before the small channel-close correction, which has targeted typecheck and tests) printed:

WARN load5=9.48 exceeds online CPU count=8; timing-sensitive checks may be delayed
PASS fast: migrations are append-only
PASS fast: workspace package dependencies
PASS fast: JavaScript dependencies
PASS fast: build web app
PASS fast: cargo fmt --check
PASS fast: cargo clippy
PASS fast: cargo test
PASS fast: svelte-check
PASS fast: UI package svelte-check
PASS fast: stream package typecheck
PASS fast: Vitest
PASS fast: UI package Vitest
PASS fast: stream package Vitest
PASS fast: OpenAPI and TypeScript client drift
PASS fast: terminal TypeScript
PASS fast: terminal Vitest

The channel-close correction means signed-out infraternal tabs stop receiving channel payloads at all. Browser gate and final-head fast gate remain pending.

Verification update through `bd1694b`: Red-before-green proof: with only `packages/web/src/lib/auth.svelte.ts` temporarily restored from reviewed head `11ff44e`, the new `auth.test.ts` suite printed verbatim: ``` Test Files 1 failed (1) Tests 6 failed | 2 passed (8) BASELINE_TEST_EXIT=1 ``` The failures included a signed-out tab becoming signed in from a forged channel message, malformed token acceptance, and missing unsupported-browser notice. The source was restored and `git status --short` was clean. At the fixed head, targeted client tests printed: ``` Test Files 2 passed (2) Tests 17 passed (17) ``` First integration gate (exit 1) failed in the unchanged engine PTY cleanup test: ``` test pty_session_drop_and_close_terminate_exec_processes_on_all_real_engines ... thread 'pty_session_drop_and_close_terminate_exec_processes_on_all_real_engines' (4053386) panicked at crates/engine/tests/integration.rs:743:27: pty close: EngineError { code: Conflict, message: "marker scan failed and no host PID namespace was captured", kind: "exec", engine_code: Some("exec_termination_unconfirmed"), retryable: false, outcome_unknown: true } FAILED FAIL integration: engine adapters against Docker and Podman ``` The exact test rerun with `--ignored --exact --nocapture --test-threads=1` printed: ``` test pty_session_drop_and_close_terminate_exec_processes_on_all_real_engines ... ok test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 10 filtered out; finished in 37.35s ``` Full integration gate rerun (exit 0) printed: ``` PASS integration: JavaScript dependencies PASS integration: build web app PASS integration: build release binary PASS integration: real HTTP behavior PASS integration: Docker socket _ping PASS integration: rootless Podman socket _ping PASS integration: engine adapters against Docker and Podman PASS integration: bounded follow:false log snapshots against Docker and rootless Podman PASS integration: TypeScript stream client against Docker and Podman PASS integration: stream authorization and CLOSE regressions against Docker PASS integration: persistent PTY termination preempts blocked send PASS integration: resource HTTP API against Docker and rootless Podman PASS integration: compiled CLI and MCP against real server and engines PASS integration: SSH contract against isolated real sshd PASS integration: SSH product API and stream against isolated real sshd PASS integration: remote mTLS infraternal agent enroll, rotate, list, exec and revoke against Docker ``` Latest fast gate (exit 0, before the small channel-close correction, which has targeted typecheck and tests) printed: ``` WARN load5=9.48 exceeds online CPU count=8; timing-sensitive checks may be delayed PASS fast: migrations are append-only PASS fast: workspace package dependencies PASS fast: JavaScript dependencies PASS fast: build web app PASS fast: cargo fmt --check PASS fast: cargo clippy PASS fast: cargo test PASS fast: svelte-check PASS fast: UI package svelte-check PASS fast: stream package typecheck PASS fast: Vitest PASS fast: UI package Vitest PASS fast: stream package Vitest PASS fast: OpenAPI and TypeScript client drift PASS fast: terminal TypeScript PASS fast: terminal Vitest ``` The channel-close correction means signed-out infraternal tabs stop receiving channel payloads at all. Browser gate and final-head fast gate remain pending.
Author
Owner

Browser evidence at e3f4498:

First scripts/verify browser (exit 1) completed the new checks, then found two expected 401 console entries on the main page after unsupported tabs had rotated the shared cookie. Its relevant output was:

PASS multi-tab supported: simultaneous tabs (1 refresh HTTP request)
PASS multi-tab locks-only: one notice per tab and sequential stale-access recovery
PASS multi-tab neither: one notice per tab and sequential stale-access recovery
error: light/desktop: browser errors: {"pageErrors":[],"consoleErrors":["Failed to load resource: the server responded with a status of 401 (Unauthorized)","Failed to load resource: the server responded with a status of 401 (Unauthorized)"]}

The browser check now refreshes that main page before resetting its console collector; the 401s remain observable inside the multi-tab exercise. The full rerun (exit 0) printed verbatim:

PASS browser: JavaScript dependencies
PASS browser: install Chromium and system dependencies
PASS browser: terminal fixture Playwright
PASS browser: build web app
PASS browser: build release binary
METRIC browser log flood: {"recordsPerSecond":18017,"frames":109,"fps":60,"retained":10000,"bytes":210000,"dropped":73800,"renderedRows":29}
PASS browser: Playwright against release server
PASS browser: terminal production first-load budget

Notice screenshots inspected at desktop and 390×844: /mnt/hdd/infraternal/artifacts/shadcn/multi-tab-locks-only-1440x900.png, /mnt/hdd/infraternal/artifacts/shadcn/multi-tab-locks-only-390x844.png, /mnt/hdd/infraternal/artifacts/shadcn/multi-tab-neither-1440x900.png, /mnt/hdd/infraternal/artifacts/shadcn/multi-tab-neither-390x844.png. The notice sits above the app content without clipping or overlap. UI rules applied: use shared spacing, type, color and surface tokens; keep the notice in flow at narrow widths; use a polite role=status; keep copy plain and action-oriented; avoid animation for this persistent compatibility state.

Browser evidence at `e3f4498`: First `scripts/verify browser` (exit 1) completed the new checks, then found two expected 401 console entries on the main page after unsupported tabs had rotated the shared cookie. Its relevant output was: ``` PASS multi-tab supported: simultaneous tabs (1 refresh HTTP request) PASS multi-tab locks-only: one notice per tab and sequential stale-access recovery PASS multi-tab neither: one notice per tab and sequential stale-access recovery error: light/desktop: browser errors: {"pageErrors":[],"consoleErrors":["Failed to load resource: the server responded with a status of 401 (Unauthorized)","Failed to load resource: the server responded with a status of 401 (Unauthorized)"]} ``` The browser check now refreshes that main page before resetting its console collector; the 401s remain observable inside the multi-tab exercise. The full rerun (exit 0) printed verbatim: ``` PASS browser: JavaScript dependencies PASS browser: install Chromium and system dependencies PASS browser: terminal fixture Playwright PASS browser: build web app PASS browser: build release binary METRIC browser log flood: {"recordsPerSecond":18017,"frames":109,"fps":60,"retained":10000,"bytes":210000,"dropped":73800,"renderedRows":29} PASS browser: Playwright against release server PASS browser: terminal production first-load budget ``` Notice screenshots inspected at desktop and 390×844: `/mnt/hdd/infraternal/artifacts/shadcn/multi-tab-locks-only-1440x900.png`, `/mnt/hdd/infraternal/artifacts/shadcn/multi-tab-locks-only-390x844.png`, `/mnt/hdd/infraternal/artifacts/shadcn/multi-tab-neither-1440x900.png`, `/mnt/hdd/infraternal/artifacts/shadcn/multi-tab-neither-390x844.png`. The notice sits above the app content without clipping or overlap. UI rules applied: use shared spacing, type, color and surface tokens; keep the notice in flow at narrow widths; use a polite `role=status`; keep copy plain and action-oriented; avoid animation for this persistent compatibility state.
Author
Owner

Finished fix-44 on branch multi-tab at head e3f4498c8435406006ff12478481e9971bac603e (base 11ff44ef5a8218ba5071512757889a894cefa1ca). Committed in seven small commits; did not push. Worktree is clean. Findings 1–5 and their evidence are in the earlier issue comments. The final checks below all ran on this head.

scripts/verify fast (exit 0), verbatim output:

PASS fast: migrations are append-only
PASS fast: workspace package dependencies
PASS fast: JavaScript dependencies
PASS fast: build web app
PASS fast: cargo fmt --check
PASS fast: cargo clippy
PASS fast: cargo test
PASS fast: svelte-check
PASS fast: UI package svelte-check
PASS fast: stream package typecheck
PASS fast: Vitest
PASS fast: UI package Vitest
PASS fast: stream package Vitest
PASS fast: OpenAPI and TypeScript client drift
PASS fast: terminal TypeScript
PASS fast: terminal Vitest

scripts/verify integration (exit 0; prestart load5=6.99), verbatim output:

PASS integration: JavaScript dependencies
PASS integration: build web app
PASS integration: build release binary
PASS integration: real HTTP behavior
PASS integration: Docker socket _ping
PASS integration: rootless Podman socket _ping
PASS integration: engine adapters against Docker and Podman
PASS integration: bounded follow:false log snapshots against Docker and rootless Podman
PASS integration: TypeScript stream client against Docker and Podman
PASS integration: stream authorization and CLOSE regressions against Docker
PASS integration: persistent PTY termination preempts blocked send
PASS integration: resource HTTP API against Docker and rootless Podman
PASS integration: compiled CLI and MCP against real server and engines
PASS integration: SSH contract against isolated real sshd
PASS integration: SSH product API and stream against isolated real sshd
PASS integration: remote mTLS infraternal agent enroll, rotate, list, exec and revoke against Docker

scripts/verify browser (exit 0; prestart load5=3.24), verbatim output:

PASS browser: JavaScript dependencies
PASS browser: install Chromium and system dependencies
PASS browser: terminal fixture Playwright
PASS browser: build web app
PASS browser: build release binary
METRIC browser log flood: {"recordsPerSecond":18017,"frames":109,"fps":60,"retained":10000,"bytes":210000,"dropped":73800,"renderedRows":29}
PASS browser: Playwright against release server
PASS browser: terminal production first-load budget

Additional evidence: node --check scripts/browser-check.mjs passed; git diff --check 11ff44e..HEAD passed; code-review-graph updated. The browser check logged PASS multi-tab supported: simultaneous tabs (1 refresh HTTP request), PASS multi-tab locks-only: one notice per tab and sequential stale-access recovery, and PASS multi-tab neither: one notice per tab and sequential stale-access recovery during its first run, then completed the same cases in the successful full rerun. The notice screenshots are at /mnt/hdd/infraternal/artifacts/shadcn/multi-tab-locks-only-{1440x900,390x844}.png and /mnt/hdd/infraternal/artifacts/shadcn/multi-tab-neither-{1440x900,390x844}.png; both widths were inspected with no overlap or clipping. Safari/iOS remain for the owner's checklist per CLAUDE.md.

Finished fix-44 on branch `multi-tab` at head `e3f4498c8435406006ff12478481e9971bac603e` (base `11ff44ef5a8218ba5071512757889a894cefa1ca`). Committed in seven small commits; did not push. Worktree is clean. Findings 1–5 and their evidence are in the earlier issue comments. The final checks below all ran on this head. `scripts/verify fast` (exit 0), verbatim output: ``` PASS fast: migrations are append-only PASS fast: workspace package dependencies PASS fast: JavaScript dependencies PASS fast: build web app PASS fast: cargo fmt --check PASS fast: cargo clippy PASS fast: cargo test PASS fast: svelte-check PASS fast: UI package svelte-check PASS fast: stream package typecheck PASS fast: Vitest PASS fast: UI package Vitest PASS fast: stream package Vitest PASS fast: OpenAPI and TypeScript client drift PASS fast: terminal TypeScript PASS fast: terminal Vitest ``` `scripts/verify integration` (exit 0; prestart load5=6.99), verbatim output: ``` PASS integration: JavaScript dependencies PASS integration: build web app PASS integration: build release binary PASS integration: real HTTP behavior PASS integration: Docker socket _ping PASS integration: rootless Podman socket _ping PASS integration: engine adapters against Docker and Podman PASS integration: bounded follow:false log snapshots against Docker and rootless Podman PASS integration: TypeScript stream client against Docker and Podman PASS integration: stream authorization and CLOSE regressions against Docker PASS integration: persistent PTY termination preempts blocked send PASS integration: resource HTTP API against Docker and rootless Podman PASS integration: compiled CLI and MCP against real server and engines PASS integration: SSH contract against isolated real sshd PASS integration: SSH product API and stream against isolated real sshd PASS integration: remote mTLS infraternal agent enroll, rotate, list, exec and revoke against Docker ``` `scripts/verify browser` (exit 0; prestart load5=3.24), verbatim output: ``` PASS browser: JavaScript dependencies PASS browser: install Chromium and system dependencies PASS browser: terminal fixture Playwright PASS browser: build web app PASS browser: build release binary METRIC browser log flood: {"recordsPerSecond":18017,"frames":109,"fps":60,"retained":10000,"bytes":210000,"dropped":73800,"renderedRows":29} PASS browser: Playwright against release server PASS browser: terminal production first-load budget ``` Additional evidence: `node --check scripts/browser-check.mjs` passed; `git diff --check 11ff44e..HEAD` passed; code-review-graph updated. The browser check logged `PASS multi-tab supported: simultaneous tabs (1 refresh HTTP request)`, `PASS multi-tab locks-only: one notice per tab and sequential stale-access recovery`, and `PASS multi-tab neither: one notice per tab and sequential stale-access recovery` during its first run, then completed the same cases in the successful full rerun. The notice screenshots are at `/mnt/hdd/infraternal/artifacts/shadcn/multi-tab-locks-only-{1440x900,390x844}.png` and `/mnt/hdd/infraternal/artifacts/shadcn/multi-tab-neither-{1440x900,390x844}.png`; both widths were inspected with no overlap or clipping. Safari/iOS remain for the owner's checklist per CLAUDE.md.
Author
Owner

Starting verify-44 on branch multi-tab, base head e3f4498c8435406006ff12478481e9971bac603e. I will verify findings 1–5, reproduce the specified regressions in a throwaway worktree, integrate origin/dev, then run the required gates under the resource constraints.

Starting verify-44 on branch `multi-tab`, base head `e3f4498c8435406006ff12478481e9971bac603e`. I will verify findings 1–5, reproduce the specified regressions in a throwaway worktree, integrate `origin/dev`, then run the required gates under the resource constraints.
Author
Owner

Verify-44 findings check on merge input head e3f4498c8435406006ff12478481e9971bac603e:

  1. Closed. packages/web/src/lib/auth.svelte.ts enables shared refresh only when Web Locks and BroadcastChannel are both available and uses navigator.locks.request; the 80 ms owner election is gone. In the throwaway worktree, a focused timer regression test passed on e3f4498 and failed with the old 11ff44e auth source (expected [80] to not include 80).
  2. Closed for signed-out sessions. openChannel() accepts a session only with an active session or a current pending refresh, a valid token shape and a newer safe generation. clear() closes the channel and increments the sign-out epoch; refresh/setup/sign-in responses also check the epoch. The tests cover forged post-sign-out messages, late refresh responses and stale/malformed generations. Stable signed-out tabs close their channel; active session restoration is the only state that adopts a shared credential.
  3. Closed. authStore.singleTabNotice is true when either coordination API is missing. AppShell.svelte renders a plain role="status" notice advising one tab, without blocking the app. Browser checks assert one notice per unsupported tab and the issue's prior run includes desktop and 390×844 screenshots.
  4. Closed. api.ts retries only GET/HEAD or requests with a nonblank Idempotency-Key, retaining the same key. With old 11ff44e API code, the new API suite failed 4/10, including the unkeyed POST, PUT and DELETE assertions; fixed code passed.
  5. Closed. docs/contracts.md §1.1 describes the same Web Locks + BroadcastChannel path, per-tab unsupported fallback and notice, sign-out fencing, and one-time retry restrictions implemented in auth.svelte.ts and api.ts.

The fixed client regressions passed 19/19 in the throwaway worktree. origin/dev merged cleanly at ebf8ab27a9dd46781abd79f1785bca107f3649d0; no generated API/client files were in the merge diff. Merged scripts/verify fast passed. Browser and integration are pending the other job's active integration gate and the required resource check.

Verify-44 findings check on merge input head `e3f4498c8435406006ff12478481e9971bac603e`: 1. **Closed.** `packages/web/src/lib/auth.svelte.ts` enables shared refresh only when Web Locks and BroadcastChannel are both available and uses `navigator.locks.request`; the 80 ms owner election is gone. In the throwaway worktree, a focused timer regression test passed on `e3f4498` and failed with the old `11ff44e` auth source (`expected [80] to not include 80`). 2. **Closed for signed-out sessions.** `openChannel()` accepts a session only with an active session or a current pending refresh, a valid token shape and a newer safe generation. `clear()` closes the channel and increments the sign-out epoch; refresh/setup/sign-in responses also check the epoch. The tests cover forged post-sign-out messages, late refresh responses and stale/malformed generations. Stable signed-out tabs close their channel; active session restoration is the only state that adopts a shared credential. 3. **Closed.** `authStore.singleTabNotice` is true when either coordination API is missing. `AppShell.svelte` renders a plain `role="status"` notice advising one tab, without blocking the app. Browser checks assert one notice per unsupported tab and the issue's prior run includes desktop and 390×844 screenshots. 4. **Closed.** `api.ts` retries only GET/HEAD or requests with a nonblank `Idempotency-Key`, retaining the same key. With old `11ff44e` API code, the new API suite failed 4/10, including the unkeyed POST, PUT and DELETE assertions; fixed code passed. 5. **Closed.** `docs/contracts.md` §1.1 describes the same Web Locks + BroadcastChannel path, per-tab unsupported fallback and notice, sign-out fencing, and one-time retry restrictions implemented in `auth.svelte.ts` and `api.ts`. The fixed client regressions passed 19/19 in the throwaway worktree. `origin/dev` merged cleanly at `ebf8ab27a9dd46781abd79f1785bca107f3649d0`; no generated API/client files were in the merge diff. Merged `scripts/verify fast` passed. Browser and integration are pending the other job's active integration gate and the required resource check.
Author
Owner

Correction to my previous finding-2 status: the broader stated guarantee is not closed. A new tab starts with sessionGeneration = 0 even if localStorage has a later session generation. In a focused temporary reproduction against e3f4498, I seeded storage generation 5, left refresh pending, then delivered a well-shaped session message with generation 1; the tab adopted that token and set signedIn = true. The temporary test failed with the adopted token where it expected null.

Evidence: auth.svelte.ts checks data.generation > sessionGeneration but does not compare the message against storedGeneration(). auth.test.ts only rejects an equal generation after it has already accepted one in the same instance. The existing sign-out test covers a stable signed-out tab after clear() closes its channel, but does not cover stale/forged messages during pending restoration. In that pending state signedIn is still false, and setSession() broadcasts its token to all open listeners. This conflicts with the requested unqualified guarantees that stale/forged messages cannot sign a tab in and that no token is sent to a signed-out tab.

No code change was made because this verification job permits only the dev merge and fixes for failures caused by that merge. Findings 1, 3, 4 and 5 remain closed by the evidence in the previous comment. This verification will be reported not verified unless the owner clarifies that pending cookie-backed restoration is excluded from the signed-out condition.

Correction to my previous finding-2 status: the broader stated guarantee is **not closed**. A new tab starts with `sessionGeneration = 0` even if `localStorage` has a later session generation. In a focused temporary reproduction against `e3f4498`, I seeded storage generation `5`, left refresh pending, then delivered a well-shaped `session` message with generation `1`; the tab adopted that token and set `signedIn = true`. The temporary test failed with the adopted token where it expected `null`. Evidence: `auth.svelte.ts` checks `data.generation > sessionGeneration` but does not compare the message against `storedGeneration()`. `auth.test.ts` only rejects an equal generation after it has already accepted one in the same instance. The existing sign-out test covers a stable signed-out tab after `clear()` closes its channel, but does not cover stale/forged messages during pending restoration. In that pending state `signedIn` is still false, and `setSession()` broadcasts its token to all open listeners. This conflicts with the requested unqualified guarantees that stale/forged messages cannot sign a tab in and that no token is sent to a signed-out tab. No code change was made because this verification job permits only the `dev` merge and fixes for failures caused by that merge. Findings 1, 3, 4 and 5 remain closed by the evidence in the previous comment. This verification will be reported **not verified** unless the owner clarifies that pending cookie-backed restoration is excluded from the signed-out condition.
Author
Owner

Starting verify-44 on branch multi-tab. The requested fix-round head is e3f4498c8435406006ff12478481e9971bac603e; this checkout is clean at ebf8ab27a9dd46781abd79f1785bca107f3649d0, a merge commit after that fix-round head. The local origin/dev target is 9d4876ae814fea4d0cf0c6b5d420fc33654ad726. I will verify the fix-round and establish whether this existing merge commit is the requested integration.

Starting verify-44 on branch `multi-tab`. The requested fix-round head is `e3f4498c8435406006ff12478481e9971bac603e`; this checkout is clean at `ebf8ab27a9dd46781abd79f1785bca107f3649d0`, a merge commit after that fix-round head. The local `origin/dev` target is `9d4876ae814fea4d0cf0c6b5d420fc33654ad726`. I will verify the fix-round and establish whether this existing merge commit is the requested integration.
Author
Owner

Verification evidence at merged checkout ebf8ab27a9dd46781abd79f1785bca107f3649d0 (parents e3f4498 and 9d4876ae814fea4d0cf0c6b5d420fc33654ad726):

  1. Closed. packages/web/src/lib/auth.svelte.ts uses Web Locks only when both Web Locks and BroadcastChannel exist; otherwise it calls performRefresh() per tab and sets singleTabNotice. There is no refresh-intent election or 80 ms timer. The 2 s timeout only bounds waiting for a generation change while holding the lock. A temporary channel-only test passed at e3 and failed against 11ff44e because the pre-fix code opened a channel without Web Locks. Contract §1.1 and auth.test.ts match the unsupported per-tab behavior.
  2. Not closed. In auth.svelte.ts, the signout handler is guarded by signedIn (line 68); a signed-out tab with an in-flight refresh therefore ignores that message. The session handler accepts a valid-shaped newer token whenever a refresh is pending in the current sign-out epoch (lines 56–64). A temporary test in /home/kayg/wt/verify-44-tmp delivered signout and then a forged newer session while refresh was pending; it observed signedIn: true and the supplied token. The committed test at auth.test.ts:105-119 also deliberately accepts a channel token in this signed-out/pending state. This leaves the requested forged/late/stale signed-out-tab guarantee unproven and reproducibly false.
  3. Closed. The unsupported mode has a role="status" notice in AppShell.svelte; it appears above the shell and does not block input. scripts/browser-check.mjs checks one notice and stale-access recovery in both locks-only and neither modes at desktop and 390×844.
  4. Closed. api.ts:31-34 retries only GET/HEAD or requests with a nonblank Idempotency-Key; the same Headers object is reused. api.test.ts:46-83 tests POST/PUT/DELETE unkeyed and keyed calls. In the throwaway worktree with api.ts restored from 11ff44e, the unkeyed POST/PUT/DELETE tests failed because each sent 2 fetches instead of 1; the blank-key test failed for the same reason.
  5. The original server concurrency tests are unchanged invariant tests. Fix-round client tests provide the red-before-green evidence described above. Contract §1.1 describes the actual supported and unsupported paths, token-generation fencing, and keyed mutation retry behavior.

Focused tests passed at the integrated checkout: 2 files, 18 tests. At fix-round head e3 in the throwaway worktree the same 2 files / 18 tests passed before restoring pre-fix client files. Provisional verdict: not verified because finding 2 remains open. Gates follow.

Verification evidence at merged checkout `ebf8ab27a9dd46781abd79f1785bca107f3649d0` (parents `e3f4498` and `9d4876ae814fea4d0cf0c6b5d420fc33654ad726`): 1. **Closed.** `packages/web/src/lib/auth.svelte.ts` uses Web Locks only when both Web Locks and BroadcastChannel exist; otherwise it calls `performRefresh()` per tab and sets `singleTabNotice`. There is no `refresh-intent` election or 80 ms timer. The 2 s timeout only bounds waiting for a generation change while holding the lock. A temporary channel-only test passed at e3 and failed against 11ff44e because the pre-fix code opened a channel without Web Locks. Contract §1.1 and `auth.test.ts` match the unsupported per-tab behavior. 2. **Not closed.** In `auth.svelte.ts`, the `signout` handler is guarded by `signedIn` (line 68); a signed-out tab with an in-flight refresh therefore ignores that message. The `session` handler accepts a valid-shaped newer token whenever a refresh is pending in the current sign-out epoch (lines 56–64). A temporary test in `/home/kayg/wt/verify-44-tmp` delivered `signout` and then a forged newer `session` while refresh was pending; it observed `signedIn: true` and the supplied token. The committed test at `auth.test.ts:105-119` also deliberately accepts a channel token in this signed-out/pending state. This leaves the requested forged/late/stale signed-out-tab guarantee unproven and reproducibly false. 3. **Closed.** The unsupported mode has a `role="status"` notice in `AppShell.svelte`; it appears above the shell and does not block input. `scripts/browser-check.mjs` checks one notice and stale-access recovery in both `locks-only` and `neither` modes at desktop and 390×844. 4. **Closed.** `api.ts:31-34` retries only GET/HEAD or requests with a nonblank `Idempotency-Key`; the same `Headers` object is reused. `api.test.ts:46-83` tests POST/PUT/DELETE unkeyed and keyed calls. In the throwaway worktree with `api.ts` restored from 11ff44e, the unkeyed POST/PUT/DELETE tests failed because each sent 2 fetches instead of 1; the blank-key test failed for the same reason. 5. The original server concurrency tests are unchanged invariant tests. Fix-round client tests provide the red-before-green evidence described above. Contract §1.1 describes the actual supported and unsupported paths, token-generation fencing, and keyed mutation retry behavior. Focused tests passed at the integrated checkout: `2` files, `18` tests. At fix-round head e3 in the throwaway worktree the same `2` files / `18` tests passed before restoring pre-fix client files. **Provisional verdict: not verified because finding 2 remains open.** Gates follow.
Author
Owner

Verdict: not verified. Finding 2 remains open on the merged head ebf8ab27a9dd46781abd79f1785bca107f3649d0.

The head is a merge commit with parents e3f4498c8435406006ff12478481e9971bac603e and the requested origin/dev head 9d4876ae814fea4d0cf0c6b5d420fc33654ad726. git fetch origin dev confirmed that origin/dev is still that SHA and already an ancestor of HEAD. The merge existed when this job started, so no additional commit was made.

Finding results

  1. Closed. auth.svelte.ts:141-164 uses Web Locks only when both required APIs exist; otherwise it refreshes per tab. There is no timed election or refresh-intent. The 2 s timer at lines 126-139 only bounds waiting for the generation announcement while holding the lock. A temporary channel-only test passed at e3 and failed against pre-fix 11ff44e auth, which opened BroadcastChannel when Web Locks were absent.
  2. Not closed. auth.svelte.ts:68 ignores signout when signedIn is false. The session handler at lines 56-64 accepts a newer shaped token while a refresh is pending. setSession() at line 80 broadcasts the access token to all open channel listeners. A temporary test in the required e3 throwaway worktree sent signout, then a forged newer session while refresh was pending in a signed-out context; it observed signedIn: true and the injected token. The throwaway test and worktree were removed afterward. The committed auth.test.ts:105-119 also explicitly expects session-message adoption while refresh is pending. This contradicts the signed-out-tab guarantee in docs/contracts.md §1.1 and does not prove that signed-out tabs avoid receiving access tokens.
  3. Unsupported-browser behavior is explicit and non-blocking. AppShell.svelte:77-80 renders a role="status" notice above the shell. The browser check exercises locks-only and neither-API modes; the channel-only temporary check also passed with the fix.
  4. Closed. api.ts:31-39 retries only GET/HEAD or a request with a nonblank Idempotency-Key, reusing the same Headers. In the throwaway worktree, restoring pre-fix 11ff44e API code made the POST, PUT, DELETE, and blank-key regression tests fail; each observed two fetches instead of one. The keyed mutation tests passed.
  5. Closed as a regression-test gap. Server concurrency tests remain invariant tests; the fix round adds client auth/API regressions that fail against pre-fix client code, as demonstrated above. The §1.1 contract matches the intended rotation, fallback, and retry behavior, but its signed-out-message guarantee is contradicted by the remaining finding 2 case.

Focused auth/API tests passed on the merged head and at e3: 2 files, 18 tests. scripts/verify fast and scripts/verify integration passed. The final browser run passed after one complete attempt failed during the neither-API navigation step with a local-server connection refusal. A separate earlier browser attempt was interrupted before Playwright when another job's browser sweep started; it is not a gate result. The final run did not fail at Jump-to-latest, so no targeted rerun was needed.

scripts/verify fast (exit 0):

PASS fast: migrations are append-only
PASS fast: workspace package dependencies
PASS fast: JavaScript dependencies
PASS fast: build web app
PASS fast: cargo fmt --check
PASS fast: cargo clippy
PASS fast: cargo test
PASS fast: svelte-check
PASS fast: UI package svelte-check
PASS fast: stream package typecheck
PASS fast: Vitest
PASS fast: UI package Vitest
PASS fast: stream package Vitest
PASS fast: OpenAPI and TypeScript client drift
PASS fast: terminal TypeScript
PASS fast: terminal Vitest

scripts/verify browser final run (exit 0):

PASS browser: JavaScript dependencies
PASS browser: install Chromium and system dependencies
PASS browser: terminal fixture Playwright
PASS browser: build web app
PASS browser: build release binary
METRIC browser log flood: {"recordsPerSecond":17675,"frames":109,"fps":60,"retained":10000,"bytes":210000,"dropped":81200,"renderedRows":18}
PASS browser: Playwright against release server
PASS browser: terminal production first-load budget

Earlier complete browser attempt (exit 1; relevant output):

FAIL browser: Playwright against release server
curl: (7) Failed to connect to 127.0.0.1 port 19115 after 0 ms: Could not connect to server
PASS multi-tab supported: simultaneous tabs (1 refresh HTTP request)
PASS multi-tab locks-only: one notice per tab and sequential stale-access recovery
1184 |           if (disableLocks) Object.defineProperty(navigator, "locks", { value: undefined });
1185 |         }, mode === "neither");
1186 |       }
1187 |       // Single-tab mode deliberately rotates the shared cookie. A stale tab must recover
1188 |       // on its next safe request, and neither tab may pretend it is coordinated.
1189 |       await pages[0].goto(`${baseUrl}/`, { waitUntil: "networkidle" });
                            ^
TimeoutError: goto: Timeout 30000ms exceeded.
Call log:
  - navigating to "http://localhost:19115/", waiting until "networkidle"

  log: [ "  - navigating to \"http://localhost:19115/\", waiting until \"networkidle\"" ]

      at checkSharedCookieTabs (/home/kayg/wt/multi-tab/scripts/browser-check.mjs:1189:22)
      at processTicksAndRejections (native:7:39)

Bun v1.4.2 (Linux x64)

scripts/verify integration (exit 0):

PASS integration: JavaScript dependencies
PASS integration: build web app
PASS integration: build release binary
PASS integration: real HTTP behavior
PASS integration: Docker socket _ping
PASS integration: rootless Podman socket _ping
PASS integration: engine adapters against Docker and Podman
PASS integration: bounded follow:false log snapshots against Docker and rootless Podman
PASS integration: TypeScript stream client against Docker and Podman
PASS integration: stream authorization and CLOSE regressions against Docker
PASS integration: persistent PTY termination preempts blocked send
PASS integration: resource HTTP API against Docker and rootless Podman
PASS integration: compiled CLI and MCP against real server and engines
PASS integration: SSH contract against isolated real sshd
PASS integration: SSH product API and stream against isolated real sshd
PASS integration: remote mTLS infraternal agent enroll, rotate, list, exec and revoke against Docker
**Verdict: not verified.** Finding 2 remains open on the merged head `ebf8ab27a9dd46781abd79f1785bca107f3649d0`. The head is a merge commit with parents `e3f4498c8435406006ff12478481e9971bac603e` and the requested `origin/dev` head `9d4876ae814fea4d0cf0c6b5d420fc33654ad726`. `git fetch origin dev` confirmed that `origin/dev` is still that SHA and already an ancestor of HEAD. The merge existed when this job started, so no additional commit was made. **Finding results** 1. **Closed.** `auth.svelte.ts:141-164` uses Web Locks only when both required APIs exist; otherwise it refreshes per tab. There is no timed election or `refresh-intent`. The 2 s timer at lines 126-139 only bounds waiting for the generation announcement while holding the lock. A temporary channel-only test passed at e3 and failed against pre-fix `11ff44e` auth, which opened BroadcastChannel when Web Locks were absent. 2. **Not closed.** `auth.svelte.ts:68` ignores `signout` when `signedIn` is false. The session handler at lines 56-64 accepts a newer shaped token while a refresh is pending. `setSession()` at line 80 broadcasts the access token to all open channel listeners. A temporary test in the required e3 throwaway worktree sent `signout`, then a forged newer session while refresh was pending in a signed-out context; it observed `signedIn: true` and the injected token. The throwaway test and worktree were removed afterward. The committed `auth.test.ts:105-119` also explicitly expects session-message adoption while refresh is pending. This contradicts the signed-out-tab guarantee in `docs/contracts.md` §1.1 and does not prove that signed-out tabs avoid receiving access tokens. 3. **Unsupported-browser behavior is explicit and non-blocking.** `AppShell.svelte:77-80` renders a `role="status"` notice above the shell. The browser check exercises locks-only and neither-API modes; the channel-only temporary check also passed with the fix. 4. **Closed.** `api.ts:31-39` retries only GET/HEAD or a request with a nonblank `Idempotency-Key`, reusing the same `Headers`. In the throwaway worktree, restoring pre-fix `11ff44e` API code made the POST, PUT, DELETE, and blank-key regression tests fail; each observed two fetches instead of one. The keyed mutation tests passed. 5. **Closed as a regression-test gap.** Server concurrency tests remain invariant tests; the fix round adds client auth/API regressions that fail against pre-fix client code, as demonstrated above. The §1.1 contract matches the intended rotation, fallback, and retry behavior, but its signed-out-message guarantee is contradicted by the remaining finding 2 case. Focused auth/API tests passed on the merged head and at e3: 2 files, 18 tests. `scripts/verify fast` and `scripts/verify integration` passed. The final browser run passed after one complete attempt failed during the neither-API navigation step with a local-server connection refusal. A separate earlier browser attempt was interrupted before Playwright when another job's browser sweep started; it is not a gate result. The final run did not fail at Jump-to-latest, so no targeted rerun was needed. **`scripts/verify fast` (exit 0):** ```text PASS fast: migrations are append-only PASS fast: workspace package dependencies PASS fast: JavaScript dependencies PASS fast: build web app PASS fast: cargo fmt --check PASS fast: cargo clippy PASS fast: cargo test PASS fast: svelte-check PASS fast: UI package svelte-check PASS fast: stream package typecheck PASS fast: Vitest PASS fast: UI package Vitest PASS fast: stream package Vitest PASS fast: OpenAPI and TypeScript client drift PASS fast: terminal TypeScript PASS fast: terminal Vitest ``` **`scripts/verify browser` final run (exit 0):** ```text PASS browser: JavaScript dependencies PASS browser: install Chromium and system dependencies PASS browser: terminal fixture Playwright PASS browser: build web app PASS browser: build release binary METRIC browser log flood: {"recordsPerSecond":17675,"frames":109,"fps":60,"retained":10000,"bytes":210000,"dropped":81200,"renderedRows":18} PASS browser: Playwright against release server PASS browser: terminal production first-load budget ``` **Earlier complete browser attempt (exit 1; relevant output):** ```text FAIL browser: Playwright against release server curl: (7) Failed to connect to 127.0.0.1 port 19115 after 0 ms: Could not connect to server PASS multi-tab supported: simultaneous tabs (1 refresh HTTP request) PASS multi-tab locks-only: one notice per tab and sequential stale-access recovery 1184 | if (disableLocks) Object.defineProperty(navigator, "locks", { value: undefined }); 1185 | }, mode === "neither"); 1186 | } 1187 | // Single-tab mode deliberately rotates the shared cookie. A stale tab must recover 1188 | // on its next safe request, and neither tab may pretend it is coordinated. 1189 | await pages[0].goto(`${baseUrl}/`, { waitUntil: "networkidle" }); ^ TimeoutError: goto: Timeout 30000ms exceeded. Call log: - navigating to "http://localhost:19115/", waiting until "networkidle" log: [ " - navigating to \"http://localhost:19115/\", waiting until \"networkidle\"" ] at checkSharedCookieTabs (/home/kayg/wt/multi-tab/scripts/browser-check.mjs:1189:22) at processTicksAndRejections (native:7:39) Bun v1.4.2 (Linux x64) ``` **`scripts/verify integration` (exit 0):** ```text PASS integration: JavaScript dependencies PASS integration: build web app PASS integration: build release binary PASS integration: real HTTP behavior PASS integration: Docker socket _ping PASS integration: rootless Podman socket _ping PASS integration: engine adapters against Docker and Podman PASS integration: bounded follow:false log snapshots against Docker and rootless Podman PASS integration: TypeScript stream client against Docker and Podman PASS integration: stream authorization and CLOSE regressions against Docker PASS integration: persistent PTY termination preempts blocked send PASS integration: resource HTTP API against Docker and rootless Podman PASS integration: compiled CLI and MCP against real server and engines PASS integration: SSH contract against isolated real sshd PASS integration: SSH product API and stream against isolated real sshd PASS integration: remote mTLS infraternal agent enroll, rotate, list, exec and revoke against Docker ```
Author
Owner

fix-44b started on branch multi-tab, base head ebf8ab27a9dd46781abd79f1785bca107f3649d0. Scope: pending-refresh sign-out fencing in web auth, contract text, regression tests, fast and browser gates. The current channel handler checks signedIn before honoring signout; a tab with a pending refresh and no access credential therefore ignores it.

fix-44b started on branch `multi-tab`, base head `ebf8ab27a9dd46781abd79f1785bca107f3649d0`. Scope: pending-refresh sign-out fencing in web auth, contract text, regression tests, fast and browser gates. The current channel handler checks `signedIn` before honoring `signout`; a tab with a pending refresh and no access credential therefore ignores it.
Author
Owner

fix-44b started on branch multi-tab, base head ebf8ab27a9dd46781abd79f1785bca107f3649d0. Scope: pending-refresh sign-out fencing in web auth, contract text, regression tests, fast and browser gates. The current channel handler checks signedIn before honoring signout; a tab with a pending refresh and no access credential therefore ignores it.

fix-44b started on branch `multi-tab`, base head `ebf8ab27a9dd46781abd79f1785bca107f3649d0`. Scope: pending-refresh sign-out fencing in web auth, contract text, regression tests, fast and browser gates. The current channel handler checks `signedIn` before honoring `signout`; a tab with a pending refresh and no access credential therefore ignores it.
Author
Owner

Finding and fix evidence for fix-44b: openChannel ignored signout whenever the receiving tab was not yet signedIn, even when its refresh HTTP request was in flight. The test at auth.test.ts:105 now sends sign-out before a forged newer session; with the implementation temporarily reverted to ebf8ab2, the focused Vitest run exited 1 with two failures: rejects a forged session after sign-out during a pending refresh (received signedIn=true) and discards a late refresh response after channel sign-out (refresh resolved true). The fixed implementation increments the sign-out epoch for local and channel sign-out, rejects later session messages, and rejects the outstanding refresh result. A later refresh call remains blocked until a local passkey ceremony. Focused test run on the fix: 13/13 passed. scripts/verify fast passed all steps, including cargo test, svelte-check, Vitest and OpenAPI/client drift. Browser gate awaits the specified load/process window.

Finding and fix evidence for fix-44b: `openChannel` ignored `signout` whenever the receiving tab was not yet `signedIn`, even when its refresh HTTP request was in flight. The test at `auth.test.ts:105` now sends sign-out before a forged newer session; with the implementation temporarily reverted to `ebf8ab2`, the focused Vitest run exited 1 with two failures: `rejects a forged session after sign-out during a pending refresh` (received `signedIn=true`) and `discards a late refresh response after channel sign-out` (refresh resolved `true`). The fixed implementation increments the sign-out epoch for local and channel sign-out, rejects later session messages, and rejects the outstanding refresh result. A later refresh call remains blocked until a local passkey ceremony. Focused test run on the fix: 13/13 passed. `scripts/verify fast` passed all steps, including cargo test, svelte-check, Vitest and OpenAPI/client drift. Browser gate awaits the specified load/process window.
Author
Owner

fix-44b finished on multi-tab at head 102ec66091d1ca4bddf3adac1e4750f4e8903cd7 (base ebf8ab27a9dd46781abd79f1785bca107f3649d0); not pushed. Commits: 9a7f88f auth/contract/unit tests and 102ec66 production browser check.

The receiving tab now honors broadcast sign-out during pending refresh, advances its sign-out epoch, discards both a forged newer channel session and the late refresh result, and stays signed out until its own passkey ceremony. Broadcast session adoption requires an active pending refresh in the current epoch, no sign-out in that epoch, and a newer session generation. Local sign-out without a credential also fences a pending refresh. docs/contracts.md records these rules.

Red/green evidence: with only the auth implementation temporarily reverted to ebf8ab2, bun --cwd packages/web test src/lib/auth.test.ts exited 1: rejects a forged session after sign-out during a pending refresh observed signedIn=true; discards a late refresh response after channel sign-out observed refresh true instead of false. The fixed run passed 13/13 tests. node --check scripts/browser-check.mjs and git diff --check passed. Production Playwright logged PASS multi-tab sign-out: pending refresh response cannot restore tab B with tab A signing out while tab B's refresh response was held.

Verbatim scripts/verify fast output:

PASS fast: migrations are append-only
PASS fast: workspace package dependencies
PASS fast: JavaScript dependencies
PASS fast: build web app
PASS fast: cargo fmt --check
PASS fast: cargo clippy
PASS fast: cargo test
PASS fast: svelte-check
PASS fast: UI package svelte-check
PASS fast: stream package typecheck
PASS fast: Vitest
PASS fast: UI package Vitest
PASS fast: stream package Vitest
PASS fast: OpenAPI and TypeScript client drift
PASS fast: terminal TypeScript
PASS fast: terminal Vitest

Verbatim scripts/verify browser output:

PASS browser: JavaScript dependencies
PASS browser: install Chromium and system dependencies
PASS browser: terminal fixture Playwright
PASS browser: build web app
PASS browser: build release binary
METRIC browser log flood: {"recordsPerSecond":17956,"frames":109,"fps":60,"retained":10000,"bytes":210000,"dropped":80600,"renderedRows":18}
PASS browser: Playwright against release server
PASS browser: terminal production first-load budget
fix-44b finished on `multi-tab` at head `102ec66091d1ca4bddf3adac1e4750f4e8903cd7` (base `ebf8ab27a9dd46781abd79f1785bca107f3649d0`); not pushed. Commits: `9a7f88f` auth/contract/unit tests and `102ec66` production browser check. The receiving tab now honors broadcast sign-out during pending refresh, advances its sign-out epoch, discards both a forged newer channel session and the late refresh result, and stays signed out until its own passkey ceremony. Broadcast session adoption requires an active pending refresh in the current epoch, no sign-out in that epoch, and a newer session generation. Local sign-out without a credential also fences a pending refresh. `docs/contracts.md` records these rules. Red/green evidence: with only the auth implementation temporarily reverted to `ebf8ab2`, `bun --cwd packages/web test src/lib/auth.test.ts` exited 1: `rejects a forged session after sign-out during a pending refresh` observed `signedIn=true`; `discards a late refresh response after channel sign-out` observed refresh `true` instead of `false`. The fixed run passed 13/13 tests. `node --check scripts/browser-check.mjs` and `git diff --check` passed. Production Playwright logged `PASS multi-tab sign-out: pending refresh response cannot restore tab B` with tab A signing out while tab B's refresh response was held. Verbatim `scripts/verify fast` output: ``` PASS fast: migrations are append-only PASS fast: workspace package dependencies PASS fast: JavaScript dependencies PASS fast: build web app PASS fast: cargo fmt --check PASS fast: cargo clippy PASS fast: cargo test PASS fast: svelte-check PASS fast: UI package svelte-check PASS fast: stream package typecheck PASS fast: Vitest PASS fast: UI package Vitest PASS fast: stream package Vitest PASS fast: OpenAPI and TypeScript client drift PASS fast: terminal TypeScript PASS fast: terminal Vitest ``` Verbatim `scripts/verify browser` output: ``` PASS browser: JavaScript dependencies PASS browser: install Chromium and system dependencies PASS browser: terminal fixture Playwright PASS browser: build web app PASS browser: build release binary METRIC browser log flood: {"recordsPerSecond":17956,"frames":109,"fps":60,"retained":10000,"bytes":210000,"dropped":80600,"renderedRows":18} PASS browser: Playwright against release server PASS browser: terminal production first-load budget ```
Author
Owner

Architecture-review follow-up (2026-10-04): this review did not reproduce the multi-tab bug. Its history analysis found browser refresh, pagehide, replay and terminal-workspace scenarios among the highest-churn lifecycle areas. It recommends short, independent fixtures with named readiness/lifecycle events, while retaining the full production-browser flow. This supports isolating the refresh handoff case here; it does not replace the production-browser gate.

Source: arch-review 2026-10-04; /mnt/hdd/infraternal/jobs/arch-review/report.md.

Architecture-review follow-up (2026-10-04): this review did not reproduce the multi-tab bug. Its history analysis found browser refresh, pagehide, replay and terminal-workspace scenarios among the highest-churn lifecycle areas. It recommends short, independent fixtures with named readiness/lifecycle events, while retaining the full production-browser flow. This supports isolating the refresh handoff case here; it does not replace the production-browser gate. Source: arch-review 2026-10-04; /mnt/hdd/infraternal/jobs/arch-review/report.md.
Commenting is not possible because the repository is archived.
No labels
No milestone
No project
No assignees
1 participant
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-v0#44
No description provided.