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
| OpenClaw | PEAC Location |
|---|---|
| Session key | workflow_id |
| Run ID + tool_call_id | interaction_id |
| Tool call params | input.digest |
| Tool call result | output.digest |
| Tool name | tool.name |
| Sandbox mode | policy.sandbox_enabled |
| Elevated flag | policy.elevated |
Payload Hashing
Payloads are hashed, not stored in plaintext. Large payloads are truncated before hashing.
| Size | Algorithm | Label |
|---|---|---|
| ≤ 1 MB | Full SHA-256 | sha-256 |
| > 1 MB | First 1 MB SHA-256 | sha-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
| Code | Description |
|---|---|
E_OPENCLAW_MISSING_FIELD | Required field missing in event |
E_OPENCLAW_INVALID_FIELD | Invalid field value in event |
E_OPENCLAW_SERIALIZATION_FAILED | Payload serialization failed |
E_OPENCLAW_SIGNING_FAILED | Receipt signing failed |
Warning Codes
| Code | Description |
|---|---|
W_OPENCLAW_PAYLOAD_TRUNCATED | Payload exceeded 1 MB limit |
W_OPENCLAW_OPTIONAL_FIELD_MISSING | Optional field not present |
W_OPENCLAW_UNKNOWN_PROVIDER | Unrecognized 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