# Proof audit, pass 1 — EAS Simulator admission

PR [#29490](https://git.cafe/pingdotgg/t3code/pull/29490), also mirrored as [#29490](https://github.com/pingdotgg/t3code/pull/29490).

Reviewed local branch `pr-29490` in `/home/ubuntu/wt/pr-29490`, head `f6557992585aa5e711cc5b6eb9b4d14e0610619a`, against merge-base [8657eecb28](https://git.cafe/pingdotgg/t3code/commit/8657eecb28bc8c767510cc6b9d852b628047c604). The local head is unpushed; its identifier here is provenance, not a claim that the forge can resolve it. Remote branch: `fix/eas-simulator-trust-expo-admission`; target: `main`. Do not use local `main` for the comparison.

## Findings first

1. **Proof is not yet sufficient.** No capture or runtime transcript was supplied for this pass. Required: R1, R2, R3 below, plus test receipt T1. Existing test code establishes fixture feasibility, not execution evidence. No concrete implementation defect was found in the scoped diff and call-path inspection.
2. **This is server-only evidence.** All eight changed files are server host/API code, GraphQL documents/config/generated schemas, or tests. No React component, client contract, layout prop, route, styling, or client renderer changes. The repository's applicable standard is “the test plus a short runtime transcript.” No screenshots or UI videos are required; runtime rows marked **recording** require an ordered textual transcript, not a still or a fabricated expected-output document.
3. **A labelled fake-HTTP harness is sufficient; a live `api.expo.dev` run is not required.** Run the real `EasDeviceHost.make` and real `makeExpoApi`/GraphQL client with an injected `HttpClient`, real isolated ledger files, and scripted Expo/session-preview HTTP responses. This proves T3's decision and cleanup, not Expo's current admin policy. An admin-only gate-off admission cannot be reproduced with a non-admin account, and a live session bills the account. The PR description's author-reported live runs are context, not reviewed captures for this local head. Do not start paid sessions merely to meet this matrix.
4. **Baseline must fail for the correct reason.** In R1, the fake must answer the old availability query with a valid owner account and `{"device-run-sessions":false}`. A baseline “unexpected response” caused by an unimplemented fake query does not prove the regression. The new host test's fake now answers that old query correctly.
5. **Credential availability remains unconfirmed.** `/home/ubuntu/.expo` exists; its contents were not inspected. `eas` was absent from normal PATH, the Bun/vite-plus/local-bin augmented PATH, and this checkout's `node_modules/.bin`; `eas whoami` could not run. No credentials were read and no sessions were started. This does not block the fake-HTTP proof.
6. **Remote checks are stale for this task.** GitCafe reports the pushed head [287a70d250](https://git.cafe/pingdotgg/t3code/commit/287a70d250b1a439050413b7df89ea08ddddc21a), with 30 successful checks and no reviews; its bot comment covers that older revision. These do not establish verification of local `f6557992585aa5e711cc5b6eb9b4d14e0610619a`.

## Required matrix

Minimum visual capture set `(viewport × theme × row)`: **empty**. For every required row, viewport = N/A and theme = N/A. Minimum behavioral capture set: **three labelled runtime harness recordings, R1–R3**, which may be sections of one transcript, plus **T1** test receipt. Do not expand these into desktop/web/mobile, light/dark, or width combinations.

| ID / named fixture | Diff affected? | Data and sequence required | Expected observable evidence | Format / minimum variants | Seeding / driver route | Status |
| --- | --- | --- | --- | --- | --- | --- |
| T1 — focused regression and related tests | Yes | Run the new gate-off admission test with the same valid gate-off fake against the base and head; run head's `EasDeviceHost.test.ts` and `ExpoApi.test.ts`. | Base regression test fails because the owner gate pre-check refuses; head regression and the two focused files pass. Include exact SHAs, commands, test names/counts, and exit codes. No broad baseline suite failure or mock decoding failure as substitute. | Text test receipt; no viewport/theme; not a UI capture. | Existing test `leaves admission to Expo's create although the account's gate is off`; existing refused-record/restart and refused-orphan tests. Head command: `vp test run apps/server/src/device/EasDeviceHost.test.ts apps/server/src/device/ExpoApi.test.ts`. Capturer/lead must keep any replay tooling and outputs in its authorized scratch area and avoid changing the reviewed worktree. | **PENDING receipt** — source inspected, not executed in this audit. |
| R1 — gate off, create admits: base → head | **Yes: primary regression** | Same synthetic bearer credential, host label `EAS Simulator`, valid app UUID, empty isolated ledger, available slot, iOS catalog device. Old query returns owner `fixture-owner`, gate `false`. Create succeeds with `session-1`; session becomes `IN_PROGRESS` with valid agent and preview endpoints; preview `/api` returns a device ID. Close host scope after readiness. | **Base:** availability query occurs; boot error reason is `EAS Simulator is not enabled for the fixture-owner Expo account.`; zero creates and no session ledger entry. **Head:** zero availability queries; ledger entry exists **before** create; exactly one create with expected app/platform/limits; boot returns `session-1`, running device is listed; scope close stops that owned session and clears its ledger entry. This makes the successful fixture leave no session behind. | **recording**, ordered before/after runtime transcript. `(N/A × N/A × R1)`, two revisions; one platform is sufficient. | **Labelled harness capture:** call real `make(config)` → `ensureReady` → `devices.list` → `devices.boot(catalog ios)` → list → close scope. Inject fake HTTP only, not a fake `ExpoApi` or fake host. See existing host test setup. The real UI equivalent is a thread's Device panel → configured EAS host → iOS catalog entry, or `device.open` RPC / MCP `device_open`; a UI route cannot seed the admin admission response deterministically. | **MISSING capture** |
| R2 — create refuses, no matching session: refused record frees capacity | **Yes: newly reached with gate off** | Head, same app/credential and gate-off-capable fake, `maxConcurrentSessions:1`. Create replies HTTP 200 GraphQL `errors:[{message:"Device run sessions are not enabled for this account.",extensions:{errorType:"USER"}}]`, `data:null`. Active list has no matching session name. Capture ledger immediately after failed boot, before closing scope. Close and recreate host against the same ledger; let later list fail and later create succeed. | Returned message exactly `EAS Simulator is unavailable: Expo could not start a simulator session: Device run sessions are not enabled for this account.`; one create, zero availability queries; matching ledger entry has `refused:true`, no tracked running device, no stop of an unrelated session. After restart the refused record does **not** consume the sole slot even if active listing fails; second host reaches successful create/readiness. Close it and show its session stopped. The refused record may legitimately remain for the reaper; do not assert immediate deletion or use that as a failure. | **recording**, ordered runtime transcript including ledger and restart. `(N/A × N/A × R2)`. | **Labelled harness capture:** `fakeExpo({rejectCreate:true,omitCreatedFromActive:true})` shape and existing `keeps a refused create's record for the reaper without counting it after a restart` sequence; add transcript observations of the exact error and ledger. New host reuses only isolated fixture state. No real account or live DB. | **MISSING capture** |
| R3 — create refuses, but a matching orphan exists: stop and forget | **Yes: refusal response/message fixture changed** | Head, same USER/null-data refusal as R2. Active-session response contains one `NEW` session whose name exactly equals the just-submitted create name, plus an unrelated session with a different name. Stop of matching session succeeds. | Same exact wrapped Expo refusal message as R2; create attempted once, no availability query; active list finds the matching orphan; stop targets its ID only; its ledger entry disappears after confirmed stop. No booted device is registered; unrelated session is untouched. R2 and R3 cannot collapse: one must retain a refused record, the other must remove it. | **recording**, ordered runtime transcript with active-list/stop operations and post-error ledger. `(N/A × N/A × R3)`. | **Labelled harness capture:** existing `stops the session a refused create made anyway` test's `fakeExpo({rejectCreate:true})` sequence, recording actual ledger contents too. Keep host scope open until evidence has been read, then close it. A UI route cannot deterministically seed an orphan/refusal race. | **MISSING capture** |

All R rows depend on boot, external response ordering, and cleanup; a still image is insufficient. A text transcript is the recording appropriate to this server-only behavior. Include actual outcomes, request operation names and relevant non-secret inputs, ledger snapshots, and terminal exit status. Do not print Authorization headers, Expo tokens, preview/agent tokens, or real account data. Use synthetic identifiers.

## Call paths and render-path inventory

There are **zero changed render components**, therefore no changed component callers, layout props, counts/length rules, empty/loading/error/theme/width/platform branches to enumerate as new visual fixtures. Relevant unchanged consumers and their existing branches were traced to confirm that conclusion:

| Changed unit | All relevant production callers / outward paths | Changed data shape or branch | Capture consequence |
| --- | --- | --- | --- |
| `EasDeviceHost.startSession` (inside `make`) | `DeviceService` configures EAS hosts via `EasDeviceHost.make`; real host `devices.boot` calls `startSession`. `DeviceService.open` calls `ready.devices.boot` only for a device not already booted when `boot !== false`. Its production entry points are `ws.ts`'s `deviceOpen` and MCP device handler's `device_open`. Tests call the host directly. | Removes availability call and gate-false/error early exit after app resolution and before ledger write. Accepted/refused create now decides admission. Public device/session and error schemas do not change. | R1–R3. Already-running attach and `boot:false` bypass this code; no separate capture. |
| `ExpoApi` factory / deleted `availability` | `EasDeviceHost` resolves credential and builds API for create/session/list/stop; `DeviceService`'s EAS account lookup builds API for `viewer`. The only production `availability` caller was the removed `startSession` check. | API method and operation removed. Create still maps iOS/Android to `IOS`/`ANDROID`; unchanged generic error wrapping classifies USER/null-data as rejected. | R1 proves absent query. R2/R3 exercise real create decoding and rejection mapping. Viewer has no changed returned shape. |
| GraphQL source, generated operation group/schema, config | `ExpoApi` consumes the operation group. Generated types are used by the same server API. Generator config is build-time only. | Removes availability query and now-unused `JSONObject` mapping/export. Generated viewer account and active-session app type names simplify, with the same surviving runtime fields. | No render path; T1 covers surviving API operations. No generated-source screenshot. |
| Two changed test files | Test runner only. | Gate-off-capable fake; explicit USER/null-data refusal; exact error assertion; new admission regression; existing generic GraphQL-error assertion uses stop after availability removal. | Test fixtures are a starting point for a labelled harness, not captured app evidence. T1. |

Existing client paths, with no changed renderer:

- **Web and Electron desktop:** `DevicePanel.selectDevice` calls shared `deviceEnvironment.open` → `deviceOpen` RPC. It already settles failures with the generic formatted string, renders it in `DeviceNotice`, and lets the user dismiss it. `DeviceService.open` publishes `bootingDevices` before boot and clears it with `ensuring`, then refreshes state after success. Existing UI distinguishes onboarding/disabled, initial snapshot/loading/error, pending boot/host busy, active session vs device loading/selection, warnings/diagnostics, and zero/multiple booting devices. These are inherited behavior, not new layout branches. Scope of this PR does not warrant reproducing them across themes, widths, or panel modes.
- **Agents:** MCP `device_open` resolves host/device and access, then calls the same `DeviceService.open`; its response carries the existing device error. Any provider or scheduled turn using this tool reaches the same service. No provider-specific admission logic was added.
- **Mobile:** no direct `deviceEnvironment.open` consumer was found in mobile production code. The mobile device preview derives thread sessions from shared state (`threadDevicePreviews`), selects one, and displays an already-open stream. An agent opening an EAS session reaches the shared server path; a refusal does not register a session. No mobile render change or separate capture required.
- **Settings/account lookup:** EAS host editor/account lookup uses `viewer`, not the removed session-admission check. Host config, account/project lists and validation retain their schemas. Settings can seed a host in a real isolated app, but does not independently start this gate pre-check.
- **Connection modes:** local browser, remote browser/relay/tunnel, desktop, and mobile share the server host/admission path. No origin, transport envelope, scope, or contract changed. One isolated host harness demonstrates the changed decision; per-connection captures add no distinguishing evidence.

## Collapsed unchanged branches / capture waivers

This is the boundary inventory around the removed branch, not a request for a broad simulator feature audit. Each row is accounted for; none requires a separate capture for this PR.

| ID / fixture family | Inputs / branches collapsed | Effect of diff | Minimum capture / status |
| --- | --- | --- | --- |
| C1 — gate on; gate absent/malformed/query-error | Old availability had `true` vs everything else or API failure. Head sends no availability request for any of them; create determines outcome. | Gate off is the discriminating base/head row. Gate on still reaches create; absent/malformed/error responses are unreachable at head. | **WAIVED** — R1's zero availability requests covers removal; no extra runtime recording. |
| C2 — iOS / Android | Two unchanged catalog entries; platform selects API enum and preview path (`/api` vs `/vendor/serve-emu/api`), not admission. | Removed pre-check is platform-independent. | **WAIVED additional capture** — R1 uses iOS; existing Android focused test remains part of T1. |
| C3 — preconditions | Saved token / environment token / EAS login / missing credential; configured app / workspace-derived app / missing app; free slot / max concurrency reached; ledger readable / read or write failure. | Preconditions and guards retain order, except the removed availability request. No credential, no app, full capacity, or bad ledger still prevents create. | **WAIVED capture** — existing focused tests; synthetic token/configured app/free slot/healthy ledger in R1–R3. No real credential needed. |
| C4 — create has uncertain outcome | Transport failure, 5xx, transient/internal GraphQL error, data beside errors, timeout/encoding errors. | Existing error classifier and uncertain/rejected branches unchanged. Only deterministic USER/null-data refusal is selected for changed proof. | **WAIVED capture** — surviving `ExpoApi.test.ts` classification coverage in T1. Do not mislabel unknown outcome as refused. |
| C5 — boot readiness and lifecycle | `NEW`/queued/starting vs `IN_PROGRESS` with endpoints; stopped/errored; missing endpoints; preview failure; timeout/retry; stop/cancel/reconfigure during boot; stale status/reap interval; ledger age/expiry. | No change to these algorithms, timings, or UI indicators. | **WAIVED extra capture** — R1 immediate readiness and successful close, R2 restart, R3 orphan cleanup are sufficient. No wall-clock sleeps or polling needed in the driver; synchronize via Deferred/operation milestones. |
| C6 — cleanup fails or no matching orphan | Active list succeeds/fails; matching name count zero/one/multiple; stop succeeds/fails; unrelated name. | Refusal cleanup itself unchanged. Its two ordinary terminal outcomes matter: retained refused record and confirmed stopped orphan. A failed stop stays unresolved/counted for retry, rather than becoming a harmless refused record. | **R2/R3 required** for zero/one matches and unrelated isolation; **WAIVED** extra stop-failure/multiple-match captures (unchanged algorithm). |
| C7 — client presentation dimensions | Existing generic error text length/wrapping; empty/loading/error; counts; light/dark; narrow/wide; web/desktop/native iOS/native Android; interactions for dismiss/retry/open/close. | No renderer, style, prop or client data schema change. Strings can differ but continue through the existing error path. | **N/A visual matrix** — no screenshots/video or browser run required for this server-only change. Optional UI demonstration is outside the minimum. |

## Capturer instructions and acceptance boundary

- Use actual modules at the exact base and local head; avoid stale `main`, remote PR head, or a fake host implementation. A module snapshot/loader inside authorized scratch is acceptable for baseline replay; do not edit, switch, or regenerate the reviewed worktree. Baseline gate-off HTTP response must use the old operation's valid schema.
- Set isolated `ServerConfig.stateDir` under the capturer's authorized scratch directory. Start from empty ledger for R1/R3; R2 deliberately reuses only its own ledger across host instances. Never use the live T3 home. All harness/driver files are scratch, uploaded with the evidence and never committed.
- Synthetic API messages and endpoints are enough. Fail any unexpected HTTP origin so a fixture can never fall through to Expo. Use the real GraphQL serialization/decoding, credential injection, host lifecycle, and ledger persistence.
- Record `refused:true` at failed-boot return for R2. Confirmed stop removes the entry in R3; these are alternative results, not both expected simultaneously. Observe ledger state before final scope close so cleanup does not obscure what boot did.
- An ordered transcript may combine R1–R3 in one file. Give each row a label, actual revision, invocation, return/error, operations and ledger outcome. Close every scope; report fixture process exit. Upload the scratch driver alongside it for reproducibility. Do not substitute a copied unit-test expectation for observed output.
- Pass 1 did not run tests, start a dev server, use a browser, or create any Expo session. No processes remain running from this audit. The reviewed worktree remained clean. This audit covered all eight changed files, host admission/ledger/error paths, relevant service/RPC/MCP entry points, and unchanged client consumers; it is not a full simulator implementation audit.

FRICTION:
tool | eas CLI absent, so whoami could not establish a usable Expo credential; only ~/.expo presence was checked | an already-installed eas CLI would permit whoami, but fake-HTTP proof needs no credential

VERDICT: INSUFFICIENT R1 gate-off base/head runtime recording; R2 refused/no-session ledger and restart runtime recording; R3 refused/orphan cleanup runtime recording; T1 focused test receipt
