# Proof audit pass 1 — slow-host session permissions

Status: matrix ready; evidence incomplete. This is a read-only proof audit, not a takeover implementation or merge approval. No code changes, pushes, PR comments, agents, browsers, dev servers, or captures were made. The report is the only new artifact.

## Revision and scope

- GitHub [#25526](https://github.com/pingdotgg/t3code/pull/25526); GitCafe [#25528](https://git.cafe/pingdotgg/t3code/pulls/25528).
- Original: [yordis/t3code](https://github.com/yordis/t3code), branch `yordis/fix-slow-host-session-read-only`, [03ab5cd](https://github.com/yordis/t3code/commit/03ab5cd0d70a40932a37892d1e3d49ed9f584dae).
- Audited takeover: `/home/ubuntu/wt/pr-25526`, branch `takeover/pr-25526`, [f871636](https://git.cafe/pingdotgg/t3code/commit/f8716369890ab99d1f171966b0019cad155743b5).
- Before: merge-base and local `origin/main` both [dd5b540](https://git.cafe/pingdotgg/t3code/commit/dd5b5401e1f240f4314af29edf431369d182f386). Full `git diff origin/main...HEAD` read: two files, 99 insertions, 10 deletions. Working tree clean before/after diagnostics. No remote metadata refresh; author description read from `/tmp/pr-25526-metadata.json` as requested. That file describes the original fork head, not the takeover head.
- Standard: `/home/ubuntu/t3code-ops/docs/EVIDENCE.md`, with the user's narrower evidence scope: visible Settings read-only symptom on web; shared-runtime unit coverage for mobile. No new visual components, so no account-count/provider/layout combinatorial matrix.

## Changed behavior and consumer trace

[session.ts](https://git.cafe/pingdotgg/t3code/blob/f8716369890ab99d1f171966b0019cad155743b5/packages/client-runtime/src/state/session.ts) catches only `RemoteEnvironmentAuthTimeoutError`. Default per-attempt budgets are **6, 12, 24, 48, 60 seconds**; starts at approximately **0, 6, 18, 42, 90 seconds**, final failure at **150 seconds** if every attempt times out. No additional backoff delay. An 8-second response on every request therefore succeeds on attempt two at approximately 14 seconds, not 8 seconds: the first request was interrupted at 6 seconds.

`createEnvironmentSessionAtoms` calls this loader. It uses SWR (30-second stale time, revalidate on mount, five-minute idle TTL). Session values may survive revalidation. Web `state/session.ts` and shared `commandPermissions.ts` require a non-Failure result with a grant; a cold pending result has no grant. Thus this change **does not make cold pending ordinary Settings editable**. It keeps waiting and can recover automatically instead of settling into a missing-grant state at 6 seconds.

Two distinct visible Settings consumer paths must appear:

1. [settingsLayout.tsx](https://git.cafe/pingdotgg/t3code/blob/f8716369890ab99d1f171966b0019cad155743b5/apps/web/src/components/settings/settingsLayout.tsx): `SettingsRow` uses `settings:write`; server-scoped controls become inert and dimmed without it, with a permission tooltip. `useScopedSettings.ts` requires every selected connected environment to grant the scope. Use one environment for the main recording; multi-environment aggregation is a unit row.
2. [ProviderSettingsPanel.tsx](https://git.cafe/pingdotgg/t3code/blob/f8716369890ab99d1f171966b0019cad155743b5/apps/web/src/components/settings/ProviderSettingsPanel.tsx) and its logic: no session + waiting -> “Loading provider settings” / “Checking what this session is allowed to change.”; failed session -> read-only provider settings; valid `providers:manage` -> editable. Cached granted session + waiting remains editable; terminal Failure denies even if old data exists.

Other direct consumers include web Connections, Diagnostics, Usage, files, terminal, media, browser reporting, thread actions, sidebar updates, and permission-update notices. They reuse the same session/grant derivation; no render code changed. Shared command permission checks cover the cross-feature boundary rather than requiring captures of all these surfaces. Settings navigation, deep links/search, and keybinding entry points converge on the same mounted Settings components; no entry-point code changed.

Mobile imports the same loader via `createEnvironmentSessionAtoms`. `apps/mobile/src/state/session.ts` uses the same non-Failure grant rule; Settings environment detail, routes, files, terminal, task flow, usage and media also consume session atoms. Native mobile is **unverified** in this pass. Shared tests are runtime evidence, not proof of native rendering. An Android emulator that already boots can support supplemental verification; device tooling is never a blocker for this requested scope. No Electron shell/IPC, provider adapter, MCP transport, or wire schema change; desktop renders web and needs no separate shell capture.

## Minimum fixture matrix

R means a recording is required: every interaction/time-affected row is marked. Each R row must show BOTH Settings consumer paths above, using independently reset runs if navigating would retrigger SWR. Use the same real fixture data before/after. U rows are unit-only controls under the user's scope exception; they add no visual permutations.

| ID | Affected? | Fixture / state sequence | Expected before -> after | Proof |
|---|---|---|---|---|
| R1 | Yes: cold late success | Authenticated admin, connected config ready, no cached session; delay every session GET by 8s; visit General and Providers in separate reset episodes | Before: settles denied/read-only around 6s and stays so in the observed mounted load. After: General still disabled while pending; Providers stays permission-loading beyond 6s; attempt two succeeds around 14s and both become editable without manual refresh | RECORDING + failing→passing loader regression + grant transition assertions |
| R2 | Yes: warm late success | Load admin session normally; retain cached grant; trigger one stale revalidation, then delay session GETs by 8s | Before: Failure around 6s removes editing. After: cached grant keeps editing through the retry and resolves successfully around 14s; no false read-only interval | RECORDING + real session-atom/SWR transition test |
| R3 | Yes: cold terminal timeout | Same admin credential/config, no cached session; hold each session GET until aborted for all five attempts | Before: terminal Failure/read-only around 6s. After: pending through starts 6/18/42/90s, terminal timeout at 150s; then General denied and Providers read-only, with no sixth attempt | RECORDING + default-schedule bounded-timeout regression |
| R4 | Yes: warm terminal timeout | Same as R2 but hold every attempt | Before: loses cached editing around 6s. After: retains it during pending retries, then terminal Failure denies at 150s; no infinite wait or silent grant retention | RECORDING + real session-atom/SWR failure test |
| U1 | No semantic change | Immediate admin success; cookie and bearer | One request, grants available; cookie credentials included and bearer header preserved | Unit tests; no recording |
| U2 | No grant semantic change | Fast authenticated read-only session; unauthenticated 200; exact empty permissions; legacy parent grants; delayed success with insufficient scope | Never invent grants; delayed grantless result still denied, not editable. Timing is represented by R1; no new visual state | Unit tests; no recording |
| U3 | No retry change intended | Session endpoint network rejection, 403 insufficient scope, undeclared HTTP status (e.g. 530), invalid schema; cookie/bearer credential rejection | Propagate the original error immediately; one request, no timeout retry. Also test timeout then one of these failures: stop at that failure | Session-loader unit table; no recording |
| U4 | No auth-recovery change intended | DPoP invalid credential or authenticated:false then renewed success; persistent rejection; authorization service failure; timeout followed by renewal/current route | Preserve the existing one credential recovery, current token/origin, fresh proof per attempt; persistent non-timeout rejection ends immediately after existing recovery. Direct routes keep their origin; relay routes may update it | Existing auth tests plus timeout/DPoP crossover case; no recording |
| U5 | Consumer invariants | Destination A granted, B pending/denied; aggregate A+B; wrong scope; no environment/config; disconnected/error | No cross-environment grant borrowing; all selected targets required; loading/config/disconnected classification unchanged | Permission and classifier unit rows; no recording |
| U6 | Cancellation / reverse state | Interrupt during a later attempt; replace prepared connection/environment; refresh after terminal failure with healthy endpoint | Old request aborts; no further attempts after cancellation; new fetch can resolve; old environment never supplies new environment's grant | Runtime tests; no extra recording (request lifecycle in R rows corroborates cancellation) |
| U7 | Independent execution deadline | Invoke a protected command programmatically while a cold session remains pending beyond 6s | `commandPermissions.authorize` still has its own 6s timeout and can deny even while loader continues. After session success, a new command can authorize. Cached-refresh grant remains usable | Unit control, explicitly an unchanged limitation; no recording |

The four recording rows are necessary because cold versus cached data changes what is shown during the retry, and eventual success versus exhaustion changes the exit. Repeated retry steps have identical presentation, so unit assertions cover 12/24/48/60s individually rather than duplicating visual fixtures. Provider model/account counts, names, empty/full lists and six agent providers do not affect the changed branch. Use one stable provider card and one environment. No matrix expansion for unrelated popovers, onboarding, terminal, or every individual setting.

## Required tests and current evidence gap

Executed at audited head: `vp test run src/state/environmentHttpAuth.test.ts src/state/commandPermissions.test.ts` from `packages/client-runtime`: **2 files, 48 tests passed**, duration 1.25s. `git diff --check origin/main...HEAD` passed. No browser tests or repo-wide checks run.

The added recovery test uses initial timeout 1s, then succeeds 1.5s into attempt two. The added cap test uses 15/30/60s. These demonstrate retry/growth/cap, but neither pins the specified production 6/12/24/48/60s sequence. Existing non-timeout 403/network/530 no-retry table exercises the PR-diff loader, not the changed session loader. Existing permission tests mock the session atom, so they do not prove real SWR transitions under this change.

Minimum acceptance test work for the implementation owner (no tests edited in this audit):

- Run the SAME new recovery assertion on base production code and head production code. Base must fail on a concrete assertion that no second request was sent / the load settled; head must pass. Preserve logs and exact revision/test patch hashes. Do not count a test timeout as the intended failure. The existing `nextRequestOrSettled` helper supports such a non-hanging failure.
- Pin the DEFAULT schedule with omitted `timeoutMs`: use TestClock and Queue/Deferred milestones to assert five attempts, no premature terminal settlement, final timeout error `timeoutMs:60000`, and no sixth request. Base fails at the first required retry. A small table can release success on later attempts to verify each longer window accepts a response beyond its predecessor's budget. Keep the existing short override case if useful; avoid duplicate cases.
- Exercise the changed loader for U3, including a non-timeout failure after a timeout. Reuse existing auth fixture helpers for U4. Existing malformed-session and unauthenticated/renewal cases already cover part of this matrix.
- Test the real session atom's cold and cached pending/success/failure transitions and the consumer grant result (R1–R4). Cover cancellation and new-connection isolation without sleeps/polling; await queues/deferreds and advance TestClock. Narrow unit coverage for U5/U7 is sufficient; do not add component-markup/prop-wiring tests.

Failing-on-base proof has **not** been independently executed in this read-only pass. The original author's description claims it for the original test, but that is not takeover-head proof. No existing recordings/captures were supplied or audited. Evidence remains missing until pass 2 checks artifacts.

## Reproducible real-app fixture and endpoint delay

For the later capture owner, not commands executed by this pass:

1. Use isolated state entirely beneath `/tmp/ev-25526`, for example separate `before` and `after` homes. Start each exact revision with `vp run dev --no-seed --home-dir /tmp/ev-25526/<revision-label>`; also set `T3CODE_DEV_SEED=0` if desired. Read actual ports from dev-runner output. Never read/copy/write `~/.t3/userdata`, never run migrate-dev-db, and never set VITE_HTTP_URL/VITE_WS_URL. Keep credentials and startup pairing links private.
2. Pair to that isolated real server with its own generated admin credential. Seed through real app commands/UI: one environment, one synthetic project rooted under `/tmp/ev-25526/fixture-project`, optionally one stopped synthetic thread (no provider turn), built-in provider settings and fixed default values. One real provider card is enough; no actual provider account/login required for testing permission gating. Pin and record the same provider availability/config on both revisions. No production database, credentials, or customer data. General must display a server-scoped row such as “Resume after usage limits”; Providers must show its permission placeholder/card. Use environment scope so project inheritance does not independently disable editing.
3. Save a public fixture manifest with synthetic project/environment label, exact settings, provider presentation, viewport/theme/scroll and planned interactions. Generate each revision's auth locally; do not copy cookies into the public fixture. Create data separately through app commands for reproducibility. For each episode restore the same settings and fresh browser atom state. Seed before installing latency. Use a separate authenticated setup context from the measured cold context; do not let setup prepopulate its session atom.
4. Inject latency only on **GET `/api/auth/session`**, identified by request path and the selected environment origin. Preferred implementation is capture tooling's request interception against the actual server response: forward the request with its real cookies/headers, retain the real status/body, and withhold delivery by 8000ms per request (R1/R2), or until the client aborts (R3/R4). If tooling lacks interception, use a local transparent capture proxy that forwards HTTP and WebSockets, delays only this path, and preserves same-origin auth; validate pairing/origin forwarding before measurement. Do not fake grants, override atoms, delay all network, or replace the page with a component harness. Capture scripts must specify which mechanism was actually used, including any proxy origin configuration.
5. Keep config/WS healthy and record request-start/abort/completion milestones without authorization headers/cookie values. Count only loader attempts in the measured episode; identify other bootstrap session calls separately. R1 should show starts at 0/6s and success near 14s. R3/R4 should show starts 0/6/18/42/90s and terminal failure 150s. Real video times may include scheduling overhead; unit tests pin exact budgets. Avoid reloads/remounts during a measured load: they can revalidate and hide the base failure. Warm runs must first obtain a grant, then trigger stale revalidation through a documented real-app remount after 30s while retaining its cache; reset the episode if a new connection replaces it.
6. Record navigation into Settings, disabled-control hover/focus, the 6s crossing, recovery/exhaustion, then a reversible toggle/save/restore after success. Show the real persistence result via the isolated app, not just opacity. Run General and Providers as separate controlled episodes if switching tabs invalidates or remounts the load. For terminal cases show each retry milestone and final failure; accelerated copies are supplemental, with original real-time recordings retained. Report the unchanged initial read-only period and independent command timeout honestly.

## Capture count, output layout and pass 2

Use four whole-surface configurations: **1440×900 light, 1440×900 dark, 390×844 light, 390×844 dark**. Same width/height/theme/data/scroll per pair. State actual browser/tool/version, no device emulation (390 is a narrow desktop browser viewport), DPR 1, zoom 100%, font scaling 100%, OS/font set, and clock treatment. Do not advance browser timers to avoid waiting: the actual >6s interaction is the proof. Hide or deterministically mask only unrelated volatile timestamps, identically on both sides, and disclose every mask.

**Minimum logical recording coverage: 4 R rows × 4 configurations = 16 before/after pairs = 32 recorded episodes.** Each episode covers the two Settings consumer paths as reset segments. These may be packaged as **8 video files total**: one before and one after per configuration, each with four labelled chapters R1–R4 and General/Providers subsegments. Packing does not waive a row. Separate clips would be 32 files, with subsegments combined. This is the minimum presentation set for all changed timing/cache exits; do not multiply it by unrelated provider/account fixtures.

Extract full-surface still checkpoints from recordings at matched relative times: initial pending, just after first timeout (e.g. 7s), post-success (e.g. 15s) for R1/R2, and just after 6s plus terminal >150s for R3/R4. Same visible settings/scroll in each paired checkpoint. For each captured pair publish Before | After | Diff-zero | Diff-threshold with full SHAs under columns, viewport/theme/fixture/time in row labels, zero-threshold amplified pixel diff and a second stated threshold (proposed 8/255 maximum per-channel delta). Explain all deltas, including loading placeholder versus provider card, dimming, tooltips, enabled controls and persisted toggle. Do not call pending ordinary Settings editable. Do not claim “nothing else changed” without zero-threshold evidence.

Publicly upload with ghdrop: recordings, checkpoint images, both diff images, exact synthetic fixture JSON, seeding/delay/capture/diff scripts, test patches if uncommitted, sanitized failing→passing transcripts and this matrix. Link scripts from the PR's future Evidence matrix; do not commit PR-only evidence or scripts. No upload may contain startup URLs, bearer tokens, cookies, private local data or secret headers. This audit uploads only this Markdown report; it does not publish to either PR.

Pass 2 checks all R rows against the two Settings render paths, SHAs, fixture reproducibility, endpoint delay and request milestones, full viewports/themes, before/after equivalence, both diff thresholds, and the complete “what else changed” list. Unit-only rows must have actual results linked or remain unverified. Native mobile remains explicitly unverified unless separately run. A moved takeover head invalidates these captures and requires recapture. Missing proof means evidence missing, not merge-ready.

## FRICTION

| Category | Issue | Prevention |
|---|---|---|
| Revision identity | Original PR head and local takeover head differ; supplied GitHub/GitCafe numbers also differ | Label original and takeover separately; capture and test the exact takeover SHA and confirmed merge-base; do not infer number parity |
| Test coverage | 48 green tests do not pin default timing; other-error table tests a different loader; permission tests mock the atom | Add minimal default-schedule and session-specific controls; obtain concrete failing→passing logs and real atom transition tests |
| Observation | Navigation or SWR revalidation can conceal the base's initial failure; warm cache changes the UI | Separate cold/warm episodes, log request starts, retain identical cache/scroll/data, avoid remount during measurement |
| Isolation | Default development seeding would touch live data; auth material could leak through scripts/logs | Explicit --no-seed and /tmp home; synthetic app commands only; publish sanitized scripts/fixtures and redacted network milestones |
| Tool paths | test-t3-app skill is project-local, not under the system skill root; some guessed source filenames do not exist | Resolve skill root from catalog and use rg --files before reads; no browser/device tooling was needed for pass 1 |
| Evidence status | No baseline failure run, recordings, diff images or native session were captured in this audit | Keep all marked unverified; capture owner produces the matrix set and pass 2 audits it before any readiness claim |
