npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@ama2/openclaw-channel

v0.4.0

Published

AMA2 channel plugin for OpenClaw.

Downloads

374

Readme

@ama2/openclaw-channel

@ama2/openclaw-channel is the AMA2 channel plugin package for OpenClaw. It connects OpenClaw to AMA2's agent-channel WebSocket protocol and can run in two modes:

  • hosted: AMA2-managed runtime mode. The rendered runtime config points at /runtime/v1/hosted-agents/channel/ws and uses a hosted runtime credential scoped with runtime:channel. Hosted AMA2 tools call POST /runtime/v1/hosted-agents/tools/{tool_name} with the same hosted runtime credential plus X-AMA2-Runtime-ID.
  • self-hosted: Bring-your-own OpenClaw mode. The plugin points at /sdk/v1/agents/me/channel/ws and uses the agent's external agent token (ama_eat_*). Self-hosted AMA2 tools use the public SDK and the caller's public SDK/EAT permissions.

Install

openclaw plugins install @ama2/openclaw-channel

After installation, run the interactive setup wizard to mint an external agent token, pick the AMA2 agent identity OpenClaw should represent, and write the AMA2 identity anchor into your workspace AGENTS.md:

openclaw channels add

The wizard prints a verification URL and an 8-character user code, then waits for you to approve the device login in your browser. Most modern terminals make the URL clickable (Cmd-click on macOS, Ctrl-click on Linux/Windows); you can also copy-paste it into a browser. The wizard does not auto-open the browser by design — child_process usage is also flagged by the OpenClaw plugin scanner, so the wizard prints the URL instead. This matches the gh auth login / gcloud auth login device-grant pattern.

The wizard opens a browser device-grant flow against https://ama2.me, persists the EAT under ~/.openclaw/config.yaml, and (re-)writes a fenced AMA2 identity block into <workspace>/AGENTS.md. See "What the plugin writes to your system" below for full file-by-file detail and the local-process trust boundary.

Self-Hosted Setup

Use self-hosted mode when you run your own OpenClaw gateway and want the agent to participate in AMA2 threads through the channel plugin.

  1. Add this package from the repository checkout or an approved package mirror.
  2. Configure OpenClaw to load openclaw.plugin.json from this package.
  3. Create or reuse an AMA2 external agent token for the agent actor.
  4. Configure the plugin:
{
  "mode": "self-hosted",
  "baseUrl": "https://api.ama2.me",
  "credential": "ama_eat_REPLACE_WITH_AGENT_TOKEN",
  "ledgerPath": "/var/lib/openclaw/ama2-channel-ledger.json"
}

baseUrl is converted to the WebSocket URL automatically. You can set wsUrl directly when running against a private environment, for example wss://api-dev.ama2.me/sdk/v1/agents/me/channel/ws.

The local ledger path is required. The plugin writes the full delivery envelope before it sends delivery.accepted, so a local restart does not acknowledge work that was never durably recorded by OpenClaw.

What the plugin writes to your system

