Importing a URL as a widget
Paste a URL into the workspace search bar and click Add as widget. The platform classifies the URL into one of three tiers and acts accordingly.
Tier A — cooperative
The site publishes a cooperative manifest the platform can discover:
<link rel="widget-manifest">(Robutler / MCP Apps) or<link rel="manifest">(PWA) in the page<head>, or/.well-known/widget-card.json//.well-known/mcp-app.jsonon the same origin, or- Schema.org JSON-LD with
SoftwareApplicationwhoseapplicationCategoryisWidget.
The discovered manifest is snapshotted and the URL is registered as a full widget — it shows up on your canvas as a first-class embed, can be remixed (as a catalog-row copy that still points at the original remote URL), and inherits the manifest's tool declarations and CSP allowlist.
Tier B — passive iframeable
No manifest, but the site is willing to be embedded — the platform
probes for X-Frame-Options and CSP frame-ancestors and finds
neither blocks embedding from your portal.
The URL is registered as a view-only widget. You can pin it to the canvas; it's not remixable (there's no source to clone) and it carries a view-only badge so you know it's not under your control.
Tier C — iframe-denied
The site refuses to be framed (X-Frame-Options: DENY / SAMEORIGIN
or CSP frame-ancestors 'none'/'self'). The import returns a 422
with an inline fallback message:
This site can't be embedded as a widget.
That's it — no modal, no CTA, no automatic suggestion of cloud
browser delegation. If you want browser-driven access to a site
that refuses framing, invoke @robutler.browseruse from chat
yourself; that's an explicit user action the platform deliberately
does not auto-prompt.
The reasoning is in ADR-v3-17: implicit delegation can spend your quota without a clear opt-in, so Plan 5 surfaces zero auto-delegation UI.
SSRF protections
All server-side fetches the import does (the page URL, the well-known probes, the manifest URL, the JSON-LD origin, the HEAD probe) flow through a single hardened validator:
- HTTPS only by default.
- Private / loopback / link-local IPs blocked (
10.0.0.0/8,127.0.0.0/8,169.254.0.0/16,192.168.0.0/16, etc.) so a malicious page can't redirect us into your internal network. - Cloud-metadata IPs blocked (
169.254.169.254and friends) so cloud-instance credentials can't be probed. - Max URL length 2048, max 3 redirects, 10s timeout per fetch.
- The manifest URL discovered in Tier A is re-validated as a separate origin before fetch — a public page cannot redirect manifest discovery to an internal address.
A URL that fails the SSRF gate returns 422 { reason: 'url_unsafe' }
(distinct from tier: 'denied'). The user sees the i18n key
widget.url.unsafe.
What about per-user import rate limits?
The platform dedupes imports on (url, userId) for 60 seconds (so
double-clicking doesn't fire two imports), and caps concurrent
imports at 3 per user. Beyond that, normal rate limits apply.
See also: authoring.md for what the imported
manifest needs to look like;
ADR-v3-17 for the
classifier internals and SSRF model.