Skip to content

GoPlausible/ac2-sdk

Repository files navigation

AC2 SDK

ac2-sdk on npm ac2-plugin-openclaw on npm Download Regent APK license

TypeScript SDK for the AC2 — Agentic Communication and Control Protocol. AC2 is a protocol for secure, identity-bound communication between autonomous agents and Controllers (wallets), built on Liquid Auth + WebRTC + DIDComm v2.

It is the protocol library: messages, envelopes, signaling, sessions, streaming, file transfer, replay defense, DID resolution. It does not include UI, wallet plumbing, or key management — those live in the Controller (the wallet that approves signing requests) and in agent-side plugins.

Companion projects in this monorepo — each maintains its own README. The SDK README focuses on the SDK and protocol; nothing else.

Status (v0)

Component Version
@goplausible/ac2-sdk 0.0.8
@goplausible/ac2-plugin-openclaw 0.0.85
regent (reference Controller — download APK) 1.1.5
@goplausible/liquid-auth-cloud (relay, internal) 1.3.0

v0 ships the core protocol over WebRTC with a PlainEnvelope (no DIDComm wrapping yet). DIDComm v2 wrapping is planned post-POC; the port lives in the standalone DIDCOMM-TS repository.

What's working end-to-end: bidirectional text chat, ed25519 sign/verify HITL, seamless reconnect across idle / app-suspend / network-blip / plugin-restart, biometric-gated approval, snake_case wire format aligned with DIDComm v2, the agent badge classifier (sticky liquid → agent mode upgrade on first AC2 envelope), Notice envelopes for agent-side status, and chat-limbo grace timeout for survival across user back-out.

Known v0 deviations from the spec (channel label, single-channel transport, to[] optional, ac2/Stream* and ac2/AttachmentBegin SDK profiles, Approval* not in spec) are noted in the SDK source comments.

Install

npm install @goplausible/ac2-sdk
# or
pnpm add @goplausible/ac2-sdk
# or
yarn add @goplausible/ac2-sdk

Peer requirements:

  • Node >= 20 (the package is ESM-only).
  • A WebRTC implementation — browser-native RTCPeerConnection / RTCDataChannel in the browser, @roamhq/wrtc (or compatible) in Node.

The SDK does not bundle a WebRTC implementation; you bring your own and pass an RTCDataChannel (or pair of channels) into the session.

Quick start — agent side

Wire the SDK on the agent (Node) side once Liquid Auth has handed you a live RTCDataChannel. The pattern: build channels, build a session, send ac2/SigningRequest, await ac2/SigningResponse.

import {
  AC2Session,
  attachAC2Channels,
  PlainEnvelope,
  pubkeyToDidKey,
  type SigningRequest,
  type SigningResponse,
} from "@goplausible/ac2-sdk";

// 1. Wrap the existing Liquid Auth control channel into AC2's transport shape.
//    v0 is single-channel; pass the peer connection plus the control channel.
const channels = await attachAC2Channels(peerConnection, controlDataChannel);

// 2. Build the session. `from` is the agent's DID, `to` is the wallet's DID
//    (resolved via Liquid Auth attestation).
const agentDid = pubkeyToDidKey(agentEd25519PublicKey);

const session = new AC2Session({
  channels,
  identity: { from: agentDid, to: [walletDid] },
  envelope: new PlainEnvelope(), // default; replace once DIDComm v2 lands
});

// 3. Hook responses before sending the request.
const pending = new Promise<SigningResponse>((resolve, reject) => {
  const off = session.onMessage((msg) => {
    if (msg.type === "ac2/SigningResponse" && msg.thid === reqId) {
      off();
      resolve(msg);
    } else if (msg.type === "ac2/SigningRejected" && msg.thid === reqId) {
      off();
      reject(new Error((msg.body as { reason: string }).reason));
    }
  });
});

// 4. Send the SigningRequest. The Controller wallet (Regent) prompts the
//    user, gates with biometrics, signs with the right key, and emits the
//    SigningResponse over the same channel.
const reqId = crypto.randomUUID();
const request: SigningRequest = {
  id: reqId,
  type: "ac2/SigningRequest",
  from: agentDid,
  to: [walletDid],
  created_time: Math.floor(Date.now() / 1000),
  body: {
    description: "Sign this proposal",
    encoding: "base64",
    payload: btoa("hello world"),
    key_type: "identity",      // 'account' or 'identity'
    display_hint: "text",      // 'text' | 'json' | 'hex'
  },
};
await session.send(request);

const response = await pending;
console.log("signature:", response.body.signature);
console.log("public key:", response.body.public_key);

Quick start — Controller (wallet) side

