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>
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
agentSafeor 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
-
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.
-
~/.gbrain/audit/subagent-jobs-*.jsonlis local only. If your worker runs on a different host than thegbrain agent logscaller, the CLI won't see heartbeats from that worker. v0.16 will unify this; for now assume worker + CLI share a filesystem. -
Tool calls always run with
ctx.remote = true. Even on local CLI invocation. Tools that gate onremote=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. -
put_pagewrites are namespace-scoped. A subagent with id 42 can only write underwiki/agents/42/.... This is enforced both in the tool schema (the slug pattern shown to the model) AND server-side in theput_pageoperation (fail-closed ifviaSubagent=true). Don't try to route around it; you'll getpermission_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.