插件与 Bridge
插件是分发容器:把命令、Skill、Hook、MCP 配置打成一个目录,团队成员一个参数就全拿到。 自己用写单个 Skill 就够,要发给十个人才需要插件。
Bridge 是另一件事——让 sid-code 接受远程客户端操控。两者放一页是因为都属于 "把 sid-code 接到别的东西上"。
快速上手
一个能跑的插件最少要 plugin.json + 一个组件目录:
mkdir -p /tmp/my-plugin/commands /tmp/my-plugin/skills/hello
cat > /tmp/my-plugin/plugin.json <<'EOF'
{
"name": "my-plugin",
"version": "0.1.0",
"description": "示例插件:一个斜杠命令 + 一个 skill",
"commands": "commands/",
"skills": "skills/"
}
EOF
cat > /tmp/my-plugin/commands/ping.md <<'EOF'
---
description: 回一句 pong 并报出当前分支
---
回答「pong」,然后用一句话说出当前 git 分支名。
EOF
cat > /tmp/my-plugin/skills/hello/SKILL.md <<'EOF'
---
name: hello-plugin
description: 演示插件提供的 skill:输出固定问候语 PLUGIN-SKILL-OK。
mode: activate
---
输出恰好一行:PLUGIN-SKILL-OK
EOF
sid-code --plugin-dir /tmp/my-plugin启动日志确认加载成功(实测):
● [PLUGIN] 加载了 1 个插件命令
● [PLUGIN] 加载了 1 个插件 Skill
● [PLUGIN] 插件组件: 1 命令, 0 Agent, 0 MCP 服务器会话里输入 /my-plugin:ping 用那个命令。Skill 名会带插件前缀 my-plugin:hello-plugin。
三层架构
| 层 | 是什么 | 在哪 |
|---|---|---|
| 意图层 | installed.json 声明装了哪些、启用哪些 | ~/.sid-code/plugins/installed.json |
| 物化层 | 插件的实际文件 | ~/.sid-code/plugins/<name>/ |
| 活跃层 | 运行时生效的命令 / Skill / Agent / Hook / MCP | 内存 |
插件不在 sid-code 进程里加载代码,组件都是 Markdown 或 JSON。 但它带的 Hook 和 MCP server 会以你的身份执行命令:Hook 能跑任意 shell(比如下面示例里的 ${PLUGIN_ROOT}/scripts/format.sh),stdio 类型的 MCP server 能启动任意进程,两者都能读写你的整个家目录, 权限和你手写的 Hook 一样大。只装可信来源的插件。
plugin.json 字段
| 字段 | 必填 | 说明 |
|---|---|---|
name | 是 | slug 格式:小写字母 / 数字 / - / _,以字母或数字开头,且 ≤ 64 字符 |
version | 是 | 版本号(semver) |
description | 是 | 一句话说明 |
author / license | 否 | 字符串 |
commands | 否 | 命令目录,默认 commands/。可给数组 |
skills | 否 | Skill 目录,默认 skills/。可给数组 |
agents | 否 | 子代理目录,默认 agents/。可给数组 |
hooks | 否 | Hook 配置文件路径,默认 hooks.json(只能是字符串,不能是数组) |
mcpServers | 否 | MCP 配置:内联对象,或指向文件的字符串 |
dependencies | 否 | 依赖的其他插件名(字符串数组) |
三个必填字段缺任一个插件都不会加载,校验会明确报错,比如 name 必须是 slug 格式(小写字母、数字、-、_,且以字母或数字开头)。
命令的命名规则
插件命令名带插件前缀,子目录变成命名空间:
commands/deploy.md → /my-plugin:deploy
commands/env/staging.md → /my-plugin:env:staging好处是不同插件的同名命令不冲突(plugin-a:deploy vs plugin-b:deploy)。
插件里带 Hook
hooks.json 和 settings.json 的 hooks 字段同格式,额外支持 ${PLUGIN_ROOT} 变量指向插件自己的目录——这样脚本可以放插件里一起分发:
{
"post_tool_use": [
{
"type": "command",
"matcher": "edit|write",
"command": "${PLUGIN_ROOT}/scripts/format.sh"
}
]
}事件名推荐 snake_case、hook 对象推荐平铺(源码也兼容 PascalCase 事件名和嵌套组写法, 但别在新插件里用),规则和普通 Hook 完全一致—— 细节和排错见 Hook 指南。
Hook 热重载是原子的
/reload-plugins 重新加载时,旧 hooks 一直有效直到新的准备好,一次性整体替换。 不会出现"旧的已清掉、新的还没注册"的窗口——那个窗口意味着约束短暂失效。
三种来源与优先级
| 来源 | 怎么来 | 标识 |
|---|---|---|
| 内置 | 随二进制分发 | name@builtin |
| 已安装 | ~/.sid-code/plugins/<name>/ | name@local |
| 会话级 | --plugin-dir <路径> | name@inline |
同名时优先级:inline > 已安装 > 内置。
安装有两条路:从插件市场装(/plugin install <name>@<市场>,见下文企业插件市场), 或从本地目录装(/plugin install <路径>)。市场只支持 url 型目录,不能直接从 git URL 安装。 /plugin marketplaces 列出本机已注册的市场:企业市场 company(由 backend.url 推出)、 官方市场 sid-code(可用 officialMarketplace: false 关闭),以及 extraKnownMarketplaces 里配的额外市场。 从市场装的插件标识是 name@<市场>,本地目录装的是 name@local。
inline 排最高是给调试用的:改插件时不用先卸载已安装的版本, 直接 --plugin-dir 指向工作副本就覆盖掉了。
--plugin-dir 可以重复给:
sid-code --plugin-dir /tmp/plugin-a --plugin-dir /tmp/plugin-b会话里管理插件
| 命令 | 作用 |
|---|---|
/plugin | 打开插件管理面板:列出已装插件,选中后可查看详情、启用 / 禁用、检查更新、卸载 |
/plugin list | 列出所有插件(启用 / 禁用 / 错误) |
/plugin info <name> | 看详情 |
/plugin market [关键词] | 浏览企业插件市场(需配置 backend.url 并企业登录) |
/plugin install <name>@company | 从企业市场安装:下载 → sha256 校验 → 安全解包 |
/plugin update [name] | 按市场目录更新市场插件,新包校验失败时保留旧版本 |
/plugin install <path> | 从本地目录安装(企业策略锁定时拒绝) |
/plugin uninstall <name> | 卸载(--delete 删文件,--force 忽略依赖) |
/plugin enable <name> | 启用 |
/plugin disable <name> | 禁用(--force 忽略反向依赖) |
/reload-plugins | 重新加载全部插件组件 |
/plugins 是 /plugin 的别名。依赖检查是双向的:卸载被依赖的插件会被拦下, 要强行来加 --force。
面板里的操作和对应子命令走同一套实现,规则一致:内置插件只能查看,不能启停或卸载; 只有从市场装的插件才有「检查更新」;面板里的卸载只把插件移出注册表、保留文件, 要删文件用 /plugin uninstall <name> --delete。改完状态同样要 /reload-plugins 才生效, 面板底部会提示。脚本和 CI 里用 sid-code plugin <子命令>(无头模式下斜杠形式 /plugin 整条不可用,会被拒绝并提示改走这个 CLI 入口)。
企业插件市场
市场地址由 backend.url 推出(<backend.url>/api/v1/ctl/marketplace/index)。请求只在 目标地址与 backend.url 同源时才带设备凭据——企业市场天然同源;官方市场和其他 不同源的市场永远不带凭据,凭据不会跟着请求走到别的 origin。 管理员怎么上架、发布、下架插件,见企业后端的插件市场页。 目录拉不到(网络错 / 5xx)时用上次缓存展示,已装插件照常加载;401 不退回缓存, 凭据被吊销后目录与下载一起失效。
安装时客户端会再做一遍服务端已经做过的检查:sha256 必须与目录登记的一致, 包里不能有符号链接、硬链接、设备文件、.. 或绝对路径,plugin.json 的名字和版本 必须与目录一致,组件路径必须是包内相对路径。任何一条不过,整包拒绝,插件目录不留残留。
锁定后只认市场来源
企业策略里任一项生效,插件来源就会被锁定:
strictPluginOnlyCustomization(锁了任意一个面)strictKnownMarketplaces: [{ "source": "url", "url": "https://.../ctl/marketplace/index" }]
锁定后:
| 来源 | 结果 |
|---|---|
| 内置插件 | 照常 |
| 企业市场插件 | 有白名单时 index 地址必须在白名单内;没有白名单时必须是本机 backend.url 那个市场 |
本地目录安装 / --plugin-dir | 拒绝安装,已装的不加载 |
strictKnownMarketplaces: [] 表示除内置插件外全部禁用。市场插件加载前会复核目录指纹, 装好后被改过内容(比如往里加了 hooks.json)的插件不会加载,需要 /plugin update 或重装。 这一层防的是误装和未审计的插件被顺手加载,挡不住有本机写权限、又存心伪造 installed.json 的人。
市场插件的自动更新
启动时对已装的市场插件有一次后台检查(不弹窗、失败一律静默记日志):
- 官方市场
sid-code默认自动升级——内容走仓库 PR review,信任点前移到了分支保护。 - 企业 / 额外市场默认只提示,有新版本时状态栏提示你跑
/plugin update; 管理员可用 managed settings 的pluginMarketplaceAutoUpdate: { "company": true }按市场名打开。 - 两条与市场无关的硬边界:只升级已装的插件,从不新装;新版本的 hooks / MCP 与已装版本不同(或市场没给组件清单、无法判断)时不静默升级,转为提示手动更新 ——hooks 会在本机执行命令,自动升级这一步不能比首次安装还宽松。
- 只有装了市场插件时才联网,每个市场一次带 ETag 的目录请求(未变时是 304); 目录拉不到时不升级(缓存里的「新版本」不可信)。企业锁定策略对它照常生效。
- 自动升级完成后同样要
/reload-plugins才生效,状态栏会提示。
Bridge:远程控制
--bridge 让 sid-code 连上一个 WebSocket 中继,接受远程客户端操控。管理员从管理台接管员工会话的用法见企业后端的遥控页:
sid-code --bridge wss://relay.example.com/session/abc --bridge-token <token>数据流是这样:
远程客户端 ──ws──▶ sid-code 内核(执行工具、改文件)
│
└──ws──▶ 输出 / 工具调用 / 权限请求 回传远程关键点:
- 权限确认也走远程。工具要确认时请求转发给远程客户端,由那边决定放行还是拒绝—— 不是自动放行。这是 Bridge 和
--dangerously-skip-permissions的本质区别。 - 一次只跑一轮。远程消息在上一轮没结束时排队串行消费,和交互模式的单轮语义一致。
- 消息去重。按 UUID 去重(有界环形缓冲),网络重传不会导致同一条消息执行两遍。
- 只认
ws://和wss://,别的协议直接报错:不支持的 Bridge 传输协议: xxx(当前仅支持 ws:// / wss://)
准入:连上之前先过本机这一关
远端拿到的是这台机器的执行权,而权限确认又是转发给远端自己批的,所以 --bridge 在建立连接之前有一道本机准入(packages/core/src/bridge/admission.ts),按顺序判:
- 企业策略可以整体关掉 Bridge:远程下发的
bridgeEnabled: false或本机 settings 的bridge.enabled: false都会直接拒绝(远程的 false 盖过本机配置):企业策略已禁用 Bridge 远程控制(settings 中 bridge.enabled = false) - URL 必须合法:解析不了的地址直接拒绝(非法协议在这里也会被拦)。
- 明文
ws://默认拒绝,要显式加--bridge-insecure才放行:拒绝明文 Bridge 连接: ws://… 改用 wss:// ,或确认风险后显式加 --bridge-insecure。 - 首次连某个地址要当面确认。会弹一个 TUI 确认框列出这个地址和风险提示(与其余审批框同一套交互,默认聚焦「拒绝」,Esc 拒绝),确认后记住,下次不再问。 记的是端点本身,URL 里的 query(常带 token)会被剥掉,不会落盘。
- 没有终端可问时 fail-closed:首次连接却处在非交互环境(比如脚本里),直接拒绝, 要先在交互式终端里跑一次完成确认。
每次准入拒绝都会记一条防线触发事件,可以在轨迹里统计。
Bridge 等于把这台机器的执行权交出去
远端能让它读文件、改代码、跑命令——权限确认虽然转发到远端,但确认的人不是你。 默认就只接受 wss://,ws:// 要显式 --bridge-insecure;另外务必带 --bridge-token、 中继服务器自己可控。不要连不明来源的中继。
常见问题
插件装了但命令找不到
/plugin list 先看状态。加载失败会显示错误原因,最常见是 plugin.json 缺必填字段 或 name 不是 slug 格式。
确认加载成功还找不到命令,检查名字——插件命令必须带前缀: 是 /my-plugin:ping,不是 /ping。
问模型"有没有 xx 斜杠命令",它说没有
斜杠命令是给你用的,不进模型上下文——模型压根不知道有哪些斜杠命令。 实测让模型列含 ping 的命令,它翻遍目录后回答"没有任何命令名含 ping", 而那个命令其实加载得好好的(日志有 [PLUGIN] 加载了 1 个插件命令)。
要确认命令在不在,用 /plugin info <name>,别问模型。
改了插件文件要重启吗
/reload-plugins 就够,不用重启进程。
插件能带 MCP server 吗
能,mcpServers 字段写内联对象或指向文件。插件带的 server 名字会带 plugin:<插件名>: 前缀, 不会和你自己的配置撞名;但它不参与签名去重,和你自己配的是同一个 server 时会连两次。 企业 mcpPolicy 对它照样生效——见 MCP。
插件里的 Skill 和自己写的 Skill 有区别吗
格式完全一样(SKILL.md + frontmatter),走同一套加载与校验。 区别只在名字带插件前缀,以及优先级按插件来源算。