KEEL · 龙骨 · A CURRICULUM FOR THE AI ERA

05 · 插件、记忆与部署:bundled 也守同一条边界 — keel 龙骨

VISION.md 第 67 到 87 行把插件体系写成项目主线:「OpenClaw has an extensive plugin API. Core stays lean; optional capabilities should usually ship as plugins.」,并给出两层门槛的区分——核心的每一次新增都摊到「每个 operator 的每一次模型请求」上,插件与技能没有这个税。第 78 行则给出接口化的判据:「O

VISION.md 第 67 到 87 行把插件体系写成项目主线:「OpenClaw has an extensive plugin API. Core stays lean; optional capabilities should usually ship as plugins.」,并给出两层门槛的区分——核心的每一次新增都摊到「每个 operator 的每一次模型请求」上,插件与技能没有这个税。第 78 行则给出接口化的判据:「Once several independent PRs or requests wire in the same kind of capability, the right response is a contract, not a queue of merges.」

这一章覆盖三个系统加一层运维:

  1. 插件的双重声明与 OpenClawPluginApi 的能力注册面;
  2. bundle-style 与 code plugin 的分工,以及官方目录的阈值签名校验;
  3. 五层记忆与「单一槽位」约束、技能加载链、压缩模式;
  4. 自托管部署形态、密钥注入与「审计只存元数据」。

一、插件双重声明:清单与包元数据

每个 bundled plugin 都同时有两份声明,缺一不可。

第一份是 package.json 里的 openclaw 块。以 extensions/telegram/package.json 为例:extensions(第 19 行,指向实现入口)、setupEntry(第 22 行)、setupFeatures(第 23 行,含 configPromotion)、channel(第 26 行,含 configuredState、approvalFlags、docsPath 等展示与就绪元数据)。不同类型插件会用到 skills / install / compat / release 等更多键。

第二份是同目录的 openclaw.plugin.json,承载 id / skills / activation / channels / contracts / configSchema。

为什么必须拆两份?extensions/AGENTS.md 第 62 到 64 行给了答案:控制平面元数据必须与运行时代码分离——「Keep control-plane metadata separate from runtime logic. Discovery, config validation, setup hints, onboarding hints, and activation planning should be expressible from manifest/descriptors whenever possible.」发现、配置校验、setup 提示都要能在不执行插件代码的前提下算出来。因此第 68 到 70 行还禁止靠「eager global registry seeding 或 import-time 副作用」让插件变可用:「Plugin availability should come from manifest ownership plus targeted activation.」

二、OpenClawPluginApi:能力注册面

插件 API 类型是 OpenClawPluginApi(src/plugins/plugin-api.types.ts:175)。已核对到的注册点:

另外三块按命名空间组织:session.* 门面(注释分布在第 107 到 147 行,涵盖「为下一次 agent turn 排队注入上下文」「把已验证文件发到 direct-outbound 路由」等能力)、agent.events.*(注释在第 150 到 158 行,订阅被脱敏的 agent 事件)、lifecycle.*(注释在第 169 到 172 行)。

文件的注释形态本身就是一次设计说明:大量条目被标 @deprecated 并指向新门面,例如 api.session.state.registerSessionExtension(...)、api.agent.events.emitAgentEvent(...)、api.runContext.setRunContext(...)。这套 API 是「扁平字段迁移到命名空间门面」的中间态,旧字段保留但不再推荐,说明扩展面在收缩而不是膨胀。

构建器是 src/plugins/api-builder.ts:buildPluginApi()(第 189 行);而插件在激活期调用的注册动作会被捕获而不是立即生效,落在 src/plugins/captured-registration.ts——这是「注册与激活分离」的实现载体。

三、两种插件风格与被强制的边界

VISION.md 第 80 到 87 行给出两种风格:code plugin 运行 OpenClaw 插件代码,适合深度运行时扩展;bundle-style plugin 打包稳定的外部面(skills、MCP servers 及相关配置),并明确「Prefer bundle-style plugins when they can express the capability. They have a smaller, more stable interface and better security boundaries.」

这条偏好解释了目录里为什么有大量「只带清单不用写代码」的插件:extensions/ 下 148 个带 package.json 的目录之外,还有只带 openclaw.plugin.json 的清单型插件;Dockerfile 第 34 行的注释也确认这点——「Manifest-only bundled plugins remain valid selections but need no workspace metadata.」

边界由 extensions/AGENTS.md 强制:第 27 到 28 行要求「Extension production code should import from openclaw/plugin-sdk/* and its own local barrels」,第 29 到 30 行禁止「import core internals from src/**, src/channels/**, src/plugin-sdk-internal/**, or another extension's src/**」。第 78 到 83 行进一步规定扩展边界只能加 typed SDK subpath:「If an extension needs a new seam, add or replace a typed Plugin SDK subpath instead of reaching into core.」,且「ALL bundled plugins must move to modern SDK seams in the same change.」——bundled 插件不许留旧接口的兼容路径。

发现与兼容:src/plugins/discovery.ts:discoverOpenClawPlugins()(第 1507 行)与 discoverConfiguredPluginLoadPaths()(第 1482 行)负责扫描 bundled / workspace / global / package / bundle 各类 root(候选类型 PluginCandidate 第 79 行、结果 PluginDiscoveryResult 第 107 行);bundled 目录解析在 src/plugins/bundled-dir.ts;API 范围兼容判定是 src/plugins/package-compat.ts:satisfiesPluginApiRange()(第 117 行,配套 resolvePackagePluginApiRange() 第 83 行)。市场入口是 src/plugins/marketplace.ts。

官方外部插件目录多一层密码学校验:src/plugins/official-external-plugin-catalog-envelope.ts:verifyOfficialExternalPluginCatalogSignedEnvelope()(第 56 行)。它做的是 Ed25519 阈值签名校验——错误码闭集含 "missing-trust-key"(第 44 行)与 "invalid-signature"(第 45 行),阈值经 Math.max(1, Math.trunc(params.threshold ?? 1)) 归一(第 93 行),成功条件是已验签的可信公钥集合达到阈值(第 115 与 120 行)。阈值签名而非单签的意义是没有单一私钥能单独伪造目录。

四、五层记忆与单槽位

记忆按用途分五层:

实现跨两处:src/memory-host-sdk/(已核对 dreaming.ts、query.ts、engine-storage.ts、event-store.ts、event-export.ts、status.ts、secret.ts)与 packages/memory-host-sdk/。内置实现插件是 extensions/memory-core、extensions/memory-lancedb、extensions/memory-wiki。

关键约束是单槽位。 src/config/types.plugins.ts 的 PluginSlotsConfig(第 50 行)只有两个字段:

export type PluginSlotsConfig = {
  /** Select which plugin owns the memory slot ("none" disables memory plugins). */
  memory?: string;        // 第 52 行
  /** Select which plugin owns the context-engine slot. */
  contextEngine?: string; // 第 54 行
};

即同一时刻只能有一个 memory 插件生效。这与 VISION.md 第 96 到 97 行的说法一致:「Memory is a special plugin slot where only one memory plugin can be active at a time. Today we ship multiple memory options; over time we plan to converge on one recommended default path.」

五、技能:SKILL.md 与运行时快照

技能的能力单位是 SKILL.md(YAML frontmatter + 正文)。契约接口是 src/skills/loading/skill-contract.ts:Skill(第 5 行),字段含 baseDir(第 15 行)、sourceInfo(第 18 行)、disableModelInvocation(第 19 行)——最后一项允许某个技能只作为资料存在、不被模型主动调用。渲染侧有 COMPACT_DESCRIPTION_MAX_CHARS = 220(第 35 行)、formatSkillsForPromptCore()(第 71 行)、formatSkillsCompactForPrompt()(第 101 行)与 escapeSkillXml()(第 26 行)。

依赖声明在 frontmatter 里。skills/github/SKILL.md 的 metadata(第 4 行)下有 requires: { bins: ["gh"] }(第 9 行)与 install(第 10 行)——技能可以声明自己需要哪些可执行文件以及怎么装。

运行时不直接读文件,而是用快照:src/skills/runtime/snapshot-hydration.ts 负责水合、refresh.ts 负责刷新、remote-skills.ts 负责远端技能、tool-dispatch.ts 负责把技能派发成工具。快照的意义是一次 run 内技能集合稳定,不会因为运行中文件变更导致提示词与实际能力不一致。

六、压缩与可插拔上下文引擎

压缩默认模式是 safeguard。类型在 src/config/types.agent-defaults.ts:AgentCompactionMode = "default" | "safeguard"(第 364 行),求值在 src/agents/agent-settings.ts:resolveEffectiveCompactionMode()(第 106 行):配置了 compaction.provider 直接提升为 safeguard(第 109 行),否则看 compaction.mode === "safeguard"(第 111 行)。

实现分三处:src/agents/embedded-agent-runner/compact.ts(主流程)、direct-compaction.ts(直连压缩)、compact.runtime.ts(运行时挂接),钩子在 src/agents/agent-hooks/compaction-safeguard.ts(默认导出 compactionSafeguardExtension,第 988 行)与 compaction-instructions.ts。压缩的硬要求是保留 tool-call / toolResult 配对切割:不能在切点把一对调用与结果拆开,否则上下文对模型来说是不合法的。

上下文引擎本身可插拔:src/context-engine/registry.ts(注册与解析,解析入口 resolveContextEngine() 第 712 行)、legacy.ts(默认实现)、init.ts(初始化)。它占用的是 PluginSlotsConfig.contextEngine 那个槽位,同样是单槽位。

七、模型接入:协议注册表

网络协议层的注册表是 packages/ai/src/api-registry.ts:ApiProvider(第 27 行)声明 { api, stream, streamSimple },工厂 createApiRegistry()(第 76 行)提供 registerApiProvider(第 79 行)、getApiProvider(第 94 行)、unregisterApiProviders(第 102 行)。协议 id 是闭集 KnownApi(packages/llm-core/src/types.ts:7),共九个:openai-completions、mistral-conversations、openai-responses、azure-openai-responses、openai-chatgpt-responses、anthropic-messages、bedrock-converse-stream、google-generative-ai、google-vertex。注意 Api = KnownApi | (string & {})(第 19 行)——自定义 provider 可以用集合外的 id,闭集只约束「有第一方适配器」的部分。

厂商实现分散在 packages/ai/src/transports/ 与 packages/ai/src/providers/,内置注册在 packages/ai/src/providers/register-builtins.ts。OpenClaw 侧的注册表是 src/llm/model-registry.ts(ModelRegistry 第 5 行)与 src/agents/prepared-model-registry.ts:loadPreparedAgentModelRegistry()(第 141 行)。

更大量的 provider 以插件形式存在。已核对的 extensions/ 目录包含 openai、anthropic、google、deepseek、mistral、xai、groq、cerebras、fireworks、together、openrouter、litellm、ollama、lmstudio、vllm、sglang、llama-cpp、qwen、kimi-coding、moonshot、minimax、zai、amazon-bedrock、amazon-bedrock-mantle、anthropic-vertex、cloudflare-ai-gateway、copilot。model ref 形如 provider/model,models.providers.* 下可配 apiKey / baseUrl / maxTokens / timeoutSeconds / contextWindow / contextTokens。

八、权限、密钥与审计

权限闭集。 Gateway 的 operator scope 在 src/gateway/operator-scopes.ts,八个常量:operator.admin(第 3 行)、operator.read(第 4 行)、operator.write(第 5 行)、operator.approvals(第 6 行)、operator.questions(第 7 行)、operator.pairing(第 8 行)、operator.talk(第 9 行)、operator.talk.secrets(第 10 行)。文件头注释点明它同时被「connection auth 与 method policy」消费。

密钥注入走 sentinel。 src/secrets/sentinel.ts 定义 SECRET_SENTINEL_PREFIX = "oc-sent-v2."(第 5 行)与 SECRET_SENTINEL_SUFFIX = ".end"(第 6 行)。替换发生在出口代理层:src/secrets/egress-proxy/stream-substitution.ts 用前后缀字节做流式定位与替换(含跨 chunk 的边界处理),proxy-server.ts 做整体替换。未解析的 sentinel 采取 fail-closed:src/secrets/runtime-degraded-state.ts 的 SecretAssignmentDisposition = "fail-closed" | "isolate"(第 25 行)就是这条策略的类型化表达。相关工具链是 src/secrets/{configure,apply,plan,audit-store,private-plan-file}.ts。

