# Proof audit, pass 1 — route sign-in refusal

PR [#33199](https://git.cafe/pingdotgg/t3code/pull/33199), mirrored as [#33199](https://github.com/pingdotgg/t3code/pull/33199). Audited [3cee08825d](https://git.cafe/pingdotgg/t3code/commit/3cee08825d0fdf2d79f0ff56eab4d98582534ed4) against merge-base [bcee0f5d28](https://git.cafe/pingdotgg/t3code/commit/bcee0f5d2872ba193c66ed9ba979bbe424f89ff9).

This is a source-derived capture plan, not captured proof or a functional approval. The PR description was read through GitCafe and contains no capture links. Existing usable captures: **zero**, as required by the brief. Its manual account names older revisions; new evidence must identify the exact base and head above. No repository files were changed, tests run, servers started, devices leased, or forge feedback posted in this pass.

## Render-path inventory

| Component/path | Inputs and branches that matter | Matrix coverage |
| --- | --- | --- |
| Web and desktop entry | The only production caller of `EnvironmentRoutesList` is `SavedBackendListRow` in Settings → Environments, URL `/settings/connections`. It mounts only when `routesOpen` is true. Both the inline route-count button and the environment menu's Routes/Hide routes toggle it. `ConnectionsSettings` renders saved, non-primary, non-desktop-local entries here. Primary and WSL-specific rows are not this component. Settings → Access does not render the list. | R01, S01; C02 |
| Native entry | The only caller of `EnvironmentRoutesSection` is `SettingsEnvironmentDetailRouteScreen`, navigation `SettingsEnvironmentDetail`, linked path `settings/environments/:environmentId`. Settings → Environments and Settings' server controls can navigate to the detail screen. It appears whether the saved environment is connected or disconnected. Missing environment gives the parent “no longer saved” message; missing catalog entry makes the section return null. Add route navigates to `SettingsEnvironmentNew` with `routeFor`. | R01, R05, C02, C03 |
| Route data and order, both | `connectionRoutes(entry)` is `[entry.target, ...alternateRoutes]`: valid entries have at least one route. Omitted and empty alternate arrays are equivalent. Saved order or a valid optimistic pending order selects displayed rows. Pending order clears once saved order matches or the saved ID set changes. Route IDs, rather than indices, select the refusal badge. | S01, S02, R07–R10, C01; H02 |
| Refusal, both | `refusedRouteIds.includes(routeId)` controls the badge. No matching IDs means no refusal output; unrelated/stale IDs render nothing. Every refused route independently renders its badge. Non-null address also renders the hint; null address suppresses both address line and hint. Empty string is non-null and is a malformed-data harness variant, not a normal saved URL. | S01–S07; H01, H02 |
| Active route, both | Web requires `connection.phase === "connected"` and prepared `Some`; mobile requires `connected === true` and prepared `Some`. Otherwise no In use pill. The renderers can draw both pills on one row, but the connected supervisor excludes its active route from `refusedRouteIds`. | S01, R02–R06, C01–C03; H01 |
| Route kinds, both | Loopback → This device; private network → LAN; tailnet hostname or `network: "tailscale"` → Tailscale; shared-address range → VPN; public hostname → hostname; malformed/no bearer profile → Remote link; relay → T3 Connect; SSH → SSH plus optional username and hostname, with alias as address if profile exists. Primary has its HTTP address but is excluded from the saved web caller. Native gives each kind an icon. | S01–S07, C01; H03 |
| Row width and text | Web label/pills are a non-wrapping flex line, address is truncated, hint wraps without a line limit, and controls reserve fixed space. Actual list lives in the parent's `col-span-2 col-start-2` area, narrower than the viewport. Mobile label/address are one line, hint wraps, title can shrink, pill and icon consume width. Both long public hostnames and SSH identities matter. | S03, S06, R08, R09 |
| Counts/removal | Web always shows a drag handle; remove appears only for count > 1 and non-learned route. Mobile Edit/Done appears only for count > 1; normal mode has no controls. Editing adds a remove icon or equal-width placeholder, and a drag handle. Learned rows append “found automatically”, cannot be removed, and can be reordered. First/middle/last positions change accessible move actions. | S02, S04, C01, R07–R10 |
| Web drag | Pointer activation distance 4; keyboard sensor; vertical and parent constraints. Active row gets background/shadow/z-index. Null target or same position is a no-op. Success keeps optimistic order until saved; failure clears pending. Remove opens a destructive confirmation including address when present; cancel does nothing. Button hover/focus, remove tooltip, and keyboard focus also render. | R07, R08, R10 |
| Native drag | Measured row heights drive thresholds and sibling translations. Lifted row background/shadow and 160 ms sibling movement must accommodate the new hint. Drop remounts rows; cancelled drag resets without saving. Accessible Move up/down actions are bounded by first/last position. Reduced motion uses system preference. | R09, R10 |
| Native permissions | Closed/open. Open and active + settled successful authenticated session shows route-context explanation and Allowed/Not granted for every scope. Other branches: no active route → connect instruction; authenticated false → session-rejected message; failed → verification-error message; initial/waiting/null → checking message. These are about the active fallback's session, not the refused route. | R11 |
| Loading/errors | Neither route renderer adds a spinner or its own error panel. Mobile missing presentation defaults refusals to `[]`; missing catalog entry returns null. Presentation initial/failure fallback uses available state and `[]`. Supervisor available/offline/connecting/backoff/blocked publish `[]`; refusal IDs are published while connected. Parent status/diagnosis can still show errors. No-fallback host rejection is therefore distinct from a refused route beside a successful fallback. | R06, C02–C04 |
| Appearance/platform | Web theme tokens color the new badge; native uses `connectionTone("error")` and compact `StatusPill`. Light/dark both matter. Web/desktop share DOM; native is separate. Android uses icon 24, denser padding/type/gaps and different minimum heights for addressed/addressless rows; iOS uses icon 22 and iOS spacing/icons. No new route-list breakpoint exists, but parent responsive layout and text wrapping change available width. | All S rows; R09; I01 |

Source anchors: [web renderer](https://git.cafe/pingdotgg/t3code/blob/3cee08825d0fdf2d79f0ff56eab4d98582534ed4/apps/web/src/components/settings/EnvironmentRoutesList.tsx), [web caller](https://git.cafe/pingdotgg/t3code/blob/3cee08825d0fdf2d79f0ff56eab4d98582534ed4/apps/web/src/components/settings/SavedEnvironmentRows.tsx), [mobile renderer](https://git.cafe/pingdotgg/t3code/blob/3cee08825d0fdf2d79f0ff56eab4d98582534ed4/apps/mobile/src/features/settings/EnvironmentRoutesSection.tsx), [mobile caller](https://git.cafe/pingdotgg/t3code/blob/3cee08825d0fdf2d79f0ff56eab4d98582534ed4/apps/mobile/src/features/settings/SettingsEnvironmentDetailRouteScreen.tsx), [route derivation](https://git.cafe/pingdotgg/t3code/blob/3cee08825d0fdf2d79f0ff56eab4d98582534ed4/packages/client-runtime/src/connection/routes.ts).

## Capture configurations and acceptance

| Code | Viewport × theme | Required use |
| --- | --- | --- |
| W | Web 1440×1000 CSS px, light and dark; web 390×844 CSS px, light and dark | Four matched base/head pairs per S row where W is specified. Capture the actual Settings parent for integrated rows. Harnesses must reproduce its available row width and theme. |
| A | Leased Android Pixel 9, default portrait viewport/density/font scale, light and dark | Two matched base/head pairs per S row where A is specified. Record actual screenshot pixel size, logical viewport, density and font scale in manifest rather than calling a web viewport Android. |
| w | Web 390×844, dark | Default minimum recording for web interactions; use light if needed for readable recording, and label it. W stills prove both themes. |
| a | Same Android as A, dark | Default minimum native recording. |
| X | Desktop Electron at 1440×1000, dark, if available | Optional shared-renderer corroboration, not a substitute for W/A. SSH real entry is desktop-only; its renderer can be captured in a labelled harness on Linux. |

For each mandatory S row, retain both **Before (merge-base)** and **After (head)**, with identical inputs/viewport/theme. Baseline fixtures omit the new field; equivalent injected refusal data must be documented as not representable on base. For harness pairs, use the actual base/head component source, not a recreated component. A screenshot may cover several named fixtures only when each is readable and unambiguously identified. Avoid multiplying unrelated controls across all themes: C/H rows use the smaller configurations stated below. Recordings can be chapters of one video with row IDs and timestamps; still endpoints do not satisfy a recording row.

Keep uncropped integrated screenshots for context; crops/contact sheets can supplement them. Show badge contrast, complete hint, address truncation, controls, and surrounding rows. Recordings must show the initiating action and settled outcome, not only the final badge. Native accessibility proof includes a UI hierarchy/accessibility dump; record TalkBack speech if claiming spoken wording. Strip pairing codes, tokens, startup URLs and unrelated data from published evidence. Attribute publicly as: **AI-assisted; verified with the tests and tools listed above.**

## Static fixture matrix

All mandatory affected rows below are currently **missing**. A row labelled harness proves rendering only; it cannot prove real transport/auth behavior.

| ID / named fixture | Concrete data and expected distinct layout | Diff affected? | Minimum capture | Real seed or labelled harness |
| --- | --- | --- | --- | --- |
| S01 — preferred address refused, fallback active | Two explicit bearer routes `[A,B]`, both with short valid addresses. Connected/prepared B, refusals `[A.id]`. A shows Sign-in refused + complete hint; B alone shows In use. Baseline connects over B with no A warning. | Yes; core bug | W + A, still base/head pairs | Real app, setup P below; revoke A while B remains valid. Browser can use loopback A + VM LAN B; Android uses two VM-reachable origins, never the emulator's localhost. |
| S02 — several refusals, larger list | At least `[A,B,C,D]`, connected D; refusals `[A.id,B.id,C.id]`. Include first/middle/penultimate refused rows and normal D last. Every matching ID has one badge/hint; controls remain aligned; no duplicated IDs. | Yes; list accumulation/count/position | W + A, still base/head pairs; may share frames with S01 if legible | P with four independent paired origins; revoke A/B/C. Synthetic data harness is acceptable for layout if proxies are too costly, but label it. R04 proves real history separately. |
| S03 — long public hostname and address | Refused explicit bearer A has hostname `workstation-with-a-very-long-environment-route-name.example.test`, valid URL with port and long pathname; short B active. New badge shares label line, address truncates, hint wraps. Compare wide versus narrow, including any overflow/clipping rather than hiding it. | Yes; longest ordinary label + hint | W + A, still base/head pairs | Labelled harness using a real BearerConnectionProfile. No need to provision public DNS/TLS. Optional real local reverse proxy with reachable hostname is equivalent if descriptor and session remain H1. |
| S04 — learned refused address | A is a bearer profile with `learned: true`, non-null address, optionally `network: "tailscale"`; refusal A, explicit valid B active. Address has “found automatically”; hint still appears. Web has no remove button for A; Android normal mode, and edit state in R09, retain the placeholder instead of a remove icon. | Yes; learned suffix + taller row + removability | W + A, still base/head pairs | Real learned endpoint from server config if available; it borrows the source credential. To refuse only this learned address while preserving a fallback, keep a separately paired B credential. Otherwise label a harness with `learned: true`; never imply separately revoking its shared token leaves its source route healthy. |
| S05 — addressless T3 Connect refused | Relay A with `profile: None`, ID `relay`, refusal `[relay]`; addressed B active. Badge appears, **no address or re-pair hint**, Add route remains present. Baseline has neither badge nor hint. | Yes; explicit null-address branch | W + A, still base/head pairs | Required labelled relay harness unless an actual T3 Connect account/tunnel can produce a host-side refusal and valid direct fallback. A tunnel-not-enabled/link failure is not this fixture. No physical device or cloud account is needed for the harness. |
| S06 — SSH label/alias | SSH A with profile target including long `username`, `hostname`, and alias; refusal A, B active. Label is `SSH user@hostname`, address is alias, hint present. Also include username-omitted SSH subcase in same contact sheet. | Yes; distinct long identity/alias | W, labelled still pairs; Android representation optional, real desktop X optional | Linux component harness. The web UI's actual SSH entry is desktop capability; do not claim a browser harness proves SSH provisioning or that mobile can create SSH routes. |
| S07 — addressless missing profile | Bearer A `profile: None` → Remote link, or SSH A `profile: None` → SSH; refusal A, B active. No address/hint, badge only; native public/SSH icons differ from relay. | Yes in renderer; defensive, impossible as a new real host refusal with missing local profile | Web 390×844 dark + Android dark, labelled still pairs; both missing-profile variants can share a sheet | Harness only. A missing local profile/credential by itself must not create the refusal flag; this is retained/injected presentation data, not a reproducible host-refusal claim. |

## Interaction and time matrix

Every row in this table is **recording**. The new hint changes geometry even where the underlying interaction was already present. A single video may satisfy several rows with explicit chapters.

| ID / named fixture | Action, states and observable proof | Diff affected? | Minimum capture | Real seed or labelled harness |
| --- | --- | --- | --- | --- |
| R01 — discover routes through every caller | On web, show collapsed saved environment → inline route-count expansion → collapse → menu Routes expansion → Hide routes. Open once by keyboard and retain focus indication. On Android, Settings → Environments → environment detail, then back; also navigate from server controls if that environment is listed there. Refused content appears only inside the route list. | Yes; entry to changed content | w + a recording | P/S01 in real clients. No separate route-list command-palette/keybinding caller was found; use normal Settings navigation. |
| R02 — host revocation and fallback | Start with A active, B healthy. Admin session revokes A only; show disconnect/reconnect and B active with A flagged. Show parent stays usable over B. Include sanitized server rejection and final route IDs/session identities in companion transcript. | Yes; driver records refusal | w + a recording, base/head | P. Revocation substitutes for expiry without altering host clock. Capture this in real app on both clients; DOM/atom injection cannot satisfy it. |
| R03 — refusal discovered by better-route check | A higher-ranked route is initially silent/unreachable; B connects with no flag. Revoke A's session while A is inaccessible. Restore A network path, then trigger application activation/network change, or let the 60-second check run. A becomes flagged **without changing B or connected generation**. | Yes; preflight + reactive presentation | w + a recording | Real P with a capturer-owned per-origin TCP proxy/gate for A; B talks directly to H1. Closing just that proxy makes A silent, reopening it reveals revoked credential. Record driver log or state trace proving unchanged B/generation; do not toggle the saved environment, which changes its connection lifecycle, or re-pair, which replaces the supervisor; neither demonstrates an unchanged connected generation. Native can background/foreground; web can visibility/focus round-trip. |
| R04 — history survives better route switch | Routes `[A,B,C]`, A refused, B initially unreachable, C active. Restore valid B and trigger better-route check. Show C → B In use, A remains flagged after switch, no duplicate flags. Repeat with two refused IDs if combined with S02. | Yes; supervisor lifetime/history regression | w + a recording | P with independent sessions and a gate on B. Direct fallback C is enough; real T3 Connect C optional. Do not use reorder or Add route to trigger this: they can rebuild the supervisor and erase history. |
| R05 — repair via Add route | From S01, open Add route, verify route-target context, cancel once (warning remains), reopen and pair **the same A address** with a fresh code, complete connection, warning disappears and A becomes eligible/In use. Reopen route list to verify no duplicate address row. Include native keyboard-up form state. | Yes; user-visible recovery and reverse state | w + a recording, base/head where the base lacks the initial warning | P; mint a fresh A code for each client/run. UI uses expected environment ID. Pairing a different address is not proof of repairing the refused address. Credentials must be redacted in exported video. |
| R06 — transient phases and reconnection | From known S01, temporarily interrupt the active fallback using a capturer-owned proxy. Show connecting/backoff/offline as applicable with no stale In use pill or published refusal badges, then reconnect the same supervisor to B and recover A warning. Separately toggle environment disabled/enabled and observe fresh connection lifecycle. | Yes; state constructors publish empty list; history survives sessions, not supervisor replacement | w + a recording | Controlled proxy for same-supervisor reconnect; UI switch for disable/enable. Parent “offline” can be seeded in harness rather than disconnecting the VM/browser under test. Disable/enable keeps the registry service, while reorder/catalog replacement and page reload can replace it; show the actual lifecycle instead of using reload as proof of same-supervisor history. |
| R07 — remove/cancel/count transition | S01 or S02: remove refused explicit route, show confirmation address + cancel, then confirm. Refused row disappears and other rows remain correct; after reaching one route, web remove disappears and native Edit entry disappears. If native removal occurs while editing, the local editing flag remains true: capture the remaining handle/placeholder as well as the missing header button; navigate away/back to obtain the normal singleton state. Addressless relay remove confirmation omits address. Learned row never offers Remove. Hover web remove to show tooltip and keyboard focus before activation. | Yes; new tall/badge rows coexist with controls | w + a recording | Real P for explicit rows; labelled S05/S04 harness for relay/learned confirmation if unavailable. UI removes routes, not host sessions. Last-route removal cannot be invoked from these row buttons. |
| R08 — web drag and keyboard reorder | Mixed short healthy, tall refused, learned and addressless rows. Pointer-lift a tall refused row over shorter row; drop and inspect optimistic/saved order and badge identity. Keyboard Space → ArrowDown/Up → Space; Escape/cancel; drop on same/no target. Show drag background/shadow and bounded movement. | Yes; new height/layout affects drag | w recording; wide desktop web segment if narrow cannot expose the full movement | Real S01/S02 for two heights; S04/S05 harness for additional row types. Catalog reorder can rebuild supervisor: obtain fresh refusal after save if needed; verify mapping by ID, not historical retention across replacement. |
| R09 — Android Edit and unequal-height drag | S02/S04/S05 mixture: Edit → show remove icons and learned placeholder + handles → drag tall refused row down/up across short addressless and normal rows → settled order → Done. Capture first/middle/last Move up/down accessibility actions, cancelled drag resetting offsets, and reduced-motion variant. New row height must not overlap/flash during 160 ms transitions. | Yes; largest native geometry change | a recording with edit-mode light still from A as well; accessibility dump | Real P plus genuine learned rows when possible; labelled native component harness for relay/mixed row data. Drive accessibility actions in harness if the device tool cannot invoke them; do not substitute a static dump for move behavior. |
| R10 — pending order/save failure and changed ID set | While refused tall rows exist: reorder, hold save pending, fail it → saved order restored; successful save → no stale pending order. Then add/remove another route at same final count so pending IDs are invalidated. Include no-op/cancel dropping without persistence. | Adjacent behavior; layout affected, save logic unchanged | Web w + native a **labelled harness recording**, one theme | Harness drives real components/atoms with a Deferred command result and real pending/saved updates. Real app UI has no deterministic “fail next local catalog write” switch. Do not fault live storage or advertise a fake RPC failure as real app proof. |
| R11 — Android permissions beside refused route | Open Your permissions with S01: explanation identifies B In use; show mixed Allowed/Not granted from B scopes. Close/reopen. Exercise settled unauthenticated → rejected message; pending/initial/null → checking; AsyncResult Failure → retry message; connected false/prepared None → connect instruction. Refused A must not be presented as B's permissions. | Adjacent branch; same section gains a new subscribed atom and taller rows | a recording; additional state variants may be labelled harness chapters; accessibility dump for S01/S05/S04 | Real P and deliberately restricted B scopes for authenticated Allowed/Not granted. Deterministic pending/failure/rejected result variants via section harness; never relabel auth rejection as a transport success. Assert native accessible label includes route label/address, learned marker if any, In use if any, Sign-in refused, Route N of M, and full hint only for non-null address. |

## Controls, unreachable combinations and documentation

| ID / named fixture | Data / expected branch | Diff affected? | Minimum evidence | Real seed or labelled harness |
| --- | --- | --- | --- | --- |
| C01 — healthy singleton and healthy pair | One valid active explicit route: In use only, no hint/refusal/remove, Android no Edit. Two healthy routes: one In use, web removes available and Android Edit available. Include This device, LAN, Tailscale, VPN, short public as label/icon contact sheet; SSH/relay are S06/S05. | Negative control | One web dark + one Android dark still; can be R02 opening frames | Real P before revocation; label/icon variants without reachable endpoints may be harness. |
| C02 — absent/initial/error presentation | No saved environments → no web route list; removed native environment → parent no-longer-saved; catalog missing → null section; catalog exists but presentation missing or state loading/error → empty refusal IDs. Valid web list cannot have zero routes. Enabled placeholder parent can say Connecting; disabled says Not connected. Include unsupportedReason/unsupported and offline/backoff/blocked parent variants as a compact harness contact sheet: route rows have no active pill or refusal output, while the parent gives the appropriate status/diagnosis. | Defaulting affected; no new visual branch | **Recording** w + a for loading/removal transitions; state failure variants may be harness chapters of R06/R07 | Fresh unseeded isolated app and UI deletion; deterministic null/AsyncResult failure through harness. Do not invent a route-list skeleton/spinner. |
| C03 — one refused route, no usable fallback | Saved singleton A with revoked session, or all saved routes rejected. Parent error/blocked diagnosis visible; no active pill, route refusal list is `[]` because no connected lease exists. Do not expect S01's per-route flag here. Retry/Add route remain available through existing UI. | State publication affected; essential boundary | **Recording** w + a, may be R06/R07 chapter | Real P: revoke A with only A saved, or revoke both A and B from admin Z. Re-pair to recover. |
| C04 — failures that must not create a new warning | Fresh supervisor with fallback B valid; A failures: unreachable/transient, local credential/profile missing (`hop: device`), relay authentication/link failure (`hop: relay`), tunnel-not-enabled (`hop: tunnel`), server clock_skew even if auth/permission. Separate positive server-hop permission refusal must produce same warning as server authentication. No history in negative fixtures; a prior genuine refusal may legitimately persist. | Yes; new classifier's positive/negative boundary | **Recording** driver/presentation harness transitions into S01-style UI in w + a; supplemental focused runtime/test transcript for every classifier case | Real gate handles unreachable; others are labelled typed-driver harness failures: `ConnectionBlockedError` with explicit hop/reason/failureCause. Permission mapping may be induced by a scoped host session if the actual failing request is verified; otherwise harness. Do not change VM clock just to synthesize clock_skew. |
| H01 — same row both badges | `connected`, prepared A and `refusedRouteIds:[A.id]` injected together. Renderer draws In use plus Sign-in refused. | Defensive render path, unreachable from valid connected supervisor | Optional labelled W narrow + A dark still; not a release gate | Harness only; `mergeRefusedRoutes` filters active route on successful connect. Do not claim a real double-badge state is expected. |
| H02 — malformed singleton/IDs | Singleton + injected refused ID gives badge/hint but no remove/Edit. Unknown refused ID gives nothing. Duplicate IDs in refused array still produce one badge per row. Empty-string address is non-null; unsupported malformed URLs/profile shapes are harness robustness, not normal seed states. | Defensive renderer path | Optional labelled contact sheet | Harness; never call this a real connected singleton refusal. S07 handles the meaningful null-profile shape. |
| H03 — primary row renderer data | PrimaryConnectionTarget with HTTP address has This device/LAN/etc label derivation if directly rendered; both changed renderers accept it structurally. | Not reached through saved web caller | Optional harness only | Production web saved list filters primary; no extra real capture required. Desktop local WSL rows likewise do not use this changed list. |
| I01 — iOS | S01 and S05 under iOS spacing/type/icons, R09 editing/drag. | Same native component, platform styling differs | Optional iOS pool stills/recording; explicitly state untested if omitted | Leased iOS simulator, same isolated backend reachable over tailnet. Android remains mandatory; iOS cannot replace it. |
| D01 — guidance | Remote-access route paragraph adds “Sign-in refused” and Add route remedy. Existing surrounding guidance gives web/desktop route-count entry and mobile detail entry. Text matches addressed rows; S05 deliberately has no inline hint. | Yes | Source inspection completed; no separate rendered docs screenshot required | [Changed user guide](https://git.cafe/pingdotgg/t3code/blob/3cee08825d0fdf2d79f0ff56eab4d98582534ed4/docs/user/remote-access.md). Recovery claim is proved by R05, not by this prose. |

## P — concrete Linux-only real-app setup

1. The capturer uses its assigned scratch directory for every state directory, log, proxy and capture, and a new timestamped run directory each time. Reuse the lead's appropriate healthy dev server or start an isolated one with `T3CODE_DEV_SEED=0 vp run dev --host 0.0.0.0 --home-dir <capture-run>/shell`. Read the actual ports/startup pairing details. No baked VITE origins; no live install database. For base/head comparison use the lead's matched checkouts, not a revision guessed from the PR's historical manual checks. Use this fixture-only empty state deliberately: it prevents unrelated personal threads or provider details from appearing in evidence; no agent turn is needed for route Settings.
2. **Use a second environment H1, not the primary that serves the web app.** The web caller excludes the primary and registry registration ignores platform-owned environments. A second local host needs no physical device: `node apps/server/src/bin.ts serve --base-dir <capture-run>/host --host 0.0.0.0 --port <free-host-port>`. Keep PID/terminal session. H1 is one descriptor/environment ID through every route. The first server is H0, the web shell; H1 is the saved target. Ports are chosen after checking occupancy. Both are this capturer's isolated instances.
3. For browser, pair H1 route A at `http://127.0.0.1:<host-port>/` and route B at H1's VM-reachable LAN/tailnet origin. The initial Add environment creates H1; use **H1 → Routes → Add route** for B to pass the expected-environment check. If the app shell uses HTTPS, choose HTTPS reachable routes rather than relying on mixed-content HTTP. In a local HTTP shell, loopback + LAN is sufficient.
4. For Android, A and B must both be VM-reachable. Use two origins to H1, e.g. VM tailnet IP at `<host-port>` and at a capturer-owned TCP forwarder's `<alternate-port>` forwarding to the same H1. A TCP forwarder must preserve HTTP paths/Host/Authorization and WS upgrades. Bind it to an interface the leased emulator can reach. A third proxy supplies C for R04. Different ports produce independent bearer route IDs while the descriptor ID stays H1. Before revoking anything, inspect actual saved/learned routes and use UI reorder so A precedes B (and C for R04), with any unrelated learned routes after the intended fallback. Adding a same-kind route can place it first, so pairing order alone is not proof of preference. Pairing an address that was already learned makes it an explicit independently credentialed route. Record actual route IDs from the catalog, not the illustrative letters used in this matrix. A raw tailnet IP can be labelled VPN unless route metadata marks Tailscale; use the actual displayed label. Emulator loopback and `10.0.2.2` do not point to this VM from a remote pool host.
5. Mint a **different one-time code for every route on every client**. Example: `node apps/server/src/bin.ts pair --base-dir <capture-run>/host --label audit-web-A`, then another labelled audit-web-B (and audit-android-A/B). Enter host/address and code separately so a code minted with a default loopback URL is redeemed at the intended address. Do not reuse the code or copy a route's bearer token onto the fallback. Pairing onboarding issues a separate bearer session per redemption and stores it under an origin-based route ID. The `--ttl` flag on `pair` is the **bootstrap-code** lifetime, not the resulting session's lifetime; making that code expire does not produce this bug.
6. Retain a third, independent **admin session Z** into H1. Use a second browser context on H0 and pair H1 with a dedicated Z-labelled code, or use a working startup admin pairing session on H1. It must have `access:read` and `access:write`. A second browser context avoids overwriting either route credential in the client under test. On web open `/settings/access`, select H1 in “Showing access for”, expand Authorized clients. Match exact A/B labels (or snapshot `client.label` and `sessionId`) before revoking. Z must not be A or B. UI's Revoke action is sufficient. Do **not** use Revoke others: it would invalidate the fallback too. Do not revoke pairing links: that does not revoke an already issued route session.
7. The exact app command is `authEnvironment.revokeClient({environmentId: H1, input:{sessionId: A_session_id}})` through the existing atom command/connected HTTP client. Wire endpoint is `POST /api/auth/clients/revoke`, payload `{"sessionId":"<A-session-id>"}`, using **Z's** authenticated headers/session (and required proof/CSRF headers for its auth method). An existing app driver can read `accessChanges` (backed by `WS_METHODS.subscribeAuthAccess`, wire method `subscribeAuthAccess`) to map labels to session IDs, then call that command. A raw unauthenticated fetch is not a seed recipe. The service refuses self-revocation; this is why Z is separate.
8. Revocation persists `revoked_at`, removes the session's connected tracking, and emits removal, invalidating the old route credential without changing any clock. Keep B and Z intact. Observe H1's sanitized `Rejected authenticated session credential` log and B In use. If the client does not reconnect immediately, exercise its Retry/reconnect or disable-enable once for R02; record the action used. For R03/R04 preserve the supervisor and use the proxy gate plus app activation/network check instead. Real expiry is optional and must be process-scoped if attempted; changing shared VM time is unnecessary.
9. Pair A again through **Add route** with a fresh A-labelled code. Same-address onboarding replaces the credential, and rebuilding the supervisor clears its refusal memory. Capture removal of A's warning and successful A connection. An ordinary reload is not equivalent recovery proof because it also loses in-memory history.

Supporting code: [session revocation HTTP handler](https://git.cafe/pingdotgg/t3code/blob/3cee08825d0fdf2d79f0ff56eab4d98582534ed4/apps/server/src/auth/http.ts#L488), [self-revocation guard](https://git.cafe/pingdotgg/t3code/blob/3cee08825d0fdf2d79f0ff56eab4d98582534ed4/apps/server/src/auth/EnvironmentAuth.ts#L1203), [session store revocation](https://git.cafe/pingdotgg/t3code/blob/3cee08825d0fdf2d79f0ff56eab4d98582534ed4/apps/server/src/auth/SessionStore.ts#L987), [client auth commands](https://git.cafe/pingdotgg/t3code/blob/3cee08825d0fdf2d79f0ff56eab4d98582534ed4/packages/client-runtime/src/state/auth.ts), [route pairing/onboarding](https://git.cafe/pingdotgg/t3code/blob/3cee08825d0fdf2d79f0ff56eab4d98582534ed4/packages/client-runtime/src/connection/onboarding.ts).

## Native capture and labelled-harness requirements

The capturer must use the Android **device pool**: prepare this checkout's dev client with `vp run mobile:install android --prepare`, connect a new named `agent-device` proxy session using the configured pool URL, then `agent-device open --platform android --device "Pixel 9" --session <capture-session>`. Install via `vp run mobile:install android --session <capture-session> --metro <VM-reachable-Metro-URL>`. Start/reuse this checkout's Metro with `vp run dev:client` from `apps/mobile`; keep its tracked session. Use that same pool session for all device commands, app opening, pairing, screenshots and recordings. Pair helper can bootstrap H0, but H1 A/B belong in the app's route form with fresh independent codes. Use VM tailnet origins for Metro and H1; no physical Android hardware is needed. Remove disposable connections, close the named device session to return the lease, and stop only capturer-owned servers/proxies/Metro at teardown.

Harnesses must mount the **actual changed component** with its UI/theme/navigation providers. Web's component props are `environment` and `onAddRoute`; set its prepared-connection hook/atom separately to match phase. Existing browser test always mocks prepared to None and sets phase available, so its current refusal assertion does **not** prove the real connected fallback combination. Native's props are only `environmentId`, `connected`, `onAddRoute`; route data, presentation/refusal IDs, prepared connection and session AsyncResult all come from atoms and must be seeded coherently. An HTML approximation or a web screenshot styled like Android is not native evidence. Put harness drivers outside the tracked worktree, publish their fixture inputs with captures, and visibly label every harness artifact.

Backend proof and visual proof serve different claims. Source tests already cover initial refusal, non-host tunnel/clock-skew exclusions, late preflight refusal, preservation across better-route switch, and presentation update with unchanged connection. They were inspected, not rerun in this audit. A capturer should attach focused test/runtime results where used; screenshots alone cannot prove generation guards or error classification. [Supervisor test cases](https://git.cafe/pingdotgg/t3code/blob/3cee08825d0fdf2d79f0ff56eab4d98582534ed4/packages/client-runtime/src/connection/supervisor.test.ts#L1688), [reactive presentation test](https://git.cafe/pingdotgg/t3code/blob/3cee08825d0fdf2d79f0ff56eab4d98582534ed4/packages/client-runtime/src/state/presentation.test.ts#L140), [current web renderer test](https://git.cafe/pingdotgg/t3code/blob/3cee08825d0fdf2d79f0ff56eab4d98582534ed4/apps/web/src/components/settings/EnvironmentRoutesList.browser.test.tsx#L98).

## Pass 1 disposition

Required affected static evidence: **S01–S07** (S06 web/desktop renderer only; S07 defensive harness). Required recording coverage: **R01–R11**; adjacent cases can be chapters, and R10/R11 failure variants can be labelled harnesses. Required negative/control coverage: **C01–C04**, using the reduced configurations above. H01–H03 and iOS I01 are optional and must not be represented as production behavior that was observed. D01 source inspection is complete; R05 remains the missing proof of the documented recovery.

No captures were supplied; every required visual row remains unproven. Missing mandatory IDs: S01–S07, R01–R11, C01–C04. This verdict concerns evidence sufficiency, not a claim that the implementation fails.

AI-assisted; verified with the tests and tools listed above.
