agent-orchestrator/packages/web
Varich ecdf0c73ec
feat(cli): support AO_PUBLIC_URL for reverse-proxied dashboards (#1757)
* feat(cli): support AO_PUBLIC_URL for reverse-proxied dashboards

When AO runs inside a remote dev container or behind a reverse proxy
(Caddy/nginx/Traefik), `http://localhost:${port}` was hardcoded across
the CLI for console output, `ao open` browser launches, and the
session URLs surfaced to the orchestrator agent. None of those URLs
were reachable from outside the host.

Add an `AO_PUBLIC_URL` env var. When set, the new `dashboardUrl(port)`
helper returns it (with trailing slashes stripped) instead of the
localhost fallback. The helper replaces every user-facing
`http://localhost:${port}` literal in:

- `commands/dashboard.ts` — startup banner + browser open
- `commands/start.ts` — 12 spots: spinner, "Dashboard:" prints,
  orchestrator URL fallback, `openUrl()` calls, and the running-state
  reuse paths
- `lib/routes.ts` — `projectSessionUrl()` (used in the orchestrator
  prompt template, so worker links land on the public hostname)

Internal IPC (`lib/daemon.ts` calling its own dashboard's
`/api/projects/reload`) is intentionally left on localhost — that
traffic never leaves the host, and routing it through a public URL
would just add latency and a failure surface.

Tests cover the env-var/localhost paths, whitespace trimming,
trailing-slash stripping, sub-path preservation, and non-default-port
URLs (`__tests__/lib/dashboard-url.test.ts`, 10 cases).

Setup guide gets a new "Public dashboard URL" entry under optional
env vars.

* docs: cover TERMINAL_WS_PATH + path-based mux routing in AO_PUBLIC_URL setup

The AO_PUBLIC_URL entry only mentioned terminal ports needing to be
reachable, which over-specifies what's required when fronting AO with
HTTPS through a reverse proxy. The dashboard's MuxProvider already
auto-detects standard ports (`loc.port === ""`/`"443"`/`"80"`) and
routes the mux WebSocket through `/ao-terminal-mux` on the same
hostname, so a single proxy rule pointing at the dashboard port is
sufficient — no extra subdomain or port forwarding for the WS.

For non-standard ports or custom paths, document the existing but
previously-undiscoverable `TERMINAL_WS_PATH` env var (read by
`/api/runtime/terminal/route.ts` and threaded through `MuxProvider`
as `proxyWsPath`).

Adds a minimal Caddy snippet so users have a working starting point.

* feat(web): accept /ao-terminal-mux as alias for /mux on direct-terminal-ws

The dashboard's MuxProvider already constructs `wss://hostname/ao-terminal-mux`
when accessed on a standard HTTPS port (443), but until now nothing on the
server side recognized that path — direct-terminal-ws only matched `/mux`,
and the Next.js dashboard doesn't handle WS upgrades at all. Deployments
fronted by a path-routing reverse proxy (cloudflared, nginx, Caddy, …) hit
the server at `/ao-terminal-mux`, fall through to Next.js, get a 404, and
the dashboard's terminal panes hang at "Connecting…" forever.

Fix is one line in the upgrade-routing allow-list: accept `/ao-terminal-mux`
in addition to `/mux`. The proxy can now route the path-based mux URL straight
at DIRECT_TERMINAL_PORT without needing a path-rewrite rule (which most
proxies — including cloudflared — don't natively support).

Existing `/mux` clients continue to work; the alias is strictly additive.
SETUP.md's AO_PUBLIC_URL section is updated to mention the path requirement
in one sentence, and a new integration test pins the behavior.

* feat(web): opt-in single-port mode (AO_PATH_BASED_MUX) for proxy-only deployments

Default behavior unchanged. When AO_PATH_BASED_MUX=1, start-all spawns a
small bundled HTTP/WS proxy on PORT that demultiplexes:

  - HTTP requests forwarded to Next.js (shifted to PORT + 1000;
    override with NEXT_INTERNAL_PORT)
  - `wss://hostname/ao-terminal-mux` upgrades tunneled to
    DIRECT_TERMINAL_PORT/mux

Use it when the reverse proxy in front of AO can only forward one
hostname:port pair upstream (e.g. Cloudflare Tunnel pointed at a single
`service:` URL with no path-based ingress, or a managed-app platform
where you don't control the proxy config). One proxy rule then
suffices — the WS path is multiplexed onto the same TCP port and
demuxed inside the AO process.

Tradeoff: one extra Node process and one extra hop per HTTP request,
in exchange for proxy-config simplicity. For deployments that *can*
do path-based routing the alias added in the previous commit
(direct-terminal-ws accepting `/ao-terminal-mux` on its own port) is
the lower-overhead path.

The new server is pure Node http; no `next` import or other extra
dependencies. It's strictly opt-in — the env-var gate keeps the code
inert by default, so existing deployments see no behavior change and
no extra startup cost.

* fix(web): correct single-port proxy header handling, WS hangs, shutdown

Addresses review feedback on single-port-server.ts:

- Strip hop-by-hop headers (RFC 9110 §7.6.1) before forwarding upstream,
  including any extras named in the client Connection header. Previously
  the whole header set was copied verbatim, so a client Connection: close
  could tear down the keep-alive socket to Next.js.
- Add X-Forwarded-For/-Proto/-Host so the upstream sees the real client
  instead of 127.0.0.1; existing values from an outer proxy are preserved.
- Handle non-101 upstream responses on the WS upgrade path. The proxy only
  listened for 'upgrade', so a 404/502/mid-restart response left the client
  socket hanging until TCP timeout. A 'response' handler now relays the
  status and closes the connection.
- Call server.closeAllConnections() on shutdown. server.close() alone waits
  for keep-alive HTTP sockets and piped WS tunnels to drain on their own,
  which they never do, so shutdown always hit the 5s force-exit timer.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* test(web): cover single-port proxy + fix response-direction headers

Follow-up to the previous commit's review fixes, adding regression
coverage so future changes can't silently break the proxy.

- Refactor single-port-server.ts into an exported createSinglePortServer()
  factory (mirrors direct-terminal-ws.ts) with a thin isMainModule()
  entrypoint, so start-all.ts still spawns it as a script while tests can
  drive it in-process against fake upstreams.
- Add single-port-server.integration.test.ts (5 tests, no tmux/Next.js
  needed — runs on CI/Windows): hop-by-hop strip + X-Forwarded-*, 502 on
  dead upstream, /ao-terminal-mux WS tunnel, non-101 upgrade relay, and
  prompt shutdown with a live WS connection.
- The shutdown test caught that server.closeAllConnections() does NOT
  destroy sockets already handed off via the 'upgrade' event — track
  upgraded sockets explicitly and destroy them in shutdown().
- The header test caught the symmetric response-direction leak: the proxy
  forwarded the upstream's Connection/Keep-Alive to the client, overriding
  a client that asked for Connection: close. Strip hop-by-hop from upstream
  responses too via filterResponseHeaders().

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Priyanshu Choudhary <57816400+Priyanchew@users.noreply.github.com>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-20 23:09:04 +05:30
..
e2e Add orchestrator-driven code review board (#1871) 2026-05-19 11:47:57 +05:30
public Fix web imports for service worker and project utils 2026-03-25 14:08:54 +05:30
screenshots fix: polish shimmer states, terminal PR inference, and gitignore .gstack 2026-04-06 22:49:09 -07:00
scripts fix: protect live dashboard artifacts (#1598) 2026-05-01 16:09:57 +05:30
server feat(cli): support AO_PUBLIC_URL for reverse-proxied dashboards (#1757) 2026-05-20 23:09:04 +05:30
src fix(web): remove XDA chip from terminal header (#1963) 2026-05-20 21:57:28 +05:30
.env.local.example feat: configurable terminal server ports for multi-dashboard support (#113) 2026-02-19 04:00:19 +05:30
.gitignore Normalize dashboard project filtering and align session layout 2026-03-25 14:34:55 +05:30
CHANGELOG.md fix(agent-plugins,lifecycle): distinguish indeterminate probe from "not found" + bump ps timeout (closes #1838) (#1839) 2026-05-14 21:50:39 +05:30
eslint.config.js fix: resolve three Next.js build warnings in web package (#1087) 2026-04-17 10:43:01 +05:30
next-env.d.ts feat: implement web dashboard with attention-zone UI and API routes (#1) 2026-02-14 14:26:59 +05:30
next.config.js fix(web): externalize ao-core and better-sqlite3 from Next.js bundle (#1944) 2026-05-20 18:19:28 +05:30
package.json Add orchestrator-driven code review board (#1871) 2026-05-19 11:47:57 +05:30
postcss.config.mjs feat: implement web dashboard with attention-zone UI and API routes (#1) 2026-02-14 14:26:59 +05:30
tsconfig.json fix: terminal servers compatible with hash-based architecture (#87) 2026-02-18 03:28:55 +05:30
tsconfig.server.json fix: scope node types to node packages 2026-04-13 18:25:21 +05:30
vitest.config.ts fix: register codex web activity plugin 2026-04-18 14:15:29 +05:30