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]>
43 lines
1.5 KiB
TypeScript
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",
|
|
};
|
|
}
|