Repository navigation
feat(file-preview): render local HTML files as sandboxed pages - #547
ideaCompany wants to merge 3 commits into
Conversation
Clicking an agent's `.html` path showed source text or downloaded it, so a generated report could only be viewed by asking the agent to start a web server. Render it instead, without serving HTML same-origin: - POST /api/sessions/:id/html-view mints a capability for the file's own directory (workspace path, or outside it under the attachment guard). - GET /html-view/:cap/* serves the page and its relative assets with `Content-Security-Policy: sandbox` WITHOUT allow-same-origin, so the page runs in an opaque origin (also as a top-level tab) and cannot read the Codeman document, its cookies or its API. - Served files: non-dot, asset-allowlisted, realpath-confined to that directory, blocklist applied; GET only. Capabilities are memory-only with a rolling TTL and are revoked with the web-tab ones on logout. - Frontend: the file preview renders HTML in a sandboxed iframe (popout opens the same URL); terminal `.html` links go to the preview, not the log viewer. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
|
Thanks a lot for this, @ideaCompany, and for writing up the security argument so carefully. The PR renders local The opaque-origin design holds up. I checked that the auth exemption only admits GET and HEAD with a live capability (POST and unknown capabilities still get 401), that 1. The capability serves a whole directory tree to the page's own scripts, with no egress limit ( The route replaces Codeman's CSP with
2. HTML loses its source view and the File Viewer editor ( The new branch returns before the 3. The new auth exemption and revocations need tests ( The route tests are good, but nothing pins the middleware side. Please add html-view cases to 4. Docs that now say the opposite
Smaller things (I can take these at merge time if you prefer):
On network access for rendered pages: whether a page keeps open network access (agent dashboards often load Chart.js from a CDN) or is limited to Codeman's own origin is a policy decision for me, and I will post it here before you touch the CSP. Items 1 (scope), 2, 3 and 4 do not depend on it. Once those are in, I will re-review and merge. |
- Refuse to mint for an HTML file in a hidden directory or directly in a broad root (/, home, tmpdir, cases and user-space roots): the capability serves the directory tree recursively to the page's own scripts. - Keep HTML's source view and editor: a Page pill (per-device htmlRendered, like MD) toggles to the file-content text path, and a refused mint (remote case, broad root, hidden dir) falls through to it automatically. - Tests: html-view auth exemption edges, revocation on logout / admin logout / user deletion, out-of-workspace mint (blocked tree, confinement), hidden and broad roots. - Docs: Working-With-Files, architecture-invariants, security-architecture, the file-routes comment and CLAUDE.md. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
|
Thanks for the thorough review, and for reproducing the scope problem. I had missed that one. Items 1 to 4 are in, and master is merged in (it was 41 commits behind, no conflicts). I've left the CSP alone and will wait for your decision on network access. 1. Scope
2. Source view and editor
3. Tests
4. Docs I've left the smaller items (Range support or dropping media types, the Checks: typecheck, lint, format, frontend syntax and browser-excludes are clean, and the full |
|
Thanks for the quick turnaround, @ideaCompany. This PR renders a clicked I checked round 2 against the code, and all four items from my last review are in. The design holds up under probing: crafted Three things before this goes in: 1. The broad-root check misses
2. Symlinks get around the dot and extension rules ( The dot check and the extension allowlist run on the requested name; after 3. A test for the frontend half ( The HTML branch in
A small one while you are in the route: the serving route checks the blocklist but not I will take these at merge time, together with last round's list (Range support, the
On network access: I still owe you the decision on what rendered pages may reach, and I will post it here separately. It does not change the three items above. Once 1 to 3 are in, I will re-review and merge. |
Why
Agents produce HTML all the time: test and coverage reports, benchmark charts, generated docs, prototypes, the "here is a dashboard of what I found" page. Today, when an agent prints the path to one of those files, Codeman can't show it:
.htmlpath opens in the log viewer as raw source;On the machine running Codeman you can work around that by opening the file yourself. From any other device, the phone and remote-browser setups Codeman is built for, there is no workaround short of asking the agent to start a web server and then opening its
localhostURL through a web tab. Codeman already makes remotelocalhostdev servers viewable through the web-tab proxy. This PR closes the matching gap for plain HTML files, which is the more common case for agent output.I know
html/htmbeing download-only is a deliberate invariant ("widening READ never widens RUN"). This PR does not touch it:file-raw, the attachment routes and the text preview still never serve HTML as HTML. It adds one separate, narrowly fenced surface whose whole design is "render it, but never same-origin".What it does
Clicking an
.htmlpath (terminal, response viewer, Files panel) opens the page rendered in the existing file-preview overlay, with its relative CSS, scripts, images and data files working. The overlay's popout button opens the same page in its own tab.How it stays safe
The danger with rendering HTML from Codeman's origin is that the page could read the Codeman document and drive the agent-spawning API. The design removes that rather than mitigating it:
/html-viewresponse carriesContent-Security-Policy: sandbox allow-scripts allow-forms allow-popups allow-modals allow-downloads, with noallow-same-origin. Because it is a response header, not just an iframe attribute, the page is opaque-origin even when opened as a top-level tab. Verified in Chromium:self.origin === 'null',window.parent.documentanddocument.cookiethrow, andfetch('/api/sessions')from the page is blocked.SameSite=laxcookie, so the route authenticates the same way the web-tab proxy does:POST /api/sessions/:id/html-view(normal auth,findSessionOrFailownership) mints a 192-bit, memory-only capability with a rolling TTL. It is revoked on logout, admin logout and user deletion, next towebviewCapabilities.revokeOwner.validateSessionFilePath, or outside it under the attachment guard (blocked trees,attachmentConfineToWorkspace). Remote (SSH) cases get a clear 400 for now.GET/HEADon/html-view/<live cap>/…; nothing else gains a bypass. The production CSP for every other route is unchanged.The one header that widens something is
Access-Control-Allow-Origin: *(no credentials) on/html-viewresponses. It is needed because the opaque-origin page'sfetch('data.json')of its own sibling file is CORS-checked withOrigin: null. It exposes nothing beyond what the capability URL already serves.Changes
src/html-view-capabilities.ts: capability store (mirrorswebview-capabilities.ts) plus the path parser.src/web/routes/html-view-routes.ts: mint route and serving route.src/web/middleware/auth.ts:hasValidHtmlViewCapability()exemption in both auth branches.session-routes.ts/admin-routes.ts: revoke on logout, admin logout and user delete.panels-ui.js: HTML branch inopenFilePreview()(sandboxed iframe; popout uses the same URL).terminal-ui.js:.htmlterminal links go to the preview instead of the log viewer.CLAUDE.md: the file-path-links invariant now names/html-viewas the one place HTML renders.Known limits
/style.css) do not resolve; relative ones do. Agent-generated pages almost always use relative paths.Testing
test/routes/html-view-routes.test.ts(new, real temp files, no fs mocks): sandbox CSP withoutallow-same-origin, sibling and subdirectory assets served, and 404 for.env,assets/../.env,%2e%2e/…,..%2f…, non-asset types and a symlink pointing outside the directory. Also covers unknown and revoked capabilities, non-HTML and missing files, and capability reuse.npm test: 457 files / 8871 tests pass.typecheck,lint,format:check,check:frontend-syntaxandcheck:browser-excludesare clean.fetch()and an image; the sandbox checks listed above hold. Remotelocalhostlinks through web tabs were checked too, unchanged.If you'd rather discuss the design first (the contributing guide asks for that on bigger changes), I'm happy to move this to a Discussion. It is opened as a PR because the security argument is easier to judge with the actual code and tests in front of you.
🤖 Generated with Claude Code