Skip to content
v0.10.13Since v0.10.7

@peac/capture-core

Runtime-neutral capture pipeline for PEAC interaction evidence. No filesystem or Node.js dependencies. Runs anywhere with WebCrypto.

Install

pnpm add @peac/capture-core

Quick Start

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

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

const result = await session.capture({
  id: 'action-001',
  kind: 'tool.call',
  platform: 'my-agent',
  started_at: new Date().toISOString(),
  tool_name: 'web_search',
  input_bytes: new TextEncoder().encode('{"query": "hello"}'),
  output_bytes: new TextEncoder().encode('{"results": []}'),
});

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

await session.close();

Determinism Contract

Identical inputs produce identical outputs. The same action stream produces the same chain digests across sessions.

Entry Digest

  • JCS (RFC 8785) serialization
  • SHA-256 of canonical JSON bytes
  • 64 lowercase hex chars

Timestamp Derivation

captured_at = action.completed_at ?? action.started_at

Wall clock is not used.

Genesis Digest

The first entry has prev_entry_digest set to GENESIS_DIGEST:

0000000000000000000000000000000000000000000000000000000000000000

64 zeros. Not the SHA-256 of an empty string.

Payload Hashing

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

The bytes field contains original size for audit.

Concurrency

Single-Writer Per Session

Each CaptureSession maintains internal state. Create one session per agent/workflow.

Capture Serialization

Concurrent capture() calls are serialized automatically:

// Runs sequentially to maintain chain integrity
const [r1, r2, r3] = await Promise.all([
  session.capture(action1),
  session.capture(action2),
  session.capture(action3),
]);

Never-Throw Guarantee

capture() never throws. All failures return as CaptureResult.

Error Codes

CodeDescription
E_CAPTURE_DUPLICATEAction ID already captured
E_CAPTURE_INVALID_ACTIONMissing required fields
E_CAPTURE_HASH_FAILEDHashing operation failed
E_CAPTURE_STORE_FAILEDStorage backend failed
E_CAPTURE_SESSION_CLOSEDSession was closed
E_CAPTURE_INTERNALUnexpected internal error

Exports

import {
  // Constants
  GENESIS_DIGEST,     // 64 zeros
  SIZE_CONSTANTS,     // { TRUNC_64K, TRUNC_1M }

  // Factories
  createHasher,
  createCaptureSession,

  // Mappers
  toInteractionEvidence,
  toInteractionEvidenceBatch,

  // Types
  type CapturedAction,
  type SpoolEntry,
  type CaptureResult,
  type Hasher,
  type SpoolStore,
  type DedupeIndex,
} from '@peac/capture-core';

Testkit

In-memory implementations for testing. Not for production.

import {
  createInMemorySpoolStore,
  createInMemoryDedupeIndex,
} from '@peac/capture-core/testkit';

Custom Backends

SpoolStore

interface SpoolStore {
  append(entry: SpoolEntry): Promise<void>;
  getHeadDigest(): Promise<string>;
  getSequence(): Promise<number>;
  commit(): Promise<void>;
  close(): Promise<void>;
}

DedupeIndex

interface DedupeIndex {
  has(actionId: string): Promise<boolean>;
  get(actionId: string): Promise<DedupeEntry | undefined>;
  set(actionId: string, entry: DedupeEntry): Promise<void>;
  markEmitted(actionId: string): Promise<boolean>;
  delete(actionId: string): Promise<boolean>;
  size(): Promise<number>;
  clear(): Promise<void>;
}

Used By

Links