What: - remove legacy option migrations for programs.openclaw.firstParty/plugins - add explicit removed-option failures pointing to bundledPlugins/customPlugins - add plugin-catalog.nix as single source of truth for bundled plugins - generate bundled option toggles, source map, linux check selection, and tool list from the catalog - update docs/wording from first-party to bundled plugins Why: - enforce forward-only API changes with fail-fast errors - eliminate duplicated plugin lists drifting across module/check/tool surfaces - keep consumer configuration mental model simple: bundledPlugins + customPlugins only Tests: - nix flake check --no-build (pass)
6.2 KiB
6.2 KiB
OpenClaw Plugin Architecture (Maintainer Memo)
Purpose: extend OpenClaw capabilities without bloating core; ship tools + skills + config as reproducible units you can pin, test, and roll back. nix-openclaw shows the contract; OpenClaw core should treat the same interface as first-class, even off-Nix.
What a Plugin Is (and is not)
- Is: bundle of binaries/CLIs, skills that teach the agent to use them, optional config/env requirements.
- Not: new transports/providers; model plumbing; secrets baked in; inline scripts or ad-hoc package-manager installs; a place for random config outside its scope.
- Why not skills-only: skills without binaries can hallucinate capability. Plugins ground skills in real tools and deliver versioned, reproducible functionality.
Interface Contract (reference implementation: nix-openclaw)
Every plugin artifact exposes the same fields (flake output openclawPlugin today, but the shape is host-agnostic):
openclawPlugin = {
name = "summarize"; # unique; last-wins on collision
skills = [ ./skills/summarize ]; # dirs containing SKILL.md
packages = [ pkgs.summarize-cli ]; # binaries placed on PATH
needs = {
stateDirs = [ ".config/summarize" ]; # created under $HOME
requiredEnv = [ "SUMMARIZE_API_KEY" ]; # must point to files
};
};
Host responsibilities (what the runtime guarantees):
- Resolve plugin source; read contract.
- Install
packages; prepend to PATH for the gateway wrapper. - Create
needs.stateDirsunder$HOME. - Fail fast if any
requiredEnvis unset or points to a missing/empty file. - Copy/symlink
skillsintoworkspace/skills/<name>/.... - If host config provides
config.settings, render it toconfig.jsonin the firststateDir. - Export
config.env(plus required envs) into the gateway wrapper. - Reject duplicate skill paths; duplicate plugin names: last entry wins.
Host-side config shape
When enabling a plugin, the host can supply:
plugins = [
{
source = "github:owner/repo";
config = {
env = { KEY = "/run/agenix/key"; EXTRA = "/path/to/file"; };
settings = { foo = "bar"; retries = 3; };
};
}
];
config.env: values forrequiredEnv(and any extra env to export).config.settings: JSON-rendered intoconfig.jsoninside the firststateDir.- Invariant: providing
settingsrequires at least onestateDir.
Dev workflow (fast iteration)
- Worktree: build and test plugins outside the core repo; point OpenClaw at a local path source (e.g.,
source = "path:/Users/you/code/my-plugin"). - Rebuild loop: change plugin →
home-manager switch(or host-equivalent) → gateway restarts with new PATH/skills/config; no manual copying. - Name collisions: use the same plugin
nameto override a pinned version (last entry wins); keep unique names otherwise to avoid surprise overrides. - Skills placement: skills land under
~/.openclaw*/workspace/skills/<plugin>/...so you can inspect quickly; delete the workspace to fully reset cached skills. - Env guardrails: required env vars must point to files (non-empty) or the activation fails—supply temp files during dev to exercise the checks.
- Settings JSON: inspect the rendered
config.jsonin the firststateDirto confirm schema and defaults before committing.
Examples
Minimal capability plugin (bundled summarize)
Enable (host side):
programs.openclaw.instances.default.plugins = [
{ source = "github:openclaw/nix-steipete-tools?dir=tools/summarize"; }
];
Plugin contract (inside the plugin repo):
openclawPlugin = {
name = "summarize";
skills = [ ./skills/summarize ];
packages = [ self.packages.${system}.summarize-cli ];
needs = { stateDirs = []; requiredEnv = []; };
};
Plugin with required config/env (community xuezh)
Enable (host side):
programs.openclaw.instances.default.plugins = [
{
source = "github:joshp123/xuezh";
config = {
env = {
# Required envs (guarded as files):
XUEZH_AZURE_SPEECH_KEY_FILE = "/run/agenix/xuezh-azure-speech-key";
XUEZH_AZURE_SPEECH_REGION = "/run/agenix/xuezh-azure-speech-region"; # file containing e.g. "westeurope"
};
settings = {
audio = {
backend_global = "azure.speech";
process_voice_backend = "azure.speech";
convert_backend = "ffmpeg";
tts_backend = "edge-tts";
inline_max_bytes = 200000;
};
azure = {
speech = {
key_file = "/run/agenix/xuezh-azure-speech-key";
region = "westeurope";
};
};
};
};
}
];
Plugin contract (inside xuezh):
openclawPlugin = {
name = "xuezh";
skills = [ ./skills/xuezh ];
packages = [ self.packages.${system}.default ];
needs = {
stateDirs = [ ".config/xuezh" ];
requiredEnv = [ "XUEZH_AZURE_SPEECH_KEY_FILE" "XUEZH_AZURE_SPEECH_REGION" ];
};
};
Host behavior: creates ~/.config/xuezh/config.json from settings; exports both envs; fails if the pointed files are missing/empty.
Bundled Plugin Set (current)
- summarize, peekaboo, poltergeist, sag, camsnap, gogcli, goplaces, bird, sonoscli, imsg.
- Source of truth:
nix/modules/home-manager/openclaw/plugin-catalog.nix. - Each follows the same contract: packages + skills; env/state declared via
needs; enabled via config toggle; sources pinned via the bundled plugin catalog.
Authoring Rules
- Keep CLIs configurable via env; honor XDG paths; no inline scripts.
- Ship
AGENTS.mdin the plugin repo with knobs/paths (no secrets). SKILL.mdshould call the CLI by its PATH name (no absolute paths).- If
config.settingsis expected, declare at least onestateDir. - Add CI to build the plugin and validate
requiredEnv/stateDirinvariants.
Why this approach
- Capability grounding: skills map to real tools, not hypothetical ones.
- Reproducibility: versioned bundle of tool + skill + config schema; easy rollback.
- Clean core: main OpenClaw stays transport/model-focused; plugins carry integrations.
- Operational sanity: one toggle wires tools, env, skills; failure is explicit and early.
- Portability: contract is host-agnostic; Nix just enforces determinism and zero drift.