@botiverse/porthole
v0.5.1
Published
A host-side sandbox for agent-made HTML: isolation, egress hardening, a themed shell, gated host functions, and the agent manual, from one config.
Keywords
Readme
Porthole
@botiverse/porthole: show HTML an agent wrote (a widget, a whole page, an MCP Apps view)
inside your app, safely. One config gives the frame its isolation, its CSP, a themed shell, the
host functions it may call (each behind a gate), and the manual your agent reads.
npm install @botiverse/portholeNode 24 or later, or Cloudflare Workers. The browser part is plain ES modules with no
dependencies (bundle it, or serve dist/ with an import map: docs/ADOPTING.md § 4).
Two ways to show content
- Opaque, in the page. No server needed: content runs in a sandboxed frame with an opaque origin. For widgets in a conversation.
- Served, on an origin of its own. Each page, or each conversation's widgets, gets an origin
like
a-<id>.example-usercontent.com, so it can keep storage, open at a clean address in a new tab, and be framed by your app only. The host kit (createContentHost) is the server side; your app writes only its policy: which pages exist and who may see them.
Try it in two minutes
The package ships a whole app, the same code on Node and on Workers:
PORTHOLE_SECRET=$(npx porthole secret) node node_modules/@botiverse/porthole/examples/node/server.mjs
# open http://localhost:8790: a stored page, served on http://a-hello.content.localhost:8790examples/app.mjs: the app (policy, routes, page), runtime-agnostic.examples/node/server.mjs(nodeServer),examples/workers/(contentHostFor, with its D1 migration andwrangler.jsonc).examples/conformance/: Porthole's conformance suite run against it, on three engines.examples/mcp/server.mjs: the agent's tools (show, guide, check) as an MCP server.
The smallest opaque display
import { createHost, presets } from "@botiverse/porthole";
const host = createHost(presets.inlineWidget());
const display = host.mount(document.getElementById("widget"), {
displayId: "w1",
title: "A chart",
scheme: "light",
});
display.setContent("<p>Hello from an agent</p>");In React: <PortholeProvider> around <PortholeFrame config displayId title scheme content /> from
@botiverse/porthole/react.
What the agent gets
import { admitContent, checkTool, guideTool, showTool } from "@botiverse/porthole";
const tools = [showTool(config), guideTool(config), checkTool(config)]; // name, description, JSON Schema
const admitted = admitContent(config, html); // refuses only what certainly fails, saying what to changecheckPage (@botiverse/porthole/check) runs a page in the real sandbox at phone and desktop
width, light and dark, before anyone sees it, and tells the agent what broke in a few lines.
examples/mcp/server.mjs wires the tools as an MCP server.
Read next
- docs/ADOPTING.md: getting started, step by step: the server, the page, its CSP, Workers, local development, the agent's tools, conformance, troubleshooting.
- Choosing egress: what content may load (closed, library CDNs, passive media from any https site), what each level opens and the risk it re-opens.
- docs/DESIGN.md: how it works. docs/DECISIONS.md: why, by P-number (the code cites them).
Subpaths
| Import | What |
| --- | --- |
| @botiverse/porthole | createHost, presets, defineDisplays, portholeClient, lintContent, the tools (browser and server) |
| @botiverse/porthole/react | PortholeProvider, PortholeFrame, PortholeView |
| @botiverse/porthole/server | createContentHost (routes), contentHostFor, prefixedName, d1TicketStore (any runtime) |
| @botiverse/porthole/node | nodeServer, nodeHost, nodeProbe (Node's http server) |
| @botiverse/porthole/mcp | the MCP Apps adapter |
| @botiverse/porthole/check | checkPage, playwrightDriver, cdpDriver: the agent's pre-show check |
| @botiverse/porthole/conformance | defineHostConformance: the suite, against your app (Playwright) |
npx porthole secret makes a secret; npx porthole d1-schema prints the D1 migration.
Apache-2.0. /server also serves copies of Chart.js 4.5.1 (MIT) and
D3 7.9.0 (ISC) to live widgets (P079), each with its license beside it
(/__porthole/lib/<name>@<version>/), from src/libraries/files.generated.ts.
