用 Codewhale plugin-creator 技能搭建本地插件 Bundle:清单、Skill 与信任流程实战
【免费下载链接】CodewhaleOpen-source coding agent for your terminal, built in Rust and on a journey of continuous community improvement. Issues and PRs welcome.项目地址: https://gitcode.com/GitHub_Trending/de/Codewhale
本篇技术指南围绕 Codewhale 内置的plugin-creator技能(crates/tui/assets/skills/plugin-creator/SKILL.md)展开,讲解如何在终端中从零搭建一个本地 Codewhale 插件 bundle:包括plugin.toml版本化清单的字段语义、Skills 与 MCP 服务器的命名空间与声明约束、/plugin命令族的校验-审查-信任-启用全流程,以及为什么 bundle 默认处于 untrusted/disabled 状态。读完本文,你将能够独立创建、校验、审查并启用一个安全的本地插件 bundle,并理解 Codewhale 插件加载器"有界装载、显式信任"的底层机制。
插件加载器的边界:v0.9.1 的"有界装载"模型
Codewhale v0.9.1 的插件加载器是**刻意有界(deliberately bounded)**的:一个被信任且启用的 bundle,只能通过既有引擎向 Codewhale 添加两类声明式组件:
- 声明式 Skills:以
SKILL.md形式挂载的、可被 Agent 调用的技能文档; - MCP 服务器:通过既有 MCP 引擎接入的 stdio 或远程服务。
其他组件类型(命令、子代理、钩子、LSP、原生扩展、文件系统根、生命周期变更)在当前构建中仅作清单登记(inventory-only),不会被执行。这一边界同时存在于命令面和源码策略中:在 crates/tui/src/plugins/activation.rs 中,PluginActivationPolicy::current()(激活策略版本 v3)把Skills、McpStdio、McpRemote(以及声明式的Commands、Agents、Hooks)列为受支持适配器,而Lsp、Native、FilesystemRoots、LifecycleMutation明确列为 inactive;激活策略还通过capability_hash把"这份策略本身"绑定进信任凭证,未来任何一次适配器策略变更都会让旧的信任凭证以CapabilitiesChanged方式失败关闭(fail closed)。
从源码结构看,这意味着插件系统的安全模型是"最小可执行面 + 显式审查":你能装进去的东西不多,但每一样都会被严格校验、逐项审查后才可能生效。
第一步:选择 Bundle 归属位置与命名
按plugin-creator技能的工作流,首先选择一个 Codewhale 拥有的位置存放 bundle,二选一:
- 用户级 bundle:
~/.codewhale/plugins/<plugin-name>/,对所有工作区生效; - 工作区级 bundle:
<workspace>/.codewhale/plugins/<plugin-name>/,仅对当前工作区生效。
随后将 bundle 名称规范化为小写连字符形式(lowercase hyphen-case)。这不是风格建议,而是硬性校验规则:在 crates/tui/src/plugins/manifest.rs 的validate_plugin_name中,名称必须为 1~64 个字符,只能由小写 ASCII 字母、数字和内部连字符组成,且首尾字符不能是连字符;plugin.json(Agent Plugins v1.0.0 格式)清单则额外允许内部点号但禁止--与..。
从源码看,
PluginManifest支持plugin.toml(Codewhale 原生/遗留格式)与plugin.json+ 兄弟文件mcp.json(Agent Plugins 标准格式,以及 Kimi 兼容格式)三种编码,解析后统一为同一个PluginManifest结构。plugin-creator技能采用最直接的plugin.toml方式。
第二步:编写版本化清单 plugin.toml
在 bundle 根目录创建plugin.toml。技能给出了最小可运行骨架:
schema_version = 1 [plugin] name = "my-plugin" version = "0.1.0" description = "What this bundle provides" [skills] path = "skills"结合 crates/tui/src/plugins/manifest.rs 中PluginManifest/PluginMeta的结构体定义,我们可以把这份清单的字段语义完整展开:
| 字段 | 位置 | 含义与约束 |
|---|---|---|
schema_version | 顶层 | 当前支持的最高版本为1(CURRENT_SCHEMA_VERSION);声明超过上限会报错。缺省会被视为遗留清单,/plugin validate会提示补写schema_version = 1 |
[plugin].name | 插件元信息 | bundle 名称,规则同上(小写连字符) |
[plugin].version | 插件元信息 | 必须是合法SemVer(如0.1.0),Version::parse校验失败即报错;遗留清单缺失版本时显示0.0.0并告警 |
[plugin].description | 插件元信息 | 可选,最长 1024 字符,不含控制字符与双向排版字符 |
[plugin].author | 插件元信息 | 可选,最长 256 字符 |
[plugin].display_name | 插件元信息 | 可选;当发布用name因标准名规则被 slug 化时,保留人类可读名 |
[plugin].homepage/repository/license/keywords | 插件元信息 | 可选,分别有长度上限(homepage/repository 2048、license 128、keyword 128 且去重) |
[skills].path | 组件路径 | 指向 Skill 目录,相对 bundle 根;也可用paths数组把同一种组件拆到多个目录 |
[commands]/[agents]/[hooks]/[lsp]/[native] | 组件路径 | 声明式组件位置,结构与[skills]相同(path/paths) |
[mcp_servers.<name>] | MCP 服务器 | 见下文专节 |
[capabilities] | 能力声明 | filesystem_roots、network_hosts、lifecycle_mutation,仅登记不执行 |
[when] | 宿主条件 | os(支持windows/linux/macos/freebsd/openbsd/netbsd/android/ios)与binaries(裸可执行名,禁止路径分隔符与 Windows 绝对路径) |
仓库自带了一个真实样例 crates/tui/assets/plugins/rust-toolkit/plugin.toml,展示了author与[when]的用法:
[plugin] name = "rust-toolkit" description = "Rust development toolkit with cargo check integration" version = "0.1.0" author = "Codewhale Team" [skills] path = "skills" [when] os = ["windows", "linux", "macos"] binaries = ["cargo"]注意:组件路径必须是相对路径,禁止绝对路径、禁止..逃逸、禁止穿越符号链接(resolve_contained_path会逐级做symlink_metadata检查并canonicalize后确认仍位于 bundle 根内)。清单本身也必须是普通文件而非符号链接,大小上限 1 MiB。
第三步:挂载 Skill 并理解命名空间
把每个 Skill 放到skills/<skill-name>/SKILL.md。Codewhale 会将其暴露为my-plugin:<skill-name>这种带命名空间的限定名称,绝不会作为不带前缀的裸命令出现。这一点同样有源码支撑:在 crates/tui/src/commands/groups/skills/skills.rs 中,/skills inspect会输出技能的source与插件来源(plugin provenance),让用户一眼看出某个 Skill 究竟来自哪个 bundle,而非全局命令空间。
这样的设计带来两个实际收益:一是避免不同 bundle 之间的技能名冲突;二是让信任边界清晰——你启用的是"某个插件带来的技能",审查对象始终指向明确的来源。
第四步:声明 MCP 服务器(仅当确实需要)
只有 bundle 确实需要既有 MCP 引擎时才添加[mcp_servers.<name>]。Codewhale 对 MCP 声明的校验异常严格(全部实现在 crates/tui/src/plugins/manifest.rs 的validate_mcp_servers中),并且把 stdio 与远程两类服务器区分管理,因为它们在审查界面上的威胁模型不同。
stdio MCP(本地子进程)
[mcp_servers.my-tool] command = "bin/my-tool" # 必须是裸可执行名,或 bundle 内的相对路径 args = ["--config", "config.json"] env = { "API_KEY" = "${MY_API_KEY}" } # 只允许精确的 ${SOURCE_ENV} 引用 cwd = "." enabled = true required = false核心约束:
command与url二选一,必须恰好声明其一;- stdio 服务器不得声明远程传输或鉴权字段(
transport、headers、env_headers、bearer_token_env_var、scopes、oauth等全部禁止); args禁止绝对路径、禁止..逃逸出 bundle 根,且不得内嵌字面量凭据——源码会对token=、api-key=、secret等敏感键名及sk-、ghp_、AKIA等凭据形态做启发式拦截,并要求改为"经审查的环境映射";env的值只允许精确的${SOURCE_ENV}引用(exact_environment_placeholder会要求字符串严格形如${VAR}),不允许字面量秘密、不允许拼接表达式;- 其余限额:
args≤ 64、env≤ 64、enabled_tools/disabled_tools各 ≤ 256,超限报错。
远程 MCP(HTTP/HTTPS)
[mcp_servers.remote-api] url = "https://api.example.com/mcp" transport = "sse" # 显式设置时只接受 sse env_headers = { "Authorization" = "${AUTH_TOKEN}" } bearer_token_env_var = "AUTH_TOKEN" enabled = true核心约束:
- URL 必须使用HTTPS,或仅当目标是
localhost/回环地址时才允许明文 HTTP; - URL禁止内嵌用户信息(userinfo/password)、查询参数与 fragment;
- 禁止字面量
headers;鉴权一律走env_headers(环境变量名来源)或bearer_token_env_var; - 远程服务器不得声明
cwd、args、env等 stdio 专属字段;OAuth 字段当前被禁用,报错提示改用环境变量鉴权; [capabilities].network_hosts必须精确等于所有远程 MCP 端点归一化后的主机集合——源码会取出每个 URL 的 host 做规范化(转小写、拒绝带端口/路径/凭据的主机串),与声明值逐一比对,不一致直接校验失败;- 清单中任何位置都不得出现凭据:无论是 URL 内嵌、字面量 header 还是参数值,一律以"经审查的环境变量引用"方式注入。
第五步:其他组件类型——只登记、不激活
如果 bundle 需要声明命令、代理(agents)、钩子(hooks)、LSP、原生扩展、文件系统根或生命周期变更,plugin-creator技能的原则是:仅在为未来工作做清单登记时声明它们。Codewhale 会把它们显示为 inactive(未激活),同时仍然激活同一 bundle 中受支持的 Skills 与 MCP——即"混合 bundle 部分激活"。
从 crates/tui/src/plugins/manifest.rs 的PluginInventory::compatibility()可以看出三种兼容性结论:
- full:所有声明面都有适配器(或 bundle 为空)——可完整激活;
- partial:支持适配器可激活,其余声明面保持 inactive——可部分激活;
- unsupported:只声明了当前构建无法激活的面(如仅含 LSP/原生扩展)——该 bundle 无法被启用。
这就是技能中"只声明那些不受支持表面的 bundle 不能被启用"的源码依据:can_activate_supported_components()在兼容性为unsupported时返回 false,/plugin enable会直接拒绝。
第六步:校验、审查与信任——先 validate,再 trust,后 enable
plugin-creator技能规定了一个"不执行 bundle 内容"的校验与审查流程(全部命令由 crates/tui/src/commands/groups/plugins/mod.rs 的/plugin分发器实现):
/plugin validate <plugin-name> # 校验清单与组件,报告 warning/error 诊断 /plugin show <plugin-name> # 查看 bundle 详情(组件、能力、兼容性、哈希) /plugin enable <plugin-name> # 打开内容/能力审查界面(未信任时) /plugin trust <plugin-name> <token> # 运行审查后给出的精确确认命令 /plugin enable <plugin-name> # 再次启用几个关键机制值得展开:
- 审查令牌绑定完整哈希:
review_token()生成的确认令牌形如<content_hash>.<capability_hash>——content_hash是对整个 bundle(含所有文件字节与可执行位)的 SHA-256,capability_hash则把激活策略与能力清单一起哈希。用户必须逐字运行界面给出的精确确认命令,而不是拍脑袋输入任何文本; - trust 只登记、不激活:从 crates/tui/src/plugins/registry.rs 的
trust()实现看,信任操作会先把 bundle 内容**暂存(stage)**为运行时快照,写入TrustReceipt(含两个哈希、被审查能力清单、审查时间),并记录进 review history;关键点:entry.enabled = false被强制置位——即使旧状态曾启用,重新审查后也绝不会隐式重新激活; - enable 需要完整前置条件:
enable()依次检查——已受信任、存在已验证的运行时快照(staged)、[when]条件适用于当前宿主、且至少有一个受支持的声明组件。任何一项不满足都会报错; - enable 立即重建目录:启用成功后会立即重建当前工作区的 Skill/MCP 目录(
AppAction::PluginRegistryChanged),无需重启或手动刷新。
最后用以下命令做闭环验证:
/skills inspect # 检查 Skill 的插件来源(plugin provenance) /plugin list # 检查预期的信任与激活状态若 bundle 内容在信任后被更新(/plugin update),其内容哈希必然变化,旧信任凭证不再匹配——必须重新走"审查 → trust → enable"流程,这正是"内容即身份"哈希设计的自愈机制。
安全基线:默认 untrusted、默认 disabled
每个用户级与工作区级 bundle 在创建之初都是未信任且未启用的。plugin-creator技能最后明确划定了 v0.9.1 的功能边界:
- 不要添加 marketplace(市场)、下载器、更新器、兼容性扫描、可执行扩展运行时或自动信任流程;
- 这些表面(surface)超出 v0.9.1 范围,不应出现在 bundle 设计中。
这一约束与整个插件信任模型一脉相承:能力边界刻意收窄,信任必须显式、逐次、绑定内容哈希,杜绝任何"装上即运行"或"自动放行"的路径。对需要更强的插件分发、更新或自动化的场景,应等待 Codewhale 后续版本对相应适配器与流程的正式支持,而不是绕过审查机制自行扩展。
小结
借助内置的plugin-creator技能,搭建 Codewhale 本地插件 bundle 的完整路径可以归纳为四步:定位置与命名 → 写版本化plugin.toml→ 挂载命名空间 Skills / 声明受约束的 MCP → 用/plugin命令族完成 validate → show → trust → enable 的显式审查闭环。整个过程由 crates/tui/src/plugins/manifest.rs 的清单校验、crates/tui/src/plugins/activation.rs 的激活策略、crates/tui/src/plugins/registry.rs 的信任状态机共同保障:内容以 SHA-256 绑定身份,能力以策略哈希防漂移,信任与激活彻底分离,最终让"可扩展性"与"可审计性"在同一个有界加载器内达成平衡。
【免费下载链接】CodewhaleOpen-source coding agent for your terminal, built in Rust and on a journey of continuous community improvement. Issues and PRs welcome.项目地址: https://gitcode.com/GitHub_Trending/de/Codewhale
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考