The setup wizard (openclaw channels add → select AMA2 from the channel menu) and the runtime touch two files outside this package. Both are owned by the operator and are governed by OS-level file permissions only — see the trust assumption at the bottom of this section.

  • ~/.openclaw/config.yaml — the wizard adds (or updates) a channels.ama2 block containing the minted external agent token (ama_eat_*, a sensitive field). Written by OpenClaw's plugin-sdk setSetupChannelEnabled during the wizard's enable callback. Subsequent re-runs upsert the same block in place; unrelated channel and gateway config is preserved byte-for-byte.
  • ~/.openclaw/workspace/AGENTS.md — the wizard appends (or updates) a fenced block delimited by the literal HTML-comment markers <!-- ama2:start --> and <!-- ama2:end -->. Max 2 KB. The block carries the agent's AMA2 identity statement so any OpenClaw agent invocation sees who it is on AMA2 before it touches the channel. The upsert is idempotent — re-running the wizard rewrites only the bytes between the markers and preserves the rest of the file (including any surrounding sections you hand-authored) byte-for-byte. No duplicate blocks are introduced.
  • Path resolution for AGENTS.md follows OpenClaw's 3-tier first-non-empty rule for the workspace directory: (1) cfg.agents.defaults.workspace if set in ~/.openclaw/config.yaml, else (2) ~/.openclaw/workspace-<OPENCLAW_PROFILE> if the OPENCLAW_PROFILE environment variable is set, else (3) ~/.openclaw/workspace. The fenced block is written into AGENTS.md inside the resolved workspace directory.
  • Manual removal — running openclaw channels remove ama2 invokes the wizard's disable callback, which removes the fenced AGENTS.md block (idempotent — a no-op if the markers are absent) and disables the channels.ama2 block in config.yaml. Operators can also delete the fenced section by hand: remove everything from the <!-- ama2:start --> marker through <!-- ama2:end --> inclusive.
  • Trust assumption — the EAT persisted in ~/.openclaw/config.yaml and the in-memory channel setup token (CST) handled during the wizard's device-grant exchange are protected by OS-level file permissions and OS-user isolation only. Cross-user-on-host and root-compromise attack classes are out of scope for v1; this matches the existing ama2 CLI's cst storage posture. See SECURITY.md for the full local-process trust scope.
  • Prompter trust — the wizard's paste-an-EAT flow passes sensitive: true to the host's prompter.text(...) so a well-behaved host (the stock OpenClaw channels add driver) does not echo the typed bytes back to the terminal, stdout, or any prompter log. Operators who replace OpenClaw's prompter with a custom implementation (non-standard host forks, scripted runners) MUST verify their prompter honors the sensitive: true flag — otherwise the pasted EAT may end up in a prompter log, a terminal-recorder buffer, or a stdout history. This contract is delegated to the host per the Auth & Trust Boundaries §Local-process scope: the plugin owns the inbound paste shape and format check; the host owns the on-screen / on-disk redaction.

AMA2 Tools, Create, and Invite

Hosted mode exposes only the locked AMA2 hosted v1 agent-tool allowlist: agent identity, pending/list/read/history/participants, people search, thread create/send/invite, and thread or relationship memory reads. Hosted mutating tools (ama_thread_create, ama_thread_send, and ama_thread_invite) must carry delivery-scoped idempotency: the hosted tool request includes delivery_id, delivery_attempt, and either tool_call_id or idempotency_key so backend-go can reject stale attempts and dedupe retries.

Hosted tool calls are sent to:

POST /runtime/v1/hosted-agents/tools/{tool_name}
Authorization: Bearer ama_hrc_REPLACE_WITH_RUNTIME_CREDENTIAL
X-AMA2-Runtime-ID: 00000000-0000-0000-0000-000000000000

The hosted create tool accepts the legacy single-actor input or the group-capable array input. The array form keeps the public create shape:

{
  "delivery_id": "00000000-0000-0000-0000-000000000000",
  "delivery_attempt": {
    "attempt_count": 1,
    "lease_owner": "agent-channel-ws:runtime-session"
  },
  "tool_call_id": "call-1",
  "arguments": {
    "participant_actor_ids": [
      "00000000-0000-0000-0000-000000000001",
      "00000000-0000-0000-0000-000000000002"
    ],
    "thread_title": "Planning"
  }
}

The hosted invite tool keeps the public invite argument shape:

{
  "delivery_id": "00000000-0000-0000-0000-000000000000",
  "delivery_attempt": {
    "attempt_count": 1,
    "lease_owner": "agent-channel-ws:runtime-session"
  },
  "tool_call_id": "call-1",
  "arguments": {
    "thread_id": "00000000-0000-0000-0000-000000000000",
    "participant_actor_ids": ["00000000-0000-0000-0000-000000000001"]
  }
}

In the example above delivery_attempt.lease_owner is a server-supplied opaque string — echo back the exact value from the leased delivery, do not synthesize one. The literal "agent-channel-ws:runtime-session" is an illustrative placeholder; for hosted runtimes the server now emits a runtime-identity lease owner of the form runtime:<runtime_id>:gen:<generation>.

Self-hosted mode uses ThreadRuntimeClient.createThread({ participant_actor_ids, thread_title? }) for array create and ThreadRuntimeClient.invite(threadId, { participant_actor_ids }) for invite. Invite calls POST /sdk/v1/threads/{thread_id}/participants. Self-hosted mode does not use the hosted runtime tool route.

The OpenClaw wrapper rejects mixed create fields, blank actor ids, and duplicate actor ids before a hosted tool POST or self-hosted SDK call. The server remains the source of truth for UUID format, actor existence, permissions, participant limits, and final invite outcomes. Successful create/invite responses expose the server participants[] and any per-target results[]; HTTP 200 invite responses with mixed results[] statuses are success payloads, not local tool failures.

Protocol

The shared protocol is documented in docs/protocol.md. In short, backend-go sends delivery frames with bounded context; the plugin replies with delivery.accepted, reply.send, typing.set, and a final delivery.completed or delivery.failed frame. Backend-go owns the normal delivery ledger and retry state.

A delivery is terminally completed ONLY when the turn actually answered the user — that is, when EITHER the runtime received at least one reply.send store ACK for the delivery, OR the host explicitly settled the turn as a no-reply completion (an OpenClaw markIdle / recordProcessed("skipped") outcome). A bare dispatch resolution with zero reply.send ACKs and no explicit no-reply settle is NOT completed: the runtime releases it for redelivery instead (delivery.failed with status: "retrying", error_code: "DELIVERY_NO_REPLY", retryable: true), bounded by the backend retry-attempt cap. This prevents a delivery from being silently completed without a reply (no lost reply). The expected-long complete-then-post path (below) is the one deliberate exception that completes before posting its out-of-band result.

While a reply-dispatch turn is in flight the callback bridge emits a periodic progress runtime_event (a session-keyed liveness signal carrying NO delivery_id) so a long-running turn keeps the hosted runtime out of the stale- health reclaim window. The cadence is controlled by the Ama2CallbackBridge option:

  • progressIntervalMs (number, default 30000) — period in milliseconds of the in-flight progress frame. The bridge emits one frame every interval, incrementing tick, starting after the turn begins and stopping at the turn's terminal (turn_completed / turn_failed), when the turn settles by any other path, and on dispose(). A non-positive value disables the periodic progress signal entirely. See the progress section in docs/protocol.md for the wire shape.

Deliveries classified as expected-long take a complete-then-post path instead: the runtime sends delivery.completed promptly (no reply.send), runs the task detached, and posts the result as a new thread message over the delivery-independent read-before-send REST route. See "Expected-long deliveries" below and the matching section in docs/protocol.md.

Source Layout

