Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
98 changes: 98 additions & 0 deletions .oo/rfcs/0011-plugin-extensibility-actions.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
# RFC 0011: 行动项与优先级

返回入口:[RFC 0011 总览](0011-plugin-extensibility.md)

行动项按"是否需要产品决策"分组。P0/P1 是纯技术改进,不改变任何对外承诺;P2 起需要先有开放程度的判断。

## P0-1:抽通用 ACP 适配器层

**问题**:`agentclientprotocol` 在 `packages/adapters/{cline,dsh,goose}` 各实现了一遍,无共享层,`packages/adapters/` 下也无 acp 包。下一个 ACP agent 需要写第四遍。

**参照**:DSH 的 `subagent-acp` 是通用的,配置里给 `command` / `args` / `env` 即可接入任意 ACP agent,`providerName` 可配,同进程可注册多个不同名字的外部 provider。

**收益**:抽出共享层后,接入新 ACP agent(Cursor、CodeBuddy、opencode 等)从"写一个适配器"降为"加一段配置"。

**风险**:低。纯内部重构,不涉及任何信任决策或对外接口变更。三个现有适配器有各自的 session 投影与能力声明,需确认可共享的是传输层与协议编解码,而非会话语义。

**建议**:先做可行性评估——对比三处实现的重叠度,确认抽象边界应落在 transport / codec 还是更上层。

## P0-2:生成式能力目录 + CI 门禁

**问题**:插件能力面分散在 `.oo/docs/usage/plugins/ui-runtime.md`(400+ 行手写)、`create-plugin/SKILL.md` 与源码之间,无生成、无门禁。本 RFC 调研中对自身能力误判三次(见[现有扩展面盘点](0011-plugin-extensibility-current-surface.md)的"已知误判记录")。

**参照**:DSH 的 `scripts/gen-cordis-api.ts` 从 AST 生成,`verify-cordis-api --check` 挂 doc-sync 门禁,产出还经 `cordis_inspect` 工具喂给模型;`docs/user/develop/framework/service.md` 明文拒绝维护第二份手工清单。

**对我们价值更大的理由**:One Works 本身是 AI 工作区,插件作者会用 Claude Code / Codex 对着我们的 API 写插件。机器可读、CI 校验新鲜度的目录直接决定生成代码的正确率。

**建议实现**:`scripts/gen-plugin-api.ts`,从 `PluginClientContext` / `PluginServerContext` / `PluginViewContext` 的 TS 声明抽结构化目录,产出机器可读 JSON + 渲染 markdown,加 `--check` 模式接入现有检查。首次运行即可量化 `ui-runtime.md` 的漂移程度。

**限定**:不是银弹。DSH 的生成文档仍有轻微漂移(`docs/subsystems/workflow.md` 引 157,实测 168),但事件部分行号全对,整体显著优于纯手工。

## P0-3:补 ErrorBoundary

**问题**:`apps/client/src/plugins/` 与 `apps/client/src/components/plugins/` 下**零个** `ErrorBoundary` / `componentDidCatch`,`PluginHost.tsx:275` 裸渲染 `view.renderNode(viewContext)`。

**风险**:插件 route 页面渲染异常直接白屏,无降级。

**与视图槽无关,应独立先做。** 详见[边界与设计纪律](0011-plugin-extensibility-boundaries.md)纪律 2。

## P1:Hook 权限面对 marketplace 场景的审视

**问题**:`resolvePluginHooksEntryPath`(`packages/utils/src/plugin-resolver.ts:703-707`)解析 `<packageId>/hooks` export,`plugin-entry-cache.ts:43-53` 无条件把能解析出 hooks entry 的实例收进中间件链,**解析链上无 gate**。

而 hook 插件的权限包括:`PreToolUse` 返回 `deny` 否决任意工具调用、`GenerateSystemPrompt` 改写系统提示词、`PreCompact.replacementPrompt` 替换压缩提示词、任意事件 `continue: false` 停机。

**需要核实的点**:

- marketplace 安装的插件是否自动获得 hook 能力,还是需要用户额外确认
- 插件详情页的 `hooks` tab(`PluginDetailPanel.tsx:313`)展示的是资产 hooks(`PluginManifestAssets.hooks`)还是运行时 hook 插件——初步判断是前者(`NativePluginDetailPanel.tsx:116` 把 'mcp' 与 'hooks' 作同类资产分组),但未读完渲染逻辑
- 这些权限是否作为"该插件请求的权限"呈现给用户

