Skip to content

Control Godmode from your phone: iOS and Android app, paired over Tailscale (GODM-33) - #37

Merged
danielehrhardt merged 3 commits into
mainfrom
agent/c-pro/3622aa5d1be8
Sep 30, 2026
Merged

danielehrhardt merged 3 commits into
mainfrom
agent/c-pro/3622aa5d1be8

Conversation

@danielehrhardt

Copy link
Copy Markdown
Contributor

Closes GODM-33.

What does this change?

Godmode now has a phone app for iOS and Android (apps/mobile, Expo SDK 57). It controls the Godmode running on your computer, and nothing else.

Phone app

  • Home: what agents are doing right now (with a stop button), live screens, things that need you (a locked vault, missing logins) and recent chats. A floating bar above the tab bar shows the current activity from any tab.
  • Chats: answers stream in as they're written. Steps show up as short lines like "Opened linear.app". Markdown, tables and code render natively. A live thumbnail of the agent's browser or VM sits above the message field, and Stop is one tap.
  • Screens: every browser tab, shared screen and VM an agent works in, live. Open one full screen, pinch to zoom, and Take control: tap to click, type, scroll. VMs can be started and stopped.
  • Agents: status, give an agent a task, run automations or switch them on and off.
  • iOS 26 Liquid Glass throughout: native tab bar with a bottom accessory, native headers and toolbar buttons, glass controls. The app icon is an Icon Composer file. Android uses Material symbols and solid surfaces. Light and dark mode.

Connecting (Settings → Phone on the computer)

Connect a phone

  1. Tailscale on the computer and the phone, same account.
  2. Settings → Phone → Connect a phone shows a QR code.
  3. Scan it in the app, or with the camera app (godmode://pair?d=… opens the app).

Phone settings

The settings page shows the Tailscale status, the phone-access switch and port, and paired phones (online state, last seen, remove).

Security

  • One-time code: 32 random bytes, valid for 5 minutes, single use. Only its hash is kept, in memory.
  • Device key: each phone gets its own key. The computer stores only its hash. On the phone it lives in the Keychain / Keystore.
  • Removing a phone disconnects it right away. Pairing and removal are audited. Pairing also notifies you.
  • Separate listener on this computer's Tailscale address only, never the LAN. It serves no dashboard, sign-in, MCP or webhooks. The access token and dashboard sessions are refused there. It checks the Host header and the decoded path (/api/%61uth is refused too).
  • Device keys only work on that listener, and only while phone access is on.
  • Route allowlist: phones reach a fixed set of routes with restricted bodies.
    • They can't reach logins, 2FA, backups, integrations, settings or folders.
    • They can't change a chat's folder, VM, instructions or shared screen, or an automation beyond on/off.
    • They can only control screens shared in a chat.
  • The app protects its key:
    • It sends the key over plain HTTP only to Tailscale addresses (100.64.0.0/10, *.ts.net), or over https anywhere, for the planned gateway.
    • Before sending it, it checks that the address answers with the paired instance id.
    • Writes are never retried on another address, so a slow network can't duplicate a message or a run.
  • Optional Face ID lock.
  • Backups never carry paired phones or phone settings.

Core changes

  • packages/core/src/mobile/: Tailscale detection (CLI or interface), pairing and devices, the listener, and the phone scope.
  • Routes under /api/mobile.
  • Migration 15 (mobile_devices).
  • WebSocket: phones only get streaming deltas for chats they have open (conversation.subscribe). New sockets get the current activity of every run, for the desktop too.
  • @godmode/shared gets mobile.ts: types, pairing-link encode/parse, URL policy.

Tooling

  • apps/mobile has its own toolchain (bun) and is excluded from the pnpm workspace. CI, Docker and the desktop build don't install React Native.
  • It imports @godmode/shared from source through Metro.
  • New CI job: Phone app (typecheck).
  • scripts/icons.ts --mobile generates the app icons from the logo.

How was it tested?

  • Core tests: bun test passes, 757 pass, 0 fail. The new test/mobile.test.ts has 15 tests. They cover:
    • pairing codes: single use, expiry, voided when access is turned off
    • device scope and body rules, and that device keys are refused off the phone listener
    • the phone listener: Host, decoded-path check, access token refused
    • WebSocket delta filtering, and removal closing the socket
    • the Tailscale parser and the URL policy
  • Other checks:
    • Typecheck passes for shared, core and desktop.
    • The mobile typecheck passes, also without the generated Expo types (a clean clone).
    • expo-doctor passes 21/21.
    • The desktop build passes, and the core binary smoke test passes.
  • End to end: I ran a development build in the iOS 26.5 Simulator against a real core over this Mac's Tailscale address.
    • Paired through the deep link.
    • Chats, streaming, stop, sending a message, agents, the new-chat sheet and settings all worked.
    • The live browser worked, and a tap clicked through on the remote page.
    • Disconnect removed the device on the computer, then re-paired.
    • Both light and dark mode were checked.
  • The desktop QR code was decoded from a screenshot with the macOS QR detector.
  • An independent review found an encoded-path bypass, device keys working on the main listener, and a vanished reply after backgrounding. All were fixed and covered by tests.

Notes

…lscale (GODM-33)

- apps/mobile: Expo SDK 57 app (Expo Router, native tabs and headers with Liquid Glass on iOS 26). Home with what
  agents are doing now, chats that stream as they're written, live browser / shared screen / VM views with tap to
  take control, agents with their automations, a new-chat sheet, Face ID lock.
- Pairing: Settings → Phone shows a one-time QR code (5 minutes, single use, only its hash kept). The phone trades
  it for its own device key (stored hashed; Keychain / Keystore on the phone). Phones are listed, shown online and
  can be removed, which disconnects them right away.
- Transport: a second listener on this computer's Tailscale address only. It answers /api for paired phones,
  checks the Host and the decoded path, and a device key opens a fixed set of routes with restricted bodies
  (no secrets, settings, folders; only screens shared in a chat can be controlled).
- The app only sends its key to Tailscale addresses (or https) that prove to be the paired instance, and never
  retries writes on another address.
- Phones get streaming replies only for chats they have open; new sockets get the current activity of every run.
# Conflicts:
#	.dockerignore
#	apps/desktop/src/lib/realtime.ts
#	packages/core/src/db/migrations.ts
#	packages/core/src/server/ws.ts
#	packages/shared/src/events.ts
#	packages/shared/src/index.ts
#	pnpm-lock.yaml
# Conflicts:
#	apps/desktop/src/lib/api.ts
#	apps/desktop/src/lib/queryKeys.ts
#	packages/core/src/db/migrations.ts
@danielehrhardt
danielehrhardt merged commit 9303ffb into main Sep 30, 2026
11 of 12 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant