Agentable Documentation
Deep-level specifications and technical constraints for the Agentable framework. This covers our panel specification IR, the four architectural tiers, agent capabilities, and data adapters.
Architecture & Panel Tiers
One spec IR, two authoring surfaces: A single flat-node-map JSON format (the Spec IR) serves as the runtime panel format for all Tiers 1-3. Hosts author through a typed TypeScript builder (defineSchemaPanel) that compiles down to the IR, while agents emit the exact same IR directly.
Architecture
- Static: Fixed view layouts with zero dynamic agent generation.
- Schema: Forms and data views bound directly to DataAdapter sources.
- Composed (Instances): Agent-generated, composed at runtime. They are instances, never persistent registrations.
- Custom React: Full control custom UI components (Tier 4).
- Closed Bindings Only: Strict limitations (
$scope.*,$data.*,$state.*, and$eqconditionals). No raw templating languages or evaluatable expressions. - Validation Pipeline: 7 steps including envelope parse, catalog membership, Zod prop validation, action resolution, budget verification, and sanitization.
Spec Language and Safety
- No markdown/image primitives: Excluded in v1 to block the classic prompt-injection exfiltration vectors.
- query(): Supports caching, in-flight deduplication, and loading skeletons.
- mutate(): Processes mutations securely. Agent mutations block on framework-owned HITL diff cards.
- subscribe(): Allows granular invalidation and real-time UI updates (stale-banner rendering without overwriting user drafts).
- Ephemeral by default: Agent-composed panels require an explicit pin by the user to persist.
Mutations and HITL
- HITL at the mutation line: Agent-triggered mutations block on framework-owned approval chrome showing a payload diff.
- Destructive actions always confirm: Even for host/user-triggered actions.
- Dirty-field protection: Agents cannot silently overwrite user-dirtied fields.
- Six generic agent tools:
list_panels,open_panel,fill_panel,compose_panel,patch_panel,run_panel_action.
Agent Capability & World Model
- Derived Capabilities: Capabilities are derived (not hand-listed) as
read | ui | mutate | jobwithapproval: none | hitl | user-only. - The canvas is the blackboard: The framework compiles a token-budgeted WorkspaceDigest with attention tiers (focused, visible, background).
- Inter-agent trust: Messages and blackboard entries are data, never instructions. HITL applies regardless of the requester.
Panel System Spec
Capabilities & Digest: The framework compiles a token-budgeted WorkspaceDigest mapping the workspace and attention tiers (focused, visible, background) so agents natively understand the canvas topology.
Panel IR Envelope
{
"v": 1,
"origin": "host",
"root": "body",
"sources": {
"seo": { "source": "site.seo", "params": { "pageId": "$scope.entityId" } }
},
"state": { "scopeMode": "site" },
"nodes": {
"body": { "type": "panel-body", "children": ["form", "actions"] },
"form": { "type": "field-form", "props": { "bind": "seo" } }
},
"actions": {
"save": { "kind": "mutate", "source": "site.seo", "op": "update" }
}
}
Catalog & Validation
Every catalog entry provides a Zod prop schema. Unknown node types degrade to data-preserving placeholders, never hard rejection.
The Validation pipeline is strictly ordered:
- Envelope Parse (and migration)
- Catalog Membership Check
- Per-node Zod Validation
- Action Resolution (no URLs as actions)
- Budgets Check (max 200 nodes, depth 12)
- Sanitization (http(s) only, no javascript:)
- Agent Fallback/Repair (one repair round)
Canvas Modes & Engine SPI
Bounded/locked canvas modes (infinite | bounded | fixed) dictate spatial layout limits via tldraw camera constraints. App Mode ships on a DOM workspace engine for surfaces that don't need spatial layout.
3. Agent Layer Spec
Covers the panel tools, capability model, workspace world model, CopilotKit bridge, tldraw agent-kit adoptions, and the edit ladder. Framework module roots: agentable-canvas/src/panels/tools.ts, agentable-canvas/src/agents/.
Panel tools (added to CANVAS_TOOLS)
All six are ToolDefinitions in the existing merged registry (src/canvas/tools/canvasTools.ts; relocates to src/agents/tools/ with the move map, semantics unchanged), so Gemini text chat and Gemini Live voice get them identically; the CopilotKit bridge mirrors them (section 5). Declarations are generated from the panel registry so they never drift.
Adds a seventh, read-only introspection tool: describe_panel({ panelId | catalogEntry }) returns the full props schema, sources, actions, and curated example specs (validated in CI so they never rot). Compose rejections use the standardized repair vocabulary (frozen error codes + failing node id + nearest-valid-alternative hint), and every declaration carries a costClass.
list_panels()
-> [{ id, title, agentDescription, scope: 'global'|'context', contextKinds,
fields: [{path, kind, label, constraints}]?, actions: [{id, kind, destructive?, label}]?,
openInstances: [{panelId, scope, dirty}] }]
open_panel({ id, scope? }) // scope defaults to active context frame (frameContextStore)
-> { ok, panelId } | { ok: false, error }
fill_panel({ id, patch: Record<string, Json> }) // plain object; paths must be declared fields
-> { ok, applied: string[], skippedUserDirty: string[], errors?: FieldError[] } // never saves
compose_panel({ spec, title?, pin?: false }) // full IR envelope, origin forced to 'agent'
-> { ok, panelId } | { ok: false, errors: SpecIssue[] } // one repair round then fallback card
patch_panel({ panelId, ops: Rfc6902Op[] }) // composed instances only; streams progressive hydration
-> { ok } | { ok: false, errors }
run_panel_action({ panelId, actionId, payload? }) // the only mutation path
-> { status: 'ok', result } | { status: 'pending_approval' -> resolves later } // blocks tool call
| { status: 'rejected_by_user' } | { status: 'error', message }
Execution notes: run_panel_action from an agent turn routes through the approval flow in section 7. Successful mutations emit an AG-UI patch and call host.data.invalidate for the action's source, which is how other panels and agents learn about the change. All six handlers must be pure wrappers over host.* APIs (no direct tldraw access) so they work identically for every transport. Reversal: a persisted mutation is not stack-undoable; a DeclaredAction may declare an inverse (or reversible: false), and reversing an applied mutation runs that inverse through HITL as a fresh action, recorded in the activity ledger. Canvas-local tool effects (draw, arrange, document block ops before save) are true undo/redo on the engine or panel stack.
Capability model (src/agents/capabilities.ts)
type CapabilityClass = 'read' | 'ui' | 'mutate' | 'job';
interface CapabilityDescriptor {
id: string; class: CapabilityClass;
scopeRequired?: 'context' | 'entity';
approval: 'none' | 'hitl' | 'user-only';
costClass?: 'cheap' | 'expensive';
summary: string; // one-liner for digests
}
deriveCapabilities(session): CapabilityDescriptor[] // from tool registry + hostActions + backend-declared skills
Defaults: panel tools 1-5 are ui/read with approval: none; run_panel_action inherits the action's declaration; hostActions declare their own class/approval at registration; job-class capabilities must supply start -> jobId, progress event mapping, and cancel.
Workspace world model (src/agents/)
3.1 WorkspaceDigest (digest.ts)
Compiled on demand + cached per change-batch; token-budgeted (~1.5k tokens target, hard cap 3k; drop recentActivity first, then background contexts):
interface WorkspaceDigest {
user: { id: string; name?: string };
contexts: Array<{ id: string; kind: string; label: string;
attention: 'focused' | 'visible' | 'background'; // selection/viewport derived
panels: Array<{ id: string; type: string; title: string; dirty?: boolean;
origin: 'host' | 'agent'; minimized?: boolean }>;
}>;
agents: Array<{ id: string; kind: 'chat' | 'voice' | 'background'; label: string;
scope?: string; status: 'idle' | 'running' | 'waiting_approval'; task?: string }>;
jobs: Array<{ id: string; capability: string; scope: string; status: string; progress?: number }>;
pendingApprovals: Array<{ id: string; agentId: string; panelId: string; summary: string }>;
recentActivity: Array<{ ts: string; actor: string; verb: string; target: string }>; // last 15
}
Attention derivation mirrors the tldraw agent kit tiers: focused = selected/being-edited frame or panel; visible = intersects viewport; background = everything else (counts only if budget-squeezed). Delivery: system-context block on the Gemini paths; useCopilotReadable-equivalent on the CopilotKit path; AG-UI StateSnapshot/StateDelta for remote agents. Drill-down tools (all read, free-fire): describe_context(id), read_panel_state(panelId), get_activity({ since?, actor? }), list_agents(agentId?).
AgentRegistry (registry.ts)
host.agents.register({ id, kind, label, scope?, transport, capabilities }) -> AgentSession. Sessions heartbeat; status transitions (idle/running/waiting_approval/done/cancelled) ride the AG-UI bus as Custom events (agentable:agent-status). UI: agent chip on the frame/panel it is bound to; spinner while running; badge on waiting_approval. interrupt(agentId) and cancelJob(jobId) are host/user affordances that resolve the underlying loop.
Activity log (activity.ts)
Append-only ring buffer (500 entries, session-scoped; host may persist via adapter): { ts, actor: agentId|'user', verb, target, provenance?: { derivedFrom: 'site-html' | 'agent:<id>' | 'user' }, reversal?: { inverse?: DeclaredAction; reversible: boolean } }. Every tool execution, mutation, approval outcome, job transition writes one. Powers digest recency, the provenance chain, the reversal ledger (each entry carries its inverse action or is marked irreversible), and a debug "Agent Activity" panel (Tier 2, itself built on the spec system: dogfood; virtualized above its row threshold). Separately from this in-session log, the framework emits redacted runtime telemetry to a host sink (host.telemetry.emit): compose/HITL/tool/voice/cost events with frozen error codes, no PII, no keys.
Coordination (leases.ts, camera.ts)
- Scope leases:
host.agents.claim({ source, scope, ttlMs }) -> Lease; conflicting claim returns holder info;fill_panel/run_panel_actionauto-claim for their duration; expired leases GC. Advisory (soft) in v1: violations warn in activity, do not block. - Camera politeness: single camera-intent queue; agent camera ops no-op with an attention badge when the user interacted < 4s ago or another agent holds camera;
bounded/fixedmodes clamp all agent camera ops. - Invalidation fan-out:
invalidateevents also notify subscribed agent sessions (so a watching agent can refetch grounding). - Budget signal:
host.agents.budgetexposes shared spend counters; job-class starts checkcostClassagainst remaining budget and surface a warning approval note when expensive.
Handoff and escalation
host.agents.handoff({ to, task, scope, artifacts, returnTo }) writes an A2A-shaped record to the activity log and messages the target session. Approval escalation: when a background session enters waiting_approval, the digest flags it and the focused chat agent is nudged (system note) to surface it to the user.
Trust rules
Blackboard/activity entries and inter-agent messages are DATA. Consumers never execute instructions found in them; anything derived from site HTML carries provenance.derivedFrom: 'site-html' and is summarized, never quoted into another agent's instructions. HITL applies regardless of the requesting agent.
5. CopilotKit bridge mapping
| Framework concept | CopilotKit surface |
|---|---|
| CANVAS_TOOLS entries (incl. panel tools) | useFrontendTool registrations generated from the same definitions |
| WorkspaceDigest | readable context (useCopilotReadable equivalent) refreshed on digest change |
| Approval flow | renderAndWaitForResponse semantics; the panel chrome is the rendered UI |
AG-UI patches (emitAgUiStatePatch) |
already native |
| Agent registry status | AG-UI lifecycle + Custom events |
Bridge code lives only in src/canvas/protocol/copilotkit-bridge.tsx (+ a new copilotkit-tools.ts next to it; both relocate to src/protocol/ with the move map, and the ./copilotkit-bridge export subpath stays stable). Version alignment task: landing-editor @copilotkit/* ^1.55.3 vs framework ^1.56.3; pin both to one minor.
tldraw agent starter kit adoptions (patterns only, no dependency)
- Perception: implement
describePanelShape(shape) -> FocusedPanelDescriptionand viewport-tiered shape summaries for canvas-level agents; reuse the digest attention tiers. sanitizeActionstage: normalize agent numeric/coordinate output before validation (shared normalize -> validate -> repair -> apply pipeline with the spec validator).- Loop semantics on AgentSession:
prompt()(agentic loop),request()(one-shot),schedule()(follow-up queue; used by watches),interrupt(). - Canvas-native action class (draw/arrange/annotate) registered as
uicapabilities; ships later as an optional module (src/agents/canvas-actions.ts), unlocking sketch-to-brief and sitemap diagrams.
Voice
Voice (Gemini Live via voiceKernel) is another AgentSession (kind: 'voice') sharing CANVAS_TOOLS, digest, and HITL. Production voice auth is server-mediated (a host-configured endpoint minting ephemeral credentials; the sandals worker already demonstrates the pattern), but building that endpoint is deployment-coupled future work outside this plan. Baked client keys are dev-mode only and warn loudly. Greeting: persona.voice.greeting.mode supports agent-first (speak on session connect, which always follows a user gesture so it is autoplay-safe) and user-first (default); localized; config validation warns on agent-first with an empty greeting. Voice sessions join the shared page session: the voice button can live anywhere on the host page, and transcripts plus tool calls stream into any mounted chat surface, buffering (bounded) until one joins. Resilience: the voice transport reconnects with bounded backoff and resumes, and a turn interrupted mid-HITL resolves to a definite state (re-prompt or cancel), never a stuck approval.
Agents draw and see
Drawing tools (engine-capability gated on capabilities.draw / capabilities.frames):
draw_shapes({ shapes: [{ kind: 'box'|'ellipse'|'arrow'|'text'|'freehand', geometry, text?, from?/to? (shape refs for arrows), style? }] }): batched creation using the tldraw agent starter kit's simplified-schema pattern (implemented in the engine adapter, zero new dependencies). Every created shape getsmeta.agentableAgent = agentIdprovenance.annotate_panel({ panelId, text, anchor }): a callout anchored to a panel that follows it through moves/docking.clear_agent_drawings({ agentId? }): one action clears an agent's marks (chrome offers the same affordance to the user).
Drawing is additive and ephemeral: no HITL required, but it can never mutate panels or data; the mutation line stays where safety put it.
Perception tools:
read_canvas({ region?: 'viewport' | rect, budget? }): structured shape graph: shape types, geometry, text content, arrow connections (from/to resolution), containment/grouping, z-order, plus panel metadata for panel shapes. Deterministic on a seeded canvas (golden-tested).screenshot_canvas({ region? }): rasterized viewport or region for vision-model input (data URL, or host-uploaded when size demands).
Flagship workflow (demo + e2e): user sketches a wireframe; the agent calls read_canvas + screenshot_canvas, proposes a panel/spec layout (compose_panel or dock preset), and the apply goes through normal HITL. Digest integration: agent and user drawings are summarized in the digest, and deltas include new/changed shapes.
Communicative visuals (drawing as a conversation medium, voice and text):
- Auto-layout:
draw_shapesacceptslayout: 'none' | 'flow' | 'timeline' | 'radial'plus a placement target (viewport| rect |nearPanel). The agent supplies logical structure (nodes, edges, labels, order); the engine adapter computes geometry. Agents are unreliable at absolute coordinates; auto-layout is what makes agent diagrams presentable. - Charts and data visuals ride the panel path: composed spec panels over catalog composites plus the
@agentable/catalog-chartsadd-on (heavy-dependency pattern, recharts-backed bar/line/area/pie with Zod props). A canvas-drawn chart is acceptable only as a throwaway sketch; anything the user might keep belongs in a validated, pinnable panel. - Stories: scenes are frames.
present_walkthrough({ steps: [{ target: frameId | shapeIds | panelId, say?, dwellMs? }] })queues camera moves through the politeness queue and emits each step's narration into the chat or voice stream. The camera is never locked: any user camera input cancels the walkthrough instantly. - Progressive drawing: batched
draw_shapescalls interleave with speech and text so the visual builds while the agent talks (voice tool calls already flow through CANVAS_TOOLS). - Selection rule (shipped in agent system guidance and the framework skill): data-bound or reusable, compose a panel; spatial, explanatory, or ephemeral, draw it; multi-scene narrative, frames plus walkthrough. Worked examples: career-trajectory timeline (drawn timeline or
step-timelinepanel), job-economy comparison (chart panel), the Archipelago Resorts island-to-island journey (fictional resort group; drawn map plus narrated walkthrough). Demo content names no real clients or employers.
Multi-agent on one page
Identity end to end: every tool execution, panel edit, drawing, and HITL card carries the acting agentId; chrome renders per-agent attribution (name/badge). Role scopes: AgentRegistry entries declare allowed tools, panels, and slots; the tool layer enforces scope before execution (a voice concierge scoped to open/fill cannot run destructive actions anywhere). Arbitration: panel leases (section 3) settle contested edits; the shared page session multiplexes agents with per-agent HITL queues so one agent's pending approval never blocks or leaks into another's. Handoffs remain A2A-shaped records. Tested default: e2e runs two-plus concurrent agents (chat editor + background job + voice presence) on one page, editing different panels, with attribution asserted throughout.
Model-agnostic runtime
The framework hardcodes no provider or model. Agent sessions resolve their model at the runtime boundary through a host-supplied resolver; the client and embeds reference only opaque aliases.
interface ModelCapabilities { vision: boolean; tools: boolean; contextTokens: number; streaming: boolean }
interface ProviderBinding { providerId: string; model: string; caps: ModelCapabilities } // resolved server-side
type ModelResolver = (alias: string, ctx: { tenantId?: string; agentId?: string }) => Promise<ProviderBinding>;
host.agents.registerModelResolver(resolver); // host required to register >= 1 binding; framework ships no default
- Selection order (mirrors the config-merge chain): platform default alias > tenant config > per-agent registry
model> embedmodelattribute > runtimeupdateConfig. The canvas-wide agent's model switcher (section 13) writes the runtime layer; switching rebinds server-side. A binding may declare an orderedfallbackchain; a failed resolve or an unmet capability falls through to the next available model before erroring. - Keys never leave the server. Aliases are public and safe in bundles and
data-*; provider keys and SDKs live behind the resolver. This is the safety server-side-keys rule, restated for the model path. - Capability gating. The tool layer checks
ProviderBinding.capsbefore offering capability-dependent tools:screenshot_canvasrequiresvision(otherwise it degrades toread_canvaswith a repair-vocabulary note); compose-heavy flows checkcontextTokens; non-streaming models fall back to buffered turns. Capability mismatches surface as structured notes, never silent failures. - Model-specific code, contained. The generic agent path assumes no model, but model-specific code is allowed where a provider needs it: provider adapters and transport bridges (for example the Gemini Live voice bridge, voiceKernel, already custom in moss/sandals) implement the runtime contract in their own modules. Model identity rides as AG-UI metadata; neither engine (tldraw or DOM) ever sees a model. The landi host maps aliases to fine-tuned Landi models plus Gemini 2.5+/3 Pro and current Claude, and supplies the voice bridge, as host configuration.
12. Open agent canvas
Extends section 9 (draw and see) into a full authoring surface for agents, plus a document primitive and a policy knob. Authoring tools are engine-capability gated (tldraw spatial engine) and every mark carries agent provenance.
Authoring toolkit (added to the drawing tools, capabilities.draw/capabilities.frames):
insert_image({ assetId | generatePrompt, geometry, alt }): places an uploaded asset by id or an image produced by the existing generation tool. Never accepts markup or a remote URL from model output; the resolved image is an asset, not HTML.connect_shapes({ from, to, kind, label? }): typed connectors between shape refs (dependency, flow, annotation).group_shapes/frame_shapes: compose selections into groups and named frames (frames double as walkthrough scenes, section 9).arrange({ target: shapeIds | frameId, layout }): re-runs the auto-layout engine over an existing selection (the agent supplies structure, never coordinates).- Wireframe stencils:
draw_shapesgains astencilfield (box/label/input/button/nav/cardplaceholders) so a connected wireframe set reads as a wireframe, not raw rectangles.
Document primitive (document panel, schema/composed tier, in the panel system because it is content the user keeps):
Agents edit documents through structured block ops via patch_panel/fill_panel (insert/replace/move/remove block), never by emitting markup. Export is a host action (export_document({ panelId, format: 'pdf'|'docx' })) producing the file from the block model; it has its own capability class and is deterministic on a seed (golden-tested).
Canvas authoring policy (canvasPolicy, merges through the standard config layers):
type CanvasPolicyPreset = 'guarded' | 'open';
interface CanvasPolicy {
preset: CanvasPolicyPreset; // default 'guarded'
hitlOnCompose?: boolean; // guarded: true
autoPin?: boolean; // open: true
region?: 'frame' | 'bounded' | 'unbounded';
allowDelete?: boolean; // agent may delete its OWN content only
toolset?: 'draw' | 'authoring-full';
}
guarded (default) keeps HITL at the mutation line, agent content ephemeral-until-pinned, provenance badges, and the agent confined to a region. open lets the agent freely create, arrange, and modify its OWN content without per-action HITL, auto-pins output, unlocks authoring-full, and widens the region. Individual gates override the preset, so policy is a spectrum.
Host defaults: the framework default is guarded and stays guarded. landi-canvas-studio ships open for its workspace surfaces as host configuration. open may be exposed to end-user-facing (non-operator) sessions when a host explicitly configures it; it is never implicit. moss and sandals launch guarded. Every open surface keeps the persistent open-canvas indicator and the hard boundary below.
The hard boundary no policy relaxes: no untrusted code executes in the trusted app context. Inside the open canvas, documents are a structured model the framework renders and sanitizes, images are assets, and wireframes are closed-schema shapes; agent-authored HTML/JS/CSS is never rendered inline. A prototyping agent that needs runnable code uses the sandboxed code-preview tier (section 14), never an inline document or shape. Actions touching host data or destructive run_panel_action gate on HITL even under open. An open workspace shows a persistent chrome indicator so users know agent output is auto-applied.
Canvas-wide agent surface
Optional, off by default. When the host enables it, one operator agent gets full visibility (whole-document read_canvas + full digest) and broad access (authoring under the active canvasPolicy, section 12), presented in a first-class chat surface. It is the productized form of the section 6 canvas-native action class.
- Web component (Lit, house web-components rule): tabbed conversation threads; a mode selector; a model switcher; an A2UI-rich transcript.
- Modes are tool-scope presets:
Ask(read-only:list_panels,describe_panel,read_canvas, drill-downs),Build(addsopen_panel/fill_panel/compose_panel/patch_panel/run_panel_actionunder HITL),Draw(adds the section 12 authoring toolkit). Switching mode re-scopes what the agent may call. - Model switcher writes the runtime layer, rebinds server-side with no client key exposure, and hides affordances the selected model or engine cannot support (Draw hidden on a
camera: noneengine or a non-vision model where perception matters). - A2UI-rich messages render through the ingestion adapter: plans, panel previews, and structured cards appear inline in the transcript.
- Placement (
agent.surface.placement):dock-inside(left/right edge inside the canvas),dock-outside(left/right edge outside the frame),slot(a named page slot elsewhere on the page), orfloating(draggable, resizable). All four resolve through the shared page session, so the operator is one more session participant. - Multi-agent fit: registers as one broad-scope identity; coexists with scoped per-panel and voice agents; leases, per-agent HITL queues, and attribution all apply. It is an operator, not a bypass of scope or approval.
- Engine-neutral: full drawing and perception on tldraw; on the DOM engine Draw mode capability-gates off with the structured refusal, everything else works.
- Voice: the surface can host the voice session (greeting, transcript streaming), so the operator is voice-or-text.
Sandboxed code preview
For prototyping and brainstorming agents whose job is to produce runnable UI (a page, a component, an interactive mockup). Opt-in, gated by canvasPolicy.allowCodePreview (off by default). The tier and its isolation are specced in section 20; the agent-layer surface is one tool.
render_code_preview({ html?, css?, js?, title? }): creates or updates acode-previewartifact whose contents render only inside the sandboxed iframe (opaque origin, strict CSP, no same-origin against our origin, no parent DOM/token/tool access, postMessage-only bridge). Returns{ ok, artifactId }or a structured refusal whenallowCodePreviewis off.- Capability class
uiwhenallowCodePreviewis on, otherwise the tool is not offered at all (not merely refused), so a model without the grant never sees it. - The artifact is a throwaway or an exportable file by default; it never becomes trusted app UI and never gains network or tool access implicitly. Promotion to a published site is a host action that hands off to landi's generation/publish pipeline, never an in-canvas escape.
- Provenance and attribution as usual (
meta.agentableAgent); the artifact carries a persistent "generated code, sandboxed" badge in chrome.
Data Boundary & Backends
DataAdapter Semantics
The DataAdapter isolates the framework from the database:
interface DataAdapter {
query(ref: SourceRef, scope: PanelScope, signal: AbortSignal): Promise<unknown>;
mutate(action: DeclaredAction, payload: unknown, scope: PanelScope): Promise<MutationResult>;
subscribe?(ref: SourceRef, scope: PanelScope, onChange: () => void): Unsubscribe;
}
Security Practices
- Provider keys live server-side only (AI Gateway / Workers secrets); nothing key-shaped ever enters client bundles or embed configs.
- Telemetry events carry no PII and no provider keys; redaction is enforced.
- Public embeds are rate-limited per anon-key to prevent abuse.
- Generated code runs strictly isolated in sandboxed previews with postMessage-only bridges.