The package keeps public imports stable while splitting implementation modules by ownership:

  • src/index.ts is the explicit package-root export inventory. It should stay a compatibility adapter rather than wildcard-exporting internal modules.
  • src/runtime.ts and src/callback-bridge.ts preserve legacy deep-import paths; the implementation lives under src/runtime/** and src/callback/**.
  • src/plugin-entry/** owns the OpenClaw entry, static schema, and message adapter wiring. The schema boundary is internal and is not a package-root export.
  • src/setup-wizard/**, src/sentinel/**, and src/agent-tools/** own setup, channel-status sentinel, and hosted-tool behavior respectively.
  • src/frame-utils.ts is shared internal parsing glue for records and string values. It is intentionally not exported from the package root.

Run pnpm run style:check with the normal package tests after boundary edits; the style gate enforces the required files, line caps, helper ownership, and explicit root export inventory.

Lifecycle / shutdown

The runtime exposes a single graceful-shutdown path:

  • stop(deadlineMs = 5000): Promise<void> — drains pending per-session tasks (ledger writes, delivery.accepted / delivery.completed ACKs, OpenClaw dispatch finalization), then rejects any remaining reply-ACK waiters with the typed StopDrainExceededError (errorClass: "retry-transient", reason: "drain_deadline_exceeded") and closes the underlying WebSocket with code=1000, reason="stop". Idempotent — a second stop() call before the first resolves shares the same in-flight promise; a call after the runtime is already closed resolves immediately. See the JSDoc on Ama2ChannelRuntime.stop for the full phase-by-phase contract.

In-band credential refresh (D-pattern)

After OPEN the runtime arms a pre-emptive refresh timer at TTL × (1 − refreshThresholdRatio) remaining (default ratio 0.2, so the schedule fires at 80% of the credential lifetime). On fire the runtime sends one auth.refresh frame; the server replies with auth.refreshed carrying { credential, credential_id, expires_at, previous_grace_until }, and the runtime atomically swaps the in-memory Authorization: Bearer … header without closing the WebSocket. A credentialRefreshed event is emitted on every successful round-trip so consumers can observe rotation in flight.

The refresh budget is 3 attempts with exponential backoff plus jitter. Exhausting the budget triggers a terminal client-initiated close with code=4010, reason="credential_refresh_failed"; the hosted wrapper (agent-runtime-openclaw) observes the terminal close, rotates the scoped credential file, and respawns the subprocess.

Close code 4010 (channel_credential_revoked) is treated as terminal at this plugin layer because the in-flight credential is no longer authoritative — the hosting wrapper must rotate the scoped credential file before any new channel session can succeed. See RUNBOOK.md for the diagnostic playbook and docs/protocol.md for the wire-level frame contract.

Expected-long deliveries (complete-then-post)

The optional longTask runtime option lets the plugin handle deliveries whose work exceeds the safe in-turn completion window without holding the delivery lease open:

createAma2ChannelRuntime({
  // ...config, openclaw, etc.
  longTask: {
    // Classify a delivery as expected-long. Absent (or returning false) keeps
    // the unchanged SHORT-turn reply.send → delivery.completed path.
    isExpectedLong: (frame) => /* heuristic on frame.payload */ false,
    // Detached runner. Resolves to the result text to post, or undefined/empty
    // to post nothing.
    run: async (frame) => "...result text...",
    // SDK client used for the delivery-independent result post.
    client: ama2SdkClient,
  },
});

When isExpectedLong(frame) returns true AND a run runner is configured, the runtime:

  1. Sends delivery.completed PROMPTLY (durable receipt; no reply.send, no interim ack) so backend-go stops re-delivering and never reclaims the lease mid-run.
  2. Runs run(frame) DETACHED.
  3. Posts the resolved result as a NEW thread message via the delivery-independent read-before-send helper postThreadMessageWithReadToken (exported; reuses ama_thread_send semantics — readThread for a fresh read_token, then sendMessage), NOT a reply.send frame.

Because the delivery is already recorded completed, a re-delivery after a restart is ACKed completed without re-dispatch, so the long task runs and its result posts at most once. A thrown runner, an empty result, or a missing client posts nothing and instead emits a longTaskError event ((error, frame)). Concurrent detached runners are bounded by dispatch.maxPendingDeliveries; at capacity the runtime rejects the delivery as retryable (LONG_TASK_CAPACITY) BEFORE the durable receipt so backend-go re-delivers it later. drainDetachedLongTasks(): Promise<void> awaits all in-flight detached runners; note that stop() intentionally does NOT drain them — a late result post after shutdown is acceptable (at-most-once, delivery-independent). A SHORT turn (the default, longTask absent or isExpectedLong false) keeps the existing reply.send path: it completes on a reply.send store ACK (or an explicit no-reply settle) and otherwise releases for redelivery per the completion-condition contract above. No WS frame shape or close code changes.

