@chamvi/channel-openclaw

中文文档
0.8.0发布于 2026/11/09Apache-2.0TypeScriptES Module零依赖构建来源已验证近 30 天下载 2950

OpenClaw channel for chamvi agents.

原始使用文档
文档目录9 节
  1. Installation
  2. OpenClaw setup
  3. Usage
  4. Answering approvals
  5. Caveats
  6. With @chamvi/app
  7. Options
  8. API
  9. License

@chamvi/channel-openclaw

@chamvi/channel-openclaw

OpenClaw channel for chamvi agents.
Messages and approval requests reach the owner in an OpenClaw chat,
once per message, even when the engine retries the send.

npm version CI License


Table of Contents

Installation

npm install @chamvi/channel-openclaw @chamvi/core --save

ESM only, Node.js >=26.0.0. @chamvi/core is a peer dependency; the channel uses the global fetch and has no runtime dependencies.

chamvi and OpenClaw talk "MCP in, hooks out": OpenClaw starts and reads runs through the app's MCP endpoint (@chamvi/http), and chamvi pushes messages to OpenClaw's hooks endpoint through this channel. The channel is out only: it has no in and no subscriptions.

OpenClaw setup

In the OpenClaw gateway configuration:

  1. Enable hooks (hooks.enabled: true; they are off by default) and set a dedicated hooks.token. It is not the gateway token; the channel sends it as Authorization: Bearer <token>.
  2. Keep hooks.path at /hooks or pass the same value as hooksPath.
  3. When you pass agentId, list it in hooks.allowedAgentIds.
  4. Add the chamvi app's MCP endpoint (http://<service>/mcp) to OpenClaw's mcp.servers, with a bearer token of a principal holding agent:<name>:run to start runs. Never give it agent:<name>:approve: the owner answers through the approvalUrl link, signed in to the chamvi UI as a principal of their own (see Answering approvals).

Usage

openclawChannel() is an IChannel; register it on the engine (channels) and send through it from a tool with ctx.channels.findOne('openclaw'): emit() for a message, approve() to pause the tool call until the owner answers:

import type { LanguageModel } from 'ai';
import { z } from 'zod';
import {
    Engine,
    createMemoryStore,
    defineModelAgent,
    defineTool,
} from '@chamvi/core';
import type { IEngine } from '@chamvi/core';
import { openclawChannel } from '@chamvi/channel-openclaw';
import type { OpenclawChannelOptions } from '@chamvi/channel-openclaw';

export function createApprovalEngine(
    model: LanguageModel,
    options: Pick<OpenclawChannelOptions, 'fetch' | 'clock'> = {},
): IEngine {
    const {
        OPENCLAW_URL,
        OPENCLAW_HOOKS_TOKEN,
        OPENCLAW_CHANNEL,
        OPENCLAW_TO,
        APP_URL,
    } = process.env;

    if (!OPENCLAW_URL || !OPENCLAW_HOOKS_TOKEN || !OPENCLAW_CHANNEL || !OPENCLAW_TO || !APP_URL) {
        throw new Error('Set OPENCLAW_URL, OPENCLAW_HOOKS_TOKEN, OPENCLAW_CHANNEL, OPENCLAW_TO and APP_URL.');
    }

    const openclaw = openclawChannel({
        baseUrl: OPENCLAW_URL,
        // OpenClaw's hooks.token, not the gateway token
        hooksToken: OPENCLAW_HOOKS_TOKEN,
        deliver: { channel: OPENCLAW_CHANNEL, to: OPENCLAW_TO },
        // a link to the approval in the chamvi UI, without the token
        approvalUrl: ({ runId, approvalId }) => `${APP_URL}/ui/approvals?run=${encodeURIComponent(runId)}&approval=${encodeURIComponent(approvalId)}`,
        ...options,
    });

    const deploy = defineTool({
        description: 'Deploy the current release to an environment.',
        inputSchema: z.object({ env: z.enum(['staging', 'prod']) }),
        execute: async ({ env }, { context }) => {
            // every deploy waits for the owner, who is asked in their chat
            const { approved } = await context.channels.findOneOrFail('openclaw').approve({
                title: `Deploy to ${env}?`,
                body: 'The current release goes out now.',
                address: OPENCLAW_TO,
            });

            return approved ? `deployed to ${env}` : 'the owner said no';
        },
    });

    const deployer = defineModelAgent({
        name: 'deployer',
        model,
        instructions: 'Deploy the release the user asks for.',
        tools: { deploy },
    });

    return new Engine({
        store: createMemoryStore({ clock: options.clock }),
        agents: [deployer],
        channels: [openclaw],
        clock: options.clock,
    });
}