**背景**:这条线是命令行时代的设计(插件由用户手写进配置),marketplace 接上后同一条链变成了分发面。DSH 至少在文档里把等价风险明说了("允许该包在你机器上、在 agent sandbox 之外执行代码")。

**注意**:宿主自身的权限执行器 `builtin-permissions.ts` 也是这条链上的一个 hook 插件,第三方插件与它同链、顺序决定优先级。

## P2:Model provider seam(需产品决策)

**问题**:`packages/model-provider-catalog/src/catalog.ts` 是硬编码内置注册表,第三方加 provider 只能提 PR。

**为什么是最值得开的注册型 seam**:

- 数据面而非控制面——provider 只负责发请求、转流,不干预 agent 决策
- RFC 0006 已把"官方模型服务商"做成一等公民,但目录硬编码
- 销毁机制现成(`addDisposable(scope, ...)` + frozen owner token + `rollbackScopeRegistrations`)

**要抄的形状**(来自 `ctx.llm`):

- `registerConfigurableProviders` 的休眠路由——插件声明能力,用户配置才激活
- 全有或全无 + 重复检测(对应我们已有的 `duplicate()` 诊断)
- **凭证 seam:插件拿 ref 不拿明文 key**。marketplace 插件碰 API key 是明确风险面
- 强制 server-only(`PluginServerManifest.roles` 已有角色概念可挂)

**落点**:常驻 server plugin runtime,不是 hook。见[边界与设计纪律](0011-plugin-extensibility-boundaries.md)纪律 3。

**需要的决策**:是否允许第三方提供模型 provider。这直接关系 RFC 0006 的商业路径。

## P2:适配器 seam 化(需产品决策)

**现状**:16 个 `@oneworks/adapter-*` 是编译期内置(根 `package.json` devDependencies + 静态 import)。加一个适配器要改仓库、进 root package.json、重新发版。

**对照**:DSH 的 `SubagentProvider` 是 seam,第三方发 npm 包、用户配置加一行即可。其社区已产出第三方版的 Codex/Claude Code/ACP provider。

**我们的优势不应低估**:16 个适配器有统一 hook 协议、账号池、历史导入、权限镜像,深度显著超过 DSH 的 3 个薄 provider(one-shot、不继承上下文、纯文本、无审批)。seam 化不等于放弃深度,但需要设计"第三方 provider 能拿到多少宿主能力"的分层。

**需要的决策**:这是本 RFC 中影响最大的一项,涉及维护成本、质量控制与品牌。DSH 的策略是核心保持瘦、扩展面全让给社区(明确不收外部 PR),并有守门测试断言可选 provider 不进 base bundle。这是一种可选路径,不是唯一路径。

## 待核实项

以下问题在调研中出现但未查清,建议在实施 P0-2 时一并解决:

1. 同 scope 内 parent 与 child 的 command id 撞名如何处理(覆盖 / 报错 / 静默保留第一个)——`runtime.ts:2802` 的检查针对内置 route key,此路径未核实
2. 插件详情页 `hooks` tab 的确切数据来源(见 P1)
3. 16 个适配器的上游版本漂移防护是否都达到 dsh 适配器的水平(`DSH_VERSION` 固定 + `isOfficialCompositionComplete` 完整性校验)。DSH 只维护 2 个 product provider 就把限制写成明文 Known Limitations 清单,我们 16 个的成本是另一个量级

## 不建议做的

- **开放 `agentLoop` / `tools` / `approval` / `sandboxPolicy` 的注册型控制面**。DSH 敢开是因为其插件等同 shell 权限(明文记录);我们是 marketplace 分发,开了即提权通道。
- **视图槽先于格式词汇表**。见[边界与设计纪律](0011-plugin-extensibility-boundaries.md)纪律 2。
- **让插件创造插件**。见纪律 1。
101 changes: 101 additions & 0 deletions .oo/rfcs/0011-plugin-extensibility-boundaries.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
# RFC 0011: 边界与设计纪律

返回入口:[RFC 0011 总览](0011-plugin-extensibility.md)

本章把已论证过的边界判断写成可引用的纪律,目的是避免每次提出新扩展点时重新论证。

## 纪律 1:插件不能创造插件

**不新增让插件在运行时实例化其他插件的能力**(相当于 Cordis 的 `ctx.plugin()`)。

需要动态插件图时,由宿主通过 plugin overlay 注入,走同一个 resolver、同一套 scope 分配、同一个 `/plugins` 列举。**动态性发生在配置解析层,不发生在插件代码里。**