Most consumers of the SDK on the wallet side won't construct a session manually — they'll either use Regent as-is or implement a WithAC2 extension. But for reference, the parsing path looks like this:

import { validateMessage, type AC2Message } from "@goplausible/ac2-sdk";

controlChannel.addEventListener("message", async (ev) => {
  if (typeof ev.data !== "string") return;
  let parsed: unknown;
  try {
    parsed = JSON.parse(ev.data);
  } catch {
    return; // not an AC2 envelope; treat as chat text
  }
  if (!validateMessage(parsed)) return;
  const msg: AC2Message = parsed;
  switch (msg.type) {
    case "ac2/SigningRequest":
      await handleSigningRequest(msg);
      break;
    case "ac2/Notice":
      surfaceNotice(msg);
      break;
    // ... other types
  }
});

For a complete reference implementation see Regent's useConnection hook which handles the full lifecycle: incoming envelope dispatch, sticky mode-upgrade, signing-modal flow, biometric gate, notice banners, and reconnect resilience.

Modules & exported API

The SDK is intentionally small and tree-shakeable. Top-level exports from @goplausible/ac2-sdk:

Module Key exports Purpose
identity pubkeyToDidKey, algorandAddressToDidKey, DidKeyResolver, DidWebResolver, Did, DidDocument, DidResolver DID derivation & resolution (did:key, did:web).
transport attachAC2Channels, AC2Signaling, AC2Channels, CONTROL_CHANNEL_LABEL, DATA_CHANNEL_LABEL, FILE_CHUNK_SIZE Wraps Liquid Auth signaling + DataChannels into AC2's AC2Channels shape.
envelope PlainEnvelope, Envelope, PackedEnvelope Pluggable envelope layer. v0 uses PlainEnvelope; DIDComm v2 lands later as a drop-in.
messages AC2Message, SigningRequest, SigningResponse, SigningRejected, StreamRequest, StreamResponse, StreamChunk, AttachmentBegin, AttachmentRef, Notice, validateMessage Wire-message types + Zod-free runtime validator.
session AC2Session, AC2SessionOptions, SessionIdentity, MessageHandler, StreamEventHandler High-level session: send/receive AC2 messages, manage streams, attachments, replay dedupe.
streaming StreamReader, StreamWriter, StreamEvent, StreamUsage Chunked streaming primitives used internally by AC2Session.writeStreamChunk/onStream.
files FileSender, FileReceiver, ReceivedFile Attachment send/receive over the ac2-data binary channel (cold-path in v0).
replay ReplayDedupe LRU dedupe keyed by (channel, message id). Used by default by AC2Session.

Wire profile (v0 — single-channel)

v0 runs single-channel: AC2 messages travel as JSON text frames on the existing liquid DataChannel from liquid-auth-js. The optional second ac2-data binary channel for file transfer is not wired in v0 (machinery exists in src/files/ but is cold-path).

Channel Label on the wire Type Purpose
Control liquid text All AC2 protocol messages (signing trio + lenient plain text), reliable + ordered.
Data ac2-data binary Reserved; not created in v0. File chunks when wired.

Spec target label is ac2-v1 (see specs/ac2.md § WebRTC DataChannel Transport). Switching to ac2-v1 requires a coordinated SDK + plugin + Controller release; tracked as a v0 deviation.

Mode: lenient text on the control channel (Path B). Plain text from the wallet is treated as user chat; JSON frames are parsed as AC2 envelopes (ac2/SigningRequest / ac2/SigningResponse / ac2/SigningRejected / ac2/Notice). Live duplex audio and video are out of scope for v0.

SDK layout

src/
├── identity/    DID derivation + resolution (did:key, did:web)
├── transport/   AC2Signaling + AC2Channels (wraps Liquid Auth DataChannels)
├── envelope/    PlainEnvelope (default); DIDComm v2 later
├── messages/    Wire-message types + runtime validator
├── streaming/   StreamReader / StreamWriter
├── files/       FileSender / FileReceiver (cold-path in v0)
├── replay/      Per-channel replay dedupe
└── session/     AC2Session — high-level send/receive

For the OpenClaw plugin source, see packages/ac2-plugin-openclaw/. For the Controller reference, see vendor/regent/. Both have their own READMEs.

Build & test

npm install
npm run build       # tsc → dist/
npm test            # vitest run
npm run typecheck   # tsc --noEmit

Pack a fresh SDK tarball:

# 1. Bump version in package.json + package-lock.json.
# 2. Build, then pack.
npm run build
npm pack --pack-destination /tmp
# → /tmp/goplausible-ac2-sdk-<version>.tgz

Recently shipped

A short log of what landed in the SDK + ecosystem since v0 first ran end-to-end. Detailed history is in commit logs and the per-component changelogs.

  • Sticky liquid → agent mode upgrade on first structured envelope — wallets can classify the badge before the first AC2 message even arrives if the agent emits a session.opened Notice on bind.
  • ac2/Notice envelope wired end-to-end — agent emits curated error/warn/info; Controller surfaces banners + persists to per-connection logs with FIFO rotation.
  • agent_type self-identification lifted from any nesting (root / body / metadata, snake or camel) — free-form so new agent authors slot in without coordinating with the Controller.
  • Lenient text mode (Path B) on the control channel so non-AC2 chat from the wallet doesn't error out the AC2 parser.
  • Replay dedupe scoped per channel label (liquid / ac2-data) so future second-channel addition doesn't cross-pollinate.
  • OpenClaw plugin v0.0.39 — depends on SDK v0.0.8 via npm (no longer bundles). Sign tool exposes both display_hint (UX preview, spec-stable text/json/hex) and sig_hint (new spec field — explicit cryptographic-operation selector with 9 values: raw-ed25519, raw-secp256k1, message-algorand, message-evm, message-solana, typed-data-evm, transaction-{algorand,evm,solana}). Ten verifier tools — one for every sig_hint value, names mapped 1:1 — including the three transaction verifiers (algosdk / viem / @solana/web3.js-backed). Proactive session.opened Notice + agent_type: "openclaw" on every envelope.

SDK / protocol TODO

Items in flight on the SDK and spec side. Regent-side and Controller-UX items live in the Regent README.

  • DIDComm v2 envelope — v0 ships PlainEnvelope; the spec target is full DIDComm v2 wrapping (from always present, to[] required, replay defense, audit, forward routing). The Envelope interface is the swap-in point. Plan parked at vendor/didcomm-ts/PORT_PLAN.md.
  • TypeScript DIDComm port from vendor/didcomm-rust (SICPA reference). No reliable pure-TS DIDComm SDK exists today; the port becomes @goplausible/didcomm and unblocks the DIDComm v2 envelope above.
  • ac2/AttachmentBegin + ac2-data binary channel — file-transfer machinery exists in src/files/ but is cold-path. Wire the second negotiated DataChannel on both peers, add the Regent-side recording UX, ship voice messages and file attachments for chats.
  • Discovery spec implementation — spec drafted at specs/ac2-ext-discovery.md; SDK + plugin code still to come.
  • A2A extension — spec drafted at specs/ac2-ext-a2a.md; agent-to-agent communication primitives.
  • AP2 mandates — generation + verification of agent payment mandates; envelope type and helper functions.
  • MPP session spec + matching SDK primitives — the Multi-Party Payment session work is gated on the spec landing first.
  • ERC-8004 support — agent-side, for AC2 sessions that involve EVM signing.
  • Claude plugin — sibling to OpenClaw under packages/. Top-level agent_type: "claude" is already wired into Regent's badge classifier; the plugin itself is open.
  • Codex plugin — same shape as Claude, for the Codex runtime. Third reference plugin alongside OpenClaw + Claude.
  • ac2 CLI — auto-injects the AC2 toolset into an existing agent runtime: detects the host framework, installs + configures the matching plugin, scaffolds the user-journey config, and provides the agent with signing-request / verification-request tooling. Goal: drop AC2 into an existing agent in one command.

SDK / protocol ROADMAP

Bigger arcs, not strictly ordered:

  • Channel-label cutover to ac2-v1. Coordinated SDK + plugin + Controller release that flips the control channel label from liquid to the spec-target ac2-v1. Tracked as a v0 deviation.
  • Second-channel (ac2-data) negotiation turned on by default once @roamhq/wrtc renegotiation interference is verified resolved — unblocks live file transfer and large attachments end-to-end.
  • Notice as the agent-side observability stream. Today four sinks (banner, log, activity, badge counter). Future: structured fields per code, per-tenant filtering, severity-aware retry/backoff hints carried in the envelope.
  • Pluggable session policies. Per-session signing-key policy ("this agent can only request key_type: identity"), per-agent rate limits, time-bound capability tokens — all enforced at the SDK boundary so Controllers don't have to reimplement the logic.
  • Test-vector parity with the Rust DIDComm reference — the TS port lands with a vector suite that round-trips against didcomm-rust outputs.

Contributing

This SDK is part of GoPlausible. Issues, PRs, and protocol-spec discussions welcome — see specs/ for the wire formats.

License

Apache-2.0

About

AC2 - Agentic Communication and Control Protocol

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors