UNPKG

@adonis-agora/agent

Version:

Governed, durable-backed AI agent (chat + tool-calling + governance) for AdonisJS — part of the Agora ecosystem.

303 lines • 15.9 kB
/** * Sandboxed generated UI: a catalog component whose props are HTML, CSS and JavaScript the model * writes for a one-off interactive answer (a calculator, a small simulation, a bespoke chart) when * no catalog component fits. Opt-in — add {@link Sandbox} (or {@link defineSandbox}`({ … })`) to a * catalog, or pass `sandbox: true` to `genui({ … })`. * * Everything here is framework-free and isomorphic: the server uses the definition (description, * schema, text fallback, streaming trim), the browser renderer uses the same policy to build the * iframe document and to check what the frame sends back. */ import { type ComponentDefinition } from './catalog.js'; import { type SandboxKitDocs } from './sandbox-kit.js'; /** The component name the builtin {@link Sandbox} registers under. */ export declare const SANDBOX_COMPONENT = "Sandbox"; /** The props a model writes, in the order it writes them (see {@link SANDBOX_FIELD_ORDER}). */ export interface SandboxProps { /** Height in CSS pixels to reserve while the code streams. The frame then sizes to its content. */ initialHeight?: number; /** Short lines shown in the placeholder while the code streams ("Setting up the calculator…"). */ placeholderMessages?: string[]; /** A short title, for the frame's accessible name and for text-only clients. */ title?: string; /** One or two sentences on what the view shows — what a client that cannot run it prints. */ summary?: string; /** Styles, injected in a `<style>` before the HTML. */ css?: string; /** The body markup. No `<script src>` (no network); inline `<script>` runs once the view is live. */ html?: string; /** Function declarations, run once as one classic script after the HTML. */ jsFunctions?: string; /** Statements run in order after `jsFunctions`, each as its own classic script (no top-level await). */ jsExpressions?: string[]; /** * The view as JSX using the app's kit (`sandbox({ kit })`): plain JavaScript + JSX defining * `function App()`, compiled and rendered in the frame. Drawn as it streams, once it parses. */ jsx?: string; } /** * The order the fields stream in, and the order the renderer uses them: a placeholder of the right * height first, then the styles (so the markup never draws unstyled), the markup, and the behavior * last — the same sequence as CopilotKit's Open Generative UI. */ export declare const SANDBOX_FIELD_ORDER: readonly ['initialHeight', 'placeholderMessages', 'title', 'summary', 'css', 'html', 'jsFunctions', 'jsExpressions', 'jsx']; /** * Who the sandbox may reach and how much it may send back. The DEFAULT IS NO NETWORK AT ALL: no * fetch, no external script, style, font, image or media — only inline code and `data:`/`blob:` * URLs. List origins (`https://tile.openstreetmap.org`, or `https://*.tile.openstreetmap.org`) per * kind to open exactly those. */ export interface SandboxPolicy { allow?: { /** `img-src` (map tiles, photos). */ images?: readonly string[]; /** `connect-src` (`fetch`, `XMLHttpRequest`, `WebSocket`). */ connect?: readonly string[]; /** `script-src` (a charting library from a CDN). */ scripts?: readonly string[]; /** `style-src`. */ styles?: readonly string[]; /** `font-src`. */ fonts?: readonly string[]; /** `media-src`. */ media?: readonly string[]; }; /** Largest `agent.send(…)` payload accepted, as JSON, in bytes. Default 8192. */ maxPayloadBytes?: number; /** Least time between two accepted sends from one view, in ms. Default 1000. */ minSendIntervalMs?: number; /** Most sends one view may make. Default 20. */ maxSends?: number; /** Tallest the frame may grow, in CSS pixels. Default 1600. */ maxHeight?: number; } export declare const SANDBOX_DEFAULTS: { readonly initialHeight: 240; readonly maxPayloadBytes: 8192; readonly minSendIntervalMs: 1000; readonly maxSends: 20; readonly maxHeight: 1600; }; /** Throws unless every listed origin is a bare `https://host[:port]` (or `https://*.host`). */ export declare function assertSandboxPolicy(policy: SandboxPolicy): void; /** * The Content-Security-Policy every sandbox document carries (as a `<meta>`, first thing in its * head). Inline scripts and styles run — they ARE the generated UI — but nothing else loads, no * form posts and no navigation of the frame's base, unless the policy lists an origin. */ export declare function sandboxCsp(policy?: SandboxPolicy): string; /** The `sandbox` attribute of the frame: scripts, and nothing else — above all no `allow-same-origin`. */ export declare const SANDBOX_IFRAME_FLAGS = "allow-scripts"; /** The type tags of the messages a sandbox frame and its host exchange. */ export declare const SANDBOX_MESSAGE: { /** frame → host: the bridge is up. */ readonly ready: 'agora:sandbox:ready'; /** frame → host: `{ height }` — the content's height. */ readonly resize: 'agora:sandbox:resize'; /** frame → host: `{ payload }` — `agent.send(payload)`. */ readonly send: 'agora:sandbox:send'; /** frame → host: `{ message }` — an uncaught error in the generated code. */ readonly error: 'agora:sandbox:error'; /** host → frame: `{ css, html }` — redraw the preview (no scripts run). */ readonly render: 'agora:sandbox:render'; /** host → frame: `{ code }` — run one more classic script. */ readonly run: 'agora:sandbox:run'; /** host → frame: `{ css, dark }` — the host's theme changed (light/dark, a new stylesheet). */ readonly theme: 'agora:sandbox:theme'; /** host → frame: `{ code, final }` — the JSX so far (`final` once the model is done). */ readonly jsx: 'agora:sandbox:jsx'; /** * frame → host: the frame is up and waits for its shared assets (a document built with * `sharedAssets`); it says `ready` once they are in. */ readonly load: 'agora:sandbox:load'; /** host → frame: `{ styles?, tailwind?, kit? }` — the shared assets' text. */ readonly assets: 'agora:sandbox:assets'; }; /** * The assets a host hands a frame built with `sharedAssets` (the answer to its `load`): the text of * each, fetched once by the page. The frame puts each in an inline `<style>` / `<script>` — what the * CSP already allows — so the policy is the same as for an inlined document. */ export interface SandboxSharedAssets { /** The app's compiled styles. */ styles?: string; /** The Tailwind browser runtime. */ tailwind?: string; /** The kit bundle (React, ReactDOM, the components, the wrapper). */ kit?: string; } export interface SandboxDocumentOptions { /** A per-frame value every bridge message carries, so the host can tell this frame's messages apart. */ token: string; /** * Where the bridge posts its messages (the host page's origin). The frame's own origin is opaque * (`null`): it can address its parent, never be addressed by name. */ hostOrigin: string; policy?: SandboxPolicy; /** * `'preview'`: CSS + markup only, scripts stripped; the host redraws it with `render` messages * while the model writes. `'live'`: the whole view, its inline scripts and `jsFunctions` / * `jsExpressions` included. */ mode: 'preview' | 'live'; /** Run the JavaScript fields inline (`live` only). Default true; false when the host streams them in with `run`. */ inlineJs?: boolean; /** * The host's theme (`hostThemeCss(collectHostThemeVars(document), dark)`): put on the frame's * `:root`, and kept current with `theme` messages. `dark` mirrors the host's `dark` class; * `attributes`, its light/dark attributes (`data-theme`, `data-bs-theme`…). */ theme?: { css: string; dark: boolean; attributes?: Record<string, string>; }; /** The app's compiled styles (`genuiSandboxKit({ styles })`), before the theme and the model's CSS. */ styles?: string; /** Tailwind: the `@tailwindcss/browser` runtime source, and the `@theme` mapping the host's variables. */ tailwind?: { runtime: string; theme?: string; }; /** * The kit bundle's source (an IIFE setting `window.Kit`, `React`, `ReactDOM`): inlined, and the JSX * runtime with it — the frame then draws `jsx` (posted with `jsx` messages, or `initialJsx`). */ kit?: string; /** JSX to draw as soon as the document loads (a whole view: `final: true`). */ initialJsx?: { code: string; final: boolean; }; /** * The heavy assets come from the host instead of the `srcdoc`: the document carries none of * `styles`, the Tailwind runtime or `kit` — only which of them it needs — and asks its host for them * (`load`); the host answers with `assets` ({@link SandboxSharedAssets}), the text it fetched once * for the whole page. The frame puts them in inline `<style>`/`<script>` elements — the same CSP as * an inlined document, nothing more allowed — and says `ready` once they are in. The body stays * hidden until then, so nothing draws unstyled. Ignored for `react-native` (inlined there). */ sharedAssets?: { styles?: boolean; tailwind?: boolean; kit?: boolean; }; /** * How the bridge reaches its host. `'parent'` (default): `parent.postMessage` to `hostOrigin`, an * iframe in a page. `'react-native'`: `window.ReactNativeWebView.postMessage(JSON)`, the document * as a `react-native-webview` source (`hostOrigin` unused), with a device-width viewport. The host * talks back the same way either way — `message` events whose `source` is the frame's parent. */ transport?: 'parent' | 'react-native'; } /** * The `srcdoc` of a sandbox frame. The CSP `<meta>` comes first, then the bridge, the base styles and * the model's CSS, then its markup — and, live, its `jsFunctions` and each `jsExpressions` entry as * classic scripts, in that order. */ export declare function buildSandboxDocument(props: SandboxProps, options: SandboxDocumentOptions): string; /** Which shared assets a document built with {@link SandboxDocumentOptions.sharedAssets} asks for. */ export declare function sandboxSharedAssetsWanted(options: Pick<SandboxDocumentOptions, 'sharedAssets' | 'transport'>): { styles: boolean; tailwind: boolean; kit: boolean; }; /** * Markup the model is still writing, made safe to draw as a preview: the half-written tag at the end * dropped, `<script>`/`<style>`/`<head>` blocks (whole or cut off) removed, a cut-off entity dropped, * and only what is inside `<body>` kept. */ export declare function previewHtml(html: string): string; /** * Sandbox props as they are while the model writes them: only what may be shown or run. CSS and * `jsFunctions` are held until their string closes, a `jsExpressions` entry until it closes (code is * never run half written), a half-written placeholder line is dropped; the markup streams as it is. */ export declare function sandboxPartialProps(props: Record<string, unknown>, input: { isOpen(container: object): boolean; pendingMember(container: object): unknown; }): Record<string, unknown>; /** * Where the kit's docs come from: the docs themselves, or a function read each time the model is * told about the sandbox (dev: the Vite plugin rewrites them as the components change). */ export type SandboxKitSource = SandboxKitDocs | (() => SandboxKitDocs | undefined); /** Where the theme's variable names (for the prompt) come from — the same shapes as the kit. */ export type SandboxThemeSource = Record<string, string> | readonly string[] | (() => Record<string, string> | readonly string[] | undefined); export interface DefineSandboxOptions { /** Component name. Default `Sandbox`. */ name?: string; /** Replaces the model-facing description (keep the "last resort" rule in it). */ description?: string; /** Extra rules appended to the description (house style, what the app wants from it). */ instructions?: string; /** Network allowances and send limits; also what the renderer enforces by default. */ policy?: SandboxPolicy; version?: number; /** * The app's theme in the frame: the renderer reads the host page's CSS custom properties from * `:root` and puts them on the frame's (and follows light/dark changes). Default `'inherit'` (on); * `false` turns it off. `vars` names them for the model (the Vite plugin finds them on its own). */ theme?: boolean | 'inherit' | { vars?: SandboxThemeSource; }; /** * The app's design system in the frame — whatever `genuiSandboxKit()` built: its compiled styles * (fonts and images inlined), the Tailwind runtime with the app's theme when the styles use * Tailwind, its components and wrapper (the model then writes `jsx`). What the model is told is * read from the docs: components and props, Tailwind tokens or the CSS class vocabulary, the * theme's variables, `stylesDocs`. `true`: the server's `sandbox({ kit: true })` finds them * through Vite; or pass the docs (or a function returning them) yourself. */ kit?: boolean | SandboxKitSource; /** * Which kit `kit: true` finds, when the app builds several (`genuiSandboxKit({ name })`): its name. * Default: the unnamed one. */ kitName?: string; } /** How a sandbox draws: theme and kit on or off. */ export interface SandboxView { theme: boolean; kit: boolean; } /** A sandbox definition: a {@link ComponentDefinition} that carries its {@link SandboxPolicy}. */ export type SandboxDefinition = ComponentDefinition<SandboxProps> & { readonly sandbox: SandboxPolicy; /** Theme and kit, as the definition was given them. */ readonly sandboxView?: SandboxView; /** The options it was defined with — what a server resolves the kit for. */ readonly sandboxOptions?: Readonly<DefineSandboxOptions>; }; /** * A sandbox component. `streaming: 'partial'`, so in tree mode it draws while the model writes it: * a placeholder of `initialHeight`, then the styled markup, then — once the code has closed — the * live view. With a kit, JSX draws as it streams, each time what has arrived parses. * * ```ts * const catalog = defineCatalog([...myComponents, defineSandbox({ policy: { allow: { images: ['https://tile.openstreetmap.org'] } } })]) * ``` */ export declare function defineSandbox(options?: DefineSandboxOptions): SandboxDefinition; /** * A minimal, valid call of a sandbox's own tool (`ui__sandbox`), as the model should write it: JSX * when the kit draws JSX (a kit `Button` when there is one), HTML otherwise. The JSX is a JSON * STRING — quoted, newlines escaped — which is what a model writing code (OpenCode) gets wrong. */ export declare function sandboxCallExample(definition: unknown): string | undefined; /** * Whether a sandbox's props draw as JSX (the kit) or as HTML. A model under strict tool calling * (OpenAI's) fills every property, so it may write a whole view in one field and a stub in the * other (`function App() { return null }` next to the real `html`): the field that holds the view — * the longer one — wins. JSX only when it has something in it. */ export declare function sandboxDrawsJsx(props: Pick<SandboxProps, 'jsx' | 'html'>): boolean; /** The builtin sandbox: no network, default limits. */ export declare const Sandbox: SandboxDefinition; /** The sandbox policy a definition carries (`{}` for a component that is not a sandbox). */ export declare function sandboxPolicyOf(definition: unknown): SandboxPolicy | undefined; //# sourceMappingURL=sandbox.d.ts.map