- Docs
- Bureau — Overview
- Audit Trail of Dispatch
Bureau — Overview
Audit Trail of Dispatch
The Bureau's signed proofs (cassettes, dossiers, exhibits, predicates) are immutable artifacts of what was attested. They do not carry who dispatched the originating runtime event into the engine – by design. This page documents the convention for operators who need that audit trail and the trade-offs that come with adopting it.
The Privacy / Compliance review surfaced this gap with a one-line probe:
"Show me the operator identity that dispatched
command.emittedat 2026-04-28T14:32:17Z."→ No actor-id on Directive events.
Bureau cannot answer that question for any of the 51 programs because the Directive engine does not carry operator-identity through system.dispatch(...). The fix is opt-in and sidecar – never embedded in signed proofs.
Why dispatch-time actor identity is sidecar
When a Bureau program emits a signed proof (a cassette, a dossier, a probe-pack, a redacted predicate), the proof's canonical body is what enters Sigstore Rekor – permanent and public. Coupling dispatch-time operator identity into that signed body would:
- Break source-protection use cases. Programs like Whistle, Refuse, and Citizen-Ledger support pseudonymous and source-protected flows. Stamping operator identity into the signed body removes that affordance.
- Force every operator-identity rotation to invalidate prior proofs. A rotated subkey or a new fingerprint after key compromise would change the digest of every previously-signed body, leaving auditors unable to re-verify a year-old proof against the current operator profile.
- Couple program-data schemas to the operator-identity schema. An audit a year from now where the operator changed their organization, role, or jurisdiction has to re-derive operator identity for every prior proof. That coupling makes proof verification fragile.
Instead, Bureau exposes the BureauActor envelope as a sidecar that operators can attach to dispatched events. The proof body stays clean. The audit trail rides alongside.
The BureauActor envelope
import type { BureauActor } from "@sizls/pluck-bureau-core";
interface BureauActor {
/** Operator's Ed25519 SPKI fingerprint, full 64-hex. */
fingerprint: string;
/** ISO 8601 UTC instant the operator dispatched. */
dispatchedAt: string;
/** Detached Ed25519 signature over { type, payload, dispatchedAt }. */
signature: string;
}
The signature covers a canonical JSON serialization of { type, payload, dispatchedAt } (using canonicalStringify). It is byte-stable across hosts and prototype-pollution-safe.
How to opt in
1. Build the envelope at dispatch
import {
buildActorEnvelope,
loadOperatorKey,
} from "@sizls/pluck-bureau-core";
const key = await loadOperatorKey("./keys");
const payload = { vendorId: "openai", finding: "policy-violation" };
const actor = buildActorEnvelope("fact.observed", payload, key.privateKeyPem);
system.dispatch({
type: "fact.observed",
payload,
actor,
});
The Directive engine treats actor as an opaque payload field – the engine never inspects it, so adoption is zero-overhead for the runtime.
2. Add an actor field to your program's event types
Programs that want a typed actor field should extend their event schema:
import { t } from "@directive-run/core";
import type { BureauActor } from "@sizls/pluck-bureau-core";
const events = {
"fact.observed": t.object<{
vendorId: string;
finding: string;
actor: BureauActor;
}>(),
};
3. Persist the envelope alongside the engine event
Two common patterns:
- Directive history plugin. If the program already runs the history plugin (most do), the actor envelope is captured in the event log automatically –
system.history.export()round-trips it. - External NDJSON log. For operators who need a regulator-grade audit trail outside the program's own state, write the envelope plus the dispatched event to a tamper-evident NDJSON file:
import { appendFileSync } from "node:fs";
const event = { type: "fact.observed", payload, actor, observedAt: hlc.now().iso };
appendFileSync(
"./audit/dispatch.ndjson",
`${JSON.stringify(event)}\n`,
{ mode: 0o600 },
);
Pair this with the HLC integration helper so dispatch timestamps are monotonic across NTP corrections.
4. Verify on the audit side
import { verifyActorEnvelope } from "@sizls/pluck-bureau-core";
const result = verifyActorEnvelope(
event.type,
event.payload,
event.actor,
operatorPublicKeyPem,
);
if (!result.ok) {
// result.reason describes the failure (bad fingerprint, bad sig, etc.)
}
verifyActorEnvelope returns a structured { ok, reason } outcome rather than throwing on a verification failure – auditors processing a long log want to surface every malformed entry, not exit on the first one.
What this doesn't solve
The actor envelope is a useful audit signal but it is bounded:
- It captures dispatch-time identity, not ingestion-time identity. If the dispatched event was forwarded through a relay, the envelope reflects whoever signed at the relay – not the original ingester. Bureau intentionally does not chain envelopes; multi-hop attribution is out of scope.
- It does not prove the operator was uncompromised at dispatch time. A stolen private key produces valid envelopes. Pair with ROTATE to detect identity-key compromise after the fact.
- It does not protect against an operator dispatching a falsified payload. The envelope binds operator identity to what was dispatched; it does not validate the dispatched data. Combine with constraint-driven validators inside the program for that.
- It is not a witness signature. A regulator-grade audit may also want a third-party witness (Cherenkov-Witness, Cosign witness). The actor envelope binds dispatch to the operator; witness signatures bind acceptance to a third party. Both can co-exist on the same artifact.
See also
- Bureau Foundations –
@sizls/pluck-bureau-coreprimitives, includingcreateBureauHlcfor monotonic dispatch timestamps. - Operator Duties – the operator covenant.
- Signing Key Handling – what
shutdown()does and does not do for key material. - ROTATE – operator-key rotation playbook.