feat: add PLANNOTATOR_URL_HOST display-only override for advertised URLs - #1225
Merged
Conversation
Remote mode binds 0.0.0.0 but every advertised URL hardcoded http://localhost:<port>, so a session opened from another device (e.g. a phone on the same tailnet) got an unopenable link (#657). - resolveUrlHost() in packages/shared/config.ts: PLANNOTATOR_URL_HOST env var over config.json urlHost, validated host-only (bare hostname, IPv4, bracketed IPv6); invalid values warn once and fall back to localhost. - buildAdvertisedUrl(port) in packages/server/remote.ts and its Pi mirror in apps/pi-extension/server/network.ts; all 7 construction sites use it. - Strictly display-only: binding stays governed by PLANNOTATOR_REMOTE, and agent-review jobs get a pinned http://127.0.0.1:<port> API URL. - Remote-ready copy says "open on your device" when the host is overridden; local sessions with an override warn it is unreachable. - Tests for validation, precedence, and URL composition in both runtimes; docs in CLAUDE.md and the marketing site.
Review follow-ups on #1225: - Local (loopback-bound) sessions no longer honor the advertised-host override: honoring it auto-opened http://<host>:<port> against a server nothing was listening on, openBrowser still reported success, and the agent blocked on waitForDecision. Local sessions now advertise and open localhost, warning once that PLANNOTATOR_REMOTE=1 is required. - The invalid-host warning JSON-encodes the echoed value so an embedded newline cannot forge extra stderr lines (hosts surface session-ready lines as clickable links); warn-once is now per value. - Docs: local-session behavior reworded, the empty-env-suppresses-config semantic documented, secure-context note generalized.
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.
TLDR: adds
PLANNOTATOR_URL_HOST(config keyurlHost), a display-only host override for advertised session URLs, so remote-mode users on a tailnet or LAN get links another device can actually open instead ofhttp://localhost:<port>. Closes #657.Behavior
Remote mode (
PLANNOTATOR_REMOTE=1) already binds0.0.0.0, so the server is reachable over Tailscale. But every advertised URL hardcodedhttp://localhost:<port>, so a user on Pi via the Paseo mobile app got a localhost link their phone cannot open. This is the concrete ask in #657, and this PR implements exactly that: a host-only display override.PLANNOTATOR_URL_HOSTenv var takes precedence, then~/.plannotator/config.json{ "urlHost": "..." }, elselocalhost. Resolution lives inresolveUrlHost()inpackages/shared/config.ts, following the existingresolveSharingEnabledpattern.[fd7a::1]) are accepted. Anything with a scheme,/,@,?,#, whitespace, or a:outside IPv6 brackets is rejected, because the port is chosen at runtime and always appended. Invalid values warn once on stderr and fall back tolocalhost; a display setting never crashes a server launch.buildAdvertisedUrl(port)composes the URL inpackages/server/remote.ts(Bun) with a behaviorally identical mirror inapps/pi-extension/server/network.ts(Node). All 7 URL construction sites now use it: plan, review, annotate, and goal-setup servers on Bun; plan, review, and annotate on Pi.shared-handlers.ts, Piindex.ts), since port-forwarding advice is wrong for a directly reachable host. Without an override the existing copy is unchanged.PLANNOTATOR_REMOTE=1is required. Honoring it locally would auto-open a URL nothing is listening on and leave the agent blocked on the review. Binding is never widened.PLANNOTATOR_URL_HOST=) suppresses a config-fileurlHost.Security stance
Strictly display-only. The override never touches
getServerHostname()or any listening behavior; onlyPLANNOTATOR_REMOTEgoverns binding. Spawned agent-review jobs run on the same machine, so theirPLANNOTATOR_API_URLis now explicitly pinned tohttp://127.0.0.1:<port>in both runtimes, meaning a tailnet-only hostname cannot break local agent jobs.Caveat
Sessions are served over plain
http. The core review, annotate, and approve flows work fine that way from another device, but browser features requiring a secure context, notably creating short share links from the UI, need HTTPS in front (e.g.tailscale serve). Documented in the remote-mode guide.Tests
resolveUrlHost/isValidUrlHost: accept/reject table and env-over-config precedence inpackages/shared/config.test.ts.buildAdvertisedUrl: host+port composition including bracketed IPv6, invalid-host fallback, and a bind-hostname non-interference check, in bothpackages/server/remote.test.tsandapps/pi-extension/server/network.test.ts.PLANNOTATOR_URL_HOST=unreachable.invalid, 1489 pass / 0 fail).bun run typecheckclean aftervendor.shregeneration.Docs updated: CLAUDE.md env table, marketing environment-variables reference, configuration page, and a new "Direct-reach hosts (Tailscale, LAN)" section in the remote-and-devcontainers guide.