本文分成两部分:第一部分面向 Baton 维护者,定义 Manager、Supervisor、Runner 和持久状态的 边界;第二部分面向三方 Plugin 作者,定义
@compforge/baton-plugin的使用规范。 Baton 的全局进程与线程模型见 kernel,长期 loop 的产品位置见 Loop Engineering。
Plugin 让长期领域 loop 在不进入 Baton core 的前提下拥有自己的 Resource、Controller、 Connector 和用户入口。Baton 不理解 Requirement、Deployment、Review 等领域语义,只提供 生命周期、调度、持久化、权限、Board、Context 和受控输出。
PluginPackage(不可变交付物)
└── PluginInstance(用户配置)
└── PluginBinding(当前 BatonSession 的一次活动绑定)
├── Command / ContextProvider
├── Controller / Source / Watch
└── Runner process
- Package 由
pluginId + version标识;已安装版本不可原地修改。 - Instance 由
plugin@marketplace派生稳定pluginInstanceId,保存 enabled、版本与配置。 - Binding 是一次临时激活,拥有全部注册与清理动作,不保存领域事实。
- Resource 以
spec表达期望,以status表达观测;属于当前 BatonSession。 - Board 是 Resource 的派生展示,不是另一份状态。
外部平台继续拥有其事实,Session Event Ledger 继续拥有 Baton 执行历史,Plugin Resource 拥有领域期望与观测。三者不能互相冒充。
Baton host process
└── Manager
├── Instance / Resource / Proposal / Interaction stores
├── reconcile queues、Sources、Watches、Board cache
└── Supervisor
└── one Runner process per active Binding
└── third-party Package + Connector
Manager 是唯一装配入口。它负责恢复 Instance、创建 Binding、注册代理、控制 reconcile 容量、持久化结果、维护 Board 缓存,并在关闭时按依赖逆序撤销。
Supervisor 只负责子进程生命周期:创建 Runner、设置调用 deadline、观察退出、强制回收 失去响应的进程,以及关闭全部 Runner。它不理解 Resource 或领域策略。
Runner 加载一份 Package,保存 Plugin 回调,并通过 IPC 执行 activate、Command、
ContextProvider、Source、Watch、reconcile、present 和 cleanup。Runner 不直接访问 Baton
Store、Controller、Harness 或 TUI。
产品路径中的 Marketplace Plugin 必须进入 Runner。进程内 Package 入口只保留给 Baton 自身的 可信内建能力和单元测试 seam,不能成为三方 Plugin 的回退路径。
隔离粒度选择 Binding,而不是 Package 或单次调用:
- Binding 已经是注册、关闭和故障撤销的原子边界;
- 同一 Package 在不同 Session 的配置、Resource 与 Connector 不应共享可变全局状态;
- 同一 Binding 内的 Source、Controller 和 cleanup 需要共享局部连接与缓存;
- 单次调用起进程会丢失上述生命周期,并引入不必要的启动成本。
一个 Plugin 的同步死循环、同步子进程或模块加载不会占用 Baton 的 UI 事件循环。它仍会阻塞 自己的 Runner;调用 deadline 到期后,Supervisor 终止该 Runner,Manager 撤销整个 Binding。
进程隔离是故障与调度边界,不是安全沙箱。Plugin 仍以当前用户身份访问文件、环境变量、 网络和子进程。权限声明、secret 注入、签名与 OS sandbox 是另一层安全能力,不能用“单独进程” 替代。
IPC 只传可结构化克隆的数据,不传函数或宿主对象:
host → Runner
activate(entry, instance, session)
invoke(handlerId, args)
start-source / stop-source
close
Runner → host
activation registrations
resource get/list/create/delete/patchStatus
source emit
toast / log / source-error
激活时,Runner 把回调保存在本进程的 handler table,只把 handlerId 和声明性 metadata
返回 Manager。Manager 据此安装宿主代理。激活完成后注册表封口,防止异步偷注册造成无法原子
回滚的半个 Binding。
所有跨进程请求都是 Promise。Manager 的队列和 UI 不同步等待进程输出;Board presentation 在后台刷新缓存,render 只读取最近一份完整快照;Context 搜索只允许最新 query 发布结果。
每个 parent call 都有 deadline 和有限 pending 表。超时、IPC 断开、异常退出或非法信封都使 Runner 进入失败态:
- 拒绝全部 pending call;
- 终止失去响应的子进程;
- 向 Source 报告失败;
- 通知 Manager 撤销 Binding 的 Command、ContextProvider、Controller、Source 和 Board;
- 保留 Resource、Proposal、Interaction 与日志,供 reload 或下次启动恢复。
当前不自动重启失败 Runner。Controller 可能刚对外产生了效果却未拿到回执;无条件重启会扩大 重复副作用。Plugin 应使用稳定 operation key,并在重试前重新观察外部状态。用户显式 reload 或下次 Baton 启动会从持久事实重建 Binding。
Source 的 emit 等待 host acknowledgement,从而形成自然背压;toast 与诊断日志是短寿命
旁路,不能承载领域状态。关闭时先撤销宿主注册,再停止 Runner;Runner 内部先 abort Source,
再逆序执行 onClose。
Manager 保持以下约束:
- 同一 Resource key 不并发 reconcile,不同 key 可受 Controller 与 Manager 容量限制并发;
- 事件只负责 wake,reconcile 每次重新读取最新 Resource;
- Resource 变化、Source、Watch、cron、
requeueAfterMs和错误退避进入同一 keyed queue; - Plugin Output 先持久化,再通知 UI;
- 激活失败整体回滚,不能留下部分注册;
- Runner 失败只撤销临时 Binding,不删除持久事实;
- Board、Context 与 TUI 都消费派生快照,不持有 Plugin 回调。
ResourceClient 的宿主实现只允许 Instance 操作自己的 Resource。Baton-owned Resource 是
Event Ledger 的只读派生视图。Resource type owner、namespace、uid 和 resourceVersion 都在
host 边界校验,不能信任 Runner 自报。
启动顺序是:
- 读取用户级启用配置和不可变 Package entry;
- 为每个 enabled Instance 创建 Runner 并执行激活;
- 原子安装注册并启动 Source;
- 恢复待处理 Proposal、Interaction、Resource due time;
- 对当前 Resource 做 initial reconcile。
升级先解析并校验新 Package,再关闭旧 Binding、切换版本并激活;失败时恢复旧版本与旧 Binding。Resource schema migration 必须由新版本显式完成,不能由 Manager 猜测。
Plugin 只能依赖公共包:
import type {
PluginPackage,
Resource,
} from "@compforge/baton-plugin";默认导出一份 PluginPackage,其 pluginId、version 必须与 manifest 一致:
const plugin: PluginPackage = {
pluginId: "example/tasks",
version: "0.1.0",
async activate(context) {
// 同步完成注册;需要等待的初始化可以 await。
},
};
export default plugin;公共包只包含协议与作者类型,不导出 Manager、Supervisor、Runner、Store、HarnessAdapter 或
Marketplace,也不提供运行期常量或 helper。Plugin 从该包一律使用 import type;ResourceType
descriptor 和纯映射 helper 留在自己的源码中。这样安装后的 Package 不需要借用宿主
node_modules。Plugin 不能导入 Baton 私有源码。
以下入口必须返回 Promise:activate、Command execute、Context search/provide、Source
start/emit、EventHandler、reconcile、present 和 ResourceClient 操作。
Plugin 作者还必须遵守:
- 不使用
spawnSync、execSync或同步网络桥接;外部命令使用异步进程 API,并设置 timeout、 输出上限和取消; - 跨边界参数与返回值只使用普通对象、数组、字符串、数字、布尔值和
null/undefined; - 不返回函数、class instance、stream、socket、文件句柄、DOM/Node 对象或带循环引用的对象;
- 不把 module global 当成可恢复状态;缓存可以丢,事实必须进入 Resource 或外部系统;
- 长期订阅在
Source.start中安装,并响应context.signal;其它连接通过onClose清理; - Connector 自己设置 HTTP、DB、Git 等外部资源的并发、timeout 与容量。
Runner 隔离能保护 composer,不等于允许 Plugin 阻塞自己的进程。同步阻塞会让该 Binding 的 所有 handler 一起停顿,并最终触发 deadline 回收。
Resource 使用版本化类型身份:
const TASK = {
apiVersion: "example.baton.dev/v1alpha1",
kind: "Task",
} as const;
type Task = Resource<
{ readonly title: string },
{ readonly phase?: "open" | "done"; readonly observedGeneration?: number }
>;spec是期望与用户认可的 contract;status是可重新观测或计算的当前状态;generation只随 spec 变化;resourceVersion是 opaque 乐观并发 token;uid固定一次具体创建,删除重建后变化;labels/annotations只放 string metadata,不替代领域引用。
Controller 管理一个 primary resourceType:
context.registerController({
resourceType: TASK,
async reconcile(_baton, resource: Task) {
const next = await observeTask(resource.spec);
await context.resources.patchStatus(resource, {
phase: next.phase,
observedGeneration: resource.metadata.generation,
});
},
async present(resource: Task) {
return {
title: resource.spec.title,
status: resource.status.phase,
};
},
});reconcile 必须 level-based、幂等:事件只代表“可能变化”,不是必须恰好执行一次的命令。
可能已生效却拿不到回执的外部操作使用稳定 operation key,重试前先 observe。
present 只做只读、可重复的派生,不改变 Resource 或外部系统。持续进度和错误进入 status /
Board;toast 只用于一次操作或状态边沿的反馈。
- Source 发现外部对象并
emitprimary Resource;不 patch status,不产生 Output。 - Watch 把已存在的 secondary Resource 变化映射成 primary
ReconcileRequest。 - cron Source 固定周期 enqueue 当前 Resource。
requeueAfterMs表示当前 Resource 本次 reconcile 后的动态复查。
四条路径最后都进入同一 keyed queue。Source.start() 只有在初始扫描和 live subscription
都 ready 后才 resolve;关闭时必须停止 watcher 和异步任务。
Command 是用户显式入口,适合创建、选择或修改 Resource。远端 picker 搜索通过
searchQuery 重入同一 Command;Plugin 返回完整结果页,Baton 负责 debounce 和丢弃旧响应。
ContextProvider 只提供用户用 @ 明确选择的只读上下文。search 不产生副作用;provide
按 maxChars 控制输出,不返回 secret。
Controller 不直接调用 Harness。当前可返回两类受控 Output:
proposed-input:用户审核、编辑或丢弃后,才成为普通 Input;interaction:Baton 先持久化决议,再重新 enqueue 原 Resource。
等待用户决议时不要在 Runner 中保存 Promise continuation。下一次 reconcile 从
baton.pluginInteractions 读取持久结果。
发布前至少确认:
- Package 与 manifest 身份一致,版本目录不可变;
- 所有跨边界回调都是 async 且返回可传输数据;
- 外部 I/O 有 timeout、容量与取消,产品代码没有同步子进程;
- reconcile、Source emit 和外部副作用可重放、可去重;
- crash 后只靠 Resource、Event Ledger 和外部事实即可恢复;
- cleanup 会停止订阅、连接与自建子进程;
- 日志和 Context 不包含 secret;
- Plugin 不导入 Baton 私有类型,也不直接访问 Harness。
@compforge/baton-plugin 0.2.0 是本进程契约的首个版本:公开回调统一 Promise 化,三方
Package 默认在独立 Runner 中执行。0.1.x 的同步作者契约不作为兼容目标。
packages/plugin/README.md— 最短作者示例docs/kernel.md— Baton 进程、事件循环与稳定内核docs/loop-engineering.md— Baton Plugin 与 Harness Plugin 的分层controller-runtime— level-based reconcile、Source、Watch 与 workqueue 的主要参照