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 singleregister(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.
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.
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.
Nav entries
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 astudio-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 itssha384-<base64>subresource-integrity hash. The host serves only integrity-listed files and injects each with its SRI hash.contributions— the declaredtool_panels,pages,settings_tabs, andnav_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:- 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. - A plugin-shipped scoped stylesheet, listed as a
.cssasset in the manifestintegritymap and host-injected as an SRI’d<link rel="stylesheet">before the bundle’s JS is imported.
- 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_pluginsfield 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.