### 依据

**(1) 清理模型以 scope 为单位。** `disposablesByScope`、`removeExtensionPointListenersByScope`、`rollbackScopeRegistrations(scope, owner)`、`disposeScope(scope)` 全部 keyed on scope。动态子插件只有两条路:自己占新 scope(谁分配?冲突检测在启动期是 fatal;且 `/plugins` store 与 `PluginDetailPanel` 按服务端解析出的 instance 列表渲染,动态 scope 对 UI、诊断、卸载全部隐形),或共享父 scope(那它就不是插件,只是父插件的代码)。

**(2) reload 会失效。** `PluginProvider.tsx:97-104` 的 `reloadPlugin(scope)` 从 `instancesRef`(服务端解析结果)里找 instance,动态创建的东西不在其中,`watch` / HMR 对它是空操作。

**(3) CSP 已堵死代码生成路径。** `script-src` 无 `blob:`(`apps/client/index.html:7`),插件代码只能同源经 `/api/plugins/:scope/client/*` 加载,即只能来自已安装包——那为什么不声明?

**(4) 卸载语义崩塌。** marketplace 有 removal journal / receipt / quotes 一整套账本,运行时拉起的东西没有 install 记录,也就没有 removal 记录。

### 三种被混为一谈的需求

| 需求 | 结论 |
| ---------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| 运行时决定要不要加载某个已安装插件 | 已有更好答案:`activation: 'optional'` + 用户配置开关 + `onAvailable` 被动等待。若不够,应加"插件请求启用某 optional child、宿主弹窗由用户确认",决定权在用户 |
| 参数化多实例 | 配置层已支持(`children` 数组 + 不同 scope)。若诉求是"运行时才知道要几个",那是插件内部数据结构问题,不是插件粒度问题 |
| 运行时生成代码注册为插件 | 一票否决。等于同时绕过 marketplace、构建期边界校验(`client-source-boundary.ts` 只在构建期跑)与 CSP |

### 正确的落点

`PluginOverlayConfig`(`packages/types/src/plugin.ts:76`)的 `mode: 'extend' | 'override'` 与 `overlaySource` 已贯穿整棵解析树,spec/entity 层已在用。要扩展动态插件图应扩展这里。

## 纪律 2:视图扩展优先扩格式词汇表,而非开组件槽

视图扩展存在一条能力光谱:

| 方式 | 贡献什么 | 表达力 | 信任成本 |
| -------------- | ------------------------- | ------ | -------------- |
| 元数据贡献 | `{id,title,icon,command}` | 低 | 无 |
| 声明式渲染描述 | path + format + item 映射 | 中高 | 无(格式封闭) |
| 协议投影 | 跨进程事件 + 上面的描述 | 中高 | 进程边界隔离 |
| 视图槽挂组件 | React 节点 | 最高 | owner 让出画布 |
| iframe | 整页 | 最高 | 强隔离,代价大 |

**决策顺序:**

1. **先扩格式词汇表。** 有人要塞组件时,先问"缺的是哪个 format"。`toolUsePresentations` 证明了很多"必须自定义渲染"的需求实际是"宿主的声明式格式不够用"——cua-driver 的嵌套对象数组 + 渐进披露,一份 schema 就解决了,还白拿 i18n、主题、无障碍与一致性。补一个 `table` / `diff` / `timeline` / `progress` 受益的是所有插件。
2. **把声明式渲染推广到别处。** 目前 `toolUsePresentations` 只服务 `chat.toolUse.presentations` 一个槽。预留的 `message.renderers`、`settings.sections`、`workspace.resourceOpeners` 应复用同一套 field/format 描述,而非各自发明。
3. **视图槽留给真正无法声明化的场景**(自由画布、图编辑器、地图)。

### 若开视图槽,四个前置条件

