Skip to content

Bureau — Overview

Bureau Foundations

The @sizls/pluck-bureau-core package ships primitives only. Every Bureau program (Dragnet, Nuclei, Oath, ...) ships in subsequent packages and composes over the surface documented here. Bureau-core is Kite-free – it stays embeddable in any host (browser, Node, Worker, Deno).

Shell
pnpm add @sizls/pluck-bureau-core

Operator key ceremony

Every Bureau artifact is signed with an operator-held signing key. The ceremony lives at pluck bureau keys generate and is wrapped by generateOperatorKey / persistOperatorKey / loadOperatorKey.

Shell
pluck bureau keys generate --out ./keys --name "alice"
# Writes ./keys/alice.key (0600) and ./keys/alice.pub.pem (0644)
# ./keys/   is forced to 0700
# operator fingerprint = lowercase 64-hex sha256 of the SPKI DER
TypeScript
import { generateOperatorKey, persistOperatorKey, loadOperatorKey } from "@sizls/pluck-bureau-core";

const key = generateOperatorKey();
// key.fingerprint = "<full 64-hex sha256(SPKI DER)>"
// key.publicKeyPem, key.privateKeyPem

await persistOperatorKey(key, { dir: "./keys", name: "alice" });

const loaded = await loadOperatorKey({ dir: "./keys", name: "alice" });

Sensitive-directory blocklist

persistOperatorKey refuses to write into a directory whose canonical path is inside any of:

  • ~/.ssh
  • ~/.gnupg
  • ~/.aws
  • ~/.config/gh

The blocklist is a fail-closed safety net, not a substitute for review. The CLI passes the operator's --out value through validateSafeOutputDir before opening any file handles.

Atomic write + refuse-overwrite

The persist path is atomic – O_EXCL-style write into a temp file, fsync, then rename. If the destination already exists, persistOperatorKey throws rather than overwriting. Operators rotating a key generate into a new directory and migrate downstream by fingerprint.

Permission enforcement

Bureau verbs that load a key (oath publish, dragnet run, etc.) refuse to open a file whose mode is wider than 0600 or whose parent directory is wider than 0700. The check is POSIX-only – Windows operators inherit looser semantics; see the Tripwire page for platform notes.


Dossier – the per-target append-only ledger

A Dossier is the canonical timeline for a single target. Every program (Dragnet, Tripwire, Mole, ...) appends TimelineDot entries to a per-target dossier whose top-level hash is sha256-anchored against the canonical-JSON body.

TypeScript
import {
  buildDossier,
  appendDot,
  computeDotId,
  computeDossierHash,
  verifyDossier,
} from "@sizls/pluck-bureau-core";

const baseDossier = buildDossier({
  program: "dragnet",
  subject: { vendor: "openai", model: "gpt-4o" },
  dots: [],
});

const dot = {
  schemaVersion: 1 as const,
  program: "dragnet" as const,
  tone: "green" as const,         // "green" | "red" | "black"
  rekorUuid: "<64-hex>",          // from notarize
  subject: { vendor: "openai", model: "gpt-4o" },
  envelopeHash: "<64-hex>",
  emittedAt: new Date().toISOString(),
  reason: "contradict-clean",
};

const dotId = computeDotId(dot);
const updated = appendDot(baseDossier, { ...dot, dotId });

const ok = await verifyDossier(updated);
// { ok: true } | { ok: false, reason: "<stable code>" }

Bounds

ConstantValueReason
MAX_DOTS100,000Bounds canonical-stringify cost on buildDossier
MAX_SUMMARY_CHARS1,024Bounds dot-hash input

Beyond these, operators split into linked dossiers (each anchoring to the prior dossier's hash). The same pattern threads through CustodyBundle/v1 chain caps – see Custody.


QuorumVote – N-of-M Ed25519 aggregation

QuorumVote is the Bureau's anti-Sybil primitive. Each signer signs the raw 32-byte digest of subjectHash (NOT the UTF-8 bytes of the hex string – the early-draft scheme broke cosign / sigstore-go / sigstore-python interop). Aggregation honors revocation and dedupes Sybils by both fingerprint AND public-key bytes – an attacker cannot register two fingerprints for the same key.

TypeScript
import { buildQuorumVote, verifyQuorumVote, MAX_SIGNERS } from "@sizls/pluck-bureau-core";

const vote = buildQuorumVote({
  schemaVersion: 1,
  subjectHash: "<64-hex>",
  threshold: { signed: 3, outOf: 5 },
  signers: [
    { fingerprint: "<64-hex>", publicKey: "<pem>", signature: "<base64>" },
    // up to MAX_SIGNERS = 1,024
  ],
});

const result = verifyQuorumVote(vote, { revokedFingerprints: new Set() });
// { ok: true } | { ok: false, reason: "<stable code>" }

A single attacker controlling N+1 keys can still forge consensus. Operators MUST deploy quorum-node identities across distinct trust boundaries – different orgs, different clouds, different human signers. See the Threat Model for the Sybil-defense rationale.


ProbePack – versioned, sha256-pinned, cryptographically signed

A ProbePack is the unit of authored adversarial work. Every probe-pack referenced by a Bureau verdict (Dragnet / Nuclei / Mole / Fingerprint) is signed by its publisher; verifiers REJECT unsigned packs.

TypeScript
import { signProbePack, verifyProbePack, computePackHash, MAX_PROBES } from "@sizls/pluck-bureau-core";

const pack = signProbePack(
  {
    schemaVersion: 1,
    packId: "canon-honesty-v0.1",
    name: "Canon AI Honesty Pack v0.1",
    authorFingerprint: key.fingerprint,
    publishedAt: new Date().toISOString(),
    targets: ["dragnet"],
    probes: [
      { id: "p1", body: { messages: [{ role: "user", content: "..." }] } },
      // bounded at MAX_PROBES = 10,000
      // each body bounded at MAX_PROBE_BODY_BYTES = 256 KiB canonical-JSON
    ],
  },
  key.privateKeyPem,
);

const ok = await verifyProbePack(pack, /* expected author publicKey */);

packHash = sha256 of the canonical-JSON body. The author signs the raw 32-byte digest of packHash. Re-publishing someone else's pack is refused at the registry layer; only the original authorFingerprint can mint a Nuclei entry – see Nuclei.


RFC 6962 inclusion-proof verifier

Sigstore Rekor returns an inclusionProof on every entry. verifyInclusionProof is a strict RFC 6962 walker – stronger than cosign verify-attestation alone, which trusts the SET timestamp but does not re-walk the audit path.

TypeScript
import { verifyInclusionProof, computeAuditPathLength, computeLeafHash } from "@sizls/pluck-bureau-core";

const result = verifyInclusionProof({
  logIndex: 12345,
  treeSize: 67890,
  leafHash: "<64-hex>",
  hashes: ["<64-hex>", "<64-hex>", "..."],
  rootHash: "<64-hex signed root>",
});
// { ok: true } | { ok: false, reason: "audit-path-length-mismatch" | ... }

The verifier validates hashes.length === computeAuditPathLength(logIndex, treeSize) BEFORE walking – a malformed proof with the wrong path length is rejected without consuming any sha256 work.


Canonical JSON + predicate-URI conventions

Every signed body in the Bureau goes through canonicalStringify:

TypeScript
import { canonicalStringify } from "@sizls/pluck-bureau-core";

const bytes = canonicalStringify({ b: 2, a: 1 });
// '{"a":1,"b":2}' – sorted keys, undefined→null, BigInt rejected

Properties:

  • Sorted keys, recursively. No locale-dependent ordering.
  • undefinednull. Field presence is meaningful; absence is null, not omission.
  • BigInt rejected. No precision drift.
  • Prototype-pollution guarded. __proto__ / constructor / prototype keys are stripped.
  • Throws on Date / Map / Set / RegExp / typed-array. These would silently emit {} under JSON.stringify and cause hash collisions; the canonicaliser fails closed instead.

Predicate-URI shape

Every predicate type is https://pluck.run/<TitleCase>.<Shape>/v1. One URI per shape, no exceptions. Verifiers discriminate on predicateType, never on an inner-body field. The current registry includes:

https://pluck.run/AgentRun/v1                  https://pluck.run/Oath.Commitment/v1
https://pluck.run/SbomAi.Entry/v1              https://pluck.run/Oath.Retraction/v1
https://pluck.run/Rotate.KeyRevocation/v1      https://pluck.run/Fingerprint.Model/v1
https://pluck.run/Rotate.KeyFreeze/v1          https://pluck.run/Fingerprint.Delta/v1
https://pluck.run/Rotate.ReWitnessReport/v1    https://pluck.run/Mole.Canary/v1
https://pluck.run/Rotate.DisclosureRebuild/v1  https://pluck.run/Mole.MemorizationVerdict/v1
https://pluck.run/Nuclei.PackEntry/v1          https://pluck.run/Whistle.Submission/v1
https://pluck.run/Bounty.Offer/v1              https://pluck.run/EvidencePacket/v1
https://pluck.run/Bounty.Claim/v1              https://pluck.run/Bounty.Submission/v1
https://pluck.run/Disclosure/v1                https://pluck.run/Custody.Bundle/v1
                                               https://pluck.run/Custody.ChainEvent/v1

schemaVersion: 1 is a literal, not a string-coerced number – no parser ambiguity, no major-version drift inside a URI.


BureauMetrics – observability surface

BureauMetrics is a host-pluggable metrics interface. NOOP_METRICS is the default; createOTLPMetricsAdapter wires to OpenTelemetry; createRecordingMetrics returns an in-memory recorder for tests.

TypeScript
import {
  createOTLPMetricsAdapter,
  createRecordingMetrics,
  shieldedMetrics,
  NOOP_METRICS,
  type BureauMetrics,
} from "@sizls/pluck-bureau-core";

const recorder = createRecordingMetrics();
const metrics: BureauMetrics = shieldedMetrics(recorder);
// shieldedMetrics wraps every emit in try/catch – a misbehaving sink can never crash a daemon.

metrics.counter("bureau.dragnet.dot.emitted", 1, { tone: "red" });
metrics.histogram("bureau.notarize.latency_ms", 142);

Every long-running daemon (Dragnet, Tripwire, Nuclei subscribe-loop) accepts a metrics option; the default is NOOP_METRICS.


redactBureauPayload – pre-notarize redactor

The Permanent-Public-Log rule (every Bureau-emitted Rekor entry is PUBLIC and PERMANENT) means redaction is non-negotiable. redactBureauPayload is exposed as a Bureau-core primitive, and redactAndSignPredicate (the cross-program glue that wraps it) runs automatically before every program's signCanonicalBody step – every Rekor-bound predicate body passes through the secret-scrub layer before its sha256 is computed. Operators can plug a BureauMetrics adapter to observe bureau.<programId>.redactor.invoked_total and bureau.<programId>.redactor.scrubs_fired_total counters. Bait-detector mode (strictThrow: true) is used by ACOUSTIC-SCRIBE – its predicate is not supposed to carry operator free-text, so any scrub activation refuses to sign. Direct callers can still invoke redactBureauPayload for one-off scrubs (e.g. evidence-locker bodyMarkdown).

TypeScript
import {
  redactBureauPayload,
  defaultBureauRedactionPolicy,
  strictBureauRedactionPolicy,
  type BureauRedactionPolicy,
} from "@sizls/pluck-bureau-core";

const policy: BureauRedactionPolicy = strictBureauRedactionPolicy();
// default = secret-pattern scrub + JSON-key-name scrub
// strict  = adds PII regex (email / phone / SSN) on top

const result = redactBureauPayload(unsafeBody, policy);
// { redacted: <safe body>, redactionCount: 3, reasons: [...] }

Default policy:

  • Authorization / X-API-Key / Cookie JSON fields → [REDACTED]
  • sk-… API keys, Bearer … tokens → [REDACTED]
  • URL ?api_key=… fragments → [REDACTED]
  • Path-segment secrets (signed-URL tokens, per-account UUIDs) → [REDACTED]

Strict policy adds PII-shape regex matchers. HIPAA / GDPR-class data MUST NOT ride a Bureau cassette unless the operator has independently confirmed the redactor catches every leak vector for their payload – see Operator Duties.


notarizeWithRetry – circuit-breaker wrapper

Rekor is a public dependency. notarizeWithRetry wraps the raw notarizeAttestation call with exponential backoff + a per-process circuit breaker so a Rekor outage cannot wedge a long-running daemon.

TypeScript
import {
  notarizeWithRetry,
  resetRekorBreaker,
  type RekorRetryConfig,
  type RekorBreakerConfig,
} from "@sizls/pluck-bureau-core";

const retry: RekorRetryConfig = {
  maxAttempts: 5,
  initialDelayMs: 200,
  maxDelayMs: 30_000,
  jitter: true,
};

const breaker: RekorBreakerConfig = {
  failureThreshold: 5,
  cooldownMs: 60_000,
};

const entry = await notarizeWithRetry({
  envelope,
  rekorUrl: "https://rekor.sigstore.dev",
  retry,
  breaker,
  signal: abortSignal,
});

Behavior:

  • Retries on transient HTTP (5xx, network), refuses on 4xx (client error).
  • 409 Conflict is treated as success (Rekor uuids are deterministic – re-uploading the same body yields the existing entry).
  • Breaker opens after failureThreshold consecutive failures, refuses subsequent calls until cooldownMs elapses.
  • resetRekorBreaker() is exposed for tests; daemons should not call it.

acquireOutputDirLock – cross-program lock

Two daemons writing to the same outputDir is a corruption hazard. acquireOutputDirLock is a filesystem mutex backed by flock (POSIX) / Windows file-locking. Every program (Dragnet / Tripwire / Nuclei subscribe / Mole) acquires it before writing to the dossier directory.

TypeScript
import { acquireOutputDirLock, type OutputDirLock } from "@sizls/pluck-bureau-core";

const lock: OutputDirLock = await acquireOutputDirLock("./.dragnet");
try {
  // write dossier, append dot, persist cassette ...
} finally {
  await lock.release();
}

A second daemon attempting to lock the same outputDir fails fast with a typed error pointing the operator at the holding process pid.


Bureau pause kill-switch

A compromised probe-pack mid-hunt can be halted across every long-running Bureau daemon by writing a sentinel file. Every program's loop polls isBureauPaused() once per iteration and exits cleanly when the sentinel is present.

Shell
pluck bureau pause                      # halt every Bureau daemon (writes ~/.pluck/bureau-paused)
pluck bureau pause --program=dragnet    # halt one program
pluck bureau resume                     # remove the sentinel after recovery
TypeScript
import { isBureauPaused, resolveBureauPauseSentinelPath } from "@sizls/pluck-bureau-core";

if (isBureauPaused({ program: "dragnet" })) {
  return { halted: true, reason: "bureau-paused" };
}

Pause is the fastest containment step – sub-second per daemon. It is the first verb operators reach for in a compromise-response runbook; see Operator Duties → Compromise response.


Cross-cutting design rules

  • Ed25519 only. No RSA, no ECDSA. PureEdDSA over the raw 32-byte digest of canonical-JSON.
  • Canonical JSON for every signed body. Sorted keys, prototype-pollution guarded, fail-closed on Date/Map/Set/RegExp.
  • envelopeHash is universal. The same digest threads through all 12 verb-modules.
  • One predicate URI per shape. Verifiers discriminate by URI, not by inner-body fields.
  • Programs compose primitives. They do not invent new envelope formats.
  • schemaVersion: 1 literal. No string drift.
  • Full 64-hex SPKI fingerprints. No truncation.
  • Strict ISO 8601 UTC. Z-terminated.
  • Bureau-core is Kite-free. It stays embeddable in any host (browser, Node, Worker, Deno).

Composition primitives

Bureau-core also exports a small set of higher-level helpers that programs compose to share lifecycle, HLC clocks, actor envelopes, retry jitter, TOCTOU guards, and meta-dossier wiring without each program reinventing the boilerplate.

HelperOne-line descriptionSee also
createBureauHlcHybrid Logical Clock for monotonically ordering Bureau events across processes – wraps pluck-core HLC with Bureau-defaults and ISO 8601 UTC validation.Key handling
buildActorEnvelopeWraps an event payload in a signed actor envelope (type, dispatchedAt, signingKey, signature) – the standard Bureau-internal event shape.Audit trail
verifyActorEnvelopeStrict verification of a signed actor envelope against an Ed25519 public key – fail-closed on shape, signature, or timestamp errors.Audit trail
assertActiveResolver TOCTOU guard – re-checks ctx.active immediately before a mutation; throws if the system has shut down between the constraint evaluation and the resolver mutation.
applyJitterPure jitter function for retry/backoff delays – bounded [min, max] window with deterministic seeding for tests.
withJitteredShouldRetryDecorator that wraps a shouldRetry predicate with applyJitter so retry windows do not synchronize across daemons.
startLifecycleSystem-side pause-poll setInterval – wires isBureauPaused() polling, AbortSignal threading, and clean shutdown into a long-running Bureau daemon.
createMetaDossierModuleDirective module factory – emits a per-program meta dossier (program-id, schema-version, operator fingerprint) and threads it into every signed body the program produces.

Each helper is documented inline in its source (see @sizls/pluck-bureau-core/src/{hlc,actor,assert-active,jitter,lifecycle,meta-dossier-module}.ts). Programs adopt them by importing from the package root – no subpath imports required.


What's next

  • Verify guidepluck bureau verify for every bundle kind.
  • Dragnet – the program that composes 11 of the 12 verb-modules.
  • Threat Model – Sybil defense, freeze window, kill-switch, operator-key compromise.
  • Concepts: Act → Notarisation – how attest / notarize / cosign verify-attestation fit beneath the Bureau.
Edit this page on GitHub
Previous
Bureau for Operators

Ready to build?

Install Pluck and follow the Quick Start guide to wire MCP-first data pipelines into your agents and fleets in minutes.

Get started →