// ============================================================= // DRAFT. Studio plugin contract — NOT yet frozen. // ============================================================= // // This file is the v0-draft WIT contract for the Studio plugin // subsystem described in `docs/advanced/studio-plugins.md`. // // Status: draft. Subject to break-change until the first // in-tree plugin (studio.theme.solarized) round-trips through // the Studio plugin-host implementation. Once it does, this // file moves to v0.1 and follows the same freeze-rules as // `world.wit`. // // Why a separate WIT file: // // - `world.wit` defines the host-to-module wire for *flow // modules* (text.extract, llm.chat etc.). Those modules // speak in payloads + flow capabilities — concepts the // plugin layer doesn't need. // - Studio plugins are also WASM modules sandboxed by the // hub, but their host is the Studio GUI, not the flow // engine. They want a different surface: theme tokens, // translation calls, output-view widget specs. // // Plugins target the `studio-plugin` world below. The host // (Studio) instantiates the component, calls the relevant // hook function per declared `studio_extension.hook` in // module.yaml, and renders / wires the result. package chain:studio-plugin@0.1.0; // ============================================================= // Shared types // ============================================================= interface plugin-types { /// What kind of hook the plugin satisfies. The hub reads /// this from `module.yaml#studio_extension.hook`. A single /// plugin module can declare multiple hooks (e.g. a theme /// plugin that also provides a translation back-end) by /// exporting more than one of the hook interfaces; the hub /// matches each exported hook against the declared hook /// list. enum hook-kind { translate, output-view, theme, } /// Studio renders in a Material 3 color-scheme. Plugins /// produce one for each brightness; both are required so /// the operator's light/dark toggle keeps working. record color-scheme { /// "light" or "dark". The plugin returns one scheme per /// brightness via separate `theme-for` calls. brightness: string, /// 32-bit ARGB packed colors. Order matches Material 3: /// primary, on-primary, secondary, on-secondary, /// tertiary, on-tertiary, error, on-error, surface, /// on-surface, surface-variant, on-surface-variant, /// outline, outline-variant. /// Studio fills any missing slot from its built-in /// fallback before applying. tokens: list, } /// Describes how to render a single flow output. Returned /// from `output-view.render`. Studio reads `kind` and /// builds the matching widget tree. variant rendered-output { /// Plain text — Studio shows a SelectableText. text(string), /// Markdown — Studio shows a Markdown widget with the /// shared FaiTheme.markdownStyle. markdown(string), /// Already-pretty-printed JSON — Studio shows it in a /// code-block container. pretty-json(string), /// PNG / JPEG image data, ready to feed Image.memory. /// MIME stays out because Flutter sniffs it; the plugin /// is responsible for delivering a real image format. image(list), /// Bag-of-fields the plugin couldn't render but wants /// passed through. Studio falls back to its built-in /// renderer. `reason` is operator-visible — explain why /// the plugin declined. passthrough(string), } /// Generic error a plugin can return without panicking. variant plugin-error { /// The plugin understands the request but refused (e.g. /// translate called with from==to). Studio falls back /// to the built-in renderer. declined(string), /// The plugin is mis-configured (e.g. translate plugin /// pointed at an unreachable Ollama endpoint). Operator /// sees the message in the friendly-error mapper. misconfigured(string), /// Generic catch-all. Avoid when one of the above fits. internal(string), } } // ============================================================= // Hook interfaces — each is optional. A plugin exports the // ones it satisfies; the hub only calls those. // ============================================================= /// `translate` hook. Used by Studio to translate server-supplied /// English text (MCP tool descriptions, n8n endpoint names, LLM /// output) into the active locale. The honest `[EN]` badge stays /// the fallback when no translate plugin is installed or every /// installed plugin declines. interface translate { use plugin-types.{plugin-error}; /// Translate `text` from `from-locale` to `to-locale`. /// BCP-47 codes (e.g. "en", "de", "en-US"). Empty /// `from-locale` lets the plugin auto-detect; an empty /// `to-locale` is an error — the host always knows the /// target locale. translate: func( text: string, from-locale: string, to-locale: string, ) -> result; } /// `output-view` hook. Used by Studio's FaiFlowOutput widget /// to ask a plugin "do you want to render this payload?". The /// plugin returns either a rendered-output variant or /// `passthrough` so Studio falls back. Plugins are queried in /// operator-configured priority order; the first non- /// `passthrough` answer wins. interface output-view { use plugin-types.{rendered-output, plugin-error}; /// `mime-type` is the value from the bytes/file payload /// (e.g. "application/pdf", "image/png"). Empty when the /// payload kind doesn't carry one (text / json). /// `data` is the payload's raw content. Text/json payloads /// arrive UTF-8 encoded; bytes/file payloads stay raw. /// The plugin decides whether to handle the type. render: func( mime-type: string, data: list, ) -> result; } /// `theme` hook. Used by Studio's theme selector at startup. /// The plugin returns one color-scheme per brightness; Studio /// merges with its built-in fallback for unspecified tokens. interface theme { use plugin-types.{color-scheme, plugin-error}; /// `brightness` is "light" or "dark". The host calls the /// function twice at theme-apply time (once per brightness) /// so plugins can implement both in one place. theme-for: func(brightness: string) -> result; } // ============================================================= // World — what a plugin component looks like to the host. // ============================================================= /// A Studio plugin component. Plugins target this world via /// `cargo build --target wasm32-wasip2` after declaring it in /// their Cargo.toml's `[package.metadata.component]`. /// /// Plugins export only the hooks they implement. The hub /// probes which exports exist via component-introspection /// before issuing the matching call — so a plugin that only /// declares `hook: theme` in module.yaml needs to export only /// the `theme` interface, not the others. world studio-plugin { /// Plugins import the same `host` interface as flow modules, /// so they can emit structured logs without a separate /// bridge. Logs flow into the hub's event log. import chain:platform/host@1.0.0; /// Hook exports. Each is optional; the hub introspects the /// component to learn which the plugin supports. export translate; export output-view; export theme; }