1. **ErrorBoundary 是前置条件,不是可选项。** 现状:`apps/client/src/plugins/` 与 `components/plugins/` 下**零个** `ErrorBoundary` / `componentDidCatch`,`PluginHost.tsx:275` 是裸渲染 `view.renderNode(viewContext)`。单插件页面崩溃只影响自己尚可接受;一旦 contributor 组件挂进 owner 页面,一个异常会带塌 owner 整页,而用户只会认为是 owner 插件坏了。**此项与是否做视图槽无关,应独立先做。**
2. **挂载权归宿主。** owner 拿到的必须是宿主包好的不透明节点(内部仍走 `PluginHost` 的 `(scope, viewId)` 路径),而非 contributor 的组件引用。否则 contributor 代码会跑在 **owner 的 viewContext** 里——`view.options.update()` 会把配置写到 owner 头上,`data.useQuery` 的 SWR key 前缀也会串(`PluginHost.tsx:136, 160`)。
3. **扩展点须显式声明接受视图**,并携带布局约束(`maxHeight` / `orientation` / 是否允许自撑高),由宿主在包裹层强制。默认应保持数据模式。
4. **顺序必须稳定可预期**——按 `order` 字段或 `pluginScope` 字典序,不能是 Map 插入顺序(那取决于插件激活顺序,而激活顺序本身不保证,这正是 `onAvailable` 要解决的问题)。

## 纪律 3:注册型 seam 走常驻 runtime,不扩 hook 事件表

hook 传输是跨进程的(`call-hook.js` 用 `spawn`,`worker-client.ts` 维护 worker 池),形态是"一次事件,JSON 进 JSON 出"。

- 对**拦截型**完美契合——事件本来就是离散的
- 对**注册型**不成立——LLM adapter 要维持流式连接、跨多次调用持有状态

因此新增注册型 seam 应落在 `registerLocalService` 那条常驻线上,而非新增 hook 事件。

**DSH 提供了一个可行的折中形态**:`SubagentProvider` 的 `start()` 只负责"怎么起、怎么说话",真正的长连接与进程生命周期由宿主的 `ctx.subprocess` 托管。`subagent-claude-code` 尤其典型——SDK 自己要拉进程,它用 `spawnClaudeCodeProcess` hook 把进程句柄夺回来交给宿主统管,于是 teardown 阶梯、孤儿进程回收、超时全归宿主。

**插件提供协议适配,宿主拥有进程和生命周期** —— 这个形态比让插件直接持有连接安全得多,且已被上游验证。

## 纪律 4:禁止 accepted-then-ignored

能力不支持时必须 fail loud,不得静默降级。

DSH 把这条作为相对 Claude Code 的**刻意分歧**记录在案:hook 误用在 CC 里退化成 `null`,DSH 一律 fatal 抛出。其远程 subagent provider 的 `NO_START_CAPABILITIES` 也是同理——服务层在 `start()` 之前就抛 `UNSUPPORTED_CAPABILITY`,而非接受后忽略。

我们已有部分实践(`resolveInstance` 的环检测抛错、scope 冲突启动期 fatal、`duplicate()` 诊断),应确立为统一纪律。

**反例警示**:`subagent-acp` 的 `toAcpPrompt()` 把非 text block **静默丢弃**,而同抽象下的 Codex / Claude Code provider 则**直接抛错**。同一 seam 两种行为是需要避免的形态。

## 纪律 5:trust / scope 字段的语义须明确写出

DSH 的 `PresetTrust` README 写得很直白:trust 字段"exists so consumers can present that difference, **not to enforce it**"。

我们的 `scope` 同理——它是**逻辑隔离**(防命名冲突、划分 API 命名空间),真正的安全边界来自进程边界、CSP、构建期校验与 proxy 白名单。这一点必须在文档中明确,避免团队产生虚假安全感。

**当前需要澄清的一处**:因为 child 默认继承 parent scope(`plugin-resolver.ts:917`),parent 与 child 落在同一 scope 命名空间。已确认 `runtime.ts:2802` 的冲突检查针对的是内置 route key,同 scope 内 command id 撞名的处理路径尚未核实,应在实现能力目录时一并查清并写入文档。

## 纪律 6:Model-visible ⟺ logged(建议采纳)

来自 DSH `AGENTS.md`:任何进入模型请求的内容必须能从 session log 重建;新增模型可见输入必须同时新增 session event。

这条对可复现性、审计与"用户能看懂 agent 为什么这么做"是根本性的,且与开放程度无关。DSH 的 `agent-preset/selected` 会话事件就是例证——因为 preset 决定模型看到的工具 schema 与 prompt,切换必须可从日志重建。

## 纪律 7:capability seam 的定义

来自 DSH `AGENTS.md`:**一个 capability seam 由 Service Definition / Service Provider / Consumer 三个 role 构成,单个 role 不构成 seam。**

这个定义可以直接用来防止"开了个接口但没人实现也没人消费"的假扩展点。新增 seam 的评审应要求三个 role 同时存在或有明确规划。
Loading
Loading