Files
gbrain/docs/guides/plugin-authors.md
T
Garry TanandClaude Opus 4.7 b4143b7ef2 feat(minions): GBRAIN_PLUGIN_PATH loader + plugin-authors guide (v0.15)
Plumbing that makes Wintermute (and future downstream agents) day-1
usable on v0.15. Host repos drop a `gbrain.plugin.json` + `subagents/`
directory somewhere, set GBRAIN_PLUGIN_PATH (colon-separated like \$PATH),
and their custom subagent defs load at worker startup.

Path policy is strict: absolute paths only. Relative, ~-prefixed, and
URL-style (https://, file://) all rejected with warnings — the user
controls where plugins live. Non-existent paths and files (not dirs) are
warned and skipped so a typo doesn't crash worker startup.

Collision policy: left-wins. If two plugins ship a subagent with the same
name, the first one in GBRAIN_PLUGIN_PATH keeps it and the other gets a
warning naming both sources. Deterministic + debuggable.

Trust policy: plugins ship subagent defs ONLY. Cannot declare new tools,
cannot extend the brain allow-list, cannot override safety flags. The
subagent def's `allowed_tools:` frontmatter MUST subset the derived
registry — validation happens at load time (worker startup), not at
dispatch time, so a typo in a skill gives a loud startup error instead
of silently "tool never fires at 3am."

Manifest `plugin_version: "gbrain-plugin-v1"` locks the contract. Unknown
versions rejected. `subagents` field escape attempts (`../../../etc` etc)
rejected. gray-matter handles the markdown frontmatter parse — subagent
defs don't conform to the page schema, so we don't use parseMarkdown.

docs/guides/plugin-authors.md is the Wintermute-facing walkthrough.
Covers the minimum viable plugin shape, the three policies, the
frontmatter fields, known caveats (audit JSONL is local-only, tool calls
always run remote=true, put_page is namespace-scoped).

22 unit tests pin path rejection, missing/invalid manifest, unsupported
version, escape-attempt, basename fallback for missing frontmatter.name,
allowed_tools round-trip, unknown-tool rejection with validAgentToolNames,
empty env, multi-path, collision warning with left-wins, trimmed paths,
manifest-rejection as warning.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-20 21:40:08 +08:00

5.9 KiB

Plugin authors guide (v0.15)

gbrain discovers subagent definitions from outside this repo via GBRAIN_PLUGIN_PATH. If you maintain a downstream agent (Wintermute, a workflow host, a private tool) and want to ship custom subagents alongside it, drop a plugin directory on that env path.

This guide is for plugin authors. The CLI user doesn't need to read it.

Minimum viable plugin

/path/to/my-plugin/
├── gbrain.plugin.json
└── subagents/
    └── my-summarizer.md

gbrain.plugin.json:

{
  "name": "my-plugin",
  "version": "1.0.0",
  "plugin_version": "gbrain-plugin-v1"
}

subagents/my-summarizer.md:

---
name: my-summarizer
model: claude-sonnet-4-6
allowed_tools:
  - brain_search
  - brain_get_page
---

You are a brain page summarizer. Given a slug, fetch the page and produce
a 3-sentence summary.

Turning it on

export GBRAIN_PLUGIN_PATH="/path/to/my-plugin"
gbrain jobs work           # worker startup prints the plugin load line
gbrain agent run "summarize meetings/2026-04-20" --subagent-def my-summarizer

Multiple plugins: colon-separated, just like $PATH.

export GBRAIN_PLUGIN_PATH="/path/to/plugin-a:/path/to/plugin-b"

Rules (strict by design)

Path policy. Absolute paths only. Relative paths, ~-prefixed paths, and URL-style paths (https://, file://) are rejected with a warning. You control where your plugin lives on disk; gbrain doesn't guess.

Collision policy. If two plugins ship a subagent with the same name, the one listed FIRST in GBRAIN_PLUGIN_PATH wins. The other is dropped with a warning naming both sources.

Trust policy. Plugins ship subagent definitions ONLY in v0.15:

  • You cannot declare new tools.
  • You cannot extend the brain tool allow-list.
  • You cannot override any agentSafe or similar flag.
  • Your allowed_tools: frontmatter field MUST subset the derived brain tool registry. Names not in the registry are rejected at plugin load time (worker startup), NOT at subagent dispatch time — so a typo in your plugin gives you a loud startup error, not a silent "tool never fires" at 3am.

v0.16+ may open up plugin-declared tools with a separate contract. Don't expect it.

gbrain.plugin.json

field type required notes
name string yes Human-readable plugin id. Shows up in warnings and collision logs.
version string yes Your plugin's semver. Informational.
plugin_version string yes Contract lock. Must equal "gbrain-plugin-v1" for v0.15.
subagents string no Subdir name (default subagents). Escape-attempts are rejected.
description string no Shown in future gbrain plugin list.

Subagent definition files

Plain markdown with YAML frontmatter. The body is the system prompt. The frontmatter controls runtime behavior.

Recognized frontmatter fields:

field type required notes
name string no Subagent identifier used as --subagent-def. Defaults to the file basename.
model string no Anthropic model id. Defaults to the handler default (sonnet).
max_turns number no Cap on assistant turns. Defaults to 20.
allowed_tools string[] no Whitelist of tool names. Must subset the derived brain registry. Rejected on mismatch.

Unknown frontmatter fields are preserved but ignored by the handler. v0.16 may consume more of them.

Caveats that will bite you

  1. Plugin definitions can't change during a run. The loader reads the disk once at worker startup. Editing a subagent def doesn't re-take effect until you restart the worker. This is deliberate — live reloads would break crash-resumable replay.

  2. ~/.gbrain/audit/subagent-jobs-*.jsonl is local only. If your worker runs on a different host than the gbrain agent logs caller, the CLI won't see heartbeats from that worker. v0.16 will unify this; for now assume worker + CLI share a filesystem.

  3. Tool calls always run with ctx.remote = true. Even on local CLI invocation. Tools that gate on remote=true (file_upload's strict confinement, put_page's namespace check) will apply. Good default; a subagent definition that wants local-filesystem reach beyond the brain can't have it.

  4. put_page writes are namespace-scoped. A subagent with id 42 can only write under wiki/agents/42/.... This is enforced both in the tool schema (the slug pattern shown to the model) AND server-side in the put_page operation (fail-closed if viaSubagent=true). Don't try to route around it; you'll get permission_denied.

Example: a Wintermute-flavored plugin

~/wintermute/
└── gbrain-plugin/
    ├── gbrain.plugin.json
    └── subagents/
        ├── meeting-ingestion.md
        ├── signal-detector.md
        └── daily-task-prep.md

~/wintermute/gbrain-plugin/gbrain.plugin.json:

{
  "name": "wintermute",
  "version": "2026.4.20",
  "plugin_version": "gbrain-plugin-v1",
  "description": "Wintermute's personal-brain subagents"
}

Environment:

export GBRAIN_PLUGIN_PATH="$HOME/wintermute/gbrain-plugin"

Then Wintermute calls gbrain agent run --subagent-def meeting-ingestion --fanout-by transcript ... and its definitions load automatically.