# Proof audit, pass 1 — Android native network policy

Target: [#25730](https://github.com/pingdotgg/t3code/pull/25730), with the supplied GitCafe counterpart [#25731](https://git.cafe/pingdotgg/t3code/pull/25731). This is a proof plan for the local, not-yet-pushed revision, not a review of either forge's currently published head.

Reviewed checkout: `/home/ubuntu/wt/pr-25730`, branch `pr-25730` (intended branch `fix/issue-5639`). Head: [bbb9042e92](https://git.cafe/pingdotgg/t3code/commit/bbb9042e92b21c2341a392677bbb9715468359d7), full SHA `bbb9042e92b21c2341a392677bbb9715468359d7` (local; its forge link may not resolve before push). Merge-base with local `origin/main`: [71ba6b374c](https://git.cafe/pingdotgg/t3code/commit/71ba6b374c0b0181130c0cc3b3d5aaa3387ada67), full SHA `71ba6b374c0b0181130c0cc3b3d5aaa3387ada67`. Capture date, native artifact fingerprint, Android version, and tested source SHA must accompany each recording.

## Findings first

1. **Proof is missing, not contradicted.** No captures exist in pass 1. Source inspection supports five rows below. The older PR body's fixture claims do not count as evidence for this revision. No confirmed implementation defect was identified within this fixture-planning scope.
2. **Pairing success is insufficient.** Pairing first fetches the environment descriptor and exchanges the pairing credential over HTTP(S). Registration then starts a separate connection attempt that obtains a WebSocket ticket over HTTP(S), opens WS(S), and synchronizes server state. A dismissed Add environment form does not prove WSS. A visible cached project does not prove a live session.
3. **The change is native and app-wide.** The plugin is unconditionally included for Android, including development, preview, and production variants. It writes a main-source-set XML resource with system and user anchors and cleartext enabled. Metro reloads alone do not apply it. Inspect the installed APK's merged manifest and resolved resource; record any debug/resource overlay that would make development behavior differ from release. Source shows no debug-only trust exception in this plugin. No additional production-device video is required if artifact inspection confirms the same effective policy.
4. **The preservation rows are justified by changed policy.** The previous plugin explicitly permitted cleartext; replacing it with an XML policy risks losing HTTP/WS support. Explicitly declaring anchors risks losing default system roots. The removed-CA row checks that adding user roots still requires a trusted chain. These are bounded regression checks, not a general TLS/security audit.

Android's [network security configuration documentation](https://developer.android.com/privacy-and-security/security-config) confirms that a manifest reference selects the resource, `base-config` supplies app-wide defaults, `system` and `user` name distinct trust stores, and cleartext is separately controlled. The expected outcomes below are deductions from that policy plus the actual mobile transport wiring; they are not observed results.

## Minimum capture matrix

**Viewport × theme for every row: one Android Pixel 9 phone, portrait, light theme, default text size.** One phone/light combination is enough because no React/UI/layout code changed. All five rows are **recording**: each depends on a connection interaction, asynchronous network outcome, or reconnection over time. Stills may supplement recordings, but cannot replace them. Five scenario recordings are the minimum; U1 can contain both initial connection and the reconnect segment.

| ID / named fixture | Native revision and necessary data | Diff relationship | Real-app seed / driver | Required observed sequence and acceptance | Capture | Status |
| --- | --- | --- | --- | --- | --- | --- |
| B1 — Before: installed user CA still rejected | Merge-base native APK; private root A installed **only in Android user store**; valid leaf signed by A, matching SAN and dates; reachable HTTPS proxy to isolated T3 server; fresh pairing credential | Before/control for the changed user-anchor behavior | Open the actual Add environment route; enter explicit `https://<private-host>:<port>` and fresh code, or use the dev pairing helper. Keep server, endpoint, leaf and user-store installation equivalent to U1. A Chrome request to the descriptor endpoint succeeding without a warning is a useful setup control, not substitute T3 evidence. | Record submit → actual mobile network/pairing failure; no successful environment synchronization. Pairing should fail at HTTPS descriptor fetch before registration/WSS. Correlate with native TLS/chain failure, not an expired code, DNS failure, refusal, or incompatible server. If base unexpectedly connects, investigate APK/debug trust policy; do not call the bug reproduced. | recording; phone portrait × light | MISSING — base reproduction not captured |
| U1 — After: user CA HTTPS, WSS, read, reconnect | Exact head native APK; same user-store root A and valid private endpoint; fresh code; one recognizable synthetic project, ideally one stopped thread; saved bearer route only, no alternate relay/public route | Directly affected: `certificates src="user"`; covers fetch and native WebSocket users plus renewed handshakes | Pair through Add environment using private HTTPS origin. Use isolated real backend data via normal app/RPC operations. Leave the resulting saved environment enabled. Stop accepting/destroy existing sockets in the disposable TLS proxy, then restore that same proxy and certificate. | Record HTTPS pairing → environment **Connected** → server-backed project/thread read; require proxy/native transcript showing HTTPS requests and a `101` WebSocket upgrade at private endpoint `/ws`. While recording, deliberately close the existing socket (do not merely background a healthy app), show disconnection/retry, restore proxy, then show automatic recovery and a new server-backed read. Transcript must show a **new** ticket request/upgrade, so an uninterrupted socket or cached screen cannot pass. | recording; phone portrait × light; initial and reconnect segments required | MISSING — head user-CA flow and reconnect not captured |
| N1 — After: same private endpoint rejected without its CA | Exact head APK and same endpoint/leaf as U1; root A removed from Android user store and never in system store; valid saved credential from U1 (or fresh code) | Affected trust-boundary regression control: user store is consulted, trust validation remains required | Remove A through Android Settings; relaunch the app to discard existing TLS connections/trust caches; retry the saved private environment, or pair afresh. Preserve endpoint reachability and valid credentials. Ensure no alternate route can connect. | Record CA absence/removal and fresh connection attempt → network/TLS rejection, no fresh live read/connection. Correlate with chain/trust error. Existing cached content may remain and is not a failure of this row. A failed HTTPS preflight means WSS may never be attempted; do not mislabel that as an independently exercised WSS rejection. | recording; phone portrait × light | MISSING — untrusted-chain control not captured |
| S1 — After: system/public CA HTTPS still works | Exact head APK; actual T3 endpoint behind valid public/system-trusted leaf and full chain; matching hostname; private root A absent; fresh code; recognizable synthetic project | Affected preservation: explicit `certificates src="system"` replaces implicit platform trust policy | Pair the real app against a controlled, publicly certified T3 proxy/server; it may still be reachable only over tailnet if DNS/SAN/certificate are valid. Reuse the same isolated backend through another TLS listener where feasible. A visit to an unrelated public website cannot seed this row. | Record HTTPS pairing → Connected → live project/thread read; include HTTPS and WSS upgrade transcript. This tests system trust without accidentally relying on installed root A. No separate outage/reconnect repetition is necessary: U1 covers that lifecycle with the changed trust source. | recording; phone portrait × light | MISSING — system-root regression not captured |
| C1 — After: cleartext HTTP and WS still work | Exact head APK; direct `http://<VM-tailnet-IP>:<actual-port>` to isolated T3 backend; fresh code; recognizable project; no alternate TLS/relay route | Affected preservation: old `usesCleartextTraffic=true` replaced by XML `cleartextTrafficPermitted=true` | Pair Add environment using explicit HTTP origin (IP with no scheme also selects HTTP, but explicit scheme removes ambiguity). Use backend's actual dev-runner port and reachable interface, not device localhost. | Record HTTP pairing → Connected → live project/thread read; transcript must show plain HTTP descriptor/token/ticket requests and a plain WS `101` upgrade. Verify there is no redirect to TLS. No CA is needed. | recording; phone portrait × light | MISSING — HTTP/WS regression not captured |

## Code paths and fixture collapse

There are no changed rendered components or props. The changed units are [app.config.ts](https://git.cafe/pingdotgg/t3code/blob/bbb9042e92b21c2341a392677bbb9715468359d7/apps/mobile/app.config.ts), the replacement [network policy plugin](https://git.cafe/pingdotgg/t3code/blob/bbb9042e92b21c2341a392677bbb9715468359d7/apps/mobile/plugins/withAndroidNetworkSecurityConfig.cjs), the deleted cleartext plugin, and one paragraph in [remote-access.md](https://git.cafe/pingdotgg/t3code/blob/bbb9042e92b21c2341a392677bbb9715468359d7/docs/user/remote-access.md). These head source links may resolve only after push; the local checkout is authoritative for pass 1.

- **Build-policy callers and branches:** Expo's plugin list calls the replacement once, unconditionally; Android manifest mod assigns the application resource reference, creating its attribute object if absent; a missing application element throws at build time. Android dangerous mod creates `app/src/main/res/xml` recursively and writes the XML. There are no hostname, certificate-count, theme, width, app-variant, or runtime network-state conditionals in this policy. The missing-application branch is build failure, not a visual fixture. iOS mods are not registered.
- **Pairing entry points:** `Stack.tsx` maps both `ConnectionsNew` (`connections/new`, form sheet) and `SettingsEnvironmentNew` (`settings/environment-new`) to `ConnectionsNewRouteScreen`. Callers are the Connections list's add action, Settings → Environments → add, New Task's missing-environment actions, and Add Project's add-environment action. Address/code entry and QR scan converge on `buildPairingUrl` → `connectAndClose` → `useRemoteConnections.onConnectPress` → `useConnectionController.connectPairingUrl` → mobile onboarding command → shared `ConnectionOnboarding.registerPairing`. Dev-only `pairingUrl` + `autoConnect` route params call the same submit function. `routeFor` checks the expected environment identity. Camera permissions, invalid QR, empty address/disabled submit, empty code, scanner lock, theme, sheet versus page, and success navigation branches do not alter transport policy; one submission entry point covers them for this diff.
- **HTTP(S) discovery and bootstrap:** shared `connection/onboarding.ts:103–170` fetches `/.well-known/t3/environment`, validates expected identity/protocol, calls `bootstrapRemoteBearerSession` (`/oauth/token`), then saves a bearer profile with HTTP and WS origins. It is descriptor discovery, not separate LAN/mDNS discovery. IP literals without scheme use HTTP; DNS names without scheme use HTTPS. Explicit HTTPS is required for a TLS IP endpoint. Pairing failure leaves the form with its error; successful registration dismisses it, without awaiting full WSS session readiness.
- **Session and read:** mobile `lib/runtime.ts` supplies global `fetch` via `layerRemoteHttpClient(fetch)` and the global WebSocket constructor. Shared bearer resolver → `authorization/service.ts` → descriptor validation and `resolveRemoteWebSocketConnectionUrl` requests `/api/auth/websocket-ticket/rpc` (legacy bare ticket path fallback) and builds `/ws`. `connection/driver.ts` advances preparing → opening → synchronizing and awaits `session.ready`. `rpc/session.ts:163+` creates the native WebSocket and subscribes to server config; fresh project/thread data demonstrates RPC/session synchronization. U1, S1, C1 therefore exercise both native network APIs.
- **Reconnect callers and state branches:** the shared supervisor handles socket close/probe failure, automatic backoff, Retry now, enable/disable, persisted startup, network changes, and app foreground wakeups. Mobile `connection/platform.ts` supplies network/AppState wakeups. Settings environment editing preserves the bearer credential while replacing the HTTP origin and deriving WS origin, then uses the same driver. Presentation maps available/offline/connecting/connected/backoff/blocked into Available, Offline, Connecting, Connected, Reconnecting, Error, or Unsupported. Failed initial pairing may never create a saved row; later failures show retry/error state and can leave cached data visible.
- **Visible state callers:** `LocalEnvironmentList` is used by Connections and Settings Environments; `ConnectionEnvironmentRow` also appears in Settings Environment Detail, with collapsed/detail status, diagnosis, attempt, error/trace, enabled/off switch, label/URL, edit/save, retry and remove branches. `EnvironmentConnectionNotice` is used in review, terminal, and file-preview errors. Home presents workspace connection status. Count/empty-list branches, URL/label lengths, expanded rows, content loading/empty states, theme/width, and resource-specific error text are unchanged. These are views of the same connection result, not additional trust-policy fixtures.
- **Collapse rationale:** U1 contains the only new successful trust source plus its fresh connection on reconnect. B1 is its before control; N1 its untrusted-chain control; S1 and C1 protect the two other rewritten policy branches. Automatic and manual retry use the same resolver/native transports; no separate matrix for every wakeup, navigation surface, provider, count, theme, width, or platform is justified. iOS, web, desktop and provider adapters are unaffected. Cloud/relay/DPoP traffic still uses Android's policy, but does not introduce a distinct anchor/protocol combination beyond S1 for this change. Keep fixture environments bearer-only so fallback cannot hide private-endpoint failure. No expired-certificate, wrong-hostname, standalone leaf, or multi-environment matrix is demanded by this diff.

## Practical seeding and evidence handoff

1. **Use the real app for all five rows.** No row intrinsically needs a harness. Capturer leases one Pixel 9 through the shared Android pool (`android-01..04`) using one `agent-device --session <s>` session. Get the correct native artifact with `vp run mobile:install android --session <s>` after the lead's existing `--prepare` build completes; never stop or switch the lead's checkout during that build. Base needs a separate provenance-checked base native artifact, not a Metro checkout switch inside the head APK. Do not use Expo Go as proof.
2. **Server/data:** reuse the lead's healthy isolated backend or start `T3CODE_DEV_SEED=0 vp run dev` in the worktree when authorized. Read the actual backend port. Metro and backend must be reachable from the device over tailnet. Do not point at live T3 userdata. Create one synthetic repository/project through normal Add Project and optionally one stopped thread through supported app/RPC operations, with a distinct label such as `Android CA proof`. A fresh server-side create/rename via normal authenticated RPC, observed in the mobile app, is a useful canary for live read/reconnect. No provider turn or rich chat fixture is needed. Disable showcase/mock environment overrides.
3. **Private endpoint:** generate a disposable root CA and leaf with OpenSSL; CA must be a valid CA and leaf must have server-auth usage, correct SAN for the exact tailnet IP/DNS name, valid dates and full needed chain. Keep the CA private key and server key private. Put a TLS-terminating proxy in front of the backend that forwards all HTTP paths **and WebSocket upgrades**, preserves usable host/origin semantics, and supports explicitly closing existing test sockets. Point pairing at the proxy origin; confirm the saved HTTP and WSS origins both use it. A plain TCP TLS terminator is also feasible if it forwards the full upgraded stream.
4. **Install A in user store:** expected feasible on the pooled Android emulator via the Settings CA certificate installer. If leased-host ADB access is available, push only the public `.crt` to Downloads, then navigate Settings → Security/privacy → encryption/credentials → install CA certificate (exact API 36 labels may differ). Otherwise serve only the public certificate over a disposable HTTP download endpoint on this VM, download it using emulator Chrome driven by `agent-device`, and select it in the system file picker. A device PIN/confirmation may be needed. `agent-device settings` has no documented CA-import option; do not invent one or assume local `adb` can address a remote pooled emulator. Check the user certificate listing; do not install into the system store. Removal for N1 uses the same Settings credential UI. Feasibility is planned, not verified by this audit.
5. **Pair via existing route:** normal Settings → Environments → add supports every row. In the development client, `.agents/skills/test-t3-mobile/scripts/pair-client.sh <backend-port> <isolated-base-dir> <device-reachable-origin> agent-device --session <s>` mints a fresh credential for that origin and opens `t3code-dev://connections/new?pairingUrl=<encoded>&autoConnect=1`. Use the private/public proxy origin for TLS rows, HTTP backend origin for C1. A fresh code is needed after a successful redemption. Avoid an expired/consumed code becoming the apparent negative result.
6. **Evidence content:** label Before/After, row ID, native revision/fingerprint and endpoint type. Retain real-client recordings plus a redacted transcript of TLS/HTTP/WS milestones. Proxy logs should retain method/path/status and connection generations, not tokens, cookies, pairing fragments, Authorization headers, DPoP proofs, or full ticket-bearing WS query strings. Mask credentials from recordings as well. Record installed APK manifest/resource policy and Android user-store condition so source/native mismatch cannot masquerade as a fix.
7. **If setup cannot reach a row:** report the exact blocker and mark the row missing. A custom native networking probe may be supplied as **labelled harness capture**, but it does not prove real pairing/WSS/reconnect. A curl request, browser success, static XML inspection, mocked connection status, or unrelated public HTTPS page cannot replace the required real-app row. S1 requires a controlled publicly certified T3 endpoint; do not silently replace it with a second private CA. No runtime, device, proxy, certificate installation, or capture was performed in this pass.

Coverage: complete diff against the recorded merge-base; Android build policy; mobile pairing entry points and form outcomes; shared descriptor/bootstrap/ticket/session/read/reconnect paths; visible connection-status surfaces; practical pool/build/certificate/proxy seeding. No unrelated PR, changes, pushes, forge comments, browser session, or build interruption. Completed within the 60-minute budget.

FRICTION: none

VERDICT: INSUFFICIENT B1, U1, N1, S1, C1 — all five required recordings are missing