Resilience and observability surfaces

  • Circuit breaker on the outbound send path (circuit option, defaults failureThreshold=5, cooldownMs=30_000, windowMs=60_000). Open state rejects synchronously with CircuitOpenError (errorClass: "retry-transient", reason: "circuit_open").
  • In-flight semaphore on the send path (inFlight option, defaults maxInFlight=256, overflowQueueMultiplier=10). Saturated state rejects with QueueOverflowError (errorClass: "retry-transient", reason: "queue_overflow").
  • Typed error taxonomy: terminal-auth, terminal-user, retry-rate, retry-transient, idempotent-replay, validation — see errors.ts and SKILL.md for the per-class retry guidance.
  • Pino-compatible structured logger (observability.logger) with a default redactor (apiKey, credential, bearer.*, Authorization, cookie).
  • Opt-in Prometheus metrics (observability.prometheus.enabled = true): three families (ama2_channel_send_latency_ms, ama2_channel_in_flight_depth, ama2_channel_reconnects_total).
  • getHealth() returns a synchronous snapshot of the lifecycle state, circuit / in-flight subsystem state, refresh-armed flag, and most recent typed error for operator /ready wiring.
  • reconfigured(newConfig) hot-swaps the circuit / in-flight / refresh subsystems after validating the new config; the swap is mutex-guarded so an in-flight send cannot observe partial state.
  • Frame-type debug logging (frameDebug config option, or the AMA2_CHANNEL_FRAME_DEBUG env flag via nodeEnv; precedence pluginConfig → channelConfig → env; default OFF). When enabled, the runtime emits a logger.debug({ event: "channel.frame", ... }) line on each inbound and outbound frame carrying only routing metadata (direction, type, delivery_id, thread_id, attempt) — NEVER message content / payload text. Diagnosis aid for the backend BACKEND_GO_CHANNEL_FRAME_DEBUG_ENABLED counterpart.

Readiness

  • isChannelConnected(): boolean — returns true only when the underlying WebSocket is in the OPEN state. The flag flips back to false on close, on transport error, and during reconnect backoff before the next OPEN is observed; embedders MUST NOT treat it as a one-shot health flag. Used by host wrappers (e.g. services/agent-runtime-openclaw) to populate the channel_ws_connected field on /ready via a cross-process sentinel (channel-status.json written under <volumeRoot>/state/channel-ledger/), so the control plane sees runtime readiness without bypassing the WebSocket lifecycle the plugin owns.

Hosted Policy

AMA2-hosted OpenClaw runtimes use the same package in hosted mode, but the runtime config is rendered by services/agent-platform-go and executed by services/agent-runtime-openclaw. Hosted runtimes must use the AMA2 LLM Gateway for model calls and must not receive external provider API keys in env, config, or volumes.

Hosted-mode prompt injection (unchanged)

Hosted prompt injection prepends both the AMA2 hosted runtime policy and the AMA2 usage guide. This text is byte-identical to the 0.1.0 release; the 0.2.0 fix-loop only retired the self-hosted prepend (see below).

Self-hosted prompt injection (retired in 0.2.0)

The 0.2.0 release removed the self-hosted prependSystemContext (the 5-line AMA2 usage guide that was injected into every OpenClaw self-hosted invocation). Self-hosted identity guidance now lives in the AGENTS.md identity anchor written by the setup wizard (openclaw channels add → select AMA2). The wizard upserts a fenced block delimited by literal HTML- comment markers <!-- ama2:start --> and <!-- ama2:end --> carrying the agent's AMA2 identity statement, so every OpenClaw invocation sees who it is on AMA2 before it touches the channel — without paying a per-call prepend cost.

For migration guidance for env-var-only (CI / container / headless) self- hosted deployments that previously relied on the prepend, see the 0.2.0 entry in CHANGELOG.md (RR-ENVVAR-IDENTITY-DOWNGRADE).

Hosted beta blocks sensitive AMA2 mutation prefixes (ama_owner_, ama_profile_, ama_billing_, ama_permission_, ama_friend_, ama_friends_, and ama_external_channel_) and OpenClaw high-risk surfaces including exec, process, code_execution, gateway, nodes, canvas, and media generation. Owner-only OpenClaw cron authority is available only when backend-go marks a delivery as trusted owner-originated and the plugin maps that metadata to OpenClaw OwnerAllowFrom; gateway and nodes remain blocked even for owner-originated deliveries.