Skip to content

feat: add PLANNOTATOR_URL_HOST display-only override for advertised URLs - #1225

Merged
backnotprop merged 2 commits into
mainfrom
feat/url-host
Aug 7, 2026
Merged

feat: add PLANNOTATOR_URL_HOST display-only override for advertised URLs#1225
backnotprop merged 2 commits into
mainfrom
feat/url-host

Conversation

@backnotprop

@backnotprop backnotprop commented Aug 6, 2026

Copy link
Copy Markdown
Owner

TLDR: adds PLANNOTATOR_URL_HOST (config key urlHost), 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 of http://localhost:<port>. Closes #657.

Behavior

Remote mode (PLANNOTATOR_REMOTE=1) already binds 0.0.0.0, so the server is reachable over Tailscale. But every advertised URL hardcoded http://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_HOST env var takes precedence, then ~/.plannotator/config.json { "urlHost": "..." }, else localhost. Resolution lives in resolveUrlHost() in packages/shared/config.ts, following the existing resolveSharingEnabled pattern.
  • Host only: bare hostnames (MagicDNS names), IPv4, and bracketed IPv6 ([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 to localhost; a display setting never crashes a server launch.
  • buildAdvertisedUrl(port) composes the URL in packages/server/remote.ts (Bun) with a behaviorally identical mirror in apps/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.
  • Remote-mode terminal copy adapts: with an override the message becomes "open on your device: " (Bun shared-handlers.ts, Pi index.ts), since port-forwarding advice is wrong for a directly reachable host. Without an override the existing copy is unchanged.
  • Local (loopback-bound) sessions ignore the override entirely: localhost is advertised and opened, with a once-per-process stderr warning that PLANNOTATOR_REMOTE=1 is 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.
  • The env var takes precedence over the config key, and an empty-but-set env var (PLANNOTATOR_URL_HOST=) suppresses a config-file urlHost.
  • The invalid-host warning JSON-encodes the echoed value, so a newline embedded in the env var cannot forge extra stderr lines (hosts surface session-ready stderr lines as clickable links).

Security stance

Strictly display-only. The override never touches getServerHostname() or any listening behavior; only PLANNOTATOR_REMOTE governs binding. Spawned agent-review jobs run on the same machine, so their PLANNOTATOR_API_URL is now explicitly pinned to http://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 in packages/shared/config.test.ts.
  • buildAdvertisedUrl: host+port composition including bracketed IPv6, invalid-host fallback, and a bind-hostname non-interference check, in both packages/server/remote.test.ts and apps/pi-extension/server/network.test.ts.
  • Local-session ignore, single-line warning, and empty-env suppression each have direct tests; the server/shared/pi suites also pass with a hostile override planted (PLANNOTATOR_URL_HOST=unreachable.invalid, 1489 pass / 0 fail).
  • Full suite: 2960 pass, 269 skip, 0 fail (3229 tests across 270 files); bun run typecheck clean after vendor.sh regeneration.

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.

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.
@backnotprop backnotprop closed this Aug 7, 2026
@backnotprop backnotprop reopened this Aug 7, 2026
@backnotprop
backnotprop merged commit b69742c into main Aug 7, 2026
22 of 23 checks passed
@backnotprop
backnotprop deleted the feat/url-host branch August 7, 2026 01:29
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.

Feature request: Support network-accessible URLs for remote/Tailscale sessions

1 participant