Files
cobble/src/utils/graphic-headers.ts
T
tbenandClaude Opus 5.5 bea656ade8 Add bot graphics
Bots can now show a concept as a visual: create_graphic (with
update_graphic, read_graphic and list_graphics, on every run, no computer
needed) stores one HTML document in bot_graphics, per user like the
conversation, and the chat renders its [graphic:ID] ref as an inline card
sized to fit, with a large view and a page of its own (no rail).

Graphic HTML is treated as hostile: it is only served at /graphics/<id>
with a CSP sandbox allowing scripts under an origin of its own and no
network, loading only the vendored Chart.js and Mermaid from
/graphics-lib/. The document carries the app's theme vars and scrollbars,
reports its height to the thread, and a frame that navigates away is
closed.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-27 08:11:15 +01:00

43 lines
1.5 KiB
TypeScript

import { GraphicsLibPrefix } from "@/src/dict/graphics-dict";
type Params = {
/**
* The app's origin as the browser reached it, e.g.
* `https://cobble.example.com`: the only place a graphic loads scripts
* from, and only its libraries.
*/
origin: string;
};
/**
* The headers a bot graphic is served with. Its HTML is a bot's and may
* carry what a prompt injection put there, so it gets:
*
* - `sandbox allow-scripts` without `allow-same-origin`: scripts run (for
* charts) but under an origin of its own, so no cookie, storage or API of
* the app's is reachable.
* - no network (`default-src 'none'`, `connect-src 'none'`): nothing it
* holds can be sent anywhere, and only the app's graphic libraries load.
* - framed only by the app (`frame-ancestors 'self'`), never cached.
*/
export default function graphicHeaders({ origin }: Params): Record<string, string> {
return {
"Content-Type": "text/html; charset=utf-8",
"Content-Security-Policy": [
"sandbox allow-scripts",
"default-src 'none'",
`script-src 'unsafe-inline' ${origin}${GraphicsLibPrefix}/`,
"style-src 'unsafe-inline'",
"img-src data: blob:",
"font-src data:",
"connect-src 'none'",
"form-action 'none'",
"base-uri 'none'",
"frame-ancestors 'self'",
].join("; "),
"X-Content-Type-Options": "nosniff",
"Referrer-Policy": "no-referrer",
"Cache-Control": "private, no-store",
};
}