mirror of
https://github.com/Ark0N/Codeman.git
synced 2026-09-30 12:39:42 +02:00
The `Run > DeepSeek web UI...` shortcut failed three ways at once against a real
install, and the three are independent.
1. It hardcoded `--port 3080`. That is dsh web's OWN default, which makes it
precisely the port a DeepSeek user is most likely to be serving on already,
so the launch died with EADDRINUSE against the user's own server. The port
now comes from `GET /api/deepseek/web-port`, which walks 3080..3119 for a
free loopback port by BINDING it (a connect probe cannot tell "free" from
"listening but not answering yet").
2. It opened the tab unconditionally. The crashed server left a saved dashboard
pointing at nothing, with the failure only visible in a shell tab nobody had
a reason to look at. The launch now polls the existing webview probe until
the URL answers, and on timeout reports the error naming the shell tab
instead of persisting a dead dashboard.
3. The saved tab was untrusted, so the frame was sandboxed without
`allow-same-origin` and the dashboard was broken twice over: the dsh
client-runtime reads `localStorage` while loading its plugins and died there
("the document is sandboxed and lacks the 'allow-same-origin' flag"), and an
opaque-origin frame sends `Origin: null`, so dsh's own trust fence 403'd
every `/api` call no matter which authority `--trusted-host` named. Passing
`location.host` only means anything once the frame actually carries that
origin, so `--trusted-host` had never once done its job. The managed tab is
now created `trusted: true`.
That trade is real and deliberate: a trusted proxied frame is same-origin
with Codeman and can reach Codeman's API. It is defensible only because this
dashboard is an agent harness Codeman just started itself, on loopback, which
can already run code as the user. It is not a precedent for trusting
third-party dashboards, which is why it is set at this one call site rather
than defaulted.
Separately, the shortcut listed its own dashboard twice: once as the menu entry
that starts it and once as the row that entry had written on the previous click.
Webviews now carry an optional `managed` marker, managed rows are filtered out
of the saved-dashboard list, and a relaunch repoints the existing row rather
than stacking one dead dashboard per restart (which the per-launch port would
otherwise guarantee). `managed` is declared in the schema because a plain
`z.object` strips undeclared keys, so an undeclared marker would never survive
the round trip.
`DEEPSEEK_WEB_PORT` is gone from constants.js; its doc comment asserted that a
hand-started `dsh web` and the shortcut "land on the same place and share one
saved tab", which is the bug stated as a feature.
Verified on a real install with the user's own `dsh web` holding 3080: the
shortcut takes 3081, the server answers, exactly one DeepSeek entry shows in the
run menu, and the proxied dashboard renders its workspaces and completes its own
API calls (the previously-403'd `api/settings.describe` now succeeds). Full gate
green (6142 passed), typecheck/lint/format/public-assets clean.
104 lines
4.2 KiB
TypeScript
104 lines
4.2 KiB
TypeScript
/**
|
|
* @fileoverview Web tab (dashboard) types.
|
|
*
|
|
* A "webview" is a saved URL that Codeman renders as a tab alongside agent
|
|
* sessions: Grafana on :3000, a Uptime-Kuma on :4000, an internal status page.
|
|
* It is deliberately NOT a sixth `SessionMode`, it has no PTY, no tmux, no
|
|
* respawn and no idle detection. Same reasoning that keeps Docker and remote-SSH
|
|
* as case overlays rather than modes.
|
|
*
|
|
* Key exports:
|
|
* - Webview, the persisted record (`~/.codeman/webviews.json`).
|
|
* - WebviewEmbedMode, 'proxy' (served through Codeman's origin) or 'direct'
|
|
* (a plain cross-origin iframe, only viable for HTTPS targets that allow framing).
|
|
* - WebviewProbe, the result of the server-side reachability/framing probe.
|
|
* - WebviewOpenData, what `POST /api/webviews/:id/open` hands the browser.
|
|
*
|
|
* No I/O here. Persistence lives in `src/webview-store.ts`, capability minting in
|
|
* `src/webview-capabilities.ts`, the proxy helpers in `src/web/webview-proxy.ts`.
|
|
*/
|
|
|
|
/**
|
|
* How the browser should embed a webview.
|
|
*
|
|
* - `proxy`: the iframe points at `/webview/<capability>/` on Codeman's own
|
|
* origin and the server relays to the target. Required whenever the target is
|
|
* plain HTTP (an HTTPS Codeman page cannot embed it: mixed content) or refuses
|
|
* framing via `X-Frame-Options` / `frame-ancestors`.
|
|
* - `direct`: the iframe points at the target URL itself. Cheaper, but only works
|
|
* for HTTPS targets that permit framing, and needs the target origin added to
|
|
* the page CSP's `frame-src`.
|
|
*/
|
|
export type WebviewEmbedMode = 'proxy' | 'direct';
|
|
|
|
/** A saved dashboard, persisted to `~/.codeman/webviews.json`. */
|
|
/**
|
|
* Dashboards Codeman creates and maintains on the user's behalf.
|
|
*
|
|
* A managed record is hidden from the saved-dashboard list, because the shortcut
|
|
* that maintains it is already a menu entry of its own: listing both showed the
|
|
* same dashboard twice, once as "DeepSeek web UI..." and once as the row it had
|
|
* just written.
|
|
*/
|
|
export type WebviewManagedKind = 'deepseek-web';
|
|
|
|
export interface Webview {
|
|
id: string;
|
|
/** Display name shown on the tab. */
|
|
name: string;
|
|
/** Absolute target URL. `http:` / `https:` only, never with embedded credentials. */
|
|
url: string;
|
|
/** Optional single-glyph tab icon (emoji or letter). */
|
|
icon?: string;
|
|
/** Default embed strategy for this dashboard. */
|
|
embedMode: WebviewEmbedMode;
|
|
/**
|
|
* When false (the default) the iframe is sandboxed WITHOUT `allow-same-origin`,
|
|
* so a proxied page runs in an opaque origin and cannot read the Codeman page or
|
|
* call its API. Setting this to true trades that isolation for the page's own
|
|
* cookies/localStorage, only for dashboards the user fully trusts.
|
|
*/
|
|
trusted: boolean;
|
|
/**
|
|
* Set when Codeman owns this record rather than the user (see
|
|
* `WebviewManagedKind`). Managed rows are maintained by the shortcut that
|
|
* created them, including repointing the URL when the port changes.
|
|
*/
|
|
managed?: WebviewManagedKind;
|
|
/** Multi-user owner (username). Undefined in single-user mode. */
|
|
owner?: string;
|
|
createdAt: number;
|
|
lastOpenedAt?: number;
|
|
}
|
|
|
|
/** Result of the server-side probe used by the "Test" button in the editor. */
|
|
export interface WebviewProbe {
|
|
/** True when the server could complete an HTTP request to the target. */
|
|
reachable: boolean;
|
|
/** Upstream status code, when a response came back. */
|
|
status?: number;
|
|
/** Raw `X-Frame-Options` value, if the target sent one. */
|
|
xFrameOptions?: string;
|
|
/** The `frame-ancestors` directive extracted from the target's CSP, if any. */
|
|
frameAncestors?: string;
|
|
/** True when the target permits being framed cross-origin by this Codeman. */
|
|
framable: boolean;
|
|
/** Strategy the UI should default to for this URL. */
|
|
recommendedMode: WebviewEmbedMode;
|
|
/** Human-readable explanation of the recommendation (or the failure). */
|
|
reason: string;
|
|
}
|
|
|
|
/** Payload of `POST /api/webviews/:id/open`. */
|
|
export interface WebviewOpenData {
|
|
/** The webview being opened (echoed so the client can refresh its copy). */
|
|
webview: Webview;
|
|
/**
|
|
* Same-origin path the iframe should load. Present for `proxy` mode only;
|
|
* `direct` mode uses `webview.url` instead.
|
|
*/
|
|
embedUrl?: string;
|
|
/** Epoch ms at which the capability behind `embedUrl` stops working. */
|
|
expiresAt?: number;
|
|
}
|