沙箱插件默认是隔离的。要执行超出读写自身 KV 和存储之外的任何操作,插件必须在其清单中声明能力。沙箱桥根据这些声明控制每个宿主提供的 API —— 未声明 content:read 的插件无法获得 ctx.content,未声明 network:request 的插件无法获得 ctx.http。
本页介绍每个能力授予什么权限、沙箱如何强制执行它们,以及哪些是无法强制执行的。
声明能力
能力位于 emdash-plugin.jsonc 中,与 slug 和其余信任契约一起:
{
"slug": "plugin-hello",
// ...身份 + 配置...
"capabilities": ["content:read", "network:request"],
"allowedHosts": ["api.example.com"]
}
只声明插件实际需要的能力。能力声明也是 Marketplace 在同意对话框中向运营者展示的内容 —— 额外的能力是安装时的摩擦和审计时的安全信号。
能力参考
| 能力 | 授予访问权限 |
|---|---|
content:read | ctx.content.get()、ctx.content.list() |
content:write | ctx.content.create()、ctx.content.update()、ctx.content.delete()(隐含 content:read) |
taxonomies:read | ctx.taxonomies.getAll()、ctx.taxonomies.getTerms()、ctx.taxonomies.getEntryTerms() |
media:read | ctx.media.get()、ctx.media.list() |
media:write | ctx.media.getUploadUrl()、ctx.media.upload()、ctx.media.delete()(隐含 media:read) |
network:request | ctx.http.fetch() —— 限制为 allowedHosts |
network:request:unrestricted | ctx.http.fetch() 无主机限制(仅用于用户配置的 URL) |
users:read | ctx.users.get()、ctx.users.getByEmail()、ctx.users.list() |
email:send | ctx.email.send()(需要已配置的邮件提供者插件) |
hooks.email-transport:register | 允许注册独占的 email:deliver 钩子(传输提供者) |
hooks.email-events:register | 允许注册 email:beforeSend / email:afterSend 钩子 |
hooks.page-fragments:register | 允许注册 page:fragments 钩子(仅限原生插件) |
一些需要了解的事项:
- 隐含关系。
content:write自动隐含content:read;media:write隐含media:read;network:request:unrestricted隐含network:request。你不需要同时列出两者。 - 分类法是一个独立的只读表面。
taxonomies:read通过ctx.taxonomies授予对分类法定义、其术语以及分配给条目的术语的访问权限。它独立于content:read—— 如果插件读取内容及其分类,请同时声明两者。插件没有对分类法的写入权限。 network:request:unrestricted用于用户配置的 URL。 运营者输入目标 URL 的 Webhook 插件需要访问不在清单中的主机。始终调用已知 API 的插件应使用network:request+allowedHosts。email:send由配置控制,不仅仅是能力。 插件可以声明email:send,但ctx.email只有在另一个插件注册了email:deliver传输时才会被填充。
网络主机允许列表
具有 network:request 的插件只能获取 allowedHosts 中列出的主机。支持子域名通配符:
"capabilities": ["network:request"],
"allowedHosts": [
"api.example.com", // 精确主机
"*.cdn.example.com" // cdn.example.com 的任何子域名
]
桥在转发请求之前会将请求 URL 的主机与允许列表进行核对。对未声明主机的请求会在插件内部抛出异常,永远不会离开沙箱。
network:request:unrestricted 完全跳过允许列表检查。它适用于运营者在运行时配置目标 URL 的插件(Webhook 发送器、通用 HTTP 转发器)。对于目标是插件设计一部分的插件,请避免使用它 —— 而是使用明确的主机声明 network:request,以便同意对话框准确告知运营者插件将调用哪里。
沙箱强制执行什么
当沙箱运行器处于活动状态时,运行时强制执行:
-
能力门控。 PluginContext 工厂仅在声明了相应能力时才填充
ctx.content、ctx.taxonomies、ctx.media、ctx.http、ctx.users、ctx.email。调用未声明能力上的方法是不可能的 —— 那里没有对象。 -
存储和 KV 作用域。 每个存储和 KV 操作都限定在插件的 slug 范围内。插件无法读取另一个插件的 KV 或存储集合,只能访问在清单中声明的存储集合。
-
网络隔离。 直接的
fetch()和其他网络原语被运行器阻止。通往网络的唯一路径是ctx.http.fetch(),它经过桥的主机验证。 -
无宿主绑定。 沙箱插件看不到环境变量、文件系统或平台绑定 —— 即使你的宿主 Worker 拥有它们。插件运行时是一个干净的隔离体,只有桥和声明的能力。
-
资源限制。 运行器可以对每次调用强制执行 CPU、子请求、挂钟时间和内存限制。确切限制取决于你使用的运行器;Cloudflare 运行器使用平台的 Worker Loader 限制(每次调用 50ms CPU、10 个子请求、30 秒挂钟时间、约 128MB 内存)。Node.js workerd 运行器(
@emdash-cms/sandbox-workerd)通过Promise.race强制挂钟时间;CPU 和内存限制是 Cloudflare 平台功能,独立的 workerd 不强制执行。超过运行器限制的钩子会被取消;EmDash 钩子超时(钩子配置中的timeout)额外强制执行更严格的上限。
沙箱不强制执行什么
能力系统不涵盖且无法涵盖的一些事项:
- 授予能力范围内的行为。 具有
content:write的插件可以编辑任何内容,而不仅仅是自己的。能力是粗粒度的 —— 它们说”这个插件可以写入内容”,而不是”这个插件只能写入它创建的内容”。审计时的审查是对插件在其授权范围内实际行为的唯一检查。 - Node.js 上的运营者信任。 如果配置的沙箱运行器报告不可用(没有 Cloudflare Worker Loader,没有安装 Node 端运行器等),
sandboxed: []插件在启动时会被跳过。你可以将它们移动到plugins: []以在进程内运行 —— 但那样就没有 V8 隔离体、没有资源限制,插件可以直接调用fetch()或读取环境变量。将此视为原生级别的信任。 - 侧信道。 时序、日志输出和存储的数据对任何合理访问宿主环境的人都是可见的。不要将沙箱用作针对运行它的运营者的机密性边界。
能力同意
当运营者从 Marketplace 安装沙箱插件时,EmDash 会显示一个包含声明能力的同意对话框。添加能力的更新 —— 例如,一个之前只读取内容的插件现在想要发起网络请求 —— 会显示为能力差异,并在新版本生效前需要重新批准。
这就是为什么声明额外的能力很重要,即使你”以后可能需要它们”。它们在每次安装和更新时都会显示为摩擦,安全审计会标记要求超出明显需要的插件。准确列出插件使用的内容,并在插件实际开始使用时在真正的版本中添加新能力。
构建时验证
emdash-plugin bundle 和 emdash-plugin publish 执行额外检查:
- 每个声明的能力必须在已识别的集合中(拼写错误会导致构建失败)。
network:request需要非空的allowedHosts;network:request:unrestricted需要它为空。参见清单参考。- 打包的
backend.js不能导入 Node.js 内置模块(fs、path、child_process等)—— 沙箱运行时不提供它们。
完整检查列表请参见打包和发布。