Robutler

Security model

Apps on Robutler are community- and agent-authored code whose whole purpose is to run custom scripts. The platform contains them so that running untrusted code is safe for the viewer and for the platform. This page describes the intended model: the sandbox boundary, the CSP baseline, how sensitive browser features are delegated, and how per-app resource access is granted.

The sandbox iframe is the boundary

Every app renders inside a sandboxed iframe served from a dedicated sandbox origin (SANDBOX_ORIGIN), separate from the portal apex. That cross-origin sandbox is the real security boundary. An app:

  • has an opaque origin isolated from the apex;
  • cannot read the apex cookies, localStorage, or session;
  • cannot reach the parent DOM or navigate the top frame;
  • cannot see another app's storage.

Because the boundary is the sandbox, the threats it defends against are credential theft and sandbox breakout. The data an app receives over the host bridge is the host's to give, and an app forwarding that data onward is not the concern the sandbox addresses. This is why script and network sources are deliberately not the gate (see below).

The portal apex sets frame-ancestors 'self' and applies Cross-Origin-Embedder-Policy: require-corp, Cross-Origin-Resource-Policy: cross-origin, and X-Content-Type-Options: nosniff to every app response.

CSP baseline

App responses carry a permissive Content-Security-Policy baseline. Apps may load scripts from any HTTPS origin (CDNs are first-class), compile WebAssembly, run workers, and reach any HTTPS or WSS endpoint. The baseline directives are:

DirectiveValue
default-src'self'
script-src'self' 'unsafe-inline' 'wasm-unsafe-eval' https: blob:
style-src'self' 'unsafe-inline' https:
img-src'self' data: blob: https:
font-src'self' data: https:
media-src'self' blob: data: https:
connect-src'self' https: wss: data: blob:
worker-src'self' blob:
frame-ancestors'self'

script-src and connect-src are intentionally permissive: they are not the security gate (the sandbox is), and an app whose point is to run custom code needs to load libraries and reach services. default-src 'self' and frame-ancestors 'self' are kept as the clickjacking and breakout boundary, and frame-src is not in the baseline, so embedding child frames requires a per-app carve-out.

Per-app carve-outs

The manifest's csp field merges into the baseline. Two keys matter:

  • frameSrc: only embed-style apps (for example one that frames third-party URLs) need this; it is otherwise absent.
  • connectSrc: the baseline already allows https: and wss:, so most apps add nothing; collab apps list their CDN and the Hocuspocus wss host for clarity.

iframe-external apps load from another origin entirely; the host injects no CSP for them, because that origin owns its own policy, and they run in a stricter sandbox with no allow-same-origin.

Permissions-Policy delegation

Sensitive browser features (camera, microphone, geolocation, motion sensors, WebXR, and the device-bus families) have a browser-default allowlist of self, so a cross-origin sandbox iframe's allow= attribute is a no-op unless the top-level document delegates the feature down to the sandbox origin. The model has three layers:

  1. Baseline features, delegated to every app iframe unconditionally with no grant: fullscreen, autoplay, picture-in-picture, encrypted-media, clipboard-write.
  2. Sensitive features, gated behind an explicit per-app-instance user grant: camera, microphone, geolocation, display-capture, xr-spatial-tracking, gyroscope, accelerometer, magnetometer, midi, usb, hid, serial, bluetooth. An app or agent may request one; only the user grants it, through the standard approval flow.
  3. Delegated features: the top-level document delegates the sensitive set plus webgpu to the sandbox origin via the Permissions-Policy header, so a granted feature actually reaches the iframe. webgpu is a trusted registry capability (not user-grant-gated) that still needs delegation, without it on-device WebGPU apps work same-origin in local dev but are denied WebGPU on a cross-origin sandbox in cloud.

The effective iframe allow= is baseline ∪ trusted-registry-features ∪ (granted ∩ sensitive). Trusted registry features come only from the first-party WidgetSpec.allow (platform apps); a community app passes no trusted features, so its sensitive features come solely from user grants. An app cannot self-escalate trust. See the manifest allow field.

Per-app resource grants

Beyond browser features, an app can be granted access to specific platform resources it may reach via host.get(kind, id), for example a connected agent ({ kind: 'agent', id }). These outbound grants are stored on the workspace item server-side and written at install, never by the sandboxed app. The bridge rejects a (kind, id) the app was not granted.

This grant is a UX and defense-in-depth filter, not the sole gate. The bridge ops it permits still run as the authenticated viewer over same-origin routes that independently authorize that viewer, so the grant narrows what an app surfaces, while the underlying authorization is enforced separately.

Three distinct grant kinds live on the workspace item, and it is worth keeping them apart:

GrantDirectionWhat it controls
browser-feature permissionsinbound featurewhich sensitive Permissions-Policy features the app may use
connected resourcesoutboundwhich resources the app may reach via host.get
agent controlinbound commandwhich agents may drive this app's commands

What apps never see

Email, phone, and payment information are never exposed to app code. host.user returns only a public profile (id, and best-effort username / displayName / avatarUrl). KV and content are visible to every collaborator who can see the app, so they are not a place for secrets; server-side secrets belong behind host.fn.

On this page