Cloud/local auth-sync, Limrun mobile dev tooling, and a Mobile Simulator pane

Merged
SA
1mo ago
+3068-94

Summary

This PR bundles 3 related features, all under one theme: making cloud (and local) sandboxes immediately usable for desktop + mobile development without manual sign-in or a Mac/Android device.

  1. Auth sync - desktop and mobile auto-sign-in as the real workspace creator/developer in cloud sandboxes, and mobile auto-signs-in locally too, instead of staying signed out or falling back to a shared local-admin account.
  2. Limrun mobile dev tooling - limrun-dev.sh + vendored skills, so verifying apps/mobile on a cloud sandbox (no local Xcode/Android SDK) is one command instead of rediscovering Limrun's quirks each time.
  3. Mobile Simulator pane - a Cmd+Shift+M desktop pane mirroring the existing Desktop/VNC pane, backed by Limrun on a sandbox or a local iOS/Android simulator on a real machine.

Part 1: Auth sync (desktop + mobile, cloud + local)

Why

Every cloud workspace's Neon branch forks from production, so real users/organizations rows already exist — there was no reason to require a manual sign-in, or to spoof a generic "local admin" identity instead of the actual developer/workspace creator.

What / How

  • Desktop (cloud): packages/auth/src/seed-cloud-auth-token.ts resolves the real creator via SUPERSET_SANDBOX_CREATOR_USER_ID (sourced from cloud_workspaces.created_by_user_id, threaded through claim.ts/sandbox-contract.ts), mints a real better-auth session server-side, and writes it through the same AES-256-GCM machine-bound encryption desktop already uses for local worktrees — extracted to packages/shared/src/auth-token-crypto.ts so a headless script can produce a file desktop can decrypt, without depending on apps/desktop. This file can't be copied across machines (the key is derived from /etc/machine-id), so it's minted fresh per sandbox rather than copied.
  • Mobile (cloud + local): @better-auth/expo uses cookie/SecureStore, not a portable encrypted file, so mobile instead uses better-auth's oneTimeToken plugin: seed-cloud-mobile-token.ts (cloud) and seed-local-mobile-token.ts (local, resolves identity by validating the existing auth-token.enc against the real API) mint a session + one-time token and bake it into .env as EXPO_PUBLIC_DEV_ONE_TIME_TOKEN. DevSignInOptions redeems it automatically on mount if present.
  • The oneTimeToken plugin is registered unconditionally in packages/auth/src/server.ts - a conditional entry in the plugins array defeats better-auth's tuple-based type inference for the whole Session/User type (confirmed: broke packages/trpc/src/router/automation/automation.ts). It's instead blocked outside development in a hooks.before check that 404s /one-time-token/* - verified live with NODE_ENV=production.
  • Removed apps/mobile/.maestro/flows/sign-in.yml - not supportable going forward and not a priority.

Key Decisions

DecisionChoiceRationale
Mint vs. copy auth stateMint fresh per sandboxauth-token.enc's key is machine-bound; copying it across machines can never work
Mobile mechanismbetter-auth oneTimeToken pluginMobile's storage (cookie/SecureStore) has no equivalent to desktop's encrypted file; this is the built-in, dev-gated primitive for "redeem a token for a real session"
Plugin registrationAlways register, gate in hooks.beforeA conditional plugins array silently breaks Session/User type inference repo-wide

Part 2: Limrun mobile dev tooling

Why

apps/mobile is iOS-only and has no local Xcode in a cloud sandbox. Verifying a change there meant hand-running several lim commands per session and rediscovering the same gotchas (SPM sandbox failures, .env not reaching the remote build, stale native projects) each time.

What / How

  • apps/mobile/scripts/limrun-dev.sh: checks Limrun asset storage for an existing branch-scoped build first (fast path: install on a fresh simulator in seconds, no Xcode sandbox at all) and only builds when none exists or --rebuild is passed. A fresh build reapplies a required Xcode SPM sandbox fix (confirmed via a dedicated test that it does not survive snapshot restores or instance recreation, so it's cheap to reapply unconditionally rather than relying on persistence), forwards .env values via --env (.env is gitignored and never reaches Limrun's remote sync), and tunnels both Metro and the API port.
  • apps/mobile/AGENTS.md documents all of the above so the next agent doesn't rediscover it.
  • Vendored limrun-expo-development, limrun-ios-simulator, limrun-maestro-testing, limrun-xcode skills via lim skills install (committed, matching how the repo already vendors other skills). limrun-android-emulator and limrun-gradle are deliberately not vendored - out of scope since apps/mobile is iOS-only today.
  • EXPO_PUBLIC_SENTRY_DSN_MOBILE placeholder added to .env.local.example: an unset/empty DSN causes @sentry/react-native's native init to SIGSEGV before any JS runs, which looks nothing like a Sentry problem. A real DSN is provisioned as an environment secret for sandboxes; local dev falls back to this inert placeholder.

Part 3: Mobile Simulator pane

Why

A pane analogous to the existing Desktop/VNC pane, so an agent or developer can see and interact with a mobile simulator inline instead of needing a second window/device - and critically, something that still works on a headless cloud sandbox where there's no local Xcode/Android SDK at all.

What / How

  • types.ts: MobilePaneData added to the PaneViewerData union.
  • hotkeys/registry.ts + useWorkspaceHotkeys.ts: Cmd+Shift+M (Ctrl+Shift+Alt+M on Windows/Linux) splits the active pane with a mobile pane. Not gated on isSandbox (unlike the Desktop/VNC hotkey) since it should also work on a real machine with local tooling.
  • usePaneRegistry.tsx: registers the mobile pane kind unconditionally, rendering <MobilePane />.
  • packages/host-service/src/trpc/router/mobile/mobile.ts (new router): detectMobileBackend() picks limrun (sandbox + LIM_API_KEY), local-ios (darwin + xcrun + a booted simulator), local-android (adb present), or none. limrunSession mints/reuses a Limrun instance server-side via a direct REST call (the org-wide LIM_API_KEY never reaches the renderer) and returns just the WebSocket URL + token <RemoteControl /> needs. localSession spawns/reuses @expo/serve-sim for iOS, or a SUPERSET_ANDROID_MIRROR_CMD-configured process for Android (no bundled default for Android - the npm screen-mirroring ecosystem there is fragmented and nothing has been run against real hardware yet; shipping a guess would overclaim confidence).
  • MobilePane.tsx: renders Limrun's own <RemoteControl /> (from the new @limrun/ui dependency) directly for the cloud path, or embeds the local tool's browser preview UI in a <webview> for the local paths.

Key Decisions

DecisionChoiceRationale
Cloud renderingLimrun's own <RemoteControl />It's a real React component with its own connecting/reconnecting UI; wrapping it beats reimplementing its streaming protocol
Local rendering<webview> embedding the local tool's own browser UI@expo/serve-sim (iOS) already ships a browser-embeddable preview; no need to speak SimulatorKit/IOSurface ourselves
Android local backendConfigurable via SUPERSET_ANDROID_MIRROR_CMD, no defaultserve-sim has no Android equivalent (scrcpy renders to a native window); the right fork of ws-scrcpy-style tooling is untested

Keyboard Shortcuts

ShortcutAction
Cmd+Shift+M (mac) / Ctrl+Shift+Alt+M (Windows/Linux)Split the current pane and open a Mobile Simulator

Manual QA Checklist

Auth sync

  • Desktop auto-signs-in as the real workspace creator on a fresh cloud sandbox (verified live via CDP - session + activeOrganizationId present)
  • Mobile auto-signs-in via the redeemed one-time token on a cloud sandbox, verified live on a real Limrun iOS simulator (twice, including through the final limrun-dev.sh script)
  • /one-time-token/verify and /generate both 404 with NODE_ENV=production; /get-session still 200 (verified live)
  • Mobile local auto-sign-in (setup.sh path, not cloud) - implemented and type-checks, not independently re-verified in this session beyond the cloud path's shared code path

Limrun tooling

  • limrun-dev.sh cold-build path verified live (build, attach, tunnel, Metro, dev-client launch)
  • limrun-dev.sh fast path (existing asset, no Xcode sandbox) verified live (~11s)
  • SPM sandbox fix non-persistence across snapshot restore confirmed via a dedicated build→snapshot→delete→restore→check test

Mobile Simulator pane

  • detectMobileBackend() verified in all three live branches (sandbox+key, no-key sandbox, no-adb)
  • Limrun REST call (limrunSession) verified live against the real API
  • bun run typecheck passes for apps/desktop and packages/host-service
  • Not verified: the hotkey → pane UI flow was not click-tested end-to-end in an open workspace (no existing local workspace in this sandbox's dev DB to test against)
  • Not verified: local-ios backend (@expo/serve-sim) - no Mac available in this cloud sandbox
  • Not verified: local-android backend - no Android device/emulator available in this cloud sandbox, and no default SUPERSET_ANDROID_MIRROR_CMD is shipped

Testing

  • bun run typecheck for apps/desktop and packages/host-service - pass
  • bunx @biomejs/biome check - pass, no outstanding lint issues
  • Live verification as described in Manual QA above (CDP screenshots for desktop/mobile auth; real Limrun sandboxes for the mobile tooling and pane backend)
  • Did not get a clean full-repo bun test run in this sandbox due to a pre-existing, unrelated Bun engine segfault (confirmed via git stash that it reproduces on main too, not introduced by this diff)

Known Limitations

  • The Mobile Simulator pane's local-ios and local-android backends are implemented but entirely unverified on real hardware - this sandbox is Linux-only. Plan is to verify these on a canary build once merged.
  • No default Android screen-mirroring tool is bundled; SUPERSET_ANDROID_MIRROR_CMD must be configured by whoever tests the local-Android path first.
  • The pane's hotkey → split → render flow itself was not click-tested in a live, populated workspace.

Follow-ups

  • Verify local-ios / local-android panes on real hardware and pick/pin a concrete Android mirroring tool.
  • Consider a platform picker for Limrun sessions once apps/mobile supports Android (today it's iOS-only, so the pane always requests ios).

Risks / Rollout

  • Risk: auth-adjacent changes (new always-on oneTimeToken plugin, dev-only gated) and a new tRPC router with a direct external REST call (api.limrun.com) using an org-wide API key that stays server-side.
  • Rollout: no feature flag; ships to desktop/mobile/host-service together since they version in lockstep. The oneTimeToken endpoint is hard-gated to NODE_ENV === "development", so it is inert in any production build regardless of this PR shipping.
  • Rollback: standard revert; no data migrations or schema changes involved.

Summary by cubic

Makes cloud and local sandboxes immediately usable for desktop and mobile development: they auto-sign-in as the real workspace creator instead of the shared dev account, a Mobile Simulator pane (Cmd+Shift+M) mirrors the Desktop/VNC pane, and verifying apps/mobile from a cloud sandbox is now one command.

  • Desktop cloud sandboxes get a better-auth session minted server-side for the creator, written through the machine-bound AES-256-GCM encryption desktop already uses (extracted to packages/shared); the key derives from machine-id, so the token is minted fresh per sandbox, never copied.
  • Mobile redeems a better-auth oneTimeToken baked into .env as EXPO_PUBLIC_DEV_ONE_TIME_TOKEN on both cloud and local setups; the plugin is always registered but hard-gated to NODE_ENV=development via a hooks.before check that 404s /one-time-token/* in production (a conditional plugins array breaks better-auth's tuple type inference repo-wide).
  • apps/mobile/scripts/limrun-dev.sh fast-paths off a shared, branch-scoped Limrun build (seconds, no Xcode) when one exists, otherwise builds (reapplying a non-persistent Xcode SPM sandbox fix) and tunnels Metro and the API port; .env is forwarded via --env since it never reaches Limrun's remote sync.
  • Vendored limrun-expo-development, limrun-ios-simulator, limrun-maestro-testing, and limrun-xcode skills; Android equivalents are deliberately skipped since apps/mobile is iOS-only.
  • EXPO_PUBLIC_SENTRY_DSN_MOBILE placeholder added to .env.local.example: an unset DSN makes @sentry/react-native native init SIGSEGV before JS runs.
  • The pane's new strings are translated across all 16 locales (check:i18n).

Rendering paths

  • Sandbox with LIM_API_KEY: a new host-service mobile tRPC router mints/reuses a Limrun instance server-side (the org-wide key never reaches the renderer) and returns the WebSocket URL + token for Limrun's own <RemoteControl />.
  • Real machine: iOS uses @expo/serve-sim, Android uses a SUPERSET_ANDROID_MIRROR_CMD-configured process — no bundled Android default since nothing is verified on real hardware. Both embed the tool's own browser UI in a <webview>.

Verification and risks

  • Live-verified: desktop/cloud mobile auto-sign-in, cloud mobile via limrun-dev.sh, the production 404-gate, Limrun cold-build and fast paths, and SPM-fix non-persistence. Not verified: local iOS/Android backends (Linux-only sandbox, no hardware) and the hotkey→pane flow end-to-end.
  • No feature flag, data migration, or schema change; ships together since desktop, mobile, and host-service version in lockstep, and the oneTimeToken endpoint is inert in production builds.

Summary by CodeRabbit

  • New Features
    • Open a mobile simulator alongside your workspace in a split pane using the new keyboard shortcut.
    • Mobile previews can connect to available local or cloud simulators.
    • Development sign-in can happen automatically when a valid one-time token is available.
  • Documentation
    • Added guides for preparing and running mobile apps, controlling cloud simulators, testing with Maestro, and using remote Xcode. These cover setup, app launch, simulator interaction, troubleshooting, and preview workflows.