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