RFC: document plugin system and maintainer memo

This commit is contained in:
DJTBOT 2026-01-11 16:31:56 +01:00
parent 942f05228d
commit 4263c570c1
2 changed files with 560 additions and 0 deletions

149
docs/plugins-maintainers.md Normal file
View File

@ -0,0 +1,149 @@
# Clawdbot Plugin Architecture (Maintainer Memo)
Purpose: extend Clawdbot capabilities without bloating core; ship tools + skills + config as reproducible units you can pin, test, and roll back. nix-clawdbot shows the contract; Clawdbot 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-clawdbot)
Every plugin artifact exposes the same fields (flake output `clawdbotPlugin` today, but the shape is host-agnostic):
```nix
clawdbotPlugin = {
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.stateDirs` under `$HOME`.
- Fail fast if any `requiredEnv` is unset or points to a missing/empty file.
- Copy/symlink `skills` into `workspace/skills/<name>/...`.
- If host config provides `config.settings`, render it to `config.json` in the first `stateDir`.
- 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:
```nix
plugins = [
{
source = "github:owner/repo";
config = {
env = { KEY = "/run/agenix/key"; EXTRA = "/path/to/file"; };
settings = { foo = "bar"; retries = 3; };
};
}
];
```
- `config.env`: values for `requiredEnv` (and any extra env to export).
- `config.settings`: JSON-rendered into `config.json` inside the first `stateDir`.
- Invariant: providing `settings` requires at least one `stateDir`.
## Dev workflow (fast iteration)
- Worktree: build and test plugins outside the core repo; point Clawdbot 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 `name` to override a pinned version (last entry wins); keep unique names otherwise to avoid surprise overrides.
- Skills placement: skills land under `~/.clawdbot*/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.json` in the first `stateDir` to confirm schema and defaults before committing.
## Examples
### Minimal capability plugin (first-party `summarize`)
Enable (host side):
```nix
programs.clawdbot.instances.default.plugins = [
{ source = "github:clawdbot/nix-steipete-tools?dir=tools/summarize"; }
];
```
Plugin contract (inside the plugin repo):
```nix
clawdbotPlugin = {
name = "summarize";
skills = [ ./skills/summarize ];
packages = [ self.packages.${system}.summarize-cli ];
needs = { stateDirs = []; requiredEnv = []; };
};
```
### Plugin with required config/env (community `xuezh`)
Enable (host side):
```nix
programs.clawdbot.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`):
```nix
clawdbotPlugin = {
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.
## First-Party Plugin Set (current)
- summarize, peekaboo, oracle, poltergeist, sag, camsnap, gogcli, bird, sonoscli, imsg.
- Each follows the same contract: packages + skills; env/state declared via `needs`; enabled via config toggle; sources pinned (see nix-clawdbot firstParty mapping).
## Authoring Rules
- Keep CLIs configurable via env; honor XDG paths; no inline scripts.
- Ship `AGENTS.md` in the plugin repo with knobs/paths (no secrets).
- `SKILL.md` should call the CLI by its PATH name (no absolute paths).
- If `config.settings` is expected, declare at least one `stateDir`.
- Add CI to build the plugin and validate `requiredEnv`/`stateDir` invariants.
## 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 Clawdbot 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.

View File

@ -0,0 +1,411 @@
# RFC: Clawdbot Plugin System — The Golden Path
- Date: 2026-01-11
- Status: Draft
- Audience: Peter (clawdbot maintainer), nix-clawdbot maintainers
## Goals
**Peter's goals:** Easy extension, maintainable core, thriving ecosystem, ship fast.
**Our goals:** Properly bundled tools with reproducible builds, single-install experience, it Just Works.
**This RFC argues:** The nix-clawdbot plugin model achieves both. It should become the golden path for extending clawdbot.
---
## The Problem with Core Bloat
Voice-call landed in core (+8K LOC). It works, but:
- Core now has Twilio/Telnyx deps even if you don't use voice-call
- Changes to voice-call require clawdbot releases
- Testing voice-call means testing all of clawdbot
- Contributors need to understand the whole codebase
This pattern doesn't scale. Every new capability bloats core.
## The Problem with Skills-Only
Skills are great for teaching the agent, but they're not enough:
- A skill says "use the `voicecall` CLI" — but where does the CLI come from?
- A skill says "set TWILIO_ACCOUNT_SID" — but what validates it's set?
- A skill describes commands — but what installs the binary?
Skills without tools are hallucinations waiting to happen.
---
## The nix-clawdbot Model: Bundle Everything
A plugin is **just a GitHub repo** that self-declares its contract:
```
plugin/
├── flake.nix # Build system + plugin contract
├── src/ # Tool source code
├── skills/ # Teaching docs for the agent
└── config/ # Default settings, schemas
```
That's it. No registry. No central authority. Point at a repo, get a plugin.
One install gives you:
- **Binary** on PATH (built from source, pinned version)
- **Skills** in workspace (agent knows how to use it)
- **Config** validated (missing env = install fails, not runtime error)
- **State dirs** created (plugin has a home)
**Everything updates in sync.** When upstream pushes changes — new CLI flags, updated skill docs, schema tweaks — you pull the new version and everything updates together. No "skill says X but binary doesn't support it" drift. No manual coordination. Just works.
### Real Example: xuezh (Chinese learning)
```nix
clawdbotPlugin = {
name = "xuezh";
skills = [ ./skills/xuezh ];
packages = [ self.packages.${system}.default ];
needs = {
stateDirs = [ ".config/xuezh" ];
requiredEnv = [
"XUEZH_AZURE_SPEECH_KEY_FILE"
"XUEZH_AZURE_SPEECH_REGION"
];
};
};
```
**What happens on install:**
1. `xuezh` binary built from Go source, added to PATH
2. 400-line skill symlinked to workspace — teaches agent pedagogy, CLI patterns, grading rubrics
3. `~/.config/xuezh/` created
4. Install **fails** if Azure env vars aren't wired up
**Result:** User wires up secrets once. Plugin Just Works. Agent knows how to teach Chinese.
### Real Example: gohome (home automation)
```nix
clawdbotPlugin = {
name = "gohome";
skills = [ ./skills/gohome ];
packages = [ self.packages.${system}.default ];
needs = {
stateDirs = [];
requiredEnv = [];
};
};
```
No secrets — uses Tailscale MagicDNS. CLI talks to a background gRPC server.
Skill teaches the agent:
```markdown
## Friendly CLI
gohome-cli roborock status
gohome-cli roborock clean kitchen
gohome-cli tado set living-room 20
## Sending maps to users
MEDIA:http://gohome:8080/roborock/map.png?labels=names
```
**Result:** Install plugin, agent can control your home. No manual binary install, no forgetting to set env vars.
### Real Example: padel (court booking)
```nix
clawdbotPlugin = {
name = "padel";
skills = [ ./skills/padel ];
packages = [ self.packages.${system}.default ];
needs = {
stateDirs = [ ".config/padel" ];
requiredEnv = [ "PADEL_AUTH_FILE" ];
};
};
```
Skill teaches CLI + booking authorization logic + personality ("respond in exaggerated Trump manner").
**Result:** Wire up Playtomic auth, agent can book padel courts. Complete workflow in one plugin.
---
## Why This Model is Better
### vs. Skills-Only
| Aspect | Skills-only | Bundled plugin |
|--------|-------------|----------------|
| Binary | Hope user installed it | Built + installed automatically |
| Config | Hope user set env vars | Validated at install time |
| Version | Whatever's on PATH | Pinned to exact commit |
| Rollback | Manual | Instant (previous generation) |
| Dependencies | User's problem | Bundled in build |
Skills-only is "here's how to use a tool that may or may not exist."
Bundled plugin is "here's the tool, its docs, and everything it needs."
### vs. Core Integration
| Aspect | In core | Plugin |
|--------|---------|--------|
| Core LOC | +8K per feature | Zero |
| Dependencies | Everyone gets them | Only if you install |
| Release cycle | Tied to clawdbot | Independent |
| Testing | Test everything | Test plugin only |
| Contributor barrier | Understand whole codebase | Understand plugin only |
Core should be transports + agent loop. Capabilities belong in plugins.
### What About TypeBox Schemas?
Peter's voice-call has TypeBox tool schemas — validated params, type safety.
Our model uses skill prose instead. Trade-off:
| Aspect | TypeBox schema | Skill prose |
|--------|---------------|-------------|
| Validation | Gateway validates params | Agent follows instructions |
| Type safety | Compile-time | None (string in/out) |
| Flexibility | Fixed schema | Agent can improvise |
| Works today | Needs gateway changes | Yes |
**Our take:** Skill prose is good enough for v1. Agent follows instructions reliably. If we need stricter validation later, skills can include JSON schemas that gateway parses.
### What About Subprocess Overhead?
CLI plugins spawn a process per call. Gateway plugins don't.
**Our take:** Subprocess overhead is negligible for most use cases. Voice-call might want tighter integration for latency — that's a v2 optimization, not a v1 blocker.
### What About Real-Time Webhooks?
Voice-call needs webhook handling for Twilio callbacks.
**Our take:** Same pattern as gohome — plugin runs a background server, CLI talks to it. Agent doesn't know or care about the server. Works today with the gohome model:
```
voicecall expose --mode funnel # Start webhook server
voicecall init --to +1... --message "..." # Agent calls CLI
voicecall status --call-id abc123 # Check for responses
```
---
## The Proposal: Make This the Golden Path
### What clawdbot core should do
1. **Document the plugin contract**`plugin.json` manifest schema (name, skills, bin, needs)
2. **Add discovery** — scan `~/.clawdbot/extensions/` at startup
3. **Validate env** — fail fast if `requiredEnv` missing
4. **Create state dirs** — from manifest
5. **Add `clawdbot plugins` CLI** — list, enable, disable, info
That's it. No dynamic code loading, no TypeBox registration, no RPC handlers. Just: find plugins, validate their needs, put binaries on PATH, copy skills to workspace.
### How nix-clawdbot fits in
nix-clawdbot is a **plugin installer** that wires plugins into clawdbot's plugin system:
```nix
# User's flake.nix
programs.clawdbot.plugins = [
# Remote: point at GitHub repo
{ source = "github:joshp123/xuezh"; }
{ source = "github:joshp123/padel-cli"; }
# Local dev: point at directory
{ source = "path:/Users/josh/code/my-plugin"; }
];
# Or enable first-party plugins (pinned in nix-clawdbot):
programs.clawdbot.firstParty.summarize.enable = true;
programs.clawdbot.firstParty.oracle.enable = true;
```
**Same contract, multiple sources:**
- `github:owner/repo` — pull from GitHub, pin to commit
- `path:/local/dir` — local checkout for dev iteration
- First-party toggles — curated plugins pinned in nix-clawdbot
At activation time, nix-clawdbot:
1. Resolves flake sources (remote or local) → builds binaries
2. Validates `requiredEnv` (fails if missing)
3. Creates state dirs
4. Installs plugins to `~/.clawdbot/extensions/<plugin>/`
5. Writes `plugin.json` manifest for each
6. Symlinks skills to workspace
7. Adds binaries to PATH
**clawdbot core sees all plugins** — it scans `~/.clawdbot/extensions/`, reads manifests, knows what's installed. The difference is nix-clawdbot does the install + validation at build time (deterministic, fail-fast), while non-Nix users do it manually or via npm.
**Same plugin system, different installers:**
- Nix users: nix-clawdbot installs plugins declaratively
- Non-Nix users: `clawdbot plugins install` or manual setup
- clawdbot core: sees the same `~/.clawdbot/extensions/` structure either way
**Local dev workflow:** Point at a local path, change code, rebuild, gateway picks up changes. No push/pull cycle. Same contract, local iteration. For non-Nix: symlink your plugin dir into `~/.clawdbot/extensions/`.
### What nix-clawdbot provides (golden path)
- **Reproducible builds** — binary built from source, same everywhere
- **Version pinning** — plugin source locked to exact commit
- **Instant rollback** — switch to previous generation
- **Declarative config** — plugins + secrets in one flake
- **Atomic updates** — CLI + skill + config update together
### What npm provides (fallback)
- `clawdbot plugins install @clawdbot/voice-call` — npm install to extensions dir
- Manual env var setup
- Manual version management
- No reproducibility guarantees
It works. It's just not as good. (They should use Nix.)
---
## Voice-Call as a Plugin
The screenshot shows the interface:
```
voicecall init --to +1... --mode conversation --message "..."
voicecall continue --call-id ... --message "..."
voicecall expose --mode funnel|serve|off
```
As a plugin:
```nix
clawdbotPlugin = {
name = "voice-call";
skills = [ ./skills/voice-call ];
packages = [ self.packages.${system}.default ];
needs = {
stateDirs = [ ".config/clawdbot-voice-call" ];
requiredEnv = [ "TWILIO_ACCOUNT_SID" "TWILIO_AUTH_TOKEN" ];
};
};
```
Install wires up Twilio creds. Binary handles webhook server. Skill teaches agent the CLI. Done.
**Migration path:**
1. Create `@clawdbot/voice-call` repo
2. Move code from core
3. Add plugin contract
4. Remove from core
5. Add to first-party plugins list
---
## The Plugin Ecosystem Vision
**First-party plugins** already exist — see [nix-steipete-tools](https://github.com/clawdbot/nix-steipete-tools/tree/main/tools):
- `summarize` — YouTube/article summarization
- `oracle` — second-model review
- `peekaboo` — screenshot capture
- `camsnap` — webcam capture
- `poltergeist` — browser automation
- `sag` — web search
- `bird` — Twitter/X integration
- `sonoscli` — Sonos control
- `imsg` — iMessage integration
- `gogcli` — Google Calendar
All follow the same contract. All pinned in nix-clawdbot. Enable with one line:
```nix
programs.clawdbot.firstParty.summarize.enable = true;
```
**Community plugins** (anyone can ship):
- Just a GitHub repo with `flake.nix` + `clawdbotPlugin`
- No registry, no approval process
- User points at repo, wires secrets, it works
**Core stays lean:**
- Transports (Telegram, Discord, Slack)
- Agent loop
- Plugin discovery (for non-Nix users)
- That's it
---
## Summary
| What | How |
|------|-----|
| **Plugin = bundle** | Binary + skills + config + declared needs |
| **Install = it works** | Wire secrets once, everything validated |
| **Core = minimal** | Transports + agent + plugin loader |
| **Golden path = Nix** | Reproducible, pinned, instant rollback |
| **Fallback = npm** | Works, less guarantees |
**The pitch to Peter:**
Your voice-call is great. But it shouldn't live in core forever. The nix-clawdbot plugin model lets you:
- Ship it independently (faster iteration)
- Keep core lean (easier maintenance)
- Let community extend clawdbot (ecosystem growth)
- Guarantee it works when installed (fail-fast validation)
All you need to add to core is plugin discovery + env validation. We already have the contract, the tooling, and real plugins in production.
---
## Open Questions
1. **Manifest format:**
- Nix users: `clawdbotPlugin` in `flake.nix` (already works)
- Non-Nix users: `plugin.json` in plugin directory
- **Suggested:** clawdbot core defines `plugin.json` schema. Same fields as `clawdbotPlugin`:
```json
{
"name": "voice-call",
"skills": ["./skills/voice-call"],
"bin": { "voicecall": "./bin/voicecall" },
"needs": {
"stateDirs": [".config/clawdbot-voice-call"],
"requiredEnv": ["TWILIO_ACCOUNT_SID", "TWILIO_AUTH_TOKEN"]
}
}
```
2. **Discovery paths:**
- **Suggested:** `~/.clawdbot/extensions/<plugin>/` for user-installed, `.clawdbot/extensions/<plugin>/` for project-local
- clawdbot core scans both paths at startup
- nix-clawdbot installs to the same paths — same structure, different installer
3. **First-party plugins for non-Nix:**
- **Suggested:** `@clawdbot/` npm scope, but don't invest heavily. Point people to Nix.
4. **Voice-call extraction:** Want to do this now, or later?
---
## Appendix: The Pattern
```
┌─────────────┐ shells out ┌─────────────┐
│ Agent │ ─────────────────► │ CLI binary │
└─────────────┘ └──────┬──────┘
┌─────────────┐
│ Backend │
│ (API/DB/ │
│ server) │
└─────────────┘
```
- **padel:** CLI → Playtomic API
- **xuezh:** CLI → SQLite + Azure Speech
- **gohome:** CLI → gRPC server → devices
- **voice-call:** CLI → webhook server → Twilio
Agent never talks to backend directly. CLI is the interface. Skill teaches the agent. Plugin bundles everything.
This is the golden path.