Files
Codeman/docs/web-tabs.md
T
Codeman maintainer 0d0b772619 feat: make Antigravity a first-class CLI across docs, installer and UI
Antigravity (agy) was wired into the session layer but never propagated to
the surfaces around it, while Gemini CLI stayed documented as a consumer
product despite being enterprise-only since Google's cutover. Gemini keeps
full support; Antigravity now sits beside it everywhere.

Functional fixes:
- docker/agent.Dockerfile never installed agy, so a docker case with
  mode 'antigravity' died on command-not-found. agy is not on npm, so it
  gets its own installer step. --dir /usr/local/bin is load-bearing: the
  default $HOME/.local/bin resolves to root's home at build time and is
  unreachable by the `agent` user the container runs as. Verified inside
  codeman/agent:base (v1.1.10, reachable as `agent`). Note the binary is
  ~190MB, the largest layer in the image.
- Welcome screen gained a Run Antigravity action, gated on agy being
  present like the other CLI buttons, with a cyan identity matching the
  toolbar run button and run-mode dot.
- install.sh now detects agy (search paths mirroring the resolver), counts
  it as a satisfying AI CLI, and recommends it over Gemini in the install
  hints. Detection only, no new auto-install path.

Docs corrected where they were factually wrong:
- architecture-invariants documented isExternalCliMode() as
  opencode/codex/gemini when the code has included antigravity for a
  while, said "all three modes", and omitted ANTIGRAVITY_ from the env
  prefix allowlist row.
- cron-guide's agentType enum, cron-discovery's SessionMode, and
  remote-sessions' RemoteCommandMode were all stale.

Also: README + README.zh-CN (five CLIs, Gemini marked enterprise-only),
package.json keyword, and comment drift in 8 places.

test/run-mode-ui.test.ts now covers the new welcome button; verified it
fails without the settings-ui wiring.

Antigravity nests its whole state under ~/.gemini/antigravity-cli/, not
~/.antigravity, so the existing .gemini docker credential seed already
covers it. Recorded as a comment so nobody adds dead config later.

isAltScreenStripMode() deliberately still excludes antigravity: whether
its TUI needs the alt-screen strip is a behavioural question that needs a
real agy session, not a guess.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-06 07:22:29 +02:00

9.0 KiB

Web Tabs (dashboards as Codeman tabs)

Open any dashboard you run, Grafana, Uptime Kuma, Portainer, a status page on port 4000, as a tab beside your Claude/Codex/Antigravity sessions. Codeman becomes one mission control instead of Codeman plus a pile of browser tabs.

Using it

  1. Click the chevron next to Run to expand the dropdown.
  2. Under Web / URL, pick Add dashboard...
  3. Give it a name and a URL, optionally hit Test, then Save.

The dashboard opens as a tab immediately, and appears in the Run dropdown from then on. Web tabs sit in the same strip as session tabs, continue the same Alt+1..9 numbering, and carry a globe icon so they never read as a running agent.

Closing a tab (the x) only closes it. The saved dashboard stays in the dropdown. To delete it for good, use the x on its dropdown row (the tab's own x is close, not delete). Each dropdown row also has a gear for editing, so a saved URL can be changed or removed without opening it first.

Switching tabs does not reload a dashboard. Frames stay alive in the background, so a dashboard that took a while to authenticate is still there when you come back. Past six live frames the least-recently-viewed one is dropped to bound memory (CODEMAN_MAX_LIVE_WEBVIEW_FRAMES).

Why dashboards are proxied

A plain <iframe src="http://your-box:4000"> does not work in the setup Codeman actually ships in, for three separate reasons:

Blocker What happens
Mixed content Production serves HTTPS (behind tailscale serve). Browsers hard-block http:// iframes on an HTTPS page, with no override, and none at all on iOS Safari.
Framing refusal Grafana, Portainer, Home Assistant and many others send X-Frame-Options: DENY or frame-ancestors 'none'.
Codeman's CSP default-src 'self' means frame-src falls back to 'self', so a cross-origin iframe is blocked before it starts.

Serving the dashboard through Codeman's own origin dissolves all three. So by default a web tab loads /webview/<capability>/ on Codeman, and Codeman relays to the dashboard: stripping the framing refusal, rewriting redirects, cookies and root-absolute URLs, and relaying WebSockets so live panels actually update.

A useful consequence: the dashboard is fetched from the Codeman server, so a tailnet-only or localhost-only dashboard works from any device that can reach Codeman, including a phone that is not on the tailnet.

direct mode (a plain cross-origin iframe) still exists and is cheaper, but it only works for an HTTPS dashboard that permits framing. The Test button probes from the server and tells you which mode applies.

The sandbox, and when to turn it off

Because a proxied dashboard is served from Codeman's own address, it is same-origin with Codeman as far as the browser is concerned. Left unchecked, its JavaScript could read the Codeman page and call the API that spawns agents.

So the iframe is sandboxed without allow-same-origin by default. The page runs in an opaque origin: it cannot touch Codeman, and it gets no cookies or localStorage of its own.

Unchecking Open sandboxed grants allow-same-origin. Do that only for a dashboard you fully trust, and only if you need it, which in practice means a dashboard with its own login that stores a session in a cookie or localStorage.

Even in trusted mode, Codeman never forwards its own credentials upstream: the Authorization header and the codeman_session cookie are stripped on the way out, so CODEMAN_PASSWORD cannot leak into a dashboard.

How the proxy authenticates

A sandboxed iframe is opaque-origin, so every request it makes is cross-site: the SameSite=lax session cookie is not sent, and writes and WebSocket upgrades arrive with Origin: null. Cookie auth cannot work.

Instead, opening a dashboard mints a capability: 192 bits of entropy in the URL path, held in memory only, with a rolling 12-hour TTL, bound to the user who minted it, and granting exactly one thing, relaying bytes to that one saved URL. Editing or deleting a dashboard revokes it, and a server restart invalidates every outstanding capability (tabs re-mint transparently on next click).

Limits and env vars

Variable Default Meaning
CODEMAN_MAX_WEBVIEWS 50 Saved dashboards per owner
CODEMAN_MAX_LIVE_WEBVIEW_FRAMES 6 Iframes kept mounted at once
CODEMAN_WEBVIEW_CAPABILITY_TTL_MS 12h Rolling capability lifetime
CODEMAN_WEBVIEW_TIMEOUT_MS 30000 Upstream request timeout
CODEMAN_WEBVIEW_PROBE_TIMEOUT_MS 8000 Timeout for the Test button
CODEMAN_MAX_WEBVIEW_HTML_BYTES 8MB Largest HTML document rewritten
CODEMAN_MAX_WEBVIEW_SOCKETS 8 Concurrent proxied WebSockets per dashboard

Saved dashboards live in ~/.codeman/webviews.json. Which tabs you have open is per-device (localStorage), since that is workspace layout rather than config.

How a dashboard's own API calls keep working

Worth knowing, because it is where this feature does its least obvious work. Three layers cooperate so a dashboard talking to its own backend just works:

  1. <base href> handles relative URLs in the markup.
  2. Attribute rewriting handles root-absolute src/href/action in the page the proxy serves.
  3. A small injected script rebases URLs built at runtime, which the first two cannot see: fetch('/api/data') and new WebSocket('/live'), but equally card.innerHTML = '<img src="/api/hero">', img.src = '/api/slide', and url(/img.png) inside a <style> the page injects. That second group is why images are covered too. A dashboard that renders its thumbnails from script would otherwise show all its data and none of its pictures, because <base> does not apply to root-absolute URLs and the attribute rewriting only ever saw the initial document.
  4. As a last resort, a request that still lands on Codeman's own root is relayed using its Referer to identify the dashboard. This only fires for a request that already missed every Codeman route, and never for one that resolves to a real route, which is what keeps it from being an authentication bypass.

On top of that, the proxy answers those requests with CORS headers. That sounds wrong for same-host requests, but a sandboxed iframe has an opaque origin, so the browser treats every one of its fetch/XHR calls as cross-origin even though the URL is on Codeman itself. Without those headers, a dashboard renders perfectly and then every API call fails, which looks like the dashboard being broken.

Known limits

  • Exotic loaders. The layers above cover normal fetch/XHR/WebSocket/ EventSource, normal markup, the DOM sinks a page uses to build markup at runtime, and url() inside stylesheets. Something that constructs requests by an unusual route can still slip through. Symptom: the page renders but a panel stays empty.
  • Root-absolute location navigation. A dashboard that navigates itself with location.href = '/login' escapes the prefix, because Location.href is unforgeable and cannot be patched the way the other sinks are. A relative location.href = 'login' is fine (<base> covers it).
  • Cross-origin redirects are not followed. If a dashboard bounces to a different host (an external SSO provider, say), the proxy hands the redirect back unchanged rather than relaying it, because relaying would make this an open proxy. Use Open in new tab for those.
  • Login-protected dashboards need trusted mode, since a sandboxed frame has no cookie jar. A server-side per-dashboard cookie jar would lift this and is the natural next step if it becomes annoying.
  • Not a security boundary. The proxy reaches whatever the Codeman server can reach. That is not an escalation for someone who already commands --dangerously-skip-permissions agents, but in multi-user mode it does mean a non-admin user's dashboard is fetched from the server's network position.

Where the code lives

Concern File
Pure rewrite helpers src/web/webview-proxy.ts
Routes + proxy + sockets src/web/routes/webview-routes.ts
Capability tokens src/webview-capabilities.ts
Persistence src/webview-store.ts
Limits src/config/webview-limits.ts
Frontend src/web/public/webview-tabs.js
Auth exemption src/web/middleware/auth.ts