返回 CodeWhale
PLUGIN_BUNDLES.md
根目录 / docs / PLUGIN_BUNDLES.md
1 # Plugin bundles
2
3 Codewhale supports a deliberately small plugin-bundle boundary. The boundary
4 was drawn in v0.9.1 and is extended deliberately in v0.9.10: a bundle may
5 contribute declarative Skills, MCP configuration, Commands, Agent profiles,
6 and Hooks through Codewhale's existing engines. Unsupported declarations stay
7 inventoried instead of disabling a mixed bundle. Discovery alone never
8 executes, enables, trusts, downloads, updates, or installs anything.
9
10 This document owns the bundle formats, discovery,
11 validation, and the trust/enable/runtime contract. [PLUGINS.md](PLUGINS.md)
12 owns how bits get onto and off disk — the `/plugin install`, `update`,
13 `uninstall`, and `suggest` on-ramp added in v0.9.4 (#5182). Compatible Claude Code bundles use the same native adapters; their supported subset is
14 [CLAUDE_PLUGIN_COMPAT.md](CLAUDE_PLUGIN_COMPAT.md).
15 For a runnable native example and explicit OpenCode/DSH data conversion, see
16 [Write your first plugin](PLUGIN_AUTHORING.md).
17
18 ## Discovery and precedence
19
20 Codewhale scans only its own roots, looking in each `<name>/` directory for a
21 manifest named `plugin.json` (the native Agent Plugins v1.0.0 format, since
22 v0.9.4), `kimi.plugin.json` (the compatible Kimi Skills/MCP subset, since
23 v0.9.8), `plugin.toml` (the legacy Codewhale format, still fully readable), or
24 `.claude-plugin/plugin.json` (the compatible Claude subset, since v0.9.13):
25
26 - User: `~/.codewhale/plugins/<name>/`
27 - Workspace: `<workspace>/.codewhale/plugins/<name>/`
28
29 A bundle that publishes multiple formats is read through `plugin.json` first,
30 then `kimi.plugin.json`, then the legacy `plugin.toml`, then
31 `.claude-plugin/plugin.json`.
32 Computer Use ships as a built-in bundle; it still requires review and
33 enablement before activation. The internal precedence order is
34 built-in, user, then workspace; the first bundle with a given name wins. This
35 prevents a repository from shadowing an explicitly installed user bundle.
36 Symbolic-link roots, manifests, component paths, and nested component files
37 fail closed.
38
39 The embedded Computer Use files are materialized under
40 `$CODEWHALE_HOME/builtin-plugins/snapshots/computer-use-<bundle-digest>/computer-use`.
41 Each process captures only its own embedded digest's discovery root. Concurrent
42 builds therefore keep separate, complete source trees; publishers never delete
43 or replace an existing snapshot. Reuse checks every embedded byte, directory
44 entry, file type, executable flag, and the stamp. A partial or altered snapshot
45 is rejected without repair. Interrupted private staging directories are not
46 discovered or reused.
47
48 The existing path-bound plugin identity and trust rules apply: identical embedded
49 bytes at the same home reuse the same identity; changed bytes require a fresh
50 review and enablement. Moving from the older mutable
51 `builtin-plugins/computer-use` layout also requires one fresh review. Legacy
52 bundles and receipts remain intact for running older binaries; no trust is
53 migrated. Diagnostics do not create a missing Codewhale home.
54 The selected home may itself be a symlink: its resolved directory is pinned
55 before creating any built-in paths. Links in the owned built-in cache paths
56 still fail closed.
57
58 New user and workspace bundles are always untrusted and disabled. Discovery is
59 read-only and does not inspect any other application's extension or credential
60 directories: ambient roots such as `.claude/plugins` or `.cursor/plugins` are
61 never scanned.
62
63 Pre-v0.9.1 `overrides.json` enablement was intentionally not imported as
64 trust; every bundle activates only through the content-hash and
65 `codewhale-plugin-capabilities-v3` activation-policy review below.
66
67 ## Manifest
68
69 All supported encodings parse into the same internal manifest, so validation, hashing,
70 review, and runtime behavior are identical downstream. On-disk auto-migration
71 between them is deliberately not performed; `/plugin export <name>
72 <target-dir>` publishes a loaded bundle as a spec-valid Agent Plugins v1.0.0
73 directory without modifying the installed one.
74
75 ### `plugin.json` (Agent Plugins v1.0.0)
76
77 The standard's manifest root is closed: `$schema`, `name`, and the optional
78 well-known fields (`version`, `description`, `author`, `homepage`,
79 `repository`, `license`, `keywords`, `extensions`). An unknown root key is a
80 parse error. Client-specific data lives under `extensions`, keyed by
81 reverse-domain namespace: unknown vendor namespaces are ignored, never
82 rejected — that is what lets a bundle authored for another client load here —
83 while unknown keys inside Codewhale's own `extensions["net.codewhale"]`
84 namespace are rejected rather than silently dropped.
85
86 Names follow the standard's rule: 1–64 lowercase ASCII letters, digits, or
87 internal single `-`/`.`, starting and ending alphanumeric, never `--` or `..`.
88 A `skills/` directory in the bundle root is picked up automatically; other
89 component locations, `capabilities`, `when`, and `display_name` ride in
90 `extensions["net.codewhale"]`.
91
92 MCP servers cannot live in `plugin.json` (the root is closed); they live in a
93 sibling `mcp.json` under `mcpServers`, with `stdio`, `streamable-http`, or
94 `sse` transports (`type` may be omitted and is inferred from `command` vs
95 `url`). Codewhale-only server options — timeouts, tool filters, env-backed
96 credentials, enablement — ride per-server under `extensions["net.codewhale"]`.
97 The `env` names `PLUGIN_ROOT` and `PLUGIN_DATA` are reserved by the standard
98 for the host runtime and are rejected in plugin definitions.
99
100 ### `plugin.toml` (legacy Codewhale format)
101
102 ```toml
103 schema_version = 1
104
105 [plugin]
106 name = "example"
107 version = "0.1.0"
108 description = "Example instruction and MCP bundle"
109 author = "Example Author"
110
111 [skills]
112 path = "skills"
113
114 [commands]
115 path = "commands"
116
117 [agents]
118 path = "agents"
119
120 [hooks]
121 path = "hooks"
122
123 [mcp_servers.local]
124 command = "node"
125 args = ["server.js"]
126 cwd = "mcp"
127
128 [mcp_servers.remote]
129 url = "https://example.invalid/mcp"
130
131 [capabilities]
132 network_hosts = ["example.invalid"]
133
134 [when]
135 os = ["macos", "linux", "windows"]
136 binaries = ["node"]
137 ```
138
139 Legacy TOML names are 1–64 lowercase ASCII letters, digits, or internal
140 hyphens. A pre-versioned manifest without `schema_version` still parses, with
141 a migration warning from `/plugin validate` (and `0.0.0` displayed when
142 `[plugin].version` is missing). An unknown top-level table or field is a
143 parse error (`deny_unknown_fields`), reported by byte offset without echoing
144 manifest values.
145
146 ### Validation (both formats)
147
148 Component paths must be relative, contained, present, and free of symbolic
149 links or Windows reparse points (including junctions and mount points). The v1
150 schema rejects unknown MCP fields, ambiguous local/remote
151 transport combinations, unbounded lists/timeouts, and overlapping tool
152 filters.
153
154 Remote MCP URLs must use HTTPS, except for explicit loopback HTTP endpoints.
155 They cannot contain user information, a query, or a fragment. Literal headers
156 are rejected: authentication must name a source environment variable through
157 `env_headers` or `bearer_token_env_var`. A remote bundle must declare exactly
158 the normalized host set used by its endpoints in
159 `capabilities.network_hosts`; endpoint scheme, normalized host, port, and path
160 remain bound to the review. Redirects are limited and must retain that exact
161 normalized origin. Reviewed remote transports use an explicit no-proxy HTTP
162 client: plugin bundles never read or use ambient `HTTP_PROXY`, `HTTPS_PROXY`,
163 or `NO_PROXY` values, because proxy credentials and proxy observation are
164 outside the reviewed authority. User-authored MCP configuration keeps its
165 existing explicit proxy support.
166
167 Local stdio environment entries must use exact `${SOURCE_ENV}` references.
168 The review shows destination and source names, but never reads or prints their
169 values. Plugin children inherit only Codewhale's base secret-scrubbed child
170 environment plus those reviewed mappings; credential-capable proxy variables
171 and the broader compatibility environment used by user-authored MCP
172 configuration are not inherited ambiently. Absolute arguments and parent
173 traversal are rejected; contained bundle entrypoints are frozen to their
174 staged paths before spawn.
175
176 Every stdio argument is shown losslessly as a JSON string during review.
177 Common credential-bearing flags and known literal token shapes are rejected
178 from argv; credentials must instead use a reviewed environment mapping.
179 Plugin-contributed MCP OAuth has been disabled since v0.9.1 and remains
180 disabled as of v0.9.6, including discovery, login, refresh, and token storage;
181 a manifest declaring OAuth fields on a plugin MCP server fails validation.
182
183 ### Active and inactive component surfaces
184
185 Codewhale 0.9.10 activates declarative `[skills]`, `[mcp_servers.*]`,
186 `[commands]`, `[agents]`, and `[hooks]` components from its content-addressed
187 runtime snapshot. Commands use markdown command files, Agents use Fleet TOML
188 profiles, and Hooks use `HooksConfig` TOML files. A component may name one file
189 or a directory of the corresponding files. Ordinary user/workspace commands
190 and Agent profiles keep precedence over plugin contributions; trusted project
191 hooks run after plugin hooks.
192
193 The manifest can additionally inventory the following inactive surfaces.
194 Those declarations stay hashed, reviewed, and displayed, but do not activate
195 and no longer disable the whole bundle:
196
197 ```toml
198 [lsp] # TOML alias: [lsp_servers]
199 path = "lsp"
200
201 [native] # TOML alias: [native_extension]
202 path = "native"
203
204 [capabilities]
205 filesystem_roots = ["workspace"]
206 network_hosts = ["api.example.invalid"]
207 lifecycle_mutation = true
208 ```
209
210 (In a `plugin.json` bundle the same tables ride under
211 `extensions["net.codewhale"]`.)
212
213 The accept/reject behavior is deliberately loud, never silent:
214
215 - Compatibility is per-component: `full` when every declared surface has an
216 adapter (or the bundle is empty), `partial` when supported components can
217 activate beside named inactive surfaces, and `unsupported` when the bundle
218 only declares surfaces Codewhale cannot activate yet. The same versioned
219 activation policy (v3) drives those labels, the runtime adapters, and the
220 capability hash. A future Codewhale that starts executing LSP or native code
221 must change that policy, which changes the capability hash and forces
222 re-review. v1 and v2 trust receipts fail closed as
223 `capabilities-changed`.
224 - A **recognized-but-inactive** declaration (`lsp`, `native`, a non-empty
225 `capabilities.filesystem_roots`, or
226 `capabilities.lifecycle_mutation = true`) parses and is validated like any
227 component (contained, present, link-free). It is counted in the inventory,
228 hashed into the capability receipt, shown in review and `/plugin show` as
229 inactive, and never executed. A reviewed, trusted, applicable mixed bundle
230 can still be enabled: supported declarative components become active, and
231 the inactive surfaces stay named as inactive.
232 - An **all-unsupported** bundle can be reviewed and trusted, but `/plugin
233 enable` fails closed and names the inactive surfaces. There is nothing
234 Codewhale can honestly activate.
235 - An **unrecognized** section or field is a validation failure, not an
236 inventory entry: unknown top-level TOML tables, unknown MCP server fields,
237 unknown `plugin.json` root keys, and unknown keys inside
238 `extensions["net.codewhale"]` are all rejected outright. The single
239 ignore-without-error case is another vendor's `extensions` namespace in the
240 Agent Plugins format, which the standard requires clients to skip.
241 - `capabilities.network_hosts` is not a future surface: it is enforced today,
242 and must exactly match the normalized host set of the bundle's remote MCP
243 endpoints (so it cannot be declared without them, or omitted with them).
244
245 A successful environment or health check is never treated as trust.
246
247 ## Review, trust, and enablement
248
249 Use the in-session command surface:
250
251 ```text
252 /plugin list
253 /plugin validate example
254 /plugin show example
255 /plugin enable example
256 ```
257
258 The first `enable` opens a review showing source, component inventory,
259 requested permissions, sanitized MCP endpoints, full content and capability
260 hashes, and inactive declarations. It also prints an exact confirmation:
261
262 ```text
263 /plugin trust example <full-content-sha256>.<full-capability-sha256>
264 ```
265
266 Run that exact command only after reviewing the bundle. The confirmation token
267 uses both complete SHA-256 receipts rather than display prefixes. The
268 capability receipt is the v3 digest: it still hashes the complete inventory
269 and also binds this build's activation policy (which adapters are executable
270 versus inventoried-only). Trust first
271 copies the complete reviewed tree into a Codewhale-owned, content-addressed
272 runtime snapshot and records the matching receipt; it does not activate
273 anything.
274 Then run `/plugin enable example` again. Trust and enablement are separate:
275
276 - `/plugin disable example` stops contribution while preserving trust.
277 - `/plugin revoke example` removes trust while preserving the enablement bit;
278 the bundle remains inactive until reviewed again.
279 - `/plugin reload` rebuilds the current workspace registry when files have
280 changed on disk.
281
282 (`/plugin install`, `update`, and `uninstall` place, replace, and remove the
283 bits themselves and always drop into this same review — see
284 [PLUGINS.md](PLUGINS.md). `/plugin suggest` ranks installed bundles and
285 any locally added marketplace catalogs; sending a matching task can toast the
286 same next step without installing anything, and a live composer CTA plus an
287 append-only `<recommended_plugins>` user block offer the same review path.)
288
289 Trust, enable, disable, revoke, and reload rebuild the current workspace's
290 Skills, MCP, Commands, Agent profiles, and Hooks immediately. Each persisted
291 transition advances a per-bundle generation under a stable cross-process lock.
292 A generation change cancels in-flight MCP work, removes cached catalog
293 entries, terminates an idle plugin stdio child, and denies persisted queued
294 Skills carrying the older authority receipt.
295
296 The review distinguishes remote MCP endpoints from local stdio MCP servers.
297 A local stdio server is a child process running with the Codewhale user's host
298 filesystem and network authority; plugin trust is not an OS sandbox. The
299 review therefore shows the command, argument count, working directory,
300 environment-variable names, and this host-authority warning without printing
301 environment or header values. MCP tool approval still applies after the
302 server starts.
303
304 Trust receipts live in `~/.codewhale/plugins/state.json`. Atomic owner-only
305 writes record the full content hash, capability hash, reviewed capability
306 inventory, generation, and review time, with the latest 32 reviews retained as
307 a bounded audit trail. Malformed or unsupported state is not overwritten: all
308 bundles fail closed until the state file is repaired or moved.
309
310 The content hash covers the manifest, complete bundle tree, and executable
311 shape in deterministic path order, including local MCP entrypoints and
312 companion assets. Staging is bounded, rejects symbolic links and unsupported
313 file kinds (plus every Windows reparse point and hard-linked files), uses an
314 atomic destination swap, and applies owner-only runtime permissions or ACLs
315 through validated object handles on Windows. The capability hash covers the
316 normalized component and permission inventory. A source or staged-content
317 edit, capability change, or unsafe runtime-root replacement invalidates the
318 receipt deterministically; an already-enabled bundle becomes inactive until
319 it is reviewed again. This is the same invalidation `/plugin update` relies
320 on: replaced bytes stop matching the receipt, forcing re-review.
321
322 ## Runtime behavior
323
324 An active bundle must be enabled, trusted for its current hashes, applicable to
325 the host, and free of validation errors. A reviewed mixed bundle may be
326 active, but only supported components in the reviewed v3 activation mask are
327 consumable. Unsupported components remain listed, hashed, reviewed, and
328 inactive.
329
330 - Skills are exposed only as `<plugin>:<skill>`. The model-facing catalogue and
331 `load_skill` use an in-memory snapshot bound to the reviewed staged tree,
332 rather than reading a mutable source path at execution time. `load_skill`
333 revalidates source, stage, receipt, workspace, and generation immediately
334 before releasing content and fails closed on drift. Queued messages persist
335 the same provenance and repeat that check at dispatch. `/skills inspect`
336 identifies the reviewed bundle without exposing its mutable source path.
337 - MCP server names are exposed as
338 `plugin-<plugin-name-byte-length>-<plugin>-<server>` so hyphens in either
339 component cannot create an authority collision. Disabled or untrusted
340 bundles are denied again at the headless MCP adapter. Authority is checked
341 before connection, immediately before every lazy stdio spawn, after
342 transport construction, and before each tool/resource/prompt operation.
343 Persisted generation/enablement/trust state is also watched while an
344 operation is in flight, so disable, revoke, or another cross-process state
345 transition cancels the operation and terminates a plugin stdio child. Full
346 source and staged-tree hashes are revalidated at dispatch/catalogue
347 boundaries; the runtime does not continuously re-hash those trees during an
348 already-running MCP call. Source or stage drift therefore fails the next
349 boundary and drops the stale connection/catalogue entry, but is not claimed
350 to interrupt a call already executing. Every failure includes instructions
351 to reload, review, trust, and enable the bundle again.
352 - Commands load after ordinary user/workspace commands and saved workflows, so
353 existing definitions keep precedence and collisions are visible. The
354 palette hides a revoked command immediately; dispatch rechecks the full
355 receipt before expanding its body and reports a visible denial on stale
356 input.
357 - Agent profiles join the Fleet roster below explicit config, personal, and
358 workspace profiles but above built-ins. Roster collisions retain the
359 existing visible shadow record. Every Agent spawn rebuilds from the current
360 registry and rechecks the selected plugin profile's authority before its
361 prompt or route can be used.
362 - Hooks merge after global hooks and before trusted project hooks. Foreground
363 Hooks recheck authority immediately before process spawn; background Hooks
364 check before enqueue and again at dequeue so a queued, revoked Hook cannot
365 start later.
366 - Plain launch, resume, fork, exec, and serve each construct an immutable
367 workspace-scoped registry before constructing their plugin-backed catalogues.
368 - Constitution, repository instructions, permission rules, sandbox policy,
369 and MCP tool approval continue to outrank plugin instructions.
370
371 `/plugin list`, `show`, `suggest`, and `validate` perform no network requests,
372 process launches, credential reads, or configuration writes. Reviews render
373 structural argv as lossless JSON strings and environment provenance without
374 values. Credential-bearing argv is rejected at manifest validation;
375 plugin-originated errors suppress URL query, authentication, argv, and
376 environment material. Legacy executable tools under `[tools].plugin_dir`
377 remain a distinct system and are listed under `/plugin tools`.
378
379 ## Explicit non-goals as of v0.9.10
380
381 Federated marketplace catalogs (`/plugin marketplace add|list|show|remove|install`)
382 parse local Kimi-, Claude-, Codex-, and Codewhale-format catalog documents; see
383 the marketplace section below (`/plugin install` fetches
384 one reviewed source, and `/plugin suggest` ranks only what is already
385 installed), no ambient compatibility discovery, no automatic trust, no
386 plugin-contributed MCP OAuth, no LSP adapter, native extension runtime, or MCP
387 subscription adapter, no
388 foreign executable plugin runtime import, and no on-disk auto-migration of a
389 legacy `plugin.toml` to `plugin.json`. The explicit offline
390 [OpenCode/DSH converter](PLUGIN_AUTHORING.md#convert-an-existing-plugin) supports
391 selected portable Skills, static Streamable HTTP MCP declarations, and
392 explicitly packaged Node `.mjs`, `.js`, or `.cjs` MCP servers selected with `--stdio-root`.
393 Local source and dependencies are copied for the same native installation,
394 capability review, hash-bound trust and enable flow; conversion executes no
395 code or package manager. It does not migrate arbitrary bundles or reproduce
396 another client's runtime or policy.
397 The other capabilities above remain later work rather than implied support.
398
399 ## Marketplace catalogs (#5311)
400
401 `/plugin marketplace` reads LOCAL catalog documents in the real published
402 schemas (Kimi, Claude, Codex, Codewhale native; Codex via its policy markers)
403 and renders every candidate with an honest install plan:
404
405 ```text
406 /plugin marketplace add <name> <path> # parse a local catalog file (no network)
407 /plugin marketplace list # catalogs + candidates + diagnostics
408 /plugin marketplace show <name> # one catalog in detail
409 /plugin marketplace remove <name> # forget a catalog (plugins unaffected)
410 /plugin marketplace install <catalog> <candidate>
411 ```
412
413 - `add` never fetches anything: it reads one local JSON file (≤4 MiB, regular
414 files only, symlinks refused) and stores the parsed catalog next to the
415 plugin state file.
416 - Catalog tiers and provenance (`official`, `curated`, …) are **display
417 only** — they never grant trust, enablement, or installation.
418 - Foreign policies are visibly ignored: a Codex `INSTALLED_BY_DEFAULT` entry
419 is listed with a `NO_AUTO_INSTALL` warning and nothing is installed until
420 an operator runs the install verb.
421 - Sources Codewhale cannot fetch (npm packages, `command:` sources, non-tarball
422 URLs) are listed as `not installable` with the reason.
423 - `install` routes through the same reviewed installer as `/plugin install`:
424 the bundle lands disabled and untrusted, and enters the hash-bound trust
425 review before anything activates.
426
426 lines MARKDOWN