返回 CodeWhale
PLUGIN_AUTHORING.md
根目录 / docs / PLUGIN_AUTHORING.md
1 # Write your first Codewhale plugin
2
3 > 阅读简体中文版:[zh_hans/PLUGIN_AUTHORING.md](zh_hans/PLUGIN_AUTHORING.md)
4
5 Start with a skill: a Markdown instruction file inside a small plugin bundle.
6 The [hello-codewhale example](examples/plugins/hello-codewhale/plugin.json)
7 contains two files, declares no server or hook, and asks for no tool use.
8 This walkthrough takes it from source files to a reviewed, enabled skill.
9
10 ## 1. Create the bundle
11
12 Use the checked-in example, or create this directory outside an installed
13 plugins directory:
14
15 ```text
16 hello-codewhale/
17 ├── plugin.json
18 └── skills/
19 └── hello/
20 └── SKILL.md
21 ```
22
23 `plugin.json`:
24
25 ```json
26 {
27 "$schema": "https://agent-plugins.org/schemas/plugin.json",
28 "name": "hello-codewhale",
29 "version": "0.1.0",
30 "description": "A minimal, explicitly invoked greeting skill."
31 }
32 ```
33
34 `skills/hello/SKILL.md`:
35
36 ```markdown
37 ---
38 name: hello
39 description: Greet the user when they explicitly try the hello-codewhale example.
40 invocation: explicit-only
41 ---
42
43 Respond with one short greeting in the user's language. Include the exact text
44 `hello-codewhale:hello` so they can identify the example they invoked.
45
46 Use only the conversation. Do not call tools, run commands, read or write files,
47 or contact external services.
48 ```
49
50 Codewhale finds `skills/` automatically. `explicit-only` keeps this example out
51 of the model's automatic skill catalogue; you load it by name. See
52 [Skills](SKILLS.md#invocation-and-alias-metadata) for invocation metadata and
53 [Plugin bundles](PLUGIN_BUNDLES.md#manifest) for the authoritative manifest
54 contract. Keep new native bundles in `plugin.json`; no second manifest is
55 needed.
56
57 ## 2. Install, inspect, and trust
58
59 Start Codewhale in the repository root. Enter these commands **inside the
60 Codewhale session**, one at a time:
61
62 ```text
63 /plugin install ./docs/examples/plugins/hello-codewhale
64 /plugin validate hello-codewhale
65 /plugin show hello-codewhale
66 ```
67
68 For your own bundle, replace the install path with its directory. Installation
69 copies it to `~/.codewhale/plugins/hello-codewhale/`, disabled and untrusted.
70 Review the installed source, skill inventory, and permissions. This example
71 should declare only Skills, with no MCP server, hook, or requested network host.
72
73 The install review prints a command containing two full hashes:
74
75 ```text
76 /plugin trust hello-codewhale <full-content-sha256>.<full-capability-sha256>
77 ```
78
79 Run the exact command printed by your review; the angle-bracket text above is
80 a placeholder. If you need a fresh review, use `/plugin trust hello-codewhale`
81 without a token. Trust records the reviewed content and capability hashes and
82 creates a runtime snapshot. It does not enable the plugin.
83
84 ```text
85 /plugin enable hello-codewhale
86 /skills hello-codewhale:
87 /skills inspect
88 ```
89
90 The skill is named `hello-codewhale:hello`: the bundle name qualifies the skill
91 name. `/skills inspect` identifies its reviewed plugin snapshot.
92
93 ## 3. Invoke it and turn it off
94
95 ```text
96 /skill hello-codewhale:hello
97 ```
98
99 Codewhale confirms activation. Then send `Say hello.` as a normal message.
100 The reply should be a short greeting containing `hello-codewhale:hello`.
101 The example contributes instructions only; the reply still uses your selected
102 model and its normal provider connection. The local install, review, and
103 activation steps do not need a model call.
104
105 ```text
106 /plugin disable hello-codewhale
107 ```
108
109 Disabling removes the plugin's contributions while preserving its trust
110 receipt. A subsequent `/skill hello-codewhale:hello` must not activate it.
111 Enable it again when needed, provided its reviewed hashes still match.
112
113 ## 4. Iterate and review changes
114
115 The installed bundle is a copy. Editing the example's original source does
116 not update that copy. To try a changed local source, disable and uninstall the
117 installed example, then install the source directory again:
118
119 ```text
120 /plugin disable hello-codewhale
121 /plugin uninstall hello-codewhale
122 /plugin install ./docs/examples/plugins/hello-codewhale
123 /plugin validate hello-codewhale
124 ```
125
126 Uninstall removes the installed copy; it leaves the original example source
127 alone. Review the new token, trust it, and enable it again. For bundles
128 installed from a remote source, use `/plugin update <name>`; see
129 [Installing plugins](PLUGINS.md#update-and-uninstall).
130
131 When files in a discovered bundle change directly, `/plugin reload` refreshes
132 the registry. Changed content invalidates the old receipt, even if you leave
133 the version unchanged. Reload does not grant trust. Use `/plugin revoke <name>`
134 to remove trust explicitly.
135
136 ## Add only the components you need
137
138 All components use the same bundle review and existing Codewhale runtime:
139
140 | Component | Authoring surface |
141 | --- | --- |
142 | Skills | `skills/<name>/SKILL.md`; [instruction and invocation contract](SKILLS.md). |
143 | MCP | A sibling `mcp.json`; [bundle transport and credential rules](PLUGIN_BUNDLES.md#validation-both-formats). |
144 | Commands | Markdown command files; [command metadata](architecture/command-dispatch.md#user-commands). |
145 | Agent profiles | Fleet TOML profiles; [Fleet authoring](FLEET.md#authoring-agent-profiles-fleet-setup). |
146 | Hooks | `HooksConfig` TOML files; [events and process behavior](HOOKS.md). |
147
148 Declare Commands, Agents, and Hooks paths under
149 `extensions["net.codewhale"]` in `plugin.json`, as specified in
150 [Plugin bundles](PLUGIN_BUNDLES.md#active-and-inactive-component-surfaces).
151 Do not place MCP server fields or arbitrary runtime entrypoints at the manifest
152 root. LSP and native extensions can be inventoried but are not executable
153 plugin adapters.
154
155 Plugin trust is **not an OS sandbox**. A local MCP server or hook can launch a
156 process; review its code and authority before enabling it. Skills do not grant
157 permissions: repository instructions, permission rules, sandbox policy, and
158 tool approval still apply. Keep credentials out of bundles and command
159 arguments. Use the reviewed environment references documented in the
160 [bundle validation contract](PLUGIN_BUNDLES.md#validation-both-formats) for MCP;
161 read the separate [hook environment contract](HOOKS.md#the-hook-process-environment)
162 before adding a hook.
163
164 ## Convert an existing plugin
165
166 [`scripts/convert-plugin.py`](../scripts/convert-plugin.py) converts explicitly
167 selected remote MCP declarations, packaged local Node MCP servers, and portable
168 Skills into a native bundle.
169 It requires Python 3.10+ and PyYAML 6+; install those separately if absent.
170 The converter installs no dependencies, scans no ambient configuration or
171 credentials, makes no network requests, and executes no source code.
172
173 ### OpenCode
174
175 Save this plain JSON as `opencode-mcp.json`:
176
177 ```json
178 {
179 "mcp": {
180 "docs": {
181 "type": "remote",
182 "url": "https://example.invalid/mcp",
183 "oauth": false,
184 "enabled": false
185 }
186 }
187 }
188 ```
189
190 From the Codewhale repository root, run this in your shell:
191
192 ```sh
193 python3 scripts/convert-plugin.py --format opencode-v1 \
194 --config ./opencode-mcp.json --name migrated-tools --output ./migrated-opencode
195 ```
196
197 Choose `--format opencode-v2` for the `mcp.servers.<name>` layout, whose server
198 flag is `disabled` instead of `enabled`. Select the format from the data;
199 filenames and upstream branch names do not determine its version. Both formats
200 require explicit `oauth: false` for remote servers. Remote MCP output uses **Streamable HTTP only**;
201 OpenCode's fallback to legacy SSE is not reproduced. For an SSE-only endpoint,
202 author native `mcp.json` with `type: "sse"` and use the same review flow.
203
204 When MCP servers are selected, configurations containing `tools`,
205 `permission`/`permissions`, `agent`/`agents`, legacy `mode`, or `default_agent`
206 are refused. These settings can restrict tool access beyond server enablement.
207 Manually preserve those restrictions in Codewhale before supplying an MCP-only
208 input; simply deleting the settings can widen access.
209
210 JSONC comments and trailing commas are not
211 accepted: provide a plain JSON copy containing the declarations you intend
212 to port.
213
214 ### DeepSeek Harness (DSH)
215
216 Save this static Cordis entry list as `dsh-mcp.yml`:
217
218 ```yaml
219 - name: '@deepseek-ai/dsh-mcp-client'
220 disabled: true
221 config:
222 serverName: docs
223 transport: streamable-http
224 url: https://example.invalid/mcp
225 ```
226
227 ```sh
228 python3 scripts/convert-plugin.py --format dsh \
229 --config ./dsh-mcp.yml --name migrated-dsh --output ./migrated-dsh
230 ```
231
232 The DSH input may also be JSON, but must be the plain entry list, not a full
233 profile or patch composition. Each row must name `@deepseek-ai/dsh-mcp-client`.
234
235 A real dsh bundle package — an npm package whose `package.json` declares
236 `dsh.bundle.patch` — converts directly with `--bundle`:
237
238 ```sh
239 python3 scripts/convert-plugin.py --format dsh \
240 --bundle ./node_modules/@demo/tools-dsh --name migrated-dsh --output ./migrated-dsh
241 ```
242
243 The converter reads the package's `cordis.patch.yml`, applies its `insert` and
244 keyed-override operations over an empty profile (matching `applyEntryPatches`),
245 and converts each resulting row. Rows it cannot represent — runtime plugins,
246 `dsh.client` UI code, `!!js` expressions outside the documented idioms,
247 conditional `disabled` flags — are listed in `CONVERSION.md` rather than
248 silently dropped. The `!!js` idioms it does lower: `process.execPath` (becomes
249 `node`), `process.env.NAME` and `process.env.NAME || 'literal'` (resolved
250 against this machine), and `` `${process.env.NAME}...` `` templates. An `args`
251 entry that resolves to a host file is snapshotted: its containing directory is
252 copied into `mcp/<server>` and the resolution is recorded in the receipt. Rows
253 of `@deepseek-ai/dsh-skill-filesystem` contribute their `customSkillDirs`
254 children as skills when those directories live inside the package.
255
256 ### Local Node MCP servers
257
258 For an already packaged Node MCP server, select its original process working
259 directory explicitly. The converter copies that directory into `mcp/<server>`
260 and sets the native server's working directory to the reviewed copy. Relative
261 entrypoint imports and read-only resources keep the same layout.
262
263 ```json
264 {
265 "mcp": {
266 "localdocs": {
267 "type": "local",
268 "command": ["node", "server.mjs"],
269 "environment": {"API_TOKEN": "{env:LOCALDOCS_TOKEN}"},
270 "enabled": false
271 }
272 }
273 }
274 ```
275
276 ```sh
277 python3 scripts/convert-plugin.py --format opencode-v1 \
278 --config ./local-mcp.json --stdio-root localdocs=./packaged-localdocs \
279 --name local-tools --output ./migrated-local
280 ```
281
282 Repeat `--stdio-root SERVER=DIRECTORY` for every local server in the selected
283 configuration. OpenCode v2 uses `mcp.servers` and `disabled`. Static DSH entries
284 use `transport: stdio`, `command: node`, and `args: [server.mjs]`; DSH `env`
285 must be absent or empty because its literals/expressions are not OpenCode
286 environment references. Optional DSH/v2 `cwd` must be absent, empty, or `.`;
287 the selected root explicitly supplies the original working directory.
288
289 Use `node` plus one relative `.mjs`, `.js`, or `.cjs` entry. Package module
290 type and sibling imports are preserved by the native launch adapter. Compile
291 TypeScript to JavaScript before packaging; the converter does not run a compiler.
292 Package dependencies and read-only resources first, inside the selected root.
293 No package manager, install script, module loader or server runs during
294 conversion. Links/reparse points, hard-linked files, hidden files/directories
295 (including `.gitignore`, `.env*`, `.npmrc` and `node_modules/.bin`), common
296 credential filenames, and private-key containers are refused. Prepare a clean
297 package directory; ignore rules are not used to silently omit files. Inspect
298 every selected file for embedded credentials before conversion. The existing
299 4,096-file / 64 MiB aggregate bundle limit applies.
300
301 The converter rejects shell launchers, Node flags, extra arguments, non-Node
302 interpreters, literal environment values, and loader-changing environment
303 names. Stateful servers that write into their working directory, depend on the
304 live workspace, or import files outside the package need a manual native port.
305 Copying files does not statically verify JavaScript import closure or sandbox
306 arbitrary code. Local MCP processes run with host-user authority; their network
307 and filesystem access are not restricted by the remote endpoint host list.
308 The same native install, capability review, hash-bound trust, and enable steps
309 are required before Codewhale launches the server. This adds a packaged Node
310 MCP subset; it does not execute DSH/Cordis plugin modules.
311
312 ### Review the result
313
314 Both examples preserve disabled servers and use a placeholder endpoint. Replace
315 the endpoint and change the source's enablement flag before reconverting when
316 you are ready to connect. The output directory must be new, with an existing
317 parent. Existing output is refused; rejected input leaves no output bundle.
318
319 Add `--skill ./my-skill` for an explicitly selected directory containing
320 `SKILL.md`, or `--skill ./my-skill.md` for a single file; repeat the option for
321 more skills. `--config` is optional for a skills-only conversion. Skills require
322 `name` and `description` frontmatter. `disable-model-invocation: true` becomes
323 native `invocation: explicit-only`. Informational `license`, `compatibility`,
324 and `metadata` fields are retained in `SOURCE_SKILL_METADATA.json` companion
325 data. Companion files from selected skill directories are copied as data;
326 review them and the instructions before loading the skill.
327
328 Only exact OpenCode header references such as `{env:MCP_TOKEN}` become native
329 `env_headers`; the converter never reads the variable's value. Literal headers,
330 DSH header expressions, and URL file/environment substitution are refused.
331 Configured timeouts must be whole seconds expressed in milliseconds, from
332 `1000` through `3600000`. Omitted timeouts use Codewhale's defaults.
333
334 Executable foreign plugins and hooks, other stdio launchers, automatic OAuth,
335 configuration JavaScript,
336 YAML aliases/tags, `__jsExpr`, and unsupported skill runtime fields (including
337 `user-invocable: false`) require a manual port. Conversion does not reproduce
338 another client's runtime or bypass Codewhale's credential and sandbox rules.
339
340 Read the generated `CONVERSION.md`, `plugin.json`, `mcp.json` when present, and
341 all selected skill and MCP source files. Then use `/plugin install ./migrated-opencode` (or the
342 DSH output path), `/plugin validate <name>`, and the same hash-bound trust and
343 enable flow above. Conversion alone proves neither connectivity nor runtime
344 compatibility; the output is not installed, trusted, or enabled.
345
346 Source audit, 2026-09-08: OpenCode's [v1 MCP documentation](https://github.com/anomalyco/opencode/blob/d6855b6b47a8433462ac6aeeba882ccf734cb7f1/packages/web/src/content/docs/mcp-servers.mdx)
347 and [v2 MCP schema](https://github.com/anomalyco/opencode/blob/d6855b6b47a8433462ac6aeeba882ccf734cb7f1/packages/core/src/config/mcp.ts)
348 at `d6855b6b47`, and DSH's [MCP client reference](https://github.com/deepseek-ai/deepseek-harness/blob/c389f96bf3a9b6807cb71ed6bdad5849be0df6d8/packages/mcp/mcp-client/README.md)
349 at `c389f96bf3`. Upstream supports more than this deliberately bounded converter.
350
351 ## Community context
352
353 This guide responds to [giancarlocp's request for plugin authoring guidance
354 and OpenCode conversion in discussion #5827](https://github.com/Hmbown/Codewhale/discussions/5827).
355 The Chinese companion follows the documentation work requested by
356 [SparkofSpike in issue #5482](https://github.com/Hmbown/Codewhale/issues/5482).
357
357 lines MARKDOWN