mirror of
https://github.com/Ark0N/Codeman.git
synced 2026-09-30 12:39:42 +02:00
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>
156 lines
9.0 KiB
Markdown
156 lines
9.0 KiB
Markdown
# 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` |
|