- Docs
- Bureau — Blue Team (defensive)
- Custody
Bureau — Blue Team (defensive)
Custody
Captures an AI conversation in a form that supports authentication under Federal Rule of Evidence 902(13). The capture binds a WebAuthn-attested operator identity to a canonical DOM snapshot, the full HTTP transcript, and a tamper-evident chain of custody.
Posture: 🔵 Blue Team (defensive) · Status: alpha
What it does
A screenshot of a chatbot interaction is generally weak evidence on its own; opposing counsel can plausibly argue that an image was edited, and a plain text copy carries no metadata. Federal Rule of Evidence 902(13) provides a path for machine-generated records to authenticate themselves when the signing key is bound to a tamper-evident hardware authenticator. Custody captures conversations in a form designed to satisfy that rule.
Custody captures an AI conversation as a bundle that contains: the full canonical DOM snapshot of the page, every HTTP request and response that fired during the session, a browser fingerprint (user-agent, viewport, time-zone, language, plugin-list hash), the system clock at start and end, and an operator-identity binding that proves you held a registered passkey (a WebAuthn credential – a tamper-evident hardware authenticator) when the capture happened. On top of that sits a chain of custody – an append-only chain of signed events (capture, handoff, publication, verification), where every event pins the prior event's hash. Reordering or dropping any event breaks verification. A journalist or opposing-counsel expert can run pluck bureau custody verify ./bundle.json, and in about a second get either exit code 0 (the bundle would survive FRE 902(13)) or exit code 1 with a specific list of broken checks.
Who would use it
- An attorney preserving an AI conversation that will become a trial exhibit.
- A regulator (FTC, FDA, state AG) capturing chatbot output during an investigation.
- A journalist preserving a chatbot's response for a story where the vendor will deny the conversation happened.
- A class-action plaintiff's investigator capturing examples of model behavior before discovery is filed.
- A corporate compliance officer preserving an AI vendor's output as part of a vendor-risk review.
What you'll need
- Node.js 20 or newer.
- The Pluck CLI:
npm i -g @sizls/pluck-bureau-cli. - A registered passkey (WebAuthn credential) – a YubiKey, Apple Touch ID, Windows Hello, or any FIDO2 authenticator. A disk-only key works for the cryptography but flunks FRE 902(13).
- A captures directory: a folder containing
capture-spec.jsonplus achain/subfolder of signed event JSON files. The companion browser extension produces these; you can also build them by hand.
Step-by-step
The library + verifier are functional today. The Chrome / Firefox extension build pipeline ships in a follow-on cycle; until then the scaffolded @sizls/pluck-bureau-custody-extension ships an MV3 manifest and skeleton you can complete locally.
You start with a capture directory. The browser extension records DOM snapshots, fetch pairs, fingerprint, and signs each chain-of-custody event as it happens. You then assemble the bundle:
pluck bureau custody build ./captures-2026-04-23 --out ./bundle.json
Sanity-check the assembled bundle:
pluck bureau custody capture ./bundle.json
Output:
custody/capture: ./bundle.json
schemaVersion: 1
captureSpec: v1
domSnapshotHash: a1b2c3...
fetchEvents: 47
chain length: 12
chainRootHash: 9f3a8b1c...
rekorUuid: (not notarized)
identity binding: webauthn
Now run the journalist's 60-second flow – the verifier:
pluck bureau custody verify ./bundle.json
Output, when everything checks out:
custody/verify: ./bundle.json
ok: yes
fre902Compliant: yes
If anything breaks, you get the specific list:
custody/verify: ./bundle.json
ok: yes
fre902Compliant: NO
reasons:
- identity-binding=disk; WebAuthn required
When the matter is heading to civil litigation or service of subpoena, export an evidence packet that wraps the chain root and every relevant chain-event uuid:
pluck bureau custody export ./bundle.json \
--subpoena <rekor-uuid> \
--vendor openai --model gpt-4o
The packet is a JSON document the receiving party (opposing counsel, regulator, journalist) can verify offline with cosign.
Run it yourself
Drop this into a Node 20+ project (npm install @sizls/pluck-bureau-custody @sizls/pluck-bureau-core tsx):
// index.ts
import {
buildCaptureSpec,
createCustodySystem,
hashPluginList,
} from "@sizls/pluck-bureau-custody";
import { generateOperatorKey } from "@sizls/pluck-bureau-core";
async function main() {
const operator = generateOperatorKey();
const startedAt = "2026-04-23T10:00:00.000Z";
const endedAt = "2026-04-23T10:05:00.000Z";
const system = createCustodySystem({
signingKey: operator.privateKeyPem,
disablePausePoll: true,
disableLogging: true,
});
try {
const captureSpec = buildCaptureSpec({
domSnapshotHash: "a".repeat(64), // sha256 of the canonical full-page DOM
fetchEvents: [
{
sequenceIndex: 0,
method: "POST",
urlCanonical: "https://api.openai.com/v1/chat/completions",
requestEnvelopeHash: "b".repeat(64),
responseEnvelopeHash: "c".repeat(64),
capturedAt: startedAt,
},
],
browserFingerprint: {
userAgent: "Mozilla/5.0 (Macintosh; Intel Mac OS X 14_3) Chrome/124.0.0.0",
viewport: { w: 1920, h: 1080 },
timezone: "America/Los_Angeles",
language: "en-US",
pluginsHash: hashPluginList(["pdf-viewer", "widevine-cdm"]),
},
systemClock: { startedAt, endedAt },
operatorIdentity: {
schemaVersion: 1,
kind: "disk", // production captures use "webauthn" for FRE 902(13) compliance
fingerprint: operator.fingerprint,
publicKeyPem: operator.publicKeyPem,
boundAt: startedAt,
},
});
system.intake(captureSpec);
system.appendChain({
kind: "capture",
collectorFingerprint: operator.fingerprint,
collectionEnvironment: { os: "darwin", browser: "chrome", bureauVersion: "0.1.0" },
occurredAt: startedAt,
details: { captureSpecVersion: "v1" },
});
const bundle = system.bundle();
const verdict = system.verify(bundle);
console.log(`custody/bundle: chainRoot=${bundle.chainRootHash.slice(0, 16)}...`);
console.log(` events: ${bundle.chain.length}`);
console.log(` ok: ${verdict.ok}`);
console.log(` fre902Compliant: ${verdict.fre902Compliant}`);
if (verdict.reasons.length) console.log(` reasons: ${verdict.reasons.join("; ")}`);
} finally {
await system.shutdown();
}
}
main().catch((err) => { console.error(err); process.exit(1); });
Run with tsx index.ts. Expected output:
custody/bundle: chainRoot=a1b2c3d4e5f6789a...
events: 1
ok: true
fre902Compliant: false
reasons: identity-binding=disk; WebAuthn required
(In production, the browser extension records DOM snapshots + signs each chain event with a YubiKey-backed key; FRE 902(13)-compliant bundles use kind: "webauthn" with a webauthnAttestationDigest.)
▶ Open in StackBlitz – runs in your browser, no install required.
What you get
Exit code 0 from pluck bureau custody verify is the entire point. It means the bundle would survive FRE 902(13)'s self-authentication standard: the chain is intact, the operator's identity is bound to a tamper-evident hardware key, the DOM snapshot hashes verify, every fetch event hash verifies, the system clock window is consistent. A judge looking at this bundle has the same procedural standing as if a notary had stamped it – except the notarization is cryptographic, not human, and survives an opposing expert's challenge.
Exit code 1 with reasons is also valuable – it tells you exactly what to fix or argue around. A disk-only identity binding still verifies cryptographically but flunks Daubert (the reliability standard); you cannot use the bundle as 902(13) self-authentication, but you can still authenticate it with witness testimony.
What it can't do
- A bundle cannot prove what the AI said was true. It only proves what the AI said.
- DOM snapshots are capped at 4 MiB canonical. Larger captures must be split or stored externally.
- The chain length is capped at 256 events per bundle. Longer investigations split into multiple bundles, each anchoring to the prior bundle's
chainRootHash. - Custody binds the operator's claim about wall-clock time, not the actual time. When the bundle is notarized to Rekor, Rekor's
integratedTimeis the independent witness. - A disk-only key fails FRE 902(13). The verifier will surface this explicitly.
A real-world example
A lawyer is preparing a defamation suit against an AI vendor. Their client claims the vendor's chatbot fabricated specific false statements about the client's business in front of a customer. The lawyer needs an admissible record of the chatbot reproducing those statements. They install the Custody extension, register their YubiKey as the operator identity, replay the conversation that triggered the fabrication, and let Custody record every DOM mutation, every fetch to the vendor's API, the system clock at start and end, and a chain of signed events. They run pluck bureau custody build and then verify. Exit 0, fre902Compliant true. Eight months later in deposition, opposing counsel argues the bundle was forged after the lawsuit was filed. The lawyer points to the Rekor uuid, opposing counsel re-runs the verifier on the spot, and the chain holds. The judge admits the bundle as self-authenticating.
For developers
Predicate URIs
| Predicate URI | What it attests |
|---|---|
https://pluck.run/Custody.Bundle/v1 | The full bundle: capture-spec, chain root, chain length, identity binding kind. |
https://pluck.run/Custody.ChainEvent/v1 | A single event in the chain. Each event pins the prior event's hash. |
The two URIs let operators stream-notarize each event independently of the bundle root. Verifiers MUST discriminate by predicate-type, not by an inner-body field.
Programs composed
attest, notarize, subpoena, witness, press, dsseSign. Custody composes Bounty on export to emit a subpoena-quality EvidencePacket/v1. Full evidence chain crosses 4 packages: custody → subpoena → bounty → studio.
Threat model
- DOM snapshot canonical-byte limit: 4 MiB.
- Chain length cap: 256 events.
- WebAuthn binding required for FRE 902(13). Disk-only keys verify cryptographically (
ok: yes) butfre902Compliant: false. pluginsHashis the sha256 of the canonical sorted plugin list (stable across browser sessions, reveals only the plugin set).- Verify rejects bundles where
endedAtprecedesstartedAt. - Manifest V3 only.
index.tstype-pure;register.tsthe only side-effect entry.
Verify a published cassette
pluck bureau custody verify ./bundle.json --json
cosign verify-blob \
--key <pubkey.pem> \
--signature <sig> \
--type https://pluck.run/Custody.Bundle/v1 \
<bundle.json>
Library surface
import {
buildCustodyBundle,
verifyCustodyBundle,
exportSubpoena,
type CustodyBundle,
type FRE902VerifyResult,
} from "@sizls/pluck-bureau-custody";
const bundle: CustodyBundle = buildCustodyBundle({ captureSpec, chain });
const verdict: FRE902VerifyResult = verifyCustodyBundle(bundle);
// verdict.ok, verdict.fre902Compliant, verdict.reasons[]
See also
- Bounty – composes Custody bundles into HackerOne / Bugcrowd evidence packets.
- Whistle – anonymous variant; Custody is identity-bound.
- Acoustic-Scribe – chain-of-custody for microphone captures rather than browser sessions.
- Mole – sealed canaries for memorization claims.
- Operator Duties → Court-evidence operators
- Bureau Foundations
- Threat Model → Identity binding
- Verify a dossier