Skip to content
v0.10.13Since v0.10.7

OpenClaw Adapter

Records OpenClaw tool calls as signed PEAC receipts. Each receipt contains an InteractionEvidence extension at evidence.extensions["org.peacprotocol/interaction@0.1"].

Built on @peac/capture-core.

Install

pnpm add @peac/adapter-openclaw

Quick Start

example.tsTypeScript
import { createCaptureSession, createHasher } from '@peac/capture-core';
import { createInMemorySpoolStore, createInMemoryDedupeIndex } from '@peac/capture-core/testkit';
import { createHookHandler } from '@peac/adapter-openclaw';

const session = createCaptureSession({
  store: createInMemorySpoolStore(),
  dedupe: createInMemoryDedupeIndex(),
  hasher: createHasher(),
});

const handler = createHookHandler({
  session,
  config: {
    platform: 'openclaw',
    platform_version: '0.2.0',
  },
});

const result = await handler.afterToolCall({
  tool_call_id: 'call_123',
  run_id: 'run_abc',
  tool_name: 'web_search',
  started_at: '2024-02-01T10:00:00Z',
  completed_at: '2024-02-01T10:00:01Z',
  status: 'ok',
  input: { query: 'hello world' },
  output: { results: ['result1'] },
});

if (result.success) {
  console.log('Captured:', result.entry.entry_digest);
}

await handler.close();

Two-Stage Pipeline

1. Capture (sync, < 10ms)

  • Map OpenClaw event to CapturedAction
  • Hash payloads inline
  • Append to tamper-evident spool

2. Emit (async background)

  • Drain spool periodically
  • Convert to InteractionEvidenceV01
  • Sign and write receipt

OpenClaw to PEAC Mapping

OpenClawPEAC Location
Session keyworkflow_id
Run ID + tool_call_idinteraction_id
Tool call paramsinput.digest
Tool call resultoutput.digest
Tool nametool.name
Sandbox modepolicy.sandbox_enabled
Elevated flagpolicy.elevated

Payload Hashing

Payloads are hashed, not stored in plaintext. Large payloads are truncated before hashing.

SizeAlgorithmLabel
≤ 1 MBFull SHA-256sha-256
> 1 MBFirst 1 MB SHA-256sha-256:trunc-1m

The bytes field always contains the original size for audit purposes.

Timestamps

The captured_at field is derived from the action, not wall clock:

captured_at = action.completed_at ?? action.started_at

This makes the chain deterministic. Replaying the same action sequence produces identical digests.

Chain Linking

Each entry links to the previous via prev_entry_digest. The first entry uses GENESIS_DIGEST:

0000000000000000000000000000000000000000000000000000000000000000

This is 64 zeros, not the SHA-256 of an empty string.

Background Emitter

emitter.tsTypeScript
import { createReceiptEmitter, createBackgroundService } from '@peac/adapter-openclaw';

const emitter = createReceiptEmitter({
  signer,
  writer,
  config: { platform: 'openclaw' },
});

const service = createBackgroundService({
  emitter,
  getPendingEntries: () => spoolStore.getPending(),
  markEmitted: (digest) => dedupeIndex.markEmitted(digest),
  drainIntervalMs: 1000,
});

service.start();
// later...
service.stop();

Error Codes

CodeDescription
E_OPENCLAW_MISSING_FIELDRequired field missing in event
E_OPENCLAW_INVALID_FIELDInvalid field value in event
E_OPENCLAW_SERIALIZATION_FAILEDPayload serialization failed
E_OPENCLAW_SIGNING_FAILEDReceipt signing failed

Warning Codes

CodeDescription
W_OPENCLAW_PAYLOAD_TRUNCATEDPayload exceeded 1 MB limit
W_OPENCLAW_OPTIONAL_FIELD_MISSINGOptional field not present
W_OPENCLAW_UNKNOWN_PROVIDERUnrecognized tool provider

Verification

Terminal
# Verify a receipt
peac verify ./receipts/receipt-001.jws

# Create dispute bundle
peac bundle create --receipts ./receipts --output evidence.peacbundle

# Verify bundle offline
peac bundle verify evidence.peacbundle --offline

Links