- Docs
- Bureau Foundations
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).
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.
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
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.
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
| Constant | Value | Reason |
|---|---|---|
MAX_DOTS | 100,000 | Bounds canonical-stringify cost on buildDossier |
MAX_SUMMARY_CHARS | 1,024 | Bounds 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.
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.
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.
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:
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.
undefined→null. Field presence is meaningful; absence isnull, not omission.- BigInt rejected. No precision drift.
- Prototype-pollution guarded.
__proto__/constructor/prototypekeys are stripped. - Throws on Date / Map / Set / RegExp / typed-array. These would silently emit
{}underJSON.stringifyand 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.
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).
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/CookieJSON 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.
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
failureThresholdconsecutive failures, refuses subsequent calls untilcooldownMselapses. 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.
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.
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
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.
envelopeHashis 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: 1literal. 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.
| Helper | One-line description | See also |
|---|---|---|
createBureauHlc | Hybrid Logical Clock for monotonically ordering Bureau events across processes – wraps pluck-core HLC with Bureau-defaults and ISO 8601 UTC validation. | Key handling |
buildActorEnvelope | Wraps an event payload in a signed actor envelope (type, dispatchedAt, signingKey, signature) – the standard Bureau-internal event shape. | Audit trail |
verifyActorEnvelope | Strict verification of a signed actor envelope against an Ed25519 public key – fail-closed on shape, signature, or timestamp errors. | Audit trail |
assertActive | Resolver 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. | – |
applyJitter | Pure jitter function for retry/backoff delays – bounded [min, max] window with deterministic seeding for tests. | – |
withJitteredShouldRetry | Decorator that wraps a shouldRetry predicate with applyJitter so retry windows do not synchronize across daemons. | – |
startLifecycle | System-side pause-poll setInterval – wires isBureauPaused() polling, AbortSignal threading, and clean shutdown into a long-running Bureau daemon. | – |
createMetaDossierModule | Directive 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 guide –
pluck bureau verifyfor 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-attestationfit beneath the Bureau.