@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
TypeScript
/**
* 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