Every message becomes one POST <baseUrl><hooksPath>/agent with the body { message, name: 'chamvi', agentId?, deliver: true, channel, to, accountId? }, the hooks token as bearer token and the message key as Idempotency-Key (<runId>:<approvalId> for an approval, the content-addressed emit key for a message). channel is deliver.channel; to is the address the tool passed, or deliver.to (the channel's defaultAddress) when the tool passed none. The text holds the message's title, its body (cut to 8 000 characters), the agent and the run id; for an approval also the expiry and the approvalUrl link when configured (without it, a line saying to answer in the chamvi UI or through POST /runs/:id/approve).

Any 2xx response counts as sent (OpenClaw answers 200 { ok: true, runId } once it admitted the hook). Every other answer throws a ChamviError with code openclaw_request (OpenClaw <status>: <response text>, with retry after <s> for a Retry-After), as does a network failure (OpenClaw request failed: <message> (<cause>), the cause naming e.g. ECONNREFUSED or ENOTFOUND) and a gateway that does not answer within timeoutMs (30 s); an aborted send (the run cancelled, the engine draining) rejects with the abort reason. A failed emit() is a tool error and records nothing, so the next identical emit sends again. A failed approval message goes to the engine's onError, and the engine sends again on the next poll, with the same key and a byte-identical body, until one send succeeds.

Answering approvals

The message carries no token and no MCP call, only the approval link:

  • A link (approvalUrl({ runId, approvalId, agent })), when configured, to the run's approval in the chamvi UI, e.g. https://<app>/ui/approvals?run=<runId>&approval=<approvalId>. The link holds ids only; the owner opens it, signs in and answers through the token-less POST /runs/:id/approve. The signed-in principal needs agent:<agent>:approve to answer and run:read or agent:<agent>:run for the UI to load the run and show its approval.
  • Without approvalUrl, the message says to answer in the chamvi UI or through POST /runs/:id/approve.

The OpenClaw agent cannot approve. The approval's title and body are what the tool wrote, which may repeat untrusted text (an issue body, a mail), so an injected instruction reaches the OpenClaw agent. The message carries no token, and a relay agent whose MCP principal holds only agent:<name>:run has neither a token nor the right to answer: whatever the body says, only the owner, signed in to the UI, approves.

Caveats

  • OpenClaw runs an agent turn on the message (it does not post raw text) and treats it as external content, so the wording in the chat is the OpenClaw agent's.
  • OpenClaw's idempotency replay lives in memory and lasts 5 minutes after the run completed; a restart clears it. chamvi's records (the emit effect, the approval's waiting.notifiedAt and its per-approval lock) are the primary dedupe; the key only covers a response lost within those minutes.
  • A misconfigured hook (400, 401, 404) fails on every send, and an approval message reaches onError on every poll, until it is fixed or the approval times out.
  • OpenClaw ignores an Idempotency-Key over 256 characters; such keys (and keys with whitespace or non-ASCII characters) are sent as sha256:<hex>.
  • Errors never carry the hooks token: it is replaced by [redacted] in the response text and the network failure message, each cut to 500 characters. A hooksToken that fetch could not send as a header (control characters, characters above U+00FF, surrounding whitespace) is refused when openclawChannel() is called.

With @chamvi/app

openclawModule() from @chamvi/channel-openclaw/module reads OPENCLAW_BASE_URL, OPENCLAW_HOOKS_TOKEN, OPENCLAW_CHANNEL, OPENCLAW_TO, OPENCLAW_HOOKS_PATH, OPENCLAW_AGENT_ID and OPENCLAW_ACCOUNT_ID when it is called, links approvals to the dashboard from CHAMVI_PUBLIC_URL and contributes the channel (module chamvi:channel:<name>); tools reach it with ctx.channels.findOne(name). @chamvi/app is an optional peer; the main entry never imports it.

import { createApp } from '@chamvi/app';
import { openclawModule } from '@chamvi/channel-openclaw/module';

export default createApp({ agents, modules: [openclawModule(), openclawModule({ name: 'ops' })] });

Options

OptionTypeDefaultDescription
baseUrlstringrequiredThe gateway, e.g. http://openclaw:18789; an absolute http:/https: URL without credentials, query or hash. A base path is kept
hooksTokenstringrequiredOpenClaw's hooks.token; a valid header value (no control characters, nothing above U+00FF, no surrounding whitespace)
deliver{ channel, to, accountId? }requiredWhere OpenClaw delivers the answer; channel and to must be concrete (last is refused); also the channel's defaultAddress, so a tool may leave the address out; an address the tool passes replaces to
namestring'openclaw'The channel name ctx.channels.findOne(name) takes; a second instance needs its own
hooksPathstring'/hooks'OpenClaw's hooks.path; starts with /, not / itself; trailing slashes are dropped
agentIdstringnoneThe OpenClaw agent that phrases the message
approvalUrl({ runId, approvalId, agent }) => stringnoneA link to the approval in the chamvi UI, added to approval messages; it carries no token (see Answering approvals)
timeoutMsnumber30_000Bound of one send, on the clock
clockIClocksystemClockTime source of the timeout
fetchtypeof fetchglobalThis.fetchThe HTTP client (tests pass a fake)

Every option is checked when openclawChannel() is called; a bad one throws InvalidAgentDefinitionError.

API

ExportKindPurpose
openclawChannelfunctionopenclawChannel(options): an out-only IChannel posting to OpenClaw's hooks endpoint
OPENCLAW_CHANNEL_NAMEconstant'openclaw', the default name
DEFAULT_HOOKS_PATHconstant'/hooks'
OPENCLAW_TIMEOUT_MSconstant30_000, the default timeoutMs
OPENCLAW_REQUEST_ERROR_CODEconstant'openclaw_request', the code of every send failure
MESSAGE_BODY_MAX_LENGTHconstant8_000, the cut of the message body

Types: OpenclawChannelOptions, OpenclawApprovalRef, OpenclawDelivery, OpenclawHookBody.

@chamvi/channel-openclaw/module: openclawModule, OpenclawModule, type OpenclawModuleOptions.

License

Made with 💚

Published under Apache License 2.0.

版本与发布记录

@chamvi/channel-openclaw 共发布 15 个版本,首次发布于 2026/10/25;过去 12 个月发布了 15 个版本。最新版本附带 npm provenance 构建来源证明,可追溯到公开的 CI 构建记录。

版本发布日期距今
0.8.0latest2026/11/09今天
0.7.32026/11/063 天前
0.7.22026/11/053 天前
0.7.12026/11/054 天前
0.7.02026/11/045 天前
0.6.12026/11/045 天前

关于 @chamvi/channel-openclaw 的常见问题

@chamvi/channel-openclaw 是什么?

OpenClaw channel for chamvi agents.

如何安装 @chamvi/channel-openclaw?

在项目目录运行 npm install @chamvi/channel-openclaw,或使用 pnpm add / yarn add / bun add。它还需要宿主项目提供 peer 依赖:@chamvi/core(^0.8.0)。npm 7 及以上与 pnpm 8 及以上默认会自动安装必需的 peer 依赖,Yarn 需要手动安装。

@chamvi/channel-openclaw 支持 TypeScript 吗?

支持。包内自带 TypeScript 类型声明(package.json 声明了 types 入口),安装后即可获得类型提示,无需再安装 @types 包。

@chamvi/channel-openclaw 支持 ESM 还是 CommonJS?

以 ES Module 形式发布,推荐使用 import 引入。在 CommonJS 项目中,Node.js 20.19+ / 22.12+ 可以直接 require() 不含顶层 await 的 ES Module,更早的版本需要改用动态 import()。

@chamvi/channel-openclaw 需要什么版本的 Node.js?

package.json 的 engines 字段要求 Node.js >=26.0.0。版本不符时 npm 默认只给出警告,开启 engine-strict 后会拒绝安装。

@chamvi/channel-openclaw 可以商用吗?

@chamvi/channel-openclaw 使用 Apache-2.0 许可。宽松许可:允许商业使用、修改和再分发,并附带明确的专利授权;分发时需保留许可与 NOTICE 文件,修改过的文件要注明改动。以上为概括,不构成法律意见,请以许可原文为准。

@chamvi/channel-openclaw 的下载量有多少?

根据 npm 官方下载统计,@chamvi/channel-openclaw 在 2026/10/10 – 2026/11/08 这 30 天内被下载 2,950 次,日均约 98 次。下载量会把 CI 构建、镜像同步和作为其他包依赖的安装一并计入,适合横向比较热度,不能直接等同于使用人数。

@chamvi/channel-openclaw 还在维护吗?

最新版本 0.8.0 发布于 2026/11/09(今天);至今共发布 15 个版本,其中过去 12 个月发布了 15 个;首次发布于 2026/10/25。发布频率只是参考信号:功能稳定的工具可能很少需要更新,建议结合源码仓库的 issue 和提交记录判断。