|
| 1 | +--- |
| 2 | +name: creating-new-builtin-plugins |
| 3 | +description: Adding a new built-in plugin to Golem. Use when creating a brand new plugin that ships with Golem and is auto-provisioned at startup, covering project scaffolding, WASM compilation, provisioning wiring, CLI embedding, and test framework integration. |
| 4 | +--- |
| 5 | + |
| 6 | +# Creating a New Built-in Plugin |
| 7 | + |
| 8 | +This skill covers how to add a **new** built-in plugin to Golem end-to-end. Built-in plugins are WASM components compiled with the Golem CLI, committed as `.wasm` files, embedded in the `golem` CLI binary, and automatically provisioned by the registry service at startup. |
| 9 | + |
| 10 | +For modifying an **existing** plugin, load the `modifying-builtin-plugins` skill instead. |
| 11 | + |
| 12 | +## Architecture Overview |
| 13 | + |
| 14 | +A built-in plugin has these integration points (all must be wired up): |
| 15 | + |
| 16 | +1. **Plugin source code** — a standalone Golem application under `plugins/` |
| 17 | +2. **Compiled WASM artifact** — committed to git at `plugins/<name>.wasm` |
| 18 | +3. **Build task** — added to `Makefile.toml` under `build-plugins` |
| 19 | +4. **Registry service config** — `BuiltinPluginsConfig` struct with WASM bytes/path fields |
| 20 | +5. **Provisioner** — `builtin_plugin_provisioner.rs` creates the component, deploys it, registers the plugin, and grants it |
| 21 | +6. **Bootstrap wiring** — `golem-registry-service/src/bootstrap/mod.rs` loads WASM and calls the provisioner |
| 22 | +7. **CLI embedding** — `cli/golem/src/launch.rs` uses `include_bytes!` to embed the WASM |
| 23 | +8. **Test framework** — `golem-test-framework/src/components/registry_service/` loads WASM from filesystem and passes it via env vars |
| 24 | + |
| 25 | +## Step-by-Step Guide |
| 26 | + |
| 27 | +### 1. Create the Plugin Project |
| 28 | + |
| 29 | +Create a new Golem application under `plugins/`: |
| 30 | + |
| 31 | +``` |
| 32 | +plugins/ |
| 33 | + my-plugin/ |
| 34 | + components-rust/my-plugin/ |
| 35 | + src/lib.rs # Plugin implementation |
| 36 | + Cargo.toml # crate-type = ["cdylib"] |
| 37 | + golem.yaml # Component manifest with copy command |
| 38 | + Cargo.toml # Workspace Cargo.toml |
| 39 | + golem.yaml # Application manifest |
| 40 | + .gitignore # Ignore target/ and golem-temp/ |
| 41 | +``` |
| 42 | + |
| 43 | +Use the existing `plugins/otlp-exporter/` as a template: |
| 44 | + |
| 45 | +**`plugins/my-plugin/.gitignore`:** |
| 46 | +``` |
| 47 | +target/ |
| 48 | +golem-temp/ |
| 49 | +``` |
| 50 | + |
| 51 | +**`plugins/my-plugin/Cargo.toml`** (workspace root): |
| 52 | +```toml |
| 53 | +[workspace] |
| 54 | +resolver = "2" |
| 55 | +members = ["components-rust/*"] |
| 56 | + |
| 57 | +[profile.release] |
| 58 | +opt-level = "s" |
| 59 | +lto = true |
| 60 | + |
| 61 | +[workspace.dependencies] |
| 62 | +golem-rust = { path = "../../sdks/rust/golem-rust", features = ["export_oplog_processor"] } |
| 63 | +# Add other dependencies as needed |
| 64 | +``` |
| 65 | + |
| 66 | +Note: The `features` on `golem-rust` depend on the plugin type. For oplog processors use `export_oplog_processor`. Adjust based on the plugin kind. |
| 67 | + |
| 68 | +**`plugins/my-plugin/golem.yaml`:** |
| 69 | +```yaml |
| 70 | +app: my-plugin |
| 71 | + |
| 72 | +includes: |
| 73 | + - components-*/*/golem.yaml |
| 74 | + |
| 75 | +environments: |
| 76 | + local: |
| 77 | + server: local |
| 78 | + componentPresets: debug |
| 79 | + cloud: |
| 80 | + server: cloud |
| 81 | + componentPresets: release |
| 82 | +``` |
| 83 | +
|
| 84 | +**`plugins/my-plugin/components-rust/my-plugin/Cargo.toml`:** |
| 85 | +```toml |
| 86 | +[package] |
| 87 | +name = "my_plugin" |
| 88 | +version = "0.0.1" |
| 89 | +edition = "2021" |
| 90 | +
|
| 91 | +[lib] |
| 92 | +crate-type = ["cdylib"] |
| 93 | +path = "src/lib.rs" |
| 94 | +
|
| 95 | +[dependencies] |
| 96 | +golem-rust = { workspace = true } |
| 97 | +``` |
| 98 | + |
| 99 | +**`plugins/my-plugin/components-rust/my-plugin/golem.yaml`:** |
| 100 | +```yaml |
| 101 | +components: |
| 102 | + my:plugin: |
| 103 | + templates: rust |
| 104 | +
|
| 105 | +customCommands: |
| 106 | + copy: |
| 107 | + - command: cp ../../golem-temp/agents/my_plugin_release.wasm ../../../my-plugin.wasm |
| 108 | +``` |
| 109 | + |
| 110 | +The copy command filename is derived from the component name: colons become underscores, suffixed with `_release.wasm`. Verify the actual output filename in `golem-temp/agents/` after building. |
| 111 | + |
| 112 | +### 2. Implement the Plugin |
| 113 | + |
| 114 | +Write the plugin code in `plugins/my-plugin/components-rust/my-plugin/src/lib.rs`. The implementation depends on the plugin type (currently only `OplogProcessor` is supported): |
| 115 | + |
| 116 | +```rust |
| 117 | +use golem_rust::oplog_processor::exports::golem::api::oplog_processor::Guest as OplogProcessorGuest; |
| 118 | +use golem_rust::bindings::golem::api::oplog::{OplogEntry, OplogIndex}; |
| 119 | +use golem_rust::golem_wasm::golem_core_1_5_x::types::{AgentId, ComponentId}; |
| 120 | +
|
| 121 | +struct MyPluginComponent; |
| 122 | +
|
| 123 | +impl OplogProcessorGuest for MyPluginComponent { |
| 124 | + fn process( |
| 125 | + _account_info: golem_rust::oplog_processor::exports::golem::api::oplog_processor::AccountInfo, |
| 126 | + config: Vec<(String, String)>, |
| 127 | + component_id: ComponentId, |
| 128 | + worker_id: AgentId, |
| 129 | + metadata: golem_rust::bindings::golem::api::host::AgentMetadata, |
| 130 | + _first_entry_index: OplogIndex, |
| 131 | + entries: Vec<OplogEntry>, |
| 132 | + ) -> Result<(), String> { |
| 133 | + // Plugin logic here |
| 134 | + Ok(()) |
| 135 | + } |
| 136 | +} |
| 137 | +
|
| 138 | +golem_rust::oplog_processor::export_oplog_processor!(MyPluginComponent with_types_in golem_rust::oplog_processor); |
| 139 | +``` |
| 140 | + |
| 141 | +### 3. Build and Commit the WASM |
| 142 | + |
| 143 | +```shell |
| 144 | +cd plugins/my-plugin |
| 145 | +golem build -P release |
| 146 | +golem exec -P release copy |
| 147 | +``` |
| 148 | + |
| 149 | +Verify `plugins/my-plugin.wasm` was created, then **commit it to git**. This file is required at compile time by the CLI. |
| 150 | + |
| 151 | +### 4. Add to `Makefile.toml` |
| 152 | + |
| 153 | +Update the `build-plugins` task in `Makefile.toml` to include the new plugin: |
| 154 | + |
| 155 | +```toml |
| 156 | +[tasks.build-plugins] |
| 157 | +dependencies = ["build"] |
| 158 | +description = "Builds built-in plugins (requires golem CLI to be built first)" |
| 159 | +script_runner = "@duckscript" |
| 160 | +script = ''' |
| 161 | +cd plugins/otlp-exporter |
| 162 | +exec --fail-on-error ../../target/debug/golem build -P release --force-build |
| 163 | +exec --fail-on-error ../../target/debug/golem exec -P release copy |
| 164 | +cd ../.. |
| 165 | +cd plugins/my-plugin |
| 166 | +exec --fail-on-error ../../target/debug/golem build -P release --force-build |
| 167 | +exec --fail-on-error ../../target/debug/golem exec -P release copy |
| 168 | +cd ../.. |
| 169 | +''' |
| 170 | +``` |
| 171 | + |
| 172 | +### 5. Add Config Fields |
| 173 | + |
| 174 | +In `golem-registry-service/src/config.rs`, add fields to `BuiltinPluginsConfig`: |
| 175 | + |
| 176 | +```rust |
| 177 | +#[derive(Clone, Debug, Serialize, Deserialize, Default)] |
| 178 | +pub struct BuiltinPluginsConfig { |
| 179 | + pub enabled: bool, |
| 180 | + #[serde(skip)] |
| 181 | + pub otlp_exporter_wasm: Option<Arc<[u8]>>, |
| 182 | + pub otlp_exporter_wasm_path: Option<PathBuf>, |
| 183 | + #[serde(skip)] |
| 184 | + pub my_plugin_wasm: Option<Arc<[u8]>>, // New |
| 185 | + pub my_plugin_wasm_path: Option<PathBuf>, // New |
| 186 | +} |
| 187 | +``` |
| 188 | + |
| 189 | +The `#[serde(skip)]` field holds the in-memory WASM bytes (set programmatically). The path field is set via environment variable `GOLEM__BUILTIN_PLUGINS__MY_PLUGIN_WASM_PATH`. |
| 190 | + |
| 191 | +### 6. Wire Up Provisioning |
| 192 | + |
| 193 | +In `golem-registry-service/src/services/builtin_plugin_provisioner.rs`, add provisioning logic for the new plugin. The provisioner follows a consistent pattern: |
| 194 | + |
| 195 | +1. **Get or skip** — check if the WASM bytes are provided; skip if not |
| 196 | +2. **Create component** — upload WASM into the `builtin-plugins` environment (idempotent: handle `ComponentWithNameAlreadyExists`) |
| 197 | +3. **Deploy environment** — call `deployment_write_service.create_deployment()` so the component becomes deployed |
| 198 | +4. **Register plugin** — create a `PluginRegistrationCreation` with the appropriate `PluginSpecDto` variant (idempotent: handle `PluginNameAndVersionAlreadyExists`) |
| 199 | +5. **Grant to environments** — iterate all environments and grant the plugin |
| 200 | + |
| 201 | +All plugins share the same `golem-system` application and `builtin-plugins` environment. Add new component and plugin constants: |
| 202 | + |
| 203 | +```rust |
| 204 | +const MY_PLUGIN_COMPONENT_NAME: &str = "my:plugin"; |
| 205 | +const MY_PLUGIN_NAME: &str = "golem-my-plugin"; |
| 206 | +const MY_PLUGIN_VERSION: &str = "1.0.0"; |
| 207 | +``` |
| 208 | + |
| 209 | +### 7. Wire Up Bootstrap |
| 210 | + |
| 211 | +In `golem-registry-service/src/bootstrap/mod.rs`, load the new plugin's WASM from the filesystem path (similar to the OTLP exporter pattern): |
| 212 | + |
| 213 | +```rust |
| 214 | +if builtin_plugins.my_plugin_wasm.is_none() { |
| 215 | + if let Some(ref path) = builtin_plugins.my_plugin_wasm_path { |
| 216 | + match std::fs::read(path) { |
| 217 | + Ok(bytes) => { |
| 218 | + tracing::info!("Loaded my-plugin WASM from {}", path.display()); |
| 219 | + builtin_plugins.my_plugin_wasm = Some(Arc::from(bytes)); |
| 220 | + } |
| 221 | + Err(e) => { |
| 222 | + return Err(anyhow!( |
| 223 | + "Failed to read my-plugin WASM from {}: {e}", |
| 224 | + path.display() |
| 225 | + )); |
| 226 | + } |
| 227 | + } |
| 228 | + } |
| 229 | +} |
| 230 | +``` |
| 231 | + |
| 232 | +### 8. Embed in the CLI |
| 233 | + |
| 234 | +In `cli/golem/src/launch.rs`, add an `include_bytes!` for the new WASM and pass it in the config: |
| 235 | + |
| 236 | +```rust |
| 237 | +static MY_PLUGIN_WASM: &[u8] = include_bytes!("../../../plugins/my-plugin.wasm"); |
| 238 | +
|
| 239 | +// In the BuiltinPluginsConfig construction: |
| 240 | +builtin_plugins: BuiltinPluginsConfig { |
| 241 | + enabled: true, |
| 242 | + otlp_exporter_wasm: Some(Arc::from(OTLP_EXPORTER_WASM)), |
| 243 | + my_plugin_wasm: Some(Arc::from(MY_PLUGIN_WASM)), |
| 244 | + ..Default::default() |
| 245 | +}, |
| 246 | +``` |
| 247 | + |
| 248 | +### 9. Wire Up Test Framework |
| 249 | + |
| 250 | +In `golem-test-framework/src/components/registry_service/`: |
| 251 | + |
| 252 | +**`spawned.rs`** — load the WASM path: |
| 253 | +```rust |
| 254 | +let my_plugin_wasm = working_directory.join("../plugins/my-plugin.wasm"); |
| 255 | +let my_plugin_wasm_path = if my_plugin_wasm.exists() { |
| 256 | + Some(my_plugin_wasm.as_path()) |
| 257 | +} else { |
| 258 | + None |
| 259 | +}; |
| 260 | +``` |
| 261 | + |
| 262 | +**`mod.rs`** — pass the path as an env var in `env_vars()`: |
| 263 | +```rust |
| 264 | +// Add parameter: my_plugin_wasm_path: Option<&Path> |
| 265 | +let builder = if let Some(wasm_path) = my_plugin_wasm_path { |
| 266 | + builder.with( |
| 267 | + "GOLEM__BUILTIN_PLUGINS__MY_PLUGIN_WASM_PATH", |
| 268 | + wasm_path.to_string_lossy().to_string(), |
| 269 | + ) |
| 270 | +} else { |
| 271 | + builder |
| 272 | +}; |
| 273 | +``` |
| 274 | + |
| 275 | +## Checklist |
| 276 | + |
| 277 | +- [ ] Plugin source created under `plugins/<name>/` |
| 278 | +- [ ] Plugin compiles: `cd plugins/<name> && golem build -P release && golem exec -P release copy` |
| 279 | +- [ ] `plugins/<name>.wasm` exists and is committed to git |
| 280 | +- [ ] `Makefile.toml` `build-plugins` task updated |
| 281 | +- [ ] `BuiltinPluginsConfig` has new WASM and path fields |
| 282 | +- [ ] `builtin_plugin_provisioner.rs` provisions the new plugin (create component, deploy, register, grant) |
| 283 | +- [ ] `bootstrap/mod.rs` loads WASM from filesystem path |
| 284 | +- [ ] `cli/golem/src/launch.rs` embeds WASM via `include_bytes!` |
| 285 | +- [ ] Test framework passes WASM path via env var |
| 286 | +- [ ] Plugin version constant defined in provisioner |
| 287 | +- [ ] Integration test written (optional but recommended) |
| 288 | + |
| 289 | +## Plugin Types |
| 290 | + |
| 291 | +Currently only `OplogProcessor` plugins are supported (see `PluginSpecDto` enum in `golem-common/src/model/plugin_registration.rs`). If adding a new plugin type, you'll also need to extend `PluginSpecDto` and the associated model/repo/service code. |
0 commit comments