Build a plugin view
A plugin adds a console surface by serving its own page and declaring it in the manifest; the console renders it in a sandboxed iframe. No host build. This is the short, copy-me entry — the full guide is Building a plugin view.
Copy the gold-standard
examples/plugins/chat_example is a single-page view that follows every rule below — start by copying it:
bash
cp -r examples/plugins/chat_example plugins/yaml
plugins:
enabled: [chat_example]Reload. (chat_example claims the chat slot; a normal view adds a rail icon instead.)
The four rules
- Serve the path you declare — the manifest's
views[].pathMUST equal a path yourregister_routerserves (default prefix/plugins/<id>; a custom prefix is fine, just keep them in sync). A mismatch is a blank iframe. - Gate by default — mount data routes under
prefix="/api/plugins/<id>"so they inherit the operator bearer gate. Use the ungated/plugins/<id>prefix only for genuinely public assets (the page itself — an iframe page-load can't carry anAuthorizationheader). - Same-origin, slug-aware, never hardcode — derive
base = location.pathname.split("/plugins/")[0]and prefix every fetch/asset. Never hardcode/api/.../,/plugins/.../, orhttp://localhost:PORT— it breaks the/agents/<slug>/fleet proxy and the same-origin token handshake. - Link the DS kit —
<base>/_ds/plugin-kit.css+<base>/_ds/plugin-kit.jsinstead of hardcoding hex or a CDN. The kit's--pl-*tokens re-skin to the operator's live theme.
The kit helper API (plugin-kit.js)
The console serves the design-system kit same-origin at <base>/_ds/plugin-kit.{css,js}. plugin-kit.js is an ES module — a classic <script src> throws Unexpected token 'export' and never runs it, so load it with a dynamic import() (the slug-aware URL can't be a static import specifier):
| Helper | What it does |
|---|---|
initPluginView(onInit?) | Listens for the protoagent:init handshake (bearer + theme) and live protoagent:theme re-themes, mapping the console theme onto the DS --pl-* tokens. Call once on load. |
getToken() | The captured operator bearer (null until the handshake delivers one). |
apiFetch(input, init?) | Same-origin fetch with Authorization: Bearer <token> attached when present, resolved slug-aware — pass a bare /api/..., no manual base. For every gated /api/... call. |
html
<script type="module">
const base = location.pathname.split("/plugins/")[0]; // "" or "/agents/<slug>"
const kit = await import(base + "/_ds/plugin-kit.js");
kit.initPluginView(); // handshake + live re-theme, hands-free
const res = await kit.apiFetch("/api/plugins/mychart/data"); // slug-aware, no manual base
</script>Next
- Building a plugin view — the full guide: the
slot: "chat"panel (ADR 0045), the event-bus bridge (ADR 0039), the sandbox split (ADR 0026 D6), and references. - Plugins — the rest of the plugin contract (tools, subagents, config, MCP).