Repository navigation
Inbox: Phones stay hidden until the iPhone app ships (PLANNOTATOR_INBOX_PHONES) - #1812
Merged
Merged
Conversation
…OX_PHONES) The phone surface (pairing, the device door, the tailnet and Wi-Fi listeners, the relay, Settings' Phones block) now exists only when PLANNOTATOR_INBOX_PHONES=1 or config.json inboxPhones is true. Off by default: its routes answer the unknown-route 404, nothing listens, connects, spawns or makes a certificate, and the window hides the block (GET /api/inbox/settings carries phones). The iPhone, relay, DAST and phone e2e proofs turn it on. Co-Authored-By: Claude <noreply@anthropic.com>
1 task
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Main carries the server side of the unreleased iPhone app: pairing, the device door, the tailnet and Wi-Fi listeners, the relay connection, and Settings' Phones block. The app and relay.plannotator.ai are not live. This PR puts all of it behind one switch, off by default, so 0.28.9 shows and serves none of it.
Audit: what main does today with a fresh data dir and no pairing
I read the code, then ran a real Inbox (
plannotator inbox --no-open, CLI from source) under a tempPLANNOTATOR_DATA_DIRand checked it withlsof -p <pid> -i.On its own, it does nothing phone-related.
127.0.0.1:<port>listener.inbox/inbox.json: notls/, norelay.json, nodevices.jsonl.relay.start()subscribes to the store, butconnect()andpushFor()return early with no mailbox.startTailnet/startLanreturn early unlessinbox.jsonholdstailnet/lan.on.But every phone route is mounted and answers. The
curlresults with no pairing:GET /api/inbox/device,GET /api/inbox/device/health,POST /api/inbox/device/revoke(no token)401 device_token_missingGET /api/inbox/device/threads(bogus token)401 device_token_invalidPOST /api/inbox/device/pair(no open offer)410 offer_expiredOrigin403 origin_not_allowedGET /api/inbox/devices200 {"devices":[]}POST /api/inbox/devices/dev_x/revoke404 device_not_foundGET /api/inbox/tailnet/GET /api/inbox/lan200, both offPOST /api/inbox/pairing201, opens a live offer: QR link, secret, six digits, and the computer's name (it runsscutil)So one local POST can switch it all on. Any caller that passes the window's same-origin/no-Origin guard can:
/api/inbox/device/pair, which returns a bearer token, creates the relay mailbox (an outboundPOSTtohttps://relay.plannotator.ai/v1/mailboxes) and then holds a WebSocket to it;POST /api/inbox/lan, which runsopensslto make a certificate, binds a TLS listener on0.0.0.0and startsdns-sd/avahi-publish;POST /api/inbox/tailnet, which runstailscale serve.The switch
Settings:
PLANNOTATOR_INBOX_PHONES=1(alsotrue/on;0/false/off/disabledturn it off), or config.json{ "inboxPhones": true }. The env var wins. It is resolved byresolveInboxPhonesinpackages/shared/config.ts, read once when the Inbox starts.startInboxServer({ phones })overrides it.Off (the default):
createInboxDevicesandcreateInboxRelayare never constructed.404 {"error":"Not found","code":"not_found"}as an unknown route.openssl/tailscale/ Bonjour, or makes a certificate. This holds even wheninbox.json,relay.jsonanddevices.jsonlhold state from an earlier run with phones on. That state is kept for the next run with phones on.The window:
GET /api/inbox/settingscarriesphones. Settings draws<PhonesBlock />and the "Phones reach it…" sentence only when it is true.On: behaviour is exactly as it is today.
Proofs that turn it on:
apps/ios/scripts/proof.tsandworkspaces-proof.ts(their env, which is the only env the Inbox gets inios.yml)apps/relay/test/relay.test.tstests/e2e/inbox-phones.spec.tsinbox-devices.test.tsandinbox-lan.test.ts, throughcreateInboxWorld(..., { phones: true })andphones: truetests/helpers/inbox-world.tsnow setsPLANNOTATOR_INBOX_PHONES=0for every other world.Docs: an env table row and a line in the "Phones" section of AGENTS.md (CLAUDE.md is a symlink to it). Nothing in the public marketing docs.
Proof
packages/server/inbox-phones-gate.test.tsrunsplannotator inbox --backgroundagainst a fake relay on loopback, withopensslandtailscalestand-ins on PATH that record every call:/api/inbox/device/health, and the Inbox holds the relay socket.lan.onandtailnetalso seeded ininbox.json:settings.phonesisfalse;opensslandtailscalewere never called andinbox/tlsdoes not exist;lsofshows only the window's port.opensslandtailscaleall come back, which shows that step 2's silence comes from the switch.The test passes with the CLI from source and with the compiled binary. With the gate forced on, the off case fails, so the test catches a regression.
tests/e2e/inbox.spec.ts's Settings test now also asserts that the Phones block is absent.Tests
bun run typecheckpasses.bun run test:e2e:inboxagainst the compiled binary: 73 pass and 1 skips. The skip is the manual tailnet test, which needs a Mac signed in to Tailscale.bun test: 6854 pass and 1 fails,agent-terminal.test.ts"node sidecar rebuilds spawn options" (JSON Parse error: Unexpected EOFunder load). This PR does not touch that code, and the file passes 5/5 on its own.Not covered by the gate
uninstall --purgestill takes down a tailnet mapping that a crashed phones-on Inbox left. That is cleanup, and it only runs wheninbox.jsonhastailnet.tailscale servemapping left by a kill -9 of a phones-on run stays until the next run with phones on, or untiluninstall --purge. Only people who turned phones on can hit this.apps/ios,apps/relay) are unchanged. They are not part of the release artifacts.