Skip to main content
A Studio plugin extends the browser shell itself — it contributes full pages, rich tool-run panels, settings tabs, and sidebar nav entries to a running Studio. It is distinct from a server code plugin: a code plugin runs in the Python process and adds HTTP surface, while a Studio plugin is a browser ESM bundle that the shell loads at runtime. A deployment turns a Studio plugin on by naming its package in the server manifest’s studio_plugins list. Each named package ships its plugin dist alongside a studio-manifest.json; the skeleton serves the assets under /api/plugins/{name}/studio/ and injects them into the shell.

The registration contract

A plugin bundle exports a single register(context) entry. The host imports the bundle, calls register with a PluginContext, and awaits it; every contribution flows through that context — there are no free registration functions.
The four methods are:
  • registerPage — a full page mounted under /plugins/{pluginId}/{path}.
  • registerToolPanel — a rich run panel for a tool by name, replacing the auto-generated form.
  • registerSettingsTab — a tab in the settings area.
  • registerNavEntry — a sidebar link to one of this plugin’s pages.
The context is sealed once register settles: a registration made afterward (from a deferred timer or a post-resolve microtask) throws instead of silently dropping, and a register that throws commits nothing. The host stamps each contribution with the plugin’s identity, so a plugin never supplies its own id and two plugins may register the same path without colliding. registerNavEntry adds a sidebar link, and its path must match a page the same plugin also registers — a nav entry with no backing page is a dead link and is rejected loudly. The optional icon is a component rendering a square inline SVG that fills its box and draws with currentColor; the host constrains it to a fixed 1em box, aria-hidden (the title is the accessible name). The icon’s bytes live inside the bundle, so there is no external fetch and no CSP change. Omit it for a text-only entry, exactly like the core nav. The same set is declared statically in the plugin’s studio-manifest.json under contributions.nav_entries, so the server-side catalog and read surfaces can report a nav-contributing plugin without executing its bundle. That manifest field is the declarative parity of the runtime registerNavEntry, and it carries the same rule: every nav_entries entry must appear in the plugin’s pages, or manifest load fails loudly.

The plugin manifest

Every plugin ships a studio-manifest.json next to its dist. It is parsed strictly — an unknown field is a loud rejection — and carries:
  • name, version — the plugin’s package identity.
  • api_version — the Studio-plugin API version the bundle targets (see below).
  • entry — the ESM bundle filename the host imports.
  • integrity — a map of every emitted asset filename (the entry bundle and every lazy chunk, plus any stylesheet) to its sha384-<base64> subresource-integrity hash. The host serves only integrity-listed files and injects each with its SRI hash.
  • contributions — the declared tool_panels, pages, settings_tabs, and nav_entries.

The api_version gate

The plugin API carries a single monotonic integer, STUDIO_PLUGIN_API_VERSION (currently 1), bumped only on a breaking change to the plugin API or to the public prop surface of a design-system component the SDK exports. The host accepts a plugin iff the plugin’s targeted api_version exactly equals the host’s; anything else is rejected with a “must be rebuilt” error card, and the plugin commits nothing. Equality is the correct gate because additive changes never bump the version — a host that gains a new optional capability keeps the same version, so every existing plugin keeps loading. The one asymmetry the equality gate leaves is a plugin that uses a newer capability on an older host that lacks it; that fails loudly by construction (calling a context method the host does not define throws during register, caught as that plugin’s error card), never through silent negotiation.

Styling: two sanctioned paths

A plugin styles itself in exactly one of two sanctioned ways, and no other:
  1. SDK components plus inline styles that read the design-system tokens (var(--tai-*)). Raw Tailwind utilities are host-internal and are not part of this contract.
  2. A plugin-shipped scoped stylesheet, listed as a .css asset in the manifest integrity map and host-injected as an SRI’d <link rel="stylesheet"> before the bundle’s JS is imported.
A scoped stylesheet is bound by three hard rules:
  • No global resets or preflight — never style html, body, :root, *, or bare element selectors at the top level.
  • Scope every selector under a plugin root class the plugin renders itself, prefixed with the plugin’s own package name.
  • Theme through the tokens — colors in plugin-authored rules come from the SDK custom properties (var(--tai-*)), never hardcoded, so the stylesheet themes itself with no logic of its own. Bundled third-party base CSS is exempt from this rule, but the no-global and scoping rules still bind it.
Stylesheet injection is a host capability. A plugin shipping a .css asset to a host that does not inject stylesheets is served but never injected — the page renders with SDK-component styling only. This is the single quiet degradation the mechanism admits, so pair a Studio host with plugins built for it.

See also

  • Plugin API reference — the PluginContext, contribution types, and the version gate, generated from the SDK.
  • The manifest — the studio_plugins field that turns a plugin on.
  • The screens — the shell surfaces a plugin’s pages and nav entries join.
  • Studio SDK reference — the hooks and shared components a plugin builds from.