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:
| Directive | Value |
|---|---|
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 allowshttps:andwss:, so most apps add nothing; collab apps list their CDN and the Hocuspocuswsshost 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:
- Baseline features, delegated to every app iframe unconditionally with no grant:
fullscreen,autoplay,picture-in-picture,encrypted-media,clipboard-write. - 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. - Delegated features: the top-level document delegates the sensitive set plus
webgputo the sandbox origin via thePermissions-Policyheader, so a granted feature actually reaches the iframe.webgpuis 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:
| Grant | Direction | What it controls |
|---|---|---|
| browser-feature permissions | inbound feature | which sensitive Permissions-Policy features the app may use |
| connected resources | outbound | which resources the app may reach via host.get |
| agent control | inbound command | which 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.
Related
- widget.json manifest:
csp,allow, and permission declarations - host.rpc: the
host.get/host.listgrant factory - host.infer: why
webgpudelegation matters - What's ready: per-app browser-feature grants status
Agent command interface
How an app exposes an agent-driveable command surface: the manifest interface.commands declaration, host.commands.handle at runtime, the built-in commands, and how agents invoke them.
Error codes
Stable codes the host bridge and the App SDK return, and when to retry versus surface them.