插件
模型实现、工具、运行时技能、审计接收端、连接器和后台服务,都使用同一个 RuntimePlugin 协议,以及每个 Harness 一个的 PluginHost。这些是代码架构层面的插件,不是 MCP 服务器。一个插件可以贡献多种能力;系统不给插件划分互斥的类别。
Agent 循环、Session、权限和各类注册表留在引擎里。现有的声明式 oharness.plugin.json 插件包仍然受支持,并且永远不会被自动当作可执行模块导入。
快速开始
无需凭据、也不发网络请求,运行这个包含四个插件的演示:
npm run demo:plugins
npm run test:plugins
script/fixtures/runtime-plugins.ts 里的示例实现了一个脚本化模型、一个本地工具、一个审计观察者和一个模拟连接器。它们只是演示。托管的 Cloud 连接器 在非临时实例中默认注册为 connector-cloud。它在启动时注册两个工具,不访问网络;缺少登录或服务器配置会变成工具错误,而不是启动失败。在 plugins.json 的 runtimePlugins 里写 { "id": "connector-cloud", "enabled": false } 即可禁用。嵌入方可以设置 cloudConnectors: false。直连的 Composio 适配器 仍是需要主动开启的开发者备选方案;不要在托管客户端里启用它。Pipedream 尚未实现。
内置的记忆插件(ID 为 memory)现在负责记忆工具、每轮提示词索引、网页页面和后台同步的生命周期。即使在临时 Harness 实例里它也是启用的(与之前本地记忆的行为一致)。在 plugins.json 里禁用它,就会移除所有这些贡献。这和保留本地记忆、只关闭云同步是两回事。
编写插件
import type { RuntimePlugin } from "@oharness/engine";
export default {
id: "hello-tools",
version: "1.0.0",
apiVersion: 1,
parseConfig(raw) {
if (raw !== undefined) throw new Error("This plugin takes no options");
return {};
},
setup(ctx) {
ctx.tools.register({
name: "hello_greet",
description: "Return a greeting",
inputSchema: { type: "object", properties: {} },
readOnly: true,
async execute() {
return { content: [{ type: "text", text: "Hello" }] };
},
});
},
} satisfies RuntimePlugin;
setup 在每个 Harness 实例上运行一次。客户端、定时器和可变状态要放在它里面,而不是放在模块作用域。插件 ID 使用小写字母、数字、点、下划线或连字符。API 版本 1 的检查独立于插件自身的版本。
加载与配置
嵌入应用可以显式提供受信任的插件定义:
import { Harness } from "@oharness/engine";
import greeting from "./hello-plugin.js";
const harness = await Harness.create({
runtimePlugins: [{ plugin: greeting, required: true }],
});
try {
console.log(harness.runtimePluginStatus());
// Create sessions / agents as usual.
} finally {
await harness.close();
}
也可以把 runtimePlugins 加到用户插件文件里(OHARNESS_HOME 下的 plugins.json,通常是 ~/.oharness)。接受绝对模块路径,或能从该目录解析到的已安装包;不会下载任何东西。模块必须以默认导出提供插件定义。分发时优先使用构建好的 JavaScript;TypeScript 源码需要符合 Node 支持的类型剥离子集。
{
"runtimePlugins": [
{
"id": "hello-tools",
"module": "/absolute/path/to/hello-plugin.js",
"enabled": true,
"required": true
}
]
}
config 会传给 parseConfig。相同 ID 的配置项可以配置或禁用一个内联插件,但不能用另一个模块替换它的实现。任一列表内出现重复 ID 都会被拒绝。enabled: false 会完全跳过模块导入、配置解析和 setup。required 默认为 true;可选插件的失败会在状态中显示,但不会中止 Harness 的创建。配置列表会整体替换之前的列表,而不是对插件选项做深度合并。改动在下一次创建 Harness 时生效,不影响正在进行的会话。
项目配置不能引入或重新配置运行时插件。提供 HarnessOptions.config 是嵌入应用做出的一个明确的信任决定。不要把密钥放进项目配置或插件元数据。通过你认可的凭据集成或环境变量加载它们,并且永远不要把它们写进日志。
能力接口
| 接口 | 贡献内容 |
|---|---|
ctx.tools.register(tool) | 工具定义和实现;现有的权限流程仍然适用 |
ctx.skills.register(skill) | 在本插件生命周期内可用的内存技能;重名会被拒绝 |
ctx.models.register(provider, models) | 模型实现及其模型定义,原子化校验 |
ctx.auth.register(provider) | 使用现有 AuthProvider 约定的认证实现 |
ctx.hooks.on(event, handler) | 现有的带类型生命周期钩子,自动取消订阅 |
ctx.audit.registerSink(sink) | 尽力而为的权限/执行事件观察者 |
ctx.prompts.register(source) | 每轮 Agent 都会刷新的异步系统提示词片段 |
ctx.pages.register(page) | 由宿主渲染的页面描述符,以及读取/操作处理函数 |
ctx.onStart(fn) | 后台工作,在所有插件的 setup 完成后启动 |
ctx.onDispose(fn) | 需要关闭的资源,按获取顺序的逆序关闭 |
所有贡献都必须在 setup 期间注册。宿主会跟踪它们并自动撤销。默认注册会拒绝冲突;插件不能悄悄替换内置项或彼此的注册。现有的链式注册表 API 保持兼容。移除一个认证实现不会删除凭据。模型厂商应当贡献自己的 ID,而不是试图覆盖已有的目录条目。用普通的 model 配置或模型选择 API 来选用插件提供的模型。
运行时技能提供 name、description 和 body,当正文引用脚本或其他文件时,还可以提供一个可选的绝对路径 directory。它和持久化技能使用同一个渐进式披露的 skill 工具,来源显示为 runtime,并在其插件停止时自动移除。运行时技能不能替换同名的内置、用户、项目、配置中的或其他运行时技能。
ctx.signal 用于通知插件关闭。工具和模型流也会收到宿主与调用合并后的取消信号。后台工作要能配合取消;资源一获取就立即注册清理,放在之后可能失败的 await 之前。onStart 必须在启动工作后就返回,而不是 await 一个无限的工作循环。应把它的完成 promise 保存下来,在 onDispose 里 await。
审计插件
引擎自带 jsonlAuditPlugin(id: "audit-jsonl"),用的就是同一套约定。把 { path: "/absolute/path/to/events.jsonl" } 作为它的配置传入,可以通过嵌入 API,也可以通过一个指向 packages/engine/dist/runtime-plugins/builtin/jsonl-audit.js 的模块配置项。它会创建父目录,以追加模式打开(新建文件权限为 0600),并在关闭前清空队列。它不轮转文件,也不修改已有文件的权限。
审计接收端收到的是 RuntimeAuditEvent:
permission:现有 PermissionEngine 的决定/回答记录,包括由宿主转发的子 agent 决定。tool_execution:已完成的调用,包括被转换为错误结果的工具抛出异常;包含耗时、工具身份以及所属的运行时插件。
执行事件不包含输入/输出正文和凭据。权限记录保留现有的规则/主体字段,其中可能包含路径或命令参数:接收端必须妥善处理其敏感性。这不是完整的事件回放或合规日志;在这一实现中,在 PermissionEngine 之前就被拦下的调用不会额外产生决定记录。
每个接收端都有自己的快照和有序队列。一个抛出异常的接收端不会阻塞其他接收端,也不会改变工具决定。每个接收端的队列上限为 1,000 个待处理事件,超出的事件会被警告并丢弃。关闭时会先清空队列,再关闭接收端资源,受清理超时限制。audit: false 会同时禁用运行时审计事件和现有的权限记录。
文件接收端设置示例(在插件内部):
const file = await open(config.path, "a", 0o600); // node:fs/promises
ctx.onDispose(() => file.close());
ctx.audit.registerSink(async (event) => {
await file.appendFile(JSON.stringify(event) + "\n");
});
这保留了引擎现有的尽力而为的审计语义。把审计插件设为必需,保证的是启动,而不是失败即拒绝的写入。强制性的持久审计需要另行设计一套等待写入完成的执行策略。
连接器插件
可以用 createConnectorPlugin 这个便捷工厂,也可以直接实现 RuntimePlugin。工厂返回的是一个普通的 RuntimePlugin,而不是另一套机制:
const plugin = createConnectorPlugin({
id: "my-connector",
version: "1.0.0",
parseConfig: validateConnectorConfig,
async connect(config, { signal, logger }) {
// Wrap your selected SDK. Bind a trusted account here, not from model input.
return createMyConnectorClient(config, { signal, logger });
},
});
ConnectorClient 暴露 tools(): Promise<readonly Tool[]> 和 close()。如果工具发现失败,已连接的客户端仍会被关闭。这个辅助函数会对外部工具强制关闭 readOnly 和 sessionLocal,所以厂商元数据无法悄悄绕过批准或 plan 模式的限制。只暴露经过批准的工具,使用命名空间,校验参数,并把每次调用的信号传递给 SDK。不要把厂商的整个工具目录塞进每一次模型提示词里。
宿主不实现厂商 OAuth、账号关联、webhook 或租户存储。Cloud 连接器 提供托管链接创建、Web 交互和经过验证的绑定;它不改变宿主协议。多用户场景下的账号身份必须来自受信任的应用上下文,绝不能来自模型选择的账号 ID。插件宿主本身不需要任何云基础设施改动。
生命周期与兼容性
- 解析已启用的配置项,校验约定和配置。
- 按配置顺序运行 setup;记录注册项和资源归属。
- 所有 setup 完成后,统一运行一次 start;对外暴露活动状态。
- 失败时,回滚失败的插件;如果该插件是必需的,则中止并全部回滚。
- Harness 关闭时,阻止新的宿主运行,取消/排空现有运行,中止插件调用,注销贡献,然后按逆序关闭资源。
清理是幂等的,单个失败之后会继续进行。Agent 排空以及每个插件的排空/清理都有五秒上限。超时会被报告;它们无法强行停止不配合的 JavaScript,超时后可能仍有工作在运行。过期的插件工具副本在其插件停止后会拒绝调用。
CLI 的 /plugins 和 Web 的 /context 会把运行时状态与声明式插件包一起展示。错误信息会省略插件提供的异常消息,以免泄露配置中的密钥。如有需要,用插件作用域的 logger 添加经过脱敏的诊断信息。
V1 有意不提供:运行中卸载、跨插件的依赖/服务图、远程包安装器、沙箱、任意前端模块加载器,或可替换的 Agent/Session 核心。安装在进程内的插件拥有进程的全部权限:上下文 API 是一条可维护性边界,不是安全沙箱。
如需迁移旧的内联配置,参见配置边界。独立的插件文件属于受信任的用户配置,永远不会从任意项目目录加载。