跳转至

网关与节点

从插件代码访问 Gateway 和已配对节点,以及长期运行的 Gateway 服务所接收的事件。属于 插件运行时辅助功能 参考的一部分。

Gateway 与节点命名空间

会话资源方法

插件在注册附加 Gateway 方法时可以声明 sessionAccess:

api.registerGatewayMethod("my-plugin.session.open", handleOpen, {
  scope: "operator.write",
  sessionAccess: { mode: "write", allowOwnSessionScope: true, requiredTool: "my_tool" },
});

这需要当前已认证 profile、一个已存在的规范顶层 sessionKey,以及(如果提供)其匹配的 agentId。广泛写入者保留会话的共享规则。allowOwnSessionScope 仅允许调用者自己的会话获得 operator.sessions.write。requiredTool 检查规范的有效会话工具策略以及已准入 agent 运行的工具限制。Agent 调用者绑定到其自己的对话。当前,资源工具策略准入不支持锁定模型选择。

处理器接收 sessionAccessAuthority。在等待准备之前和之后,以及在产生效果之前立即调用 assertCurrent()。路由器在处理器完成时关闭此调用句柄;之后无法重用它来采用新资源。

在处理器期间使用 retain() 获取带有 signal、assertCurrent() 和 release() 的原始人员访问借用,例如用于交互式查看器。对于由独立授权协作者共享的资源,使用 retainSession()。第二次借用仅检查确切的会话实例和存储代际;它不授权操作。每个操作仍需要其自身的人员/运行准入。正常行进度保留两种生命周期;重置、删除或物理存储替换会使会话借用失效。在启动失败、替换和服务清理时释放所有借用。

sandboxRequired 描述已准入的会话/角色约束。消费者必须提供合规后端,或报告其后端不受支持。此元数据不会将 Gateway 托管进程变为沙箱。

对于工具展示,来自 openclaw/plugin-sdk/agent-harness-runtime 的 readGatewayToolOperatorScopes() 在检查该权限后返回当前已准入 operator 作用域的副本。对于没有 operator 捕获的系统/本地调用,它返回 undefined。它不授予权限;所选 Gateway 方法仍会授权请求。

人员访问生命周期

api.registerGatewayAccessPolicy({ authorize }) 向已认证人员准入添加插件拥有的访问要求。回调接收当前配置以及规范 profile 的 ID、电子邮件别名、可选的已验证 githubAccountIds 和已分配角色。账户 ID 来自现有身份所有者;显示登录名和公共电子邮件不会建立该绑定。 Gateway 根据人员的生效角色和本插件的 ID 解析 requiredByRole;角色绑定策略使用该事实,而不是从默认角色名称推断绑定。 当策略不管理该人员时返回 undefined。否则返回 { assertCurrent, signal };当所需访问缺失时拒绝准入。

具名 Gateway 角色可以将 accessPolicyPlugin 设置为你插件的 ID。该角色要求来自你注册策略的当前权限,包括当插件无法加载或其 manifest 不可用时。返回 undefined 不能满足显式角色绑定。没有该绑定的角色保留其现有策略行为,且 Gateway 所有者保持独立。

断言必须在操作之前立即检查原始访问源。当该源过期或被撤销时中止其 signal,包括插件服务关闭。已结束的源即使后来创建授权也必须保持结束。续期可以延长未中断的源。将源保留在其现有生命周期所有者中,并在接受人员访问之前初始化它。

返回原生 AbortSignal。注册保留原生取消和清理,同时保持断言和可调用中止原因值绑定到插件实例。插件退役也会结束已捕获的访问。

Gateway 将返回的权限绑定到原始人员,并通过 WebSocket 和 HTTP 请求携带,以及通过为关联通道发送者准入的命令携带。关联管理员必须满足相同的角色绑定人员策略,包括在等待命令准备之后。撤销已准入授权会结束该命令的权限;替换授权仅适用于新准入。显式配置的命令所有者保留其独立权限。 普通传输断开与撤销不同。策略必须保留独立员工访问;它不得从会话创建者、显示名称或沙箱状态推断请求人员的权限。共享密钥系统权限仍位于人员策略之外。