审计只存元数据。 src/audit/audit-event-store.ts 第 1 行的文件头注释直接写:「SQLite persistence and stable cursor queries for metadata-only audit events.」写入是 recordAuditEvent()(第 605 行),读取是 listAuditEvents()(第 661 行)且用稳定游标分页(cursor 走 sequence < cursor,第 678 到 679 行),保留期 AUDIT_EVENT_RETENTION_MS = 30 * 24 * 60 * 60_000(第 49 行,30 天),清理是 pruneExpiredAuditEvents()(第 724 行)。身份经 pseudonymizeAuditIdentity() 化名(第 494 行调用)。设计承诺是「永不存 prompt、消息正文、工具参数、工具结果」——这对上的是 VISION.md 第 61 到 63 行的隐私立场。

配置侧的自检在 src/security/audit-*.ts(已核对包含 audit-channel.ts、audit-gateway-config.ts、audit-plugins-trust.ts、audit-model-refs.ts、audit-extra.sync.ts 等),静态规则在 security/opengrep/(含 rules/、precise.yml、compile-rules.mjs)。

九、部署形态与自更新

镜像。 Dockerfile 是多阶段:依赖层用 node:24-bookworm(基础镜像按 digest 固定,第 16 行),构建层用 oven/bun:1.3.14(第 21 行),运行时是 bookworm-slim(第 17 到 18 行),且「runtime image is always bookworm-slim」(第 10 行注释)。构建参数 OPENCLAW_EXTENSIONS / OPENCLAW_BUNDLED_PLUGIN_DIR(第 11 到 12 行)配合 scripts/lib/docker-plugin-selection.mjs 只挑选需要的插件,依赖层仅抽取 package.json,这样「main build layer is not invalidated by unrelated source changes」。

Compose。 docker-compose.yml 单服务 openclaw-gateway,把宿主 ~/.openclaw 挂到 /home/node/.openclaw(第 45 行),并显式设 OPENCLAW_STATE_DIR / OPENCLAW_CONFIG_PATH / OPENCLAW_WORKSPACE_DIR(第 18 到 21 行,注释说明是为了防止宿主路径经 .env 泄漏进容器)。安全加固是 cap_drop: [NET_RAW](第 59 到 60 行),restart: unless-stopped(第 71 行)。

Fly 与 Render。 fly.toml 的进程命令是 node dist/index.js gateway --allow-unconfigured --port 3000 --bind lan(第 18 行),健康检查打 /startupz(第 33 行),挂载 openclaw_data 到 /data(第 39 到 41 行)。deploy/fly.private.toml 是私有变体——没有 [http_service] 段即无公网入口。render.yaml 用 dockerCommand: node openclaw.mjs gateway --allow-unconfigured(第 9 行),OPENCLAW_GATEWAY_TOKEN 走 generateValue: true(第 19 行),磁盘 sizeGB: 1(第 23 行)。

自更新。 appcast.xml 是 macOS 端的 Sparkle 更新源(根元素声明 xmlns:sparkle,第 2 行),条目带 sparkle:minimumSystemVersion(第 11 行,已核对为 15.0)与 sparkle:edSignature 签名的 enclosure(第 1753 行)。CLI 侧的对应命令是 update 子命令。

CI 与本地钩子。 .github/workflows/ 中与发布直接相关的有 macos-release.yml、ios-periphery.yml、linux-app-release.yml、android-release.yml、openclaw-npm-release.yml、docker-release.yml、full-release-validation.yml。本地 git-hooks/pre-commit 只对 staged 文件跑格式化(先经 scripts/pre-commit/filter-staged-files.mjs 过滤),并在 rebase / cherry-pick / merge 进行中直接 exit 0 跳过。

QA。 qa/ 是私有 QA 资产:scenarios/(按子系统分组的 yaml 场景,含 channels / matrix-e2ee / agents / security 等)、frontier-harness-plan.md、maturity-scores.yaml、convex-credential-broker/(Convex 实现的凭据代理)。

代码地图

机制 位置 要点
插件包元数据 extensions/telegram/package.json: openclaw 第 18 行;含 extensions/setupEntry/channel
插件清单 extensions/telegram/openclaw.plugin.json 与 package.json 双重声明
插件 API src/plugins/plugin-api.types.ts: OpenClawPluginApi 第 175 行;runtime 191、runContext 201
工具/钩子注册 src/plugins/plugin-api.types.ts: registerTool 第 204 行;registerHook 208、registerChannel 222、registerProvider 271
工具元数据注册 src/plugins/plugin-api.types.ts: registerToolMetadata 第 354 行
API 构建 src/plugins/api-builder.ts: buildPluginApi 第 189 行
注册捕获 src/plugins/captured-registration.ts 注册与激活分离的载体
插件发现 src/plugins/discovery.ts: discoverOpenClawPlugins 第 1507 行;候选类型第 79 行
bundled 目录解析 src/plugins/bundled-dir.ts bundled plugin 根目录
API 范围兼容 src/plugins/package-compat.ts: satisfiesPluginApiRange 第 117 行
市场入口 src/plugins/marketplace.ts 插件分发接入口
官方目录签名校验 src/plugins/official-external-plugin-catalog-envelope.ts: verifyOfficialExternalPluginCatalogSignedEnvelope 第 56 行;Ed25519 阈值;错误码第 44-45 行
边界定义 extensions/AGENTS.md: 第 27-30 行、第 78-83 行 只走 plugin-sdk subpath,禁止深导入 src/**
记忆槽位 src/config/types.plugins.ts: PluginSlotsConfig 第 50 行;memory 52、contextEngine 54
根记忆文件名 src/memory/root-memory-files.ts: CANONICAL_ROOT_MEMORY_FILENAME 第 7 行;值 MEMORY.md
dreaming 实现 src/memory-host-sdk/dreaming.ts 与 query / engine-storage / event-store 同目录
技能契约 src/skills/loading/skill-contract.ts: Skill 第 5 行;disableModelInvocation 第 19 行
技能依赖声明 skills/github/SKILL.md: metadata 第 4 行;requires.bins 第 9 行、install 第 10 行
技能运行时快照 src/skills/runtime/snapshot-hydration.ts 与 refresh / remote-skills / tool-dispatch 同目录
压缩模式求值 src/agents/agent-settings.ts: resolveEffectiveCompactionMode 第 106 行;有 provider 直接 safeguard
压缩模式类型 src/config/types.agent-defaults.ts: AgentCompactionMode 第 364 行;default / safeguard
压缩钩子 src/agents/agent-hooks/compaction-safeguard.ts: compactionSafeguardExtension 第 988 行默认导出
上下文引擎注册 src/context-engine/registry.ts: resolveContextEngine 第 712 行;legacy 为默认实现
协议注册表 packages/ai/src/api-registry.ts: ApiProvider 第 27 行;createApiRegistry 第 76 行
协议 id 闭集 packages/llm-core/src/types.ts: KnownApi 第 7 行;九个 id,Api 允许集合外扩展
模型注册表加载 src/agents/prepared-model-registry.ts: loadPreparedAgentModelRegistry 第 141 行
operator scope src/gateway/operator-scopes.ts: ADMIN_SCOPE 第 3 行;八项止于 TALK_SECRETS_SCOPE 第 10 行
sentinel 前缀 src/secrets/sentinel.ts: SECRET_SENTINEL_PREFIX 第 5 行;值 oc-sent-v2.,后缀第 6 行
解密失败策略 src/secrets/runtime-degraded-state.ts: SecretAssignmentDisposition 第 25 行;fail-closed / isolate
审计库 src/audit/audit-event-store.ts: recordAuditEvent 第 605 行;listAuditEvents 661(游标)、保留 30 天第 49 行
审计身份化名 src/audit/audit-event-store.ts: pseudonymizeAuditIdentity 调用 第 494 行
安全自检 src/security/audit-gateway-config.ts 与 audit-channel.ts / audit-plugins-trust.ts 并列
静态规则 security/opengrep/rules/ 配 precise.yml 与 compile-rules.mjs
镜像构建 Dockerfile: FROM workspace-deps 第 30 行;node:24 依赖层、bun 构建层、slim 运行时
Compose 加固 docker-compose.yml: cap_drop 第 59-60 行 NET_RAW;服务名第 2 行
Fly 进程 fly.toml: [processes] app 第 18 行;健康检查 /startupz 第 33 行
私有 Fly 变体 deploy/fly.private.toml 无 http_service 段即无公网入口
Render 蓝图 render.yaml: OPENCLAW_GATEWAY_TOKEN 第 18-19 行 generateValue;磁盘 1GB 第 23 行
macOS 自更新源 appcast.xml: sparkle:edSignature 第 2 行命名空间;minimumSystemVersion 第 11 行
本地钩子 git-hooks/pre-commit 只处理 staged 文件;sequencer 中直接 exit 0

关键取舍

bundled 插件被强制当成第三方,代价是核心重构要同步迁移全部内置插件。
extensions/AGENTS.md 要求 bundled 插件只能用 openclaw/plugin-sdk/*,并且「ALL bundled plugins must move to modern SDK seams in the same change」——不允许保留扩展侧的兼容路径。收益是这道门永远是热的,API 不会因为「核心自己人要用」而腐化;代价是任何 SDK seam 变更都变成上百个目录的同步改动,且必须是一次性完成。

注册动作在激活期被捕获而不立即生效,代价是要维护两阶段状态。
captured-registration.ts 的存在意味着「插件调用了 registerTool」不等于「工具已可用」。收益是激活可以是计划式的(见 activation-planner.ts),能在真正加载运行时代码之前算清「这个插件会带来什么」,从而实现 manifest 优先的发现与校验;代价是任何依赖「注册即生效」的直觉写法都会出错。

官方外部插件目录用阈值签名,代价是信任根与密钥轮换有运维负担。
verifyOfficialExternalPluginCatalogSignedEnvelope 要求达到 threshold 个可信公钥验签通过,缺少信任根时报 missing-trust-key、签名不符报 invalid-signature。收益是没有单一私钥能伪造整个目录;代价是公钥集合本身成为必须分发与更新的资产,且阈值越高对签名流程的可用性要求越高。

记忆与上下文引擎都是单槽位,代价是无法组合多个记忆实现。
PluginSlotsConfig 只给 memory 与 contextEngine 各一个字符串位置。收益是「记忆」这个横切能力在任何时刻只有一条写入路径,不需要解决多实现的写冲突与读取优先级;代价是想要「向量检索 + 日志归档 + 知识库」的组合能力,只能由一个插件内部去编排,或者在 code plugin 里自己实现路由器。

审计只存元数据,代价是事后取证能力有上限。
audit-event-store.ts 的文件头与保留期常量把承诺写死:metadata-only、30 天、稳定游标分页、身份化名。收益是审计库即便泄露也不直接暴露对话内容,且体量小、可长期保留;代价是排查「模型为什么回了这句话」时审计帮不上忙,必须回到 transcript 体系(那是另一套存储,拥有不同的访问控制)。

自测题

  1. OpenClawPluginApi 里既有 registerTool 这种扁平方法,也有 api.session.* / api.agent.events.* 命名空间。请说明这种「扁平 + 门面」并存对插件作者意味着什么,以及为什么迁移不能一次删掉旧字段。
  2. 为什么控制平面元数据(发现、配置校验、setup 提示)必须能在不执行插件代码的前提下算出来?请举一个如果必须执行代码才能算,就会出问题的具体场景。
  3. 阈值签名相比单签名,在「官方目录被篡改」这个威胁上分别提供什么保证?如果阈值配成 1,与单签名有什么实质区别?
  4. 记忆是单槽位。如果你要同时提供「向量检索」与「按日归档」两种能力,说明两种可行做法(一种在单个 memory 插件内部编排,一种绕开槽位),并各指出一个代价。
  5. apps 之外,本仓库同时存在 Dockerfile、docker-compose.yml、fly.toml、deploy/fly.private.toml、render.yaml 五个部署描述。请说明 deploy/fly.private.toml 与 fly.toml 的关键差异,以及这个差异为什么不能靠环境变量实现。

进入 keel 阅读