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.
packages/ac2-plugin-openclaw/— reference agent-side plugin (OpenClaw channel plugin). Includes its own build & pack instructions.vendor/regent/— reference Controller wallet (RN / Expo). Includes its own architecture, key model, and build lifecycle.specs/— AC2 protocol specifications: core, pre-authorized, a2a, discovery.
| 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.
npm install @goplausible/ac2-sdk
# or
pnpm add @goplausible/ac2-sdk
# or
yarn add @goplausible/ac2-sdkPeer requirements:
- Node >= 20 (the package is ESM-only).
- A WebRTC implementation — browser-native
RTCPeerConnection/RTCDataChannelin 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.
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);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.
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. |
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.
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.
npm install
npm run build # tsc → dist/
npm test # vitest run
npm run typecheck # tsc --noEmitPack 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>.tgzA 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 → agentmode upgrade on first structured envelope — wallets can classify the badge before the first AC2 message even arrives if the agent emits asession.openedNotice on bind. ac2/Noticeenvelope wired end-to-end — agent emits curated error/warn/info; Controller surfaces banners + persists to per-connection logs with FIFO rotation.agent_typeself-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-stabletext/json/hex) andsig_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 everysig_hintvalue, names mapped 1:1 — including the three transaction verifiers (algosdk/viem/@solana/web3.js-backed). Proactivesession.openedNotice +agent_type: "openclaw"on every envelope.
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 (fromalways present,to[]required, replay defense, audit, forward routing). TheEnvelopeinterface is the swap-in point. Plan parked atvendor/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/didcommand unblocks the DIDComm v2 envelope above. ac2/AttachmentBegin+ac2-databinary channel — file-transfer machinery exists insrc/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-levelagent_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.
ac2CLI — 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.
Bigger arcs, not strictly ordered:
- Channel-label cutover to
ac2-v1. Coordinated SDK + plugin + Controller release that flips the control channel label fromliquidto the spec-targetac2-v1. Tracked as a v0 deviation. - Second-channel (
ac2-data) negotiation turned on by default once@roamhq/wrtcrenegotiation 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-rustoutputs.
This SDK is part of GoPlausible. Issues, PRs, and protocol-spec discussions welcome — see specs/ for the wire formats.
Apache-2.0