Multica 插件实战:从 Deploy Sentinel 示例看生产事故响应插件的完整实现
本文以 examples/plugins/deploy-sentinel 目录下的生产事故响应插件为蓝本,完整拆解一个"真实团队会怎么写"的 Multica 插件:四类触发器(agent / manual / ui / event)与两种传输(http / mcp)如何在一份清单中协作,钩子后端如何做签名验签与业务拒绝,以及技能(SKILL.md)与钩子如何配合训练 Agent 的事故处理流程。读完后你应能独立发布并安装一个带签名校验、回滚拒绝规则和 MCP 工具采纳的 Multica 插件。
Deploy Sentinel 解决的是真实运维场景:当一个"事故 issue"出现时,把最近的相关部署关联起来、让 Agent 按团队自己的规则提交回滚申请、在事故创建时呼叫值班工程师,并采纳团队已有的 metrics MCP 服务器。它存在于仓库中,正是因为同一目录下的其他示例各自只演示一件事,而它演示的是一个完整插件应有的形状——README 原话概括为"four triggers, both transports, real business rules that refuse things, and a skill that teaches an agent when to use any of it"。
插件贡献清单:六个组成部分各自演示什么
清单文件 multica.plugin.json 声明了六项贡献,每一项都对应一种插件能力或一种设计要点:
| 贡献 | 类型 | 演示的要点 |
|---|---|---|
incident-response |
skill 资源 | 一份 SKILL.md 成为工作区里的普通技能;卸载插件时一并移除 |
correlate_deploys |
hook,agent + manual + ui 触发 |
一个钩子、三个调用方:Agent 拿到工具,人拿到按钮,面板拿到函数 |
request_rollback |
hook,agent + manual 触发 |
一个会拒绝的写操作:部署太旧、理由太短都拒绝 |
page_oncall |
hook,event 触发 |
在 issue.created / issue.status_changed 事件上触发,永不阻塞任何东西 |
metrics |
hook,mcp 传输 |
采纳外部 MCP 服务器的工具,须经管理员逐一审批 |
incident |
issue_panel 表面 |
人看的一面,运行在无凭证的沙箱 iframe 中 |
对应的清单结构是:
contributes.surfaces:incident表面,类型issue_panel,入口ui/main.js,平台限定["web", "desktop"];contributes.hooks:上面列出的四个钩子,各自声明triggers、input_schema、transport与timeout_ms;contributes.resources:技能资源incident-response,入口指向skills/incident-response/SKILL.md。
权限与配置:清单里声明了什么
清单的 scopes 数组声明插件需要的全部权限:
"scopes": [
"issues:read",
"issues:write",
"comments:read",
"comments:write",
"storage:workspace",
"net:sentinel.example.com",
"net:metrics.example.com"
]
前五项是平台内能力(读写 issue 与评论、工作区存储),后两项 net: 范围是"钩子可以外联哪些主机"的白名单——net: 指向精确的主机名而非 URL,sentinel.example.com 与 metrics.example.com 必须分开声明。这也是插件安装审批时管理员看到的授权范围,钩子永远无法触达管理员未批准的主机。
config 段定义安装后由管理员填写的表单字段,六种类型各有一例:
| 配置项 | 类型 | 说明 | 必填 |
|---|---|---|---|
service_prefix |
string | 服务名前缀,关联部署时拼在用户输入前 | 是 |
rollback_window_minutes |
number | 部署必须多"新鲜"才有资格回滚 | 是 |
require_incident_label |
bool | 只关联打了 incident 标签的 issue | 否 |
environment |
enum | 环境,取值 production / staging |
是 |
sentinel_token |
secret | Deploy Sentinel API 令牌 | 是 |
metrics_credential |
secret | metrics MCP 服务器令牌 | 否 |
注意两个 secret 字段:manifest 声明两个机密配置项,意味着安装存储配置时需要一套加密存储(仓库的集成测试里正是为此注入了 secretbox,见 plugin_example_test.go)。
四个钩子的声明要点:
correlate_deploys:triggers: ["agent", "manual", "ui"],传输http,指向https://sentinel.example.com/hooks/correlate,超时 15 秒。输入 schema 要求service,可选window_minutes(默认 120)。工具描述里特意写明"空列表本身也是有用的答案",Agent 读到这段描述会改变它的行为。request_rollback:triggers: ["agent", "manual"],超时 20 秒。描述里直接写明它不执行回滚、只提交变更申请,且会拒绝超出时间窗的部署、要求书面理由。page_oncall:triggers: ["event"],events: ["issue.created", "issue.status_changed"],超时 10 秒。没有输入 schema——它是被事件驱动的。metrics:triggers: ["agent"],传输{"type": "mcp", "url": "https://metrics.example.com/mcp"},超时 30 秒。它不声明工具,工具由远端 MCP 服务器提供。
值得照抄的三个设计决策
README 用整节篇幅强调这个示例"值得抄的部分",逐条对应到源码:
1. request_rollback 会说"不"
在 handler.mjs 中,requestRollback 有三道拒绝:
- 部署 ID 不存在 →
rejected: Unknown deploy ...; - 理由长度不足 20 字符 → 拒绝文本是 "A rollback request needs a written reason (at least 20 characters) describing the evidence, not the conclusion.";
- 部署年龄超过
rollback_window_minutes配置 → 拒绝文本解释"超过这个点,向前修复通常更安全,且该由人决定"。
只有三道全过才返回 status: "filed" 和一个变更 ID,且响应里附带 next_step: "A human must approve this change request. Nothing has been rolled back yet.",防止 Agent 把"申请已提交"误报成"事故已处理"。
README 的观点是:只成功的钩子处理器是简单情形,会拒绝的才有用。被迫在提交变更申请前写出证据的 Agent 会产生质量更高的申请——而且拒绝文本会解释原因,Agent 可以据此行动,而不是盲目重试。
2. correlate_deploys 把"没有变化"当答案报告,而不是返回空列表
correlateDeploys 的返回体永远包含 summary 字段:窗口内没有部署时,summary 是 "No deploys to X in the last N minutes. This is unlikely to be a deploy.",而不是把"空数组"留给 Agent 自己推断。技能侧(见下文)则明确告诉 Agent:空结果意味着应该停止在部署方向上找原因。README 的对比很精确——返回 [] 的工具和明确说"这个窗口没有部署、这大概率不是部署问题"的工具,书写成本相同,在 Agent 手里的行为截然不同。
3. 技能与钩子是配套设计的
SKILL.md 按名字点出这些工具并规定使用顺序:先确定影响面,再确定"开始时间"(第一个时间戳通常是有人注意到而非发生的时刻),然后才调 correlate_deploys,窗口起点必须早于第二步得到的时间戳。它还规定了回滚理由的写法——"写证据,不写结论",并给了好/坏示例("error rate on checkout-api went 0.2% → 6% within 90s of deploy d-4821..." vs "this deploy broke checkout")。README 的结论是:两半单独都不如合在一起好用。
钩子后端:一个真实 hook server 必须做对的事
handler.mjs 是插件作者自己运行的服务器("Multica never runs this — the plugin author does"),文件头注释直接点明这里集中了"一个真实钩子后端必须做对、且别处找不到"的四件事:验签、拒重放、应用团队规则、用单次调用凭证回写 issue。
签名与重放防护
Multica 每次外发钩子调用都携带 x-multica-signature 与 x-multica-timestamp 头,签名算法是对 timestamp + "." + rawBody 做 HMAC-SHA256。服务端校验流程(verifySignature):
- 先查时间戳年龄,超出
REPLAY_WINDOW_SECONDS = 300秒直接拒绝——源码注释解释:Multica 特意把 timestamp 纳入签名,正是为了让一个"旧的、签名有效的请求"无法稍后被重放; - 计算期望签名后,先比较长度再用
timingSafeEqual做恒定时间比较——因为timingSafeEqual在长度不等时会抛异常而非返回 false,而长度本身不是机密,可以先查。
签名密钥来自 MULTICA_SIGNING_SECRET 环境变量,即在工作区设置中签发插件令牌时一次性显示的 whsec_... 值;未配置时每个请求都会被拒。
单次调用的回写凭证
Multica 在请求体中随附 callback_url 与 callback_token。该令牌只限于本次调用,钩子一返回即被吊销——所以处理器想写的东西必须在回复之前写完。commentOnIssue 因此是被 await 的而非 fire-and-forget;同时回写失败只打警告不使钩子失败,源码注释解释:Agent 仍然拿到了它的答复,"一条缺失的评论可以补救,一个失败的工具调用则不行"。
还有一个细节体现了触发方差异:correlate_deploys 在 trigger 为 ui 或 manual 时才会把结果以评论形式写回 issue(人在面板里点击,答案留在同事能看见的地方),而 agent 调用不写评论——Agent 自己会把发现写进 issue。
事件钩子:快速确认,别阻塞
page_oncall 分支只打印并立即 202 Acknowledged。源码注释说明原因:事件钩子的响应会被丢弃,返回没人读的东西没有意义,在这里阻塞只会白白占住一个 Multica worker。
MCP 传输:采纳外部服务器的工具,审批与 schema 钉住
metrics-mcp.mjs 是 metrics 钩子背后的 MCP 服务器,存在的意义就是演示 mcp 传输的适用场景:团队已经跑着一个 MCP 服务器,不想把它重新包装成 HTTP 钩子。它用 Bearer token(METRICS_TOKEN 环境变量,对应插件配置的 metrics_credential 机密字段)做鉴权,实现 JSON-RPC 的 initialize / notifications/initialized / tools/list / tools/call 四个方法,提供三个工具:
query_timeseries:查询时间窗内的指标序列;list_alerts:窗口内触发的告警,按时间倒序,附阈值;service_dependencies:该服务调用了谁、被谁调用,用于判断错误是源自本服务还是来自上游。
与 http 钩子的关键区别在审批模型上:在管理员打开 metrics 审批面板并勾选之前,没有任何 metrics 下的工具可以被调用——README 称"这是 mcp 传输与 http 钩子之间全部的差异"。README 的"Try breaking it"一节给出两个破坏性实验:往 metrics-mcp.mjs 里加一个新工具再重开审批面板,新工具以未批准状态出现,勾之前 Agent 调不到;修改已批准工具的 inputSchema 并重启,Agent 任务能正常开始,但该连接会被拒绝,因为 schema 摘要(digest)与当初批准的不再匹配。也就是说,批准是"按 schema 摘要钉住"的,工具改了形状等于未批准。
issue 面板表面:沙箱 iframe 里的行为脚本
ui/main.js 是 incident 表面的入口。文件头注释与 plugin-sdk 文档 共同说明了表面的运行约束:
- 表面是单个脚本、无模块图:Multica 存储发布的制品并在一个生成的文档中提供服务,没有可供静态
import解析的模块图。真实插件会把@multica/plugin-sdk打进这个单文件;示例则内联了几个所需调用,保持可读且零安装。 - iframe 以
sandbox="allow-scripts"挂载且不带allow-same-origin,即不透明源:没有宿主 cookie、没有宿主存储、没有任何凭证。 - 页面与宿主的全部通信走桥(bridge):
main.js从globalThis.__multicaPluginBridgePortV2取到宿主注入的MessagePort,所有 API 调用(/context、/issues/...、/storage/workspace/...、/hooks/...)都是发给宿主的消息,宿主以登录用户自己的会话重新发起请求,并检查插件被授予的范围。net:范围决定 CSP 的connect-src;未声明任何net:范围时,表面连发不出一个网络请求。
行为上,correlate() 先读工作区存储里的 default_service(服务名是工作区级设置而非逐 issue 手填,保证同一服务的所有调查者得到同一答案),再经 ui 触发器调用自家的 correlate_deploys 钩子——宿主对出站调用签名并施加限流,"frame 永远看不到端点"。面板上每个部署卡片带一个 "Request rollback" 按钮,点击后 prompt 要求填写理由("审批人会读它——写证据,不写结论"),再经 manual 语义调用 request_rollback。错误处理也有讲究:钩子调用失败时明确显示"Deploy Sentinel could not reach its backend",而不是渲染一个空面板让人误读为"没有部署"。
本地跑起来:完整流程
本地需要两个服务器,都是插件作者自己的;Multica 从不运行它们。两者都必须提供 HTTPS——因为钩子的传输 URL 必须是 https:// URL,否则清单装不上。
第一步:生成一次性开发证书
openssl req -x509 -newkey rsa:2048 -nodes -days 365 \
-keyout dev-key.pem -out dev-cert.pem \
-subj "/CN=127.0.0.1" -addext "subjectAltName=IP:127.0.0.1"
第二步:启动两个服务器
# 钩子端点。签名密钥就是签发插件令牌时一次性显示的 whsec_…
MULTICA_SIGNING_SECRET=whsec_... node server/handler.mjs # :8788
# `metrics` 钩子背后的 MCP 服务器。
METRICS_TOKEN=metrics-dev-token node server/metrics-mcp.mjs # :8789
两个服务器都读取 TLS_CERT / TLS_KEY,缺省为工作目录下的 dev-cert.pem / dev-key.pem;handler.mjs 读不到证书文件时会退出并打印 openssl 命令提示。端口可用 PORT 覆盖,缺省 8788(钩子)与 8789(MCP)。
第三步:让 Multica 指向它们
两个端点都在回环地址上,而出站防护(outbound guard)按设计拒绝回环,所以要用开发开关显式声明:
export MULTICA_PLUGIN_DIR=examples/plugins
export MULTICA_PLUGIN_DEV_ORIGINS=https://127.0.0.1:8788,https://127.0.0.1:8789
export MULTICA_PLUGIN_DEV_CA=/path/to/your-dev-ca.pem
README 对这三个开关的边界界定得很清楚,值得记住:
MULTICA_PLUGIN_DIR只是前端制品的发布快捷方式——上面两个服务器仍然是你自己要跑的,net:范围仍然是触达它们的授权依据;MULTICA_PLUGIN_DEV_ORIGINS决定"地址是否必须公网",MULTICA_PLUGIN_DEV_CA决定"信任哪张证书";- 它们从不关闭证书校验,也从不放宽
net:范围——钩子仍然够不到管理员没有批准的主机。本地也必须 HTTPS,这是清单校验器要求的;MULTICA_PLUGIN_DEV_CA的作用是让自签名证书被信任。
第四步:发布、安装、审批
从 MULTICA_PLUGIN_DIR 以 deploy-sentinel 发布,安装出现的版本,填写配置表单,然后打开 metrics 审批面板勾选你希望 Agent 能触及的工具——勾选之前 metrics 下没有任何工具可被调用。
handler.mjs 内置了固定的演示部署数据(d-4821 4 分钟前、d-4820 51 分钟前、d-4816 92 分钟前、d-4802 380 分钟前),保证 README 的推演可复现。四个破坏性验证场景:
- 让 Agent 回滚
d-4802:它已经 380 分钟了,插件拒绝并解释原因; - 用理由 "broken" 请求回滚:太短,被拒;
- 往
metrics-mcp.mjs加工具再重开审批面板:新工具以未批准出现,勾选前 Agent 调不到; - 修改已批准工具的
inputSchema并重启:schema 摘要不再匹配批准记录,连接被拒。
测试:安装的是这份真实清单
plugin_example_test.go 是这个示例的守门人。测试注释解释了自己为什么这么写:它安装的是仓库里这份真实清单而不是旁边手写的一份 fixture——"手写的 fixture 会在示例腐烂后继续通过,而示例才是插件作者要抄的那个文件。如果有人弄坏了清单,这个测试会说出来。"
具体流程是:把 examples/plugins/deploy-sentinel 拷贝到临时目录作为 MULTICA_PLUGIN_DIR,只改写两处——把两个端点主机指向测试服务器、把对应 net: 范围指向测试主机(清单里的每个工具描述、每个字段原样保留);然后走真实的发布与安装路径。断言覆盖:技能落入技能表、Agent 工具列表恰好包含声明了 agent 触发的钩子、一次 Agent 钩子调用带签名出去并按签名校验回来、metrics 工具被发现、批准前被拒、批准后按 schema 摘要钉住。安装前测试还为插件注入了 DeploymentKey——注释说"签名是让钩子调用可被插件自己的服务器验证的手段;没有部署密钥,引擎干脆拒绝外呼"。
README 最后一段解释了两套文件的分工:.mjs 文件是你手工运行的真实后端(需要 Node),测试用的是 Go 写成的、应答同一套契约的服务器,所以 CI 不需要 Node 环境。
小结:从示例到你自己插件的抄写清单
Deploy Sentinel 的价值在于它把一个插件作者容易省略的部分都摆了出来,且每一处都有源码对应:
- 清单:
scopes(含net:主机白名单)+config表单(六种字段类型)+contributes(surfaces / hooks / resources 三类),见 multica.plugin.json; - 钩子后端:HMAC-SHA256 验签、300 秒重放窗口、恒定时间比较、单次调用回写令牌、按触发方决定是否回写评论、事件钩子快速 ACK,见 handler.mjs;
- 会拒绝的写操作:拒绝理由写进响应文本,让 Agent 能据此修正行为而不是盲目重试;
- 把"无变化"当答案:
summary字段显式陈述空结果的含义,并在 SKILL.md 中规定 Agent 收到该含义后的行动; - MCP 采纳:外部服务器提供工具,管理员按 schema 摘要逐一审批,见 metrics-mcp.mjs;
- 沙箱表面:单脚本、无模块图、不透明源 iframe,一切经桥以用户会话重放,见 ui/main.js 与 plugin-sdk 文档。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0623
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00