持久人员访问授权

支持延迟共享发布的策略在其访问权限上返回稳定 UUID 作为 grantId,并实现 resume({ config, profile, requiredByRole, grantId })。Gateway 将插件 ID 和此原始授权引用与已接受的请求者及作用域上限一起记录;它不持久化权限回调、signal、凭据或电子邮件别名。

Gateway 还保留不透明生命周期 ID,用于人员的原始电子邮件绑定。将原始别名移至另一个 profile 会结束该发布权限,即使该别名后来恢复。显示编辑和准入后添加的别名变更保留原始绑定。插件的授权 UUID 仍独立检查;这些身份事实不能替代它。

resume 必须检查该确切的原始授权,即使人员的当前角色否则可豁免。只要授权仍有效,就返回其当前权限;仅当授权确定结束、缺失或被替换时返回 undefined。在服务启动或其状态不可用时抛出异常,以便恢复保留待处理请求,而不是将不可读授权视为已撤销。续期只有在旧授权过期前提交时才能保留 UUID;过期或撤销后的重新邀请必须使用新 UUID。

不支持持久授权支持的策略仍会管理实时准入。其未分类的权限无法转换为可重启的共享发布请求。共享发布当前仅支持一个原始治理授权;无法从该单一引用推断出多个依赖项。新适用的策略也需要新的发布准入。

api.runtime.gateway

在进程内调用另一个 Gateway 方法,同时保留当前插件的可信运行时身份。这旨在供捆绑插件或受信任的官方插件组合插件拥有的 Gateway 能力,而无需打开回环 WebSocket 连接。

if (await api.runtime.gateway.isAvailable()) {
  const result = await api.runtime.gateway.request<{ callId: string }>(
    "voicecall.start",
    { to: "+15550001234", mode: "conversation" },
    { timeoutMs: 60_000 },
  );
}

请求使用 operator.write 作用域,并且不会授予管理员作用域。来自任意外部插件的调用会被拒绝。失败的方法会抛出 GatewayClientRequestError,并保留结构化的 details、重试元数据以及 Gateway 错误代码,用于恢复流程。对于也可能在独立 agent 进程中运行的工具,在选择此路径之前请使用 isAvailable()。

await api.runtime.gateway.readSessionFacts({ sessionKeys }) 最多读取 40 个会话,并返回类型化的 { sessions, warnings? } 数据。每个会话包含其 key、identity、agent、有界脱敏标题和消息预览、运行状态(active、idle 或 failed)、可选的 observer digest(health、headline、assessment、revision)、pull-request 编号和状态、归档状态以及最后活动时间。消息预览上限为 400 个字符。pullRequestsUnavailable 用于区分未知的 PR 状态和已确认的空列表。此生命周期绑定的读取会复用 Gateway 的会话投影和上下文绑定的 PR 快照 owner,保留当前调用者权限和会话可见性,并省略 incognito 会话。保留的句柄在其 owner 关闭后会被拒绝;无需新的 SDK barrel export。

await api.runtime.gateway.withUserProfileIdentity({ profileId, emails, githubAccountIds }, run) 在现有 users.list / operator.read 权限下,为最多 500 个选定的 email 别名准备规范 profile 的原始绑定生命周期。可选的 githubAccountIds 会将最多 500 个正的安全数值 account ID 绑定到同一规范 profile。回调会收到一个同步的 assertCurrent() 函数。请将其与操作自身的授权组合,在 store 的最终提交守卫中以及分发外部 mutation 之前立即使用。它会读取由 profile owner 发布的当前事实,而不会在调用线程上查询 SQLite。将某个 alias 移走再移回、合并所选 profile、未完成的 profile mutation,或关闭调用者都会使该检查失效。所选 account ID 必须仍然是该 profile 的已验证成员。未选定的 email 绑定可以独立变化。当回调完成时,准备工作会被释放,之后保留的断言会被拒绝。此能力只检查所选身份;它不会授予执行该操作的权限,也不会回滚已在外部接受的 mutation。

api.runtime.nodes
列出已连接的节点,并从 Gateway 加载的插件代码或插件 CLI 命令中调用 node-host 命令。当插件拥有配对设备上的本地工作时使用此功能,例如另一台 Mac 上的浏览器或音频桥接。

```typescript
const controller = new AbortController();
const { nodes } = await api.runtime.nodes.list({ connected: true });

const result = await api.runtime.nodes.invoke({
  nodeId: "mac-studio",
  command: "my-plugin.command",
  params: { action: "start" },
  timeoutMs: 30000,
  signal: controller.signal,
});
```

当调用者可以被取消时,请将 agent 工具或请求的 `AbortSignal` 作为 `signal` 传入。Gateway 加载的调用会将取消转发到配对节点;node-host 命令处理器会将其作为 `context.signal` 接收,以便停止进行中的请求并释放本地资源。省略 signal 的现有调用保留其先前行为。

Gateway 加载的插件可以使用 `nodes.openDuplex(...)` 向已注册的 node-host 命令打开一个连接作用域的二进制通道:

```typescript
const controller = new AbortController();
const channel = await api.runtime.nodes.openDuplex({
  nodeId: "paired-node",
  command: "my-plugin.image-bridge",
  params: { format: "png" },
  timeoutMs: 30000,
  maxMessageBytes: 4 * 1024 * 1024,
  signal: controller.signal,
});

const unsubscribe = channel.onMessage((message: Uint8Array) => {
  console.log("Received one complete binary message:", message.byteLength);
});

try {
  await channel.send(Uint8Array.of(1, 2, 3));
  const result = await channel.closed;
} finally {
  unsubscribe();
  channel.close();
}
```

`openDuplex` 接受与 `nodes.invoke` 相同的 node、command、parameters、timeout、idempotency key、session key、caller signal 和 requested scopes,外加可选的 `maxMessageBytes` 和 `maxOutstandingDeliveryBytes` 限制。每条消息的限制默认为 100 MiB,可以降低,但绝不能超过 100 MiB。`maxOutstandingDeliveryBytes` 限制那些异步监听器回调尚未完成的完整消息的总大小;它默认为 `maxMessageBytes`,不能小于该限制,也不能超过 100 MiB。如果某个协议可以在最大大小的响应之后跟随一个有界的异步通知,它可以请求更大的未决投递预算,而无需提高其每条消息上限。OpenClaw 将每个二进制消息拆分为有序的 8 KiB 有效载荷片段,以适配现有的 16 KiB 传输帧限制;调用者始终发送和接收完整的 `Uint8Array` 消息。并发发送会保留消息边界。

在 openDuplex 解析后立即注册通道的单个消息监听器。在注册监听器之前,OpenClaw 最多缓冲八条完整消息和总共 1 MiB;超过任一限制都会关闭该调用。取消订阅回调会移除该监听器。监听器可以返回 Promise<void>;抛出的错误或被拒绝的 Promise、调用方中止、close()、节点断开、配对变更、插件重新加载或退役,或 Gateway 关闭都会关闭通道并取消未完成的节点工作。成功的节点命令完成和 channel.closed 会等待已在进行中的异步消息监听器。close() 是幂等的,保留的通道方法在关闭后会被拒绝。channel.closed 会以成功的命令结果解析,或以节点、授权、传输或取消错误拒绝。通道无法重连,也无法在节点断开后存活。

节点插件声明 duplex: true,并通过可选的帧式命令 I/O 能力注册消息监听器。当同一命令也支持一元调用时,使用 duplex: "optional";它仍会在不支持 duplex 的节点上被通告。请从显式请求参数中选择二进制行为,而不是仅根据 I/O 是否存在来选择:

api.registerNodeHostCommand({
  command: "my-plugin.image-bridge",
  duplex: true,
  async handle(_paramsJSON, io) {
    if (!io?.frames) {
      throw new Error("Framed node command I/O is unavailable.");
    }

    const frames = io.frames;
    return await new Promise<string>((resolve, reject) => {
      frames.onMessage((message) => {
        void frames.send(message).then(() => resolve('{"ok":true}'), reject);
      });
      io.signal.addEventListener(
        "abort",
        () => reject(new Error("Node command was canceled.")),
        { once: true },
      );
    });
  },
});

在发送之前注册 frames.onMessage(...):节点只有在监听器存在后才会通告帧式就绪,并且 openDuplex 只有在命令分发和帧式就绪都完成后才会解析。这可防止输入在插件能够消费之前到达。现有的原始 emitChunk 和 onInput 辅助函数仍可用于终端式命令。

使用来自 openclaw/plugin-sdk/node-host 的 runNodePtyCommand 的交互式命令可以传入一个 assertCurrent 回调,用于已准备的源权限。PTY 所有者会在异步原生加载之后、生成之前立即重新检查该回调以及调用的中止信号。

openDuplex 仅对当前受信任的进程内 Gateway 插件运行时可用。插件 CLI 运行时会以可操作的错误拒绝它;没有远程轮询或本地回退。每次调用都会使用与 nodes.invoke 相同的配对、已声明命令允许列表、插件策略、审批、授权和连接所有权检查。

当节点向 agent 暴露插件或 MCP 支持的工具时,nodes.list(...) 会包含每个已连接节点通告的 nodePluginTools 描述符。这些描述符是实时连接状态:Gateway 会在节点断开时丢弃它们,节点可以在本地插件/MCP 清单变化后使用 node.pluginTools.update 替换它们。

在 Gateway 内部,该运行时是进程内的。在插件 CLI 命令中,它会通过 RPC 调用已配置的 Gateway,因此诸如 openclaw googlemeet recover-tab 之类的命令可以从终端检查已配对的节点。节点命令仍会经过正常的 Gateway 节点配对、命令允许列表、插件 node-invoke 策略和节点本地命令处理。

当为已准入的运行启用执行身份审计时,这些 Gateway 关卡会显示为已执行的决策回执。成功的节点结果仅用于归因。如果策略在未调用其提供的 invokeNode 回调的情况下返回,则操作状态未知;返回成功的插件结果并不能证明节点操作已经发生。

暴露节点托管 agent 工具的插件可以为应当默认加入允许列表的非危险命令设置 agentTool.defaultPlatforms。当操作员必须使用 gateway.nodes.commands.allow 选择加入时,应省略它。危险的节点宿主命令应使用 api.registerNodeInvokePolicy(...) 注册 node-invoke 策略;该策略在 Gateway 中于命令允许列表检查之后、命令转发到节点之前运行,因此直接的 node.invoke 调用、节点托管插件工具和更高级别的插件工具共享同一执行路径。

node-invoke 策略会接收所选节点通告的 commands 和可选的 caps。使用这些事实来要求受支持的命令行为,然后通过提供的 invokeNode 回调分发;该回调在转发命令前会重新验证当前连接和配对。

allow-always 仍保持为一次策略决策,除非 node-invoke 策略显式声明 standingApproval: { kind: "placement", scope: "<capability>" }。该选择加入仅允许对同一当前受管 placement、节点配对、环境所有者、工作区和语义能力范围内的同一高风险命令进行后续启动,最长 30 天,且绝不跨越 Gateway 重启。对于其审批有意覆盖后续参数变化的能力,请使用稳定的、不包含内容的 scope。当已批准的目标或其他请求参数必须保持精确时,不要选择加入。

节点命令可以声明 prepare(context) 用于异步原生启动。节点宿主初始化会在发布初始 manifest 或连接到 Gateway 之前等待它;插件注册本身保持同步。共享的准备回调每次节点注册表初始化只运行一次,而不是每次调用或重连都运行。可选提供者在预期的准备失败时应保持已知的不可用状态,并让 isAvailable 扣留其命令;抛出异常会中止节点启动。使用 watchAvailability 处理后续可用性变化,使用 onDisconnect 处理执行清理。watchAvailability 返回的清理回调可以返回一个 Promise。节点关闭会等待该回调并报告清理失败。

Warning

可选的 `scopes` 字段为该调用请求 Gateway 操作者作用域。OpenClaw 仅对捆绑插件和受信任的官方插件安装予以认可;来自其他插件的请求不会提升该调用。当 `openDuplex` 在已认证的 Gateway 请求内运行时,其有效作用域永远不会超过该已认证调用方的实际作用域,即使受信任插件请求了更强的作用域。在没有已认证入站客户端时,适用现有受信任插件作用域行为。仅当受信任插件必须使用更严格的 Gateway 作用域调用节点命令时,才使用请求的作用域,例如 `operator.admin`。

Gateway 服务事件

由 Gateway 托管的服务可以使用 ctx.invokeNode?.() 用于其自身注册的 节点命令。这使用该服务的身份,因此只读文档请求 可以获取远程文件,而无需向其调用方授予 operator.write。 在使用此能力之前,请授权公开操作。节点配对、 命令授权和插件路径策略仍然适用。该能力不接受 调用方选择的作用域,并在服务停止或其 Gateway 关闭时停止接受工作。普通的 api.runtime.nodes.invoke 保留其调用方的权限。

ctx.openNodeDuplex?.() 为服务自身以 duplex: true 或 duplex: "optional" 注册的 命令打开相同的帧二进制传输。它使用与 invokeNode 相同的节点策略和 服务生命周期;调用方不能选择身份或作用域。 可选的 assertCurrent 回调会在分发和每一帧之前添加当前操作的存活检查。 关闭服务会取消打开的通道。

由 Gateway 托管的服务还会接收 ctx.getCron?.(),用于 Gateway 钩子已经可用的调度器操作: list、add、update、remove 和 removeStaleJobFamily。非 Gateway 服务宿主会省略此 getter。

当前服务句柄还暴露 enqueueRun(id, mode),用于服务拥有的 工作。它使用常规 cron 准入队列,并带有服务的实时权限, 独立于已完成的 agent 工具调用方。使用 "if-enabled" 可请求 立即运行,而不覆盖已禁用的作业。保留的句柄仍会 在服务停止或调度器被替换时拒绝,包括在等待 准入期间。此可选方法在旧宿主上不存在;它没有调用方作用域 回退。

当前 Gateway 服务句柄还提供 await cron.isEnabled(),用于观察 是否启用了自动调度,包括 OPENCLAW_SKIP_CRON 覆盖。它只返回布尔值,而不是存储元数据或修改作业的权限。 该方法在面向旧宿主实现的公共类型中是可选的; 其缺失表示未知,而不是启用或禁用。支持旧宿主的消费者 可以在其缺失时保持先前的协调行为。 禁用调度不会禁用作业 CRUD 或必需的插件清理。

服务清理会保留所属插件的清理上下文,以便 stop() 可以 在常规调用准入关闭后释放资源。保留该启动尝试获取的资源和解 订阅函数,并释放这些确切句柄。清理失败并不意味着原生资源已被终止; 参见 插件生命周期与清理。

使用服务的 start() 和 stop() 方法来管理周期性协调。 它们会在服务或插件替换以及 Gateway 启动和关闭时运行; 完整插件替换还会为受影响的插件运行 gateway_stop 和 gateway_start。 仅服务配置重载不会重放这些钩子。 每个返回的调度器句柄属于一个服务生命周期和一个调度器 实例。调用(包括排队写入)会在服务关闭开始或 该调度器被替换后拒绝。再次调用 ctx.getCron() 以获取替换后的 调度器,同时服务仍保持活动。

服务可以与其 id、start 和 stop 一起声明 reload: { configPrefixes: ["myConfig.service"] }。在匹配的配置变更提交后,Gateway 会停止该服务,并使用新的 ctx.config 再次调用 start(ctx)。只有 声明了匹配前缀的已加载服务会被替换;重叠的所有者 都会刷新。现有相等或更窄的重启或无操作策略仍然优先。 每次启动都会收到新的能力租约和健康报告器。停止必须在解决前释放 资源;替换清理或启动失败会触发 Gateway 恢复。完整插件替换涵盖这些服务重启。 停止钩子在该尝试的原始启动稳定后运行。替换 截止时间可以结束调用方的等待并撤销服务能力,同时最终 清理仍由所有者负责。

受信任的官方诊断导出器服务还可以接收 ctx.internalDiagnostics.getRuntimeIdentity?.()。它返回宿主 进程的规范 processInstanceId 和可选的已加载 buildId,不进行 文件系统查找或 RPC。在服务启动期间捕获它;保留的 getter 会在服务租约被撤销后抛出异常。不提供此 可选能力的宿主会使运行时身份不可用。此诊断事实 不会授予权限,也不会标识服务重载纪元。

它们的 ctx.internalDiagnostics.onEvent(listener, filter?, options?) 订阅 会传递事件、信任元数据和一个冻结的私有数据对象。只需要 事件和元数据的导出器可以将 { includePrivateData: false } 作为 第三个注册参数传入,以跳过私有负载副本,并改为接收一个冻结的 空对象。这会保留事件过滤器、仅受信任投递和 服务租约清理。私有数据默认保持启用;旧宿主 会忽略该可选参数,并保留其现有复制行为。

通过 api.registerService(...) 注册的长生命周期服务,在进程运行 Gateway 广播器时, 会接收一个进程本地的 ctx.gatewayEvents 外观;在没有广播器的运行时中, 该字段不存在,因此请进行功能检测并保留回退(例如粗略轮询)。使用 onSessionsChanged(...) 在 Gateway 广播 sessions.changed 通知后进行响应:

let unsubscribeSessionsChanged: (() => void) | undefined;

api.registerService({
  id: "session-index",
  start(ctx) {
    unsubscribeSessionsChanged = ctx.gatewayEvents?.onSessionsChanged((event) => {
      // event: { sessionKey, agentId?, label?, displayName?, reason?, phase? }
      // refreshSession is your plugin's own handler, not an SDK export.
      refreshSession(event.sessionKey);
    });
  },
  stop() {
    unsubscribeSessionsChanged?.();
    unsubscribeSessionsChanged = undefined;
  },
});

处理函数在 Gateway 进程中运行,并且不会添加 Gateway 协议订阅。请保留返回的取消订阅函数,并在服务清理期间调用它。该负载是一个轻量级变更通知;当插件需要完整的当前会话条目时,请使用 api.runtime.agent.session.getSessionEntry(...)。

OpenClaw 每次启动尝试最多调用一次服务的 stop(),包括在启动失败之前替换超时的情况。启动失败回滚和关闭共享相同的清理结果;清理失败会被记录,而不会在该次尝试内重试。

如果替换失败,Gateway 可能会再次调用先前服务的 start() 以恢复它。请重新创建由 stop() 释放的资源,并重置每次启动的标志,以便工具和后台工作在回滚后仍可使用。

由返回或 await 的 Promise 引发的服务启动失败会自动记录。有意在后台启动必需工作的服务,必须通过其绑定到代次的健康报告器报告后续的失败和恢复:

// startIndexWorker and stopIndexWorker are your plugin's own background-work
// helpers, not SDK exports.
api.registerService({
  id: "index-worker",
  start(ctx) {
    void startIndexWorker().then(
      () => ctx.serviceHealth?.clearFailure(),
      (error) => ctx.serviceHealth?.reportFailure(error),
    );
  },
  stop() {
    stopIndexWorker();
  },
});

当服务停止或其插件注册表代次被替换时,报告器会被撤销,因此来自旧代次的迟到回调无法覆盖当前健康状态。如果服务在该 Promise 解决之前不可用,请优先返回启动 Promise;仅对于刻意非阻塞且拥有自身停止路径的工作使用报告器。

本页原文 Markdown:在 AtomGit 查看·内容源自开源项目 cl/openclaw