Robutler

widget.json manifest

Apps are built as widgets, and a widget's manifest is the single declaration the platform reads to render it, sandbox it, and expose it to agents. The shape is the WidgetSpec. First-party apps declare it in the registry; community and bundle apps declare it in widget.json (or, for a single HTML file, in a <meta name="robutler:widget"> tag, see the meta form).

WidgetSpec fields

interface WidgetSpec {
  kind: 'native' | 'iframe' | 'iframe-external' | 'projection-only';
  itemType: WorkspaceItemType;          // workspace_items.type to insert (iframe/projection-only use 'content')
  size: { width: number; height: number };
  entry?: string;                       // iframe path or external URL
  csp?: { frameSrc?: string[]; connectSrc?: string[] };
  allow?: string;                       // iframe allow= (Permissions-Policy); first-party only, derived not hand-coded
  interface?: WidgetInterface;          // agent-facing command + event surface
  collab?: boolean;                     // declares use of host.collab realtime rooms
  preferBundle?: boolean;               // serve from the self-contained DB bundle
}

kind

The render strategy:

KindHow it renders
nativeA dedicated React component owns the UI (terminal, agent, daemon, ssh, python, files). No iframe.
iframeA sandboxed iframe loading entry from /widgets/... on the portal origin. Talks to the host over the postMessage bridge. This is the common case for App SDK apps.
iframe-externalA sandboxed iframe loading a fully-qualified external URL. Stricter sandbox (no allow-same-origin); the host injects no CSP because the external origin owns its own.
projection-onlyNo iframe; the canvas renders the app's declarative DSL projection directly. For purely visual surfaces (Weather, agent-status) with no interactive chrome.

itemType

The workspace_items.type to insert when the app is added to the canvas. Native apps use their dedicated type (terminal, agent, daemon, ssh, python, files); iframe and projection-only apps all use content, and the content row's metadata.widgetType carries the actual identity.

size

The default canvas footprint { width, height } on first spawn.

entry

For kind: 'iframe', a path under public/widgets/ (for example /widgets/snake/index.html). For kind: 'iframe-external', a fully-qualified URL. Ignored for native and projection-only apps.

csp

A per-app CSP carve-out merged into the baseline CSP at response time. Two keys:

  • frameSrc: origins the app may embed in child frames. Needed only by embed-style apps (for example browser-embed sets frameSrc: ['https:']).
  • connectSrc: extra connect origins. The baseline already allows https: and wss:, so most apps need nothing here; collab apps list their CDN and the Hocuspocus wss host explicitly.
{
  "csp": {
    "connectSrc": [
      "https://esm.sh",
      "https://cdn.jsdelivr.net",
      "wss://*.robutler.ai"
    ]
  }
}

Keep carve-outs minimal: the sandbox, not the CSP, is the security boundary (see the security model), but a tight csp keeps the app's network surface honest.

allow

The iframe allow= (Permissions-Policy) attribute, for first-party apps only. It is not hand-coded per app: the platform derives it from the app's declared permissions and the delegation rules. A community app can never self-escalate trust through allow; its sensitive features come only from per-app user grants. See Permissions-Policy delegation.

interface

The agent-facing declaration: a description plus the commands an agent can invoke and the events the app emits. Optional. An app without an interface still renders, but it will not appear in workspace_widgets_list with anything for agents to drive. See the agent command interface for the full shape.

{
  "interface": {
    "description": "A pitch deck: fullscreen, navigable slides.",
    "commands": {
      "next": { "description": "Advance one slide." },
      "goTo": { "description": "Jump to a slide.", "args": { "index": "number" } }
    },
    "events": {
      "deck.slide_view": { "description": "Fires when a slide becomes active." }
    }
  }
}

collab

true declares that the app uses host.collab realtime rooms. This is the gate the room-scope collab token mint checks: only collab-enabled apps may open code-based lobby rooms under their content id, so a room token cannot be turned into access to arbitrary item or workspace rooms. Set it whenever your app calls host.collab.room(...).join(...).

preferBundle

true serves the app from its self-contained DB bundle (the seeded *-bundle folder, via the sandbox route) instead of the static public/ entry, whenever the content row carries a folder bundle. This makes a db seed widgets enough to push an app update to a cloud environment, no CI redeploy of public/ required. It falls back to the static entry when no bundle is present. The trade-off: the mount loads through the sandbox double-iframe plus a DB read instead of a static asset.

Declaring via a meta tag

A single HTML file can be an app by dropping a <meta name="robutler:widget"> tag. Two forms, which can be mixed (per-field tags win over the JSON form for keys they cover):

<!-- JSON form -->
<meta name="robutler:widget" content='{"title":"Snake","size":{"width":360,"height":420}}' />

<!-- per-field form -->
<meta name="robutler:widget:title" content="Snake" />
<meta name="robutler:widget:size" content="360x420" />
<meta name="robutler:widget:description" content="A classic snake game." />
<meta name="robutler:widget:permissions" content="microphone,webgpu" />

Recognized fields: title, description, size, icon, kv, author, version, permissions, kind, events, commands, and csp.frame-src / csp.connect-src. The kv field is a private-storage disclosure (surfaced in the install confirmation), not an enforced allowlist. At publish time the platform reads this declaration once and stores the resulting interface, size, and CSP on the content row's metadata.

On this page