feat: v0.1.0 — Studio Plugin SDK
First release of the SDK that Studio plugins depend on. What v0.1 ships: - WIT contract under wit/ (studio-plugin.wit + deps/platform/world.wit, both verbatim from fai/platform/wit/). - wit-bindgen 0.36 generate! call with pub_export_macro enabled so the emitted __export_studio_plugin_impl! macro is reachable from downstream plugin crates. - export_plugin!(MyType) convenience macro that wraps the bindgen-generated impl macro. - README documenting usage + v0.1 limits (single-world -> all-three-hooks-required, no proc-macro yet, edition 2021 pinned by wit-bindgen output). What v0.1 doesn't yet do: - Plugins still declare all three Guest impls (theme, translate, output-view). v0.2 splits the WIT into one world per hook. - No #[plugin(...)] proc-macro. v0.2 wraps generate! + Guest impl generation into a single attribute. - Host-side: the hub doesn't yet load studio-plugin-world components or expose InvokePlugin RPC — that's the next push. Validated against the in-tree studio-theme-solarized plugin: 22 KiB stripped .wasm artefact, valid Component Model, passes wasm-tools component wit inspection. Signed-off-by: flemming-it <sf@flemming.it>
This commit is contained in:
commit
3753c6bc37
7 changed files with 562 additions and 0 deletions
4
.gitignore
vendored
Normal file
4
.gitignore
vendored
Normal file
|
|
@ -0,0 +1,4 @@
|
||||||
|
target/
|
||||||
|
**/*.rs.bk
|
||||||
|
.DS_Store
|
||||||
|
Cargo.lock
|
||||||
57
Cargo.toml
Normal file
57
Cargo.toml
Normal file
|
|
@ -0,0 +1,57 @@
|
||||||
|
# fai-studio-plugin-sdk
|
||||||
|
#
|
||||||
|
# Ergonomic wrapper around the WIT bindings declared in
|
||||||
|
# `wit/studio-plugin.wit`. Studio plugins depend on this crate
|
||||||
|
# rather than dealing with raw wit-bindgen output. v0.1 ships
|
||||||
|
# WITHOUT the `#[plugin(...)]` proc-macro: plugins write the
|
||||||
|
# `export!()` invocation themselves with the structs the SDK
|
||||||
|
# provides. The macro lands in v0.2 once the contract is
|
||||||
|
# stable enough.
|
||||||
|
#
|
||||||
|
# Build a plugin:
|
||||||
|
#
|
||||||
|
# cargo build --release --target wasm32-wasip2
|
||||||
|
#
|
||||||
|
# Use:
|
||||||
|
#
|
||||||
|
# use fai_studio_plugin_sdk::{export_plugin, theme, ColorScheme,
|
||||||
|
# PluginError};
|
||||||
|
#
|
||||||
|
# struct MyTheme;
|
||||||
|
# impl theme::Guest for MyTheme {
|
||||||
|
# fn theme_for(brightness: String)
|
||||||
|
# -> Result<ColorScheme, PluginError> {
|
||||||
|
# ...
|
||||||
|
# }
|
||||||
|
# }
|
||||||
|
#
|
||||||
|
# export_plugin!(MyTheme with_types_in fai_studio_plugin_sdk::bindings);
|
||||||
|
|
||||||
|
[package]
|
||||||
|
name = "fai-studio-plugin-sdk"
|
||||||
|
version = "0.1.0"
|
||||||
|
# edition 2021 for now — wit-bindgen 0.36 emits the older
|
||||||
|
# `#[link_section]` / `unsafe`-attribute syntax that the
|
||||||
|
# edition-2024 compiler rejects. Bump to 2024 when moving
|
||||||
|
# to a wit-bindgen version with edition-2024-compatible
|
||||||
|
# codegen.
|
||||||
|
edition = "2021"
|
||||||
|
authors = ["Dr. Stefan Flemming <platform@flemming.ai>"]
|
||||||
|
license = "Apache-2.0"
|
||||||
|
publish = false
|
||||||
|
description = "F∆I Studio Plugin SDK — Rust wrapper for the studio-plugin WIT"
|
||||||
|
repository = "https://git.flemming.ai/fai/studio-plugin-sdk"
|
||||||
|
rust-version = "1.85"
|
||||||
|
|
||||||
|
[lib]
|
||||||
|
crate-type = ["rlib"]
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
wit-bindgen = "0.36"
|
||||||
|
|
||||||
|
[package.metadata.component]
|
||||||
|
package = "fai:studio-plugin-sdk"
|
||||||
|
|
||||||
|
[package.metadata.component.target]
|
||||||
|
path = "wit"
|
||||||
|
world = "studio-plugin"
|
||||||
107
README.md
Normal file
107
README.md
Normal file
|
|
@ -0,0 +1,107 @@
|
||||||
|
# fai-studio-plugin-sdk
|
||||||
|
|
||||||
|
Rust SDK for writing F∆I Studio plugins.
|
||||||
|
|
||||||
|
## Status
|
||||||
|
|
||||||
|
**v0.1 builds.** Studio plugins targeting the `studio-plugin`
|
||||||
|
world declared in `wit/studio-plugin.wit` can be compiled to
|
||||||
|
`wasm32-wasip2` against this crate today. The proof-of-
|
||||||
|
contract plugin is `studio-theme-solarized` in the
|
||||||
|
fai-modules namespace.
|
||||||
|
|
||||||
|
What's NOT yet implemented:
|
||||||
|
|
||||||
|
- **Hub side**: a Component-Model loader path for
|
||||||
|
`studio-plugin`-world components + the `InvokePlugin`
|
||||||
|
gRPC RPC that Studio talks to. Tracked in
|
||||||
|
`fai/platform/docs/advanced/studio-plugin-sdk.md`.
|
||||||
|
- **Studio side**: `plugin_host.dart` that discovers
|
||||||
|
installed `studio.*` capabilities and dispatches.
|
||||||
|
- **Per-hook worlds**: today's v0.1 ships a single world
|
||||||
|
(`studio-plugin`) that exports all three hooks
|
||||||
|
(`theme`, `translate`, `output-view`). A plugin only
|
||||||
|
caring about one hook still has to declare empty /
|
||||||
|
declined impls for the other two. v0.2 splits into one
|
||||||
|
world per hook.
|
||||||
|
- **`#[plugin(...)]` proc-macro**: v0.1 ships a plain
|
||||||
|
`export_plugin!` macro that wraps the wit-bindgen-emitted
|
||||||
|
`__export_studio_plugin_impl!`. The proc-macro that
|
||||||
|
generates the WIT bindings + the `Guest` impls in one
|
||||||
|
pass arrives in v0.2.
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
```toml
|
||||||
|
# Cargo.toml of your plugin
|
||||||
|
[package]
|
||||||
|
name = "my_studio_plugin"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2021" # match SDK edition
|
||||||
|
|
||||||
|
[lib]
|
||||||
|
crate-type = ["cdylib"]
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
fai-studio-plugin-sdk = { git = "https://git.flemming.ai/fai/studio-plugin-sdk", branch = "main" }
|
||||||
|
```
|
||||||
|
|
||||||
|
```rust
|
||||||
|
// src/lib.rs of your plugin
|
||||||
|
use fai_studio_plugin_sdk::{bindings, export_plugin};
|
||||||
|
|
||||||
|
type ColorScheme = bindings::fai::studio_plugin::plugin_types::ColorScheme;
|
||||||
|
type PluginError = bindings::fai::studio_plugin::plugin_types::PluginError;
|
||||||
|
type RenderedOutput = bindings::fai::studio_plugin::plugin_types::RenderedOutput;
|
||||||
|
|
||||||
|
struct MyPlugin;
|
||||||
|
|
||||||
|
impl bindings::exports::fai::studio_plugin::theme::Guest for MyPlugin {
|
||||||
|
fn theme_for(brightness: String) -> Result<ColorScheme, PluginError> {
|
||||||
|
// Return a ColorScheme or PluginError::Declined.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// v0.1: every plugin must declare all three hooks. Plugins
|
||||||
|
// that don't actually serve a hook return Declined.
|
||||||
|
impl bindings::exports::fai::studio_plugin::translate::Guest for MyPlugin {
|
||||||
|
fn translate(_: String, _: String, _: String) -> Result<String, PluginError> {
|
||||||
|
Err(PluginError::Declined("not a translate plugin".to_string()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl bindings::exports::fai::studio_plugin::output_view::Guest for MyPlugin {
|
||||||
|
fn render(_: String, _: Vec<u8>) -> Result<RenderedOutput, PluginError> {
|
||||||
|
Err(PluginError::Declined("not a view plugin".to_string()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export_plugin!(MyPlugin);
|
||||||
|
```
|
||||||
|
|
||||||
|
Then:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cargo build --release --target wasm32-wasip2
|
||||||
|
fai pack . -o my-plugin-0.1.0.fai
|
||||||
|
```
|
||||||
|
|
||||||
|
## WIT layout
|
||||||
|
|
||||||
|
```
|
||||||
|
wit/
|
||||||
|
├── studio-plugin.wit # main world, fai:studio-plugin@0.1.0
|
||||||
|
└── deps/
|
||||||
|
└── platform/
|
||||||
|
└── world.wit # fai:platform@1.0.0 (host imports)
|
||||||
|
```
|
||||||
|
|
||||||
|
Both files are verbatim copies of the authoritative versions
|
||||||
|
in `fai/platform/wit/`. When the contract changes upstream,
|
||||||
|
re-copy here in lock-step.
|
||||||
|
|
||||||
|
## Why edition 2021
|
||||||
|
|
||||||
|
wit-bindgen 0.36 emits `#[link_section]` / unsafe-attribute
|
||||||
|
syntax that the edition-2024 compiler rejects. Bump when
|
||||||
|
moving to a wit-bindgen version with edition-2024-compatible
|
||||||
|
codegen.
|
||||||
4
rust-toolchain.toml
Normal file
4
rust-toolchain.toml
Normal file
|
|
@ -0,0 +1,4 @@
|
||||||
|
[toolchain]
|
||||||
|
channel = "1.86"
|
||||||
|
targets = ["wasm32-wasip2"]
|
||||||
|
components = ["rustfmt", "clippy"]
|
||||||
104
src/lib.rs
Normal file
104
src/lib.rs
Normal file
|
|
@ -0,0 +1,104 @@
|
||||||
|
//! `fai-studio-plugin-sdk` — ergonomic wrapper around the
|
||||||
|
//! Studio-plugin WIT contract.
|
||||||
|
//!
|
||||||
|
//! Studio plugins are WASM components targeting the
|
||||||
|
//! `studio-plugin` world declared in `wit/studio-plugin.wit`
|
||||||
|
//! (a verbatim copy of `fai/platform`'s authoritative file).
|
||||||
|
//! They export between one and three hooks — `translate`,
|
||||||
|
//! `output-view`, `theme` — and import the same `host`
|
||||||
|
//! interface every flow module uses for structured logging.
|
||||||
|
//!
|
||||||
|
//! # SKELETON
|
||||||
|
//!
|
||||||
|
//! v0.1 of this crate gives plugins:
|
||||||
|
//!
|
||||||
|
//! * `wit_bindgen!`-generated bindings re-exported under
|
||||||
|
//! [`bindings`] so plugins reference one stable name.
|
||||||
|
//! * Ergonomic re-exports for the most-used types
|
||||||
|
//! ([`ColorScheme`], [`PluginError`], [`RenderedOutput`]).
|
||||||
|
//! * An `export_plugin!` macro that hides the `export!`
|
||||||
|
//! incantation. v0.1 takes a single struct that
|
||||||
|
//! implements one or more `Guest` traits; v0.2 will
|
||||||
|
//! turn this into a proper proc-macro
|
||||||
|
//! (`#[plugin(theme, translate)]`).
|
||||||
|
//!
|
||||||
|
//! What v0.1 deliberately does NOT yet do:
|
||||||
|
//!
|
||||||
|
//! * Generate any glue for the *host* side (the hub still
|
||||||
|
//! needs a dedicated component loader for
|
||||||
|
//! `studio-plugin`-world components — that lands as part
|
||||||
|
//! of the `InvokePlugin` RPC implementation).
|
||||||
|
//! * Provide testing helpers. Plugins author their own
|
||||||
|
//! unit tests against the trait impls directly.
|
||||||
|
|
||||||
|
#![allow(missing_docs)]
|
||||||
|
|
||||||
|
/// Raw bindings generated from the WIT contract. Plugins
|
||||||
|
/// access types through the convenient re-exports below in
|
||||||
|
/// most cases; this module is here for when they need the
|
||||||
|
/// underlying definitions (e.g. to implement multiple Guest
|
||||||
|
/// traits in one struct).
|
||||||
|
pub mod bindings {
|
||||||
|
wit_bindgen::generate!({
|
||||||
|
world: "studio-plugin",
|
||||||
|
path: "wit",
|
||||||
|
// Generate bindings for the entire WIT package
|
||||||
|
// (incl. fai:platform/host that studio-plugin imports
|
||||||
|
// for log emission), so plugins don't need a separate
|
||||||
|
// `with: …` mapping.
|
||||||
|
generate_all,
|
||||||
|
// Export the `export!` macro publicly so downstream
|
||||||
|
// plugin crates can invoke it via the SDK's
|
||||||
|
// `export_plugin!` wrapper without each plugin
|
||||||
|
// re-running `wit_bindgen::generate!()` against a
|
||||||
|
// copy of the WIT files.
|
||||||
|
pub_export_macro: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// v0.1: plugin authors reference paths inside `bindings::`
|
||||||
|
// directly. The typical ones are:
|
||||||
|
//
|
||||||
|
// bindings::exports::fai::studio_plugin::theme::Guest
|
||||||
|
// bindings::exports::fai::studio_plugin::translate::Guest
|
||||||
|
// bindings::exports::fai::studio_plugin::output_view::Guest
|
||||||
|
// bindings::fai::studio_plugin::plugin_types::ColorScheme
|
||||||
|
// bindings::fai::studio_plugin::plugin_types::PluginError
|
||||||
|
// bindings::fai::studio_plugin::plugin_types::RenderedOutput
|
||||||
|
//
|
||||||
|
// Stable re-exports under terser names land in v0.2 — they
|
||||||
|
// have to wait for the proc-macro that materialises the
|
||||||
|
// `Guest` impls anyway.
|
||||||
|
|
||||||
|
/// Convenience constructor: declare a plugin component by
|
||||||
|
/// pointing this macro at the struct that implements the hook
|
||||||
|
/// `Guest` traits.
|
||||||
|
///
|
||||||
|
/// ```ignore
|
||||||
|
/// use fai_studio_plugin_sdk::{export_plugin, bindings};
|
||||||
|
///
|
||||||
|
/// struct MyTheme;
|
||||||
|
/// impl bindings::exports::fai::studio_plugin::theme::Guest for MyTheme {
|
||||||
|
/// fn theme_for(b: String)
|
||||||
|
/// -> Result<bindings::fai::studio_plugin::plugin_types::ColorScheme,
|
||||||
|
/// bindings::fai::studio_plugin::plugin_types::PluginError>
|
||||||
|
/// {
|
||||||
|
/// // ...
|
||||||
|
/// }
|
||||||
|
/// }
|
||||||
|
///
|
||||||
|
/// export_plugin!(MyTheme);
|
||||||
|
/// ```
|
||||||
|
///
|
||||||
|
/// One struct can implement multiple `Guest` traits — the SDK
|
||||||
|
/// does not require a separate struct per hook.
|
||||||
|
///
|
||||||
|
/// Under the hood this resolves to the `__export_studio_plugin_impl!`
|
||||||
|
/// macro that `wit_bindgen::generate!(pub_export_macro: true)`
|
||||||
|
/// emitted at the crate root.
|
||||||
|
#[macro_export]
|
||||||
|
macro_rules! export_plugin {
|
||||||
|
($struct:ident) => {
|
||||||
|
$crate::__export_studio_plugin_impl!($struct with_types_in $crate::bindings);
|
||||||
|
};
|
||||||
|
}
|
||||||
99
wit/deps/platform/world.wit
Normal file
99
wit/deps/platform/world.wit
Normal file
|
|
@ -0,0 +1,99 @@
|
||||||
|
// =============================================================
|
||||||
|
// FROZEN. Wire contract between hub and modules.
|
||||||
|
// =============================================================
|
||||||
|
//
|
||||||
|
// This file is the stable wire contract between the F∆I Hub
|
||||||
|
// and any module compiled against it. It is **frozen at v1.0**.
|
||||||
|
//
|
||||||
|
// Frozen means:
|
||||||
|
//
|
||||||
|
// - No removal, rename, or signature change of any existing
|
||||||
|
// interface, function, type, field, or variant case.
|
||||||
|
// - Additive changes (new types, new variant cases at the end,
|
||||||
|
// new fields with stable defaults) require a minor bump
|
||||||
|
// (1.0 -> 1.1) and a coordinated review.
|
||||||
|
// - Any breaking change requires a major bump (1.0 -> 2.0)
|
||||||
|
// and a parallel-world transition plan.
|
||||||
|
//
|
||||||
|
// The fai_runtime crate ships a snapshot test that fails if
|
||||||
|
// this file changes without an intentional, reviewed update.
|
||||||
|
// See `crates/fai_runtime/tests/wit_freeze.rs`.
|
||||||
|
//
|
||||||
|
// Module authors do NOT depend on this file directly. They
|
||||||
|
// use `fai-module-sdk`, which wraps these bindings behind a
|
||||||
|
// stable, ergonomic surface. That is what protects modules
|
||||||
|
// from any future evolution of this contract.
|
||||||
|
|
||||||
|
package fai:platform@1.0.0;
|
||||||
|
|
||||||
|
/// Types that flow between the host (hub) and modules.
|
||||||
|
interface types {
|
||||||
|
/// A typed value passed between the host and a module.
|
||||||
|
variant payload {
|
||||||
|
/// Plain UTF-8 text.
|
||||||
|
text(string),
|
||||||
|
/// Arbitrary JSON encoded as a string.
|
||||||
|
json(string),
|
||||||
|
/// Raw bytes with a MIME type.
|
||||||
|
bytes(bytes-value),
|
||||||
|
/// A reference to a file, by URI.
|
||||||
|
file-ref(file-ref),
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Inline byte content.
|
||||||
|
record bytes-value {
|
||||||
|
mime-type: string,
|
||||||
|
data: list<u8>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// File reference by URI.
|
||||||
|
record file-ref {
|
||||||
|
uri: string,
|
||||||
|
mime-type: string,
|
||||||
|
size-bytes: u64,
|
||||||
|
sha256: string,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Context passed with every invocation.
|
||||||
|
record invocation-context {
|
||||||
|
invocation-id: string,
|
||||||
|
capability-namespace: string,
|
||||||
|
capability-name: string,
|
||||||
|
deadline-ms: u64,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Structured error type returned by modules.
|
||||||
|
variant invocation-error {
|
||||||
|
invalid-input(string),
|
||||||
|
permission-denied(string),
|
||||||
|
resource-exhausted(string),
|
||||||
|
deadline-exceeded,
|
||||||
|
internal(string),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The host-provided interface that modules can import to talk back.
|
||||||
|
interface host {
|
||||||
|
/// Log a structured message. Level is "trace", "debug", "info", "warn", "error".
|
||||||
|
log: func(level: string, message: string);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The core interface every module exports.
|
||||||
|
interface invoke {
|
||||||
|
use types.{payload, invocation-context, invocation-error};
|
||||||
|
|
||||||
|
/// Invoke the module with inputs, receive outputs or an error.
|
||||||
|
invoke: func(
|
||||||
|
ctx: invocation-context,
|
||||||
|
inputs: list<tuple<string, payload>>,
|
||||||
|
) -> result<list<tuple<string, payload>>, invocation-error>;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The world a module component targets.
|
||||||
|
///
|
||||||
|
/// Modules `export` the invoke interface to be callable by the hub.
|
||||||
|
/// Modules `import` the host interface to emit logs.
|
||||||
|
world module {
|
||||||
|
import host;
|
||||||
|
export invoke;
|
||||||
|
}
|
||||||
187
wit/studio-plugin.wit
Normal file
187
wit/studio-plugin.wit
Normal file
|
|
@ -0,0 +1,187 @@
|
||||||
|
// =============================================================
|
||||||
|
// 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 fai: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<u32>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 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<u8>),
|
||||||
|
/// 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<string, plugin-error>;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// `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<u8>,
|
||||||
|
) -> result<rendered-output, plugin-error>;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// `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<color-scheme, plugin-error>;
|
||||||
|
}
|
||||||
|
|
||||||
|
// =============================================================
|
||||||
|
// 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 fai: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;
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue