Robutler
GuidesWidgets

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.json on the same origin, or
  • Schema.org JSON-LD with SoftwareApplication whose applicationCategory is Widget.

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.254 and 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.

On this page