Help us shape the future of Go! Take the 2026 Go Developer Survey

audit

package
v0.11.1 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Oct 4, 2026 License: Apache-2.0 Imports: 29 Imported by: 0

Documentation

Overview

Package audit turns an agent's durable journal into a verifiable, tamper-evident record. The journal already captures every step of a run (model turns, tool calls and results, approvals, saga outcomes); this package commits to that history with a hash chain and Merkle trees and lets you sign/verify the commitment under ed25519, ML-DSA-65, or a hybrid of both. Stdlib-only.

Security model — read this before relying on it for compliance:

  • Head computes a SHA-256 hash chain over the run's journal in its persisted order. Any modification, insertion, deletion, or reordering of a record changes the head.
  • That gives INTEGRITY (detect corruption / partial writes) unconditionally, and TAMPER-EVIDENCE only if you ANCHOR the head out-of-band — sign it with a key the app tier doesn't fully control, and/or publish it to a separate trust domain. A hash chain stored in the same database an attacker controls can be rewritten and rehashed; the guarantee is "you committed the head elsewhere, so divergence is provable." This package returns the head and provides signing; anchoring policy is yours.

Index

Constants

View Source
const (
	// ProofFormat is the format of a ProofBundle. v3 carries the record as the bytes the journal
	// stores (record_bytes) rather than a decoded record.
	ProofFormat = "bide.audit.proof.v3"
	// AbsenceFormat is the format of an AbsenceBundle. v3 carries a bide.audit.sth.v5 head.
	AbsenceFormat = "bide.audit.absence.v3"
	// RunCertificateFormat is the format of a RunCertificate. v3 carries bide.audit.sth.v5 heads
	// and bide.audit.proof.v3 bundles.
	RunCertificateFormat = "bide.audit.runcert.v3"
	// CurrentGrantFormat is the format of a CurrentGrantProof. v3 carries a bide.audit.proof.v3
	// leaf and bide.audit.grant.v2 grants.
	CurrentGrantFormat = "bide.audit.current-grant.v3"
	// EventInclusionFormat is the format of an EventInclusion. v3 proves a
	// bide.audit.event-leaf.v3 leaf, whose event JSON is snake_case.
	EventInclusionFormat = "bide.audit.event-inclusion.v3"
	// STHFormat is the format of a SignedTreeHead, and the tag of the bytes it signs. v5 signs
	// the scheme (Alg) with the head.
	STHFormat = "bide.audit.sth.v5"
	// AnchorEntryFormat is the format of an AnchorEntry.
	AnchorEntryFormat = "bide.audit.anchor-entry.v1"
)

The format tags an artifact carries in its "format" field. Each names the artifact's JSON layout and what its proofs commit to, so a verifier refuses a layout it does not read with ErrFormat instead of misreading it or failing on a field it does not know.

Before 1.0 a verifier reads only the current format of each artifact; an artifact an older release made is refused with ErrFormat, and is re-created from the journal with this release's producer. From 1.0 on, a verifier reads the current format and the one before it.

View Source
const (
	// TreeJournal is a run's journal: one leaf per record, in persisted order (NewTreeHead).
	TreeJournal = "journal"
	// TreeEvents is a run's semantic event stream (EventLog.TreeHead).
	TreeEvents = "events"
	// TreeToolUse is the sorted, distinct tool-use keys of a run's completed tool calls
	// (ToolUseKeys), the tree tool-call absence proofs verify against.
	TreeToolUse = absenceKindPrefix + "tool-use"
	// TreePolicyUsed is the sorted, distinct policy digests a run's governed actions used
	// (PolicyUsedKeys), the tree used-policy absence proofs and run certificates verify against.
	TreePolicyUsed = absenceKindPrefix + "policy-used"
)

Tree kinds, committed in every signed tree head.

View Source
const DefaultClockSkew = 5 * time.Minute

DefaultClockSkew is how far past the verifier's clock a signed head's TimestampNanos may be before it is refused: five minutes, for clocks that are not exactly in step.

View Source
const EarnedScopeLimit = "limit"

EarnedScopeLimit is the scope key an earned grant sets to the earned limit (at most the root's).

View Source
const EvidenceFormat = "bide.audit.evidence.v5"

EvidenceFormat is the format/version tag written into every EvidencePackage. It is domain-separated and versioned so a verifier rejects a package produced by an incompatible layout rather than misreading it. v2 binds tree heads to their run, seals the package, and carries the earlier signed head a consistency proof starts from. v3 is v2 over journals that key a tool call's records by its encoded ID (agent.ToolResultStep, agent.ApprovalTallyStep) and a sub-agent's run by agent.SubRunID, so the record names a v3 package proves differ from a v2 package's for the same run. v4 is v3 with snake_case names in every proof it carries (the inclusion and consistency proofs spelled "Index", "Size", "Path" and "First" before) and a "format" on each ProofBundle and on the RunCertificate. v5 names the signing key by scheme and key ({alg, public_key}) rather than by ed25519 hex, carries bide.audit.proof.v3 bundles (each record as its stored bytes) and bide.audit.sth.v5 heads, and labels its actions snake_case kinds.

View Source
const GrantFormat = "bide.audit.grant.v2"

GrantFormat tags the canonical bytes of a grant (Grant.Bytes), which are both signed and digested. v2 names the expiry not_after_unix and tags the signed bytes as well as the digest.

View Source
const JournalExportFormat = "bide.audit.journal-export.v1"

JournalExportFormat is the format of a JournalExport.

Variables

View Source
var (
	// ErrMalformed reports an artifact that cannot be read as what it claims to be.
	ErrMalformed = fmt.Errorf("audit: malformed artifact: %w", agent.ErrProtocol)
	// ErrFormat reports an artifact whose "format" is missing or is not the one this version of
	// the package reads: an artifact produced by an older (or newer) release. Re-create the
	// artifact with this release's producer (ProveRecord, Evidence, CertifyRun, ...) from the
	// journal.
	ErrFormat = fmt.Errorf("audit: unsupported artifact format: %w", ErrMalformed)
	// ErrNotVerified reports an artifact that was read and understood and does not hold.
	ErrNotVerified = errors.New("audit: not verified")
	// ErrRedacted reports a journal holding a redacted record (agent.Record.Redacted) given to a
	// producer that projects what the records say (an absence key set, a used-policy set, the
	// event stream): the redacted record's content is gone, so the projection would silently omit
	// it, and a key set without it would prove absent a call the journal tree commits. Proofs of the
	// other records, and the journal root, are unaffected. It wraps agent.ErrConfig: the caller
	// asked for a claim the journal can no longer support.
	ErrRedacted = fmt.Errorf("audit: the journal holds a redacted record: %w", agent.ErrConfig)
)

The errors a verifier returns. A verifier returns nil when the artifact verifies. Otherwise its error wraps exactly one of:

  • ErrNotVerified: the artifact was read and understood, and it does not hold (a signature that does not verify, a proof against another root, a record of the wrong kind for its role);
  • ErrFormat: the artifact, or one it carries, is not of the format this version reads;
  • ErrMalformed: the artifact cannot be read as what it claims to be (JSON that does not read strictly, record bytes that do not decode, a key that is not of its scheme's form).

ErrFormat wraps ErrMalformed, and ErrMalformed wraps agent.ErrProtocol. A caller's mistake (no verifier, an invalid expected policy) wraps agent.ErrConfig instead. Only nil means verified.

EarnedRules is the ScopeRules an earned grant attenuates its root under.

View Source
var ErrWeakKey = errors.New("audit: weak ed25519 public key")

ErrWeakKey reports an Ed25519 public key that is not a usable verification key (see CheckEd25519PublicKey).

View Source
var PolicyUsedKeys = KeySet{Kind: TreePolicyUsed, Prefix: policyUsedKeyPrefix, Key: PolicyUsedKey}

PolicyUsedKeys is the key set of policy digests exercised by governed actions, keyed "policy_used:<digest>" (see PolicyUsedKey).

View Source
var ToolUseKeys = KeySet{Kind: TreeToolUse, Prefix: toolUseKeyPrefix, Key: ToolUseKey}

ToolUseKeys is the key set of the tool calls a run's model requested, keyed "tooluse:<ToolUseID>" (see ToolUseKey). It holds every call the run started, since the run starts only a call its model requested, so it proves "no tool call with this ID was requested, and so none happened, in the run": neither completed, nor started and left unresolved, nor failed in a saga.

Functions

func AbsenceRoot

func AbsenceRoot(records []agent.Record, set KeySet) (root []byte, size int, err error)

AbsenceRoot is the RFC 6962 Merkle root over the run's sorted, distinct keys in set, and the number of keys. Anyone holding the journal recomputes it to confirm a signed key-set head reflects the run: the head's Root and Size must equal it. It refuses records it cannot project as NewAbsenceTreeHead does, so a recomputation never confirms a head that omits a record: a journal holding a redacted record is ErrRedacted (the record's keys cannot be read), and a record whose stored bytes read two ways is ErrMalformed.

func AttenuatingSubAgent

func AttenuatingSubAgent(name, description string, sub *agent.Agent, cfg AttenuationConfig, opts ...agent.ToolOption) agent.Tool

AttenuatingSubAgent wraps a sub-agent as a delegation Tool that attenuates authority by default. On each call, if a signed grant is bound to ctx (WithGrant), it mints a narrower child grant (ParentRef hash-linked to the parent, checked with CheckAttenuation under rules, signed by the same signer), records it as a durable leaf in the sub-run so the chain is anchored, rebinds the sub-run's identity to the child (Actor = this sub-agent, OnBehalfOf = the parent's Subject, AuthorityRef = the child's digest), and propagates the child grant so a deeper delegation narrows again. With no grant on ctx it is a plain sub-agent (it inherits the identity), so it is safe to use either way. With a grant, it must be called from an agent run (Agent.Run), whose run scope gives each call its own sub-run (agent.SubRunID of the parent run and the tool-use id); called outside one it refuses rather than fall back to a sub-run ID that every parent run would share.

The effect is that capabilities only ever shrink down a delegation tree, by default rather than by remembering to wire it, and the whole chain is provable end to end via VerifyDelegationChain. It is a composition of the grant machinery into the sub-agent seam, not a new agent type: the wrapped `sub` still runs its own full agent loop and reasons autonomously within the narrower authority.

cfg says where the child grants are recorded and how authority narrows; opts are passed to agent.SubAgent, so agent.WithApproval makes the parent wait for a human before it delegates. AttenuatingSubAgent panics, with an error wrapping agent.ErrConfig, on a nil cfg.Store or cfg.Narrow, and on an option agent.SubAgent refuses.

func CheckAttenuation added in v0.7.0

func CheckAttenuation(parent, child Grant, rules ScopeRules) error

CheckAttenuation returns nil if child is a valid delegation of parent, and otherwise an error naming the first rule it breaks:

  1. child.ParentRef is parent's Digest (the hash link);
  2. child.Issuer is parent.Subject (only the holder of a grant can delegate it);
  3. child expires no later than parent: if parent.NotAfterUnix is set, child.NotAfterUnix is set and at most parent.NotAfterUnix (a child of an expiring grant can never be non-expiring);
  4. every parent scope key is in child, with the same value or, where rules has a ScopeRule for that key, a value the rule accepts as narrower.

Scope entries are constraints: each one restricts what the grant allows, so a child may add keys its parent lacks (adding a constraint narrows) but never drop or loosen one. A domain whose scope key grants rather than restricts must express it so that its ScopeRule decides narrowing (for example a comma-separated allowlist whose rule requires a subset).

func CheckEd25519PublicKey added in v0.9.0

func CheckEd25519PublicKey(pub []byte) error

CheckEd25519PublicKey reports whether pub is a usable Ed25519 verification key: 32 bytes, the canonical encoding (y < p, and no sign bit on x = 0) of a point on the curve, in the prime-order subgroup ([L]A is the identity), and not the identity itself, which leaves no small-order point. It returns nil for every key crypto/ed25519.GenerateKey produces, and an error wrapping ErrWeakKey otherwise. The check does arithmetic on the public key only, so it need not run in constant time.

Cost: a key that decodes to a curve point needs a subgroup check, a scalar multiplication in math/big that takes milliseconds of CPU: 1.1 to 3.6 ms per key measured on an Apple M1 Pro, depending on load, against about 45 microseconds for a signature check. Results for such keys are kept in a small LRU cache (keyCheckCacheMax keys), and concurrent first checks of one key run it once, so a verifier that checks the same few keys pays it once per key. An application that resolves keys from untrusted input pays it for every new key an attacker sends that decodes to a point: bound or rate-limit such lookups. Keys that do not decode are refused cheaply and not cached, so they cannot fill the cache.

func CheckTimestamp added in v0.8.0

func CheckTimestamp(th TreeHead, now time.Time, skew time.Duration) error

CheckTimestamp applies the timestamp rule to one signed head: its TimestampNanos must be positive and not later than now plus skew. A head that breaks the rule is an error wrapping ErrNotVerified; a negative skew is an error wrapping agent.ErrConfig.

func CheckTimestampOrder added in v0.8.0

func CheckTimestampOrder(earlier, later TreeHead) error

CheckTimestampOrder checks that later, a head that extends earlier, was not signed before it. A pair out of order is an error wrapping ErrNotVerified.

func ConvergenceLeafName added in v0.8.0

func ConvergenceLeafName(digest string) string

ConvergenceLeafName is the journal name of the convergence leaf RecordConvergence writes for digest. A verifier handed a bundle as a convergence leaf checks that its record is a StepValue of this name.

func FormatPublicKey added in v0.9.0

func FormatPublicKey(alg Alg, pub []byte) string

FormatPublicKey returns the text form of a public key that ParsePublicKey reads: the scheme, a colon, and the encoded key in lowercase hex ("ed25519:1a2b...").

func GovernedPolicyDigest added in v0.8.0

func GovernedPolicyDigest(result json.RawMessage) (string, error)

GovernedPolicyDigest returns the policy digest a governed-action payload (a tool result, as an attested govern.EventTool journals it) carries. It is the one reading PolicyUsedKey and bide-audit share, so an action cannot be said to run under one policy by the used-policy set and under another by the CLI. The payload is open (it may hold other fields, such as the acting identity), so its names are not checked against a type, but it must decode strictly as an object (see UnmarshalStrict: no duplicate names, valid UTF-8, no lone surrogate escapes), and the digest is read from the exact name "policy_digest" only, never from a case variant, as a reader of the file would read it. A payload without that name, or whose value is not a string, is an error.

func GrantFrom

func GrantFrom(ctx context.Context) (SignedGrant, Signer, bool)

GrantFrom returns the acting signed grant and signer bound to ctx, and whether one was set.

func Head(ctx context.Context, store *agent.Journal, runID string) ([]byte, error)

Head returns the hash-chain commitment to runID's journal: head_0 = H(domain), and head_i = H(head_{i-1} || stored(record_i)) over the records in persisted order, where stored is the bytes the journal stores for the record (agent.Record.Raw), verbatim. Two runs produce the same head iff their journals are byte-identical in the same order, so the head is a deterministic fingerprint of the entire execution history.

func JournalLeafHash added in v0.9.0

func JournalLeafHash(recordBytes []byte) []byte

JournalLeafHash returns the RFC 6962 leaf hash of a journal record from the bytes the journal stores for it: SHA-256(0x00 || "bide.audit.journal-leaf.v1\x00" || recordBytes). A redaction records it, hex-encoded, in the tombstone that replaces the bytes (see agent.Record.Redacted), so the record keeps its place in every tree over the journal.

func KeyID added in v0.9.0

func KeyID(alg string, pub []byte) string

KeyID is the key identity the audit verifiers report from KeyIDs: alg, a colon, and the lowercase hex SHA-256 of the public key's encoding (for Ed25519 the 32-byte key, for ML-DSA its FIPS 204 encoding). It depends only on the key, so every verifier over one key reports it.

func NumericAtMost added in v0.7.0

func NumericAtMost(parent, child string) bool

NumericAtMost is the ScopeRule for an integer upper bound (a limit): the child's value, parsed as a base-10 integer, must be at most the parent's. Unparseable values fail closed.

func PersistEvent

func PersistEvent(ctx context.Context, evStore EventStore, runID string, seq int, e agent.RunEvent) error

PersistEvent appends one event's canonical leaf to evStore at position seq, for callers streaming events into a store directly; prefer PersistJournal for the crash-safe path. The leaf commits to a fresh random salt (agent.SaltSize bytes from crypto/rand), which is stored in the leaf. If seq is already stored, the event is salted with the stored leaf's salt, so a retry of the same event is a no-op and a different event is still refused as a fork. Do not mix it with PersistJournal on one run: the two salt the same event differently, so the second reports a fork.

func PersistJournal

func PersistJournal(ctx context.Context, evStore EventStore, jStore *agent.Journal, runID string) error

PersistJournal mirrors runID's durable event trail into evStore by projecting the journal (agent.ReplayEvents) and appending each canonical event leaf at its position. It is IDEMPOTENT: call it after each turn, once at run end, or on a schedule — re-runs append only what is new and never fork the trail, because the projection is deterministic and resume-stable. After it returns, LoadEventLog(evStore, runID) reconstructs the committed trail for anchoring/proofs even if the journal is later deleted.

Each leaf commits to its event's salt, derived from the salt of the journal record the event projects (see EventLogFromJournal), so a re-run computes the same leaf bytes and a stored leaf carries the salt its proof discloses. It errors if a source record has no agent.SaltSize salt, and refuses a journal it cannot project (ErrRedacted, ErrMalformed; see EventLogFromJournal).

The event store is a copy: redacting a journal record later does not reach it. Leaves persisted before the redaction keep the redacted record's content (a tool's arguments or result, a model turn's text), so a deployment that redacts must delete or redact the run's event trail in evStore as well.

func PoliciesUsed

func PoliciesUsed(records []agent.Record) ([]string, error)

PoliciesUsed returns the sorted, distinct policy digests exercised by governed actions in the run. An auditor compares this against the approved set; for any disallowed digest it then obtains an absence proof (ProveAbsentBundle with PolicyUsedKeys) showing no action ran under it. It refuses records it cannot project, as CertifyRun does, so a recomputation never omits a policy the journal commits: a journal holding a redacted record is ErrRedacted (the redacted action's policy cannot be read), and a record whose stored bytes read two ways is ErrMalformed.

func PolicyLeafName added in v0.8.0

func PolicyLeafName(digest string) string

PolicyLeafName is the journal name of the policy leaf RecordPolicy writes for digest. A verifier handed a bundle as a policy leaf checks that its record is a StepValue of this name: any other record, such as a tool result whose output has a policy leaf's shape, anchors no policy.

func PolicyUsedKey

func PolicyUsedKey(r agent.Record) []string

PolicyUsedKey is the KeyFunc of PolicyUsedKeys (see ProveAbsent / ProveAbsentBundle) over governed-action leaves: a completed tool call whose result carries a policy_digest, as an attested govern.EventTool records. It keys by that digest, so the absence machinery commits the set of policies actually exercised in a run. Proving a digest ABSENT under this KeyFunc shows that no governed action ran under that policy; recomputing AbsenceRoot lists exactly which policies were used, so an auditor can confirm every one is in the approved set.

Scope: this is a policy-level negative ("no action ran under a disallowed policy"), which, combined with the approved policies being oracle-certified convergent and invariant-preserving, supports "no violation was admitted". It is not a per-action state-validity proof (a per-transition state digest is roadmap), and it does not close the runtime refinement gap: the runtime is differentially tested against the verified reference, not proven equal to it.

func PolicyUsedKeyFor

func PolicyUsedKeyFor(digest string) string

PolicyUsedKeyFor is the absence key for a specific policy digest: pass it to ProveAbsent / ProveAbsentBundle with PolicyUsedKeys to prove no governed action ran under that policy.

func RecordConvergence

func RecordConvergence(ctx context.Context, store *agent.Journal, runID string, cert []byte, digest string) (agent.Record, error)

RecordConvergence commits the serialized convergence certificate as a dedicated journal leaf (idempotent per (runID, digest)), so it is covered by the same STH and inclusion proofs as the policy leaf with the same digest and the actions taken under it. Pass the certificate bytes from govern.ConfluenceCertificate.Marshal and the same digest used for the policy leaf.

func RecordGrant

func RecordGrant(ctx context.Context, store *agent.Journal, runID string, sg SignedGrant) (agent.Record, error)

RecordGrant commits a signed grant as a dedicated journal leaf keyed by its digest (idempotent per (runID, digest)), so it is covered by the same signed tree head and inclusion proofs as the actions taken under it. A governed action whose Identity.AuthorityRef equals the grant's digest then links to an anchored, issuer-signed authority.

func RecordPolicy

func RecordPolicy(ctx context.Context, store *agent.Journal, runID string, policy []byte, digest string) (agent.Record, error)

RecordPolicy commits the serialized policy as a dedicated journal leaf (idempotent per (runID, digest)), so the policy is covered by the same STH and inclusion proofs as the actions taken under it. The digest is the policy owner's stable identifier (e.g. gsm.Registry.PolicyDigest); audit treats it as opaque and does not recompute it from the bytes. A verifier recomputes the digest from the disclosed bytes and runs the external oracle (bide-audit verify-governance), so a leaf that lies about its digest is caught there.

func RecordRunCertificate

func RecordRunCertificate(ctx context.Context, store *agent.Journal, runID string, cert RunCertificate) (agent.Record, error)

RecordRunCertificate anchors a RunCertificate as a dedicated journal leaf (idempotent per runID), so the certificate is itself covered by a signed tree head and inclusion proofs, mirroring RecordPolicy / RecordConvergence. Anchoring the certificate lets a later verifier prove the certificate was committed in the run, not produced after the fact out of band.

Note the ordering: the certificate commits to a run STH taken at some size; anchoring it grows the journal past that size. That is intended: the run STH inside the certificate is the tree the policy/convergence bundles are proven against, and a fresh STH taken after RecordRunCertificate commits the certificate leaf itself (ProveRunCertificate proves it against that later STH).

func RecordStream added in v0.11.0

func RecordStream(log *EventLog, stream *agent.RunStream, onEvent func(agent.RunEvent)) (*agent.Result, error)

RecordStream drains stream through log, committing every event, while forwarding each event to onEvent (nil to skip), then returns the run's Result and error, as RunStream.Result does (the Result is non-nil whenever the run ID is valid, whatever the error, including *ApprovalPending and *OutcomeUnknown). One pass gives you both the live UI feed and a committed, provable audit trail. A failure to commit an event is returned only if the run itself did not fail.

func Root

func Root(ctx context.Context, store *agent.Journal, runID string) ([]byte, error)

Root returns the RFC 6962 Merkle root committing to runID's journal (records in persisted order, each leaf = the canonical record bytes). Anchor it like Head; unlike Head it supports per-record inclusion proofs (Prove / VerifyInclusion).

func Sign

func Sign(head []byte, s Signer) ([]byte, error)

Sign returns s's signature over a head commitment ("bide.audit.head.v1\x00" || head). Anchor it (store it in a separate trust domain) to make the journal tamper-evident against later rewrites.

func ToolUseKey

func ToolUseKey(r agent.Record) []string

ToolUseKey is the KeyFunc of ToolUseKeys: every record that shows a call ran or may have run, keyed by its ToolUseID. The model turn that requested calls (StepModel) adds each call it requested, whether or not the call ran: a call can run and leave no other record (a retry-safe call journals no attempt marker, and a crash or a lost write can drop its result), and the request is journaled before the call starts. The completed call's result (StepToolResult), the attempt marker journaled before a side effect fired (StepAttempt), and a saga step's failure (StepSagaFail) add their ToolUseID too. An attempt marker of a durable Step carries the step's name as its ToolUseID, so it adds that name as a key: the set may hold a key no tool call has, or a call the model requested that never ran, which only makes an absence proof for that key impossible, never a wrong one. Journal records a redaction replaced cannot be projected; producers refuse a journal holding one (ErrRedacted).

func ToolUseKeyFor

func ToolUseKeyFor(toolUseID string) string

ToolUseKeyFor is the absence key for a tool-use id: pass it to ProveAbsent / ProveAbsentBundle with ToolUseKeys to prove no tool call with that id happened in the run. It mirrors PolicyUsedKeyFor for the PolicyUsedKeys key set.

func UnmarshalStrict added in v0.7.0

func UnmarshalStrict(data []byte, v any) error

UnmarshalStrict decodes a JSON proof artifact (a bundle, certificate, evidence package, or tree head) so that what a person reads in the file is exactly what is verified. encoding/json accepts a duplicate key and keeps the last value, matches keys case-insensitively (so "Size" and "SIZE" are the same field), ignores unknown fields, and rewrites invalid UTF-8; each lets a file show a reader one value while the verifier checks another. UnmarshalStrict rejects all four: duplicate names (at any depth, including inside embedded raw JSON), names that do not match a field exactly, unknown fields, and invalid UTF-8.

It also rejects the spellings encoding/json decodes lossily, so each decoded value has one spelling: an escaped lone surrogate ("\ud800"), which becomes U+FFFD, in any string decoded into Go, and, in a []byte field, base64 that is not the standard encoding of the bytes it decodes to (line breaks, which encoding/json skips, or unused bits set in the last character). JSON kept verbatim (json.RawMessage) is checked for duplicate names only: it is committed as written.

A message (agent.Message) decodes with its own UnmarshalJSON, which matches names loosely, so UnmarshalStrict checks it against its wire shape: exactly "role" and "parts", and each part exactly the fields of the part its "type" names.

An artifact with a format (ProofBundle, AbsenceBundle, RunCertificate, CurrentGrantProof, EventInclusion, EvidencePackage, SignedTreeHead, AnchorEntry, JournalExport) is checked for its "format" first, at the top level and wherever one artifact carries another: one that has none, or not the one this version reads, is refused with an error wrapping ErrFormat, before any rule above can report a field its own format names differently. Every other error wraps ErrMalformed.

It uses only the standard encoding/json: the input is checked token by token against the target type before it is decoded, so the rules above hold without depending on encoding/json/v2.

func VerifyAbsence

func VerifyAbsence(root []byte, proof Absence) error

VerifyAbsence returns nil if proof shows Key is absent from the key set committed by root: each named neighbor is included at its index, they bracket Key in sort order, and they are ADJACENT (or Key sits before the first / after the last / the set is empty). Adjacency is the load-bearing check: without it, bracketing alone does not preclude Key being present between two non-consecutive neighbors. It checks the proof against a bare root; AbsenceBundle.Verify is the form that also authenticates the root and its key set. A proof that does not hold is an error wrapping ErrNotVerified.

func VerifyAnchorInclusion

func VerifyAnchorInclusion(root []byte, entry AnchorEntry, proof Inclusion) error

VerifyAnchorInclusion returns nil if entry is the leaf at proof.Index in an anchor log of proof.Size entries committed by root, from the entry and proof alone. The entry must be of AnchorEntryFormat, carrying a head of STHFormat (ErrFormat), and state what the proof proves: its Seq is proof.Index, and its RunID is the run its signed head names (the only entries MemAnchorLog.Publish writes). An entry that misstates either, or is not the leaf, is an error wrapping ErrNotVerified, since the anchor log is another party's and a monitor reads both fields. It does not verify the entry's signed head; do that with entry.STH.Verify.

func VerifyConsistency

func VerifyConsistency(firstRoot, laterRoot []byte, proof Consistency) error

VerifyConsistency returns nil if firstRoot (an earlier Root over First records) is an append-only prefix of laterRoot (a Root over Size records), given the proof, checked from the two roots and the proof alone. A rewrite or reorder of any early record fails with an error wrapping ErrNotVerified.

func VerifyCurrentGrant added in v0.7.0

func VerifyCurrentGrant(sg SignedGrant, ledgerRunID string, p CurrentGrantProof, lastSeen *SignedTreeHead, logV Verifier) error

VerifyCurrentGrant returns nil if sg is the current earned grant of the ledger run ledgerRunID, under logV (the log key, out of band). It requires that:

  • the proof is about ledgerRunID. A grant recorded in any other run signed by the same log key (an agent run that records the grant it acts under, say) proves nothing about the ledger;
  • the proof verifies, its record is the anchored leaf of exactly sg, and that leaf is the LAST leaf of the signed ledger head (index Size-1). A grant superseded by a later promotion or demotion fails, because a later ledger head's last leaf is the newer grant;
  • when lastSeen is not nil, lastSeen is an authentic head of ledgerRunID under logV and the presented head is an append-only extension of it (checked with the proof's consistency proof), so a head older than one the verifier already saw cannot be presented as current.

How an offline verifier obtains the latest head: it takes the newest head of ledgerRunID from the anchor log (anchor the ledger through an AuditedStore, or sign and publish its heads to an Anchor), confirms that head is in the anchor log (MemAnchorLog.Prove, VerifyAnchorInclusion), and keeps the newest head it has verified as lastSeen for its next check, asking the prover for a proof from lastSeen.Size. Pass nil only on first contact with a ledger: the check then shows that sg was current at the presented head, which may be older than the latest one. Freshness beyond what the verifier has seen comes from the anchor log, not from the proof.

It does not check sg's chain to the root or its expiry; do that with VerifyDelegationChain(..., EarnedRules) and Grant.Expired. A proof that does not hold is an error wrapping ErrNotVerified; one of another format, ErrFormat.

func VerifyDelegationChain

func VerifyDelegationChain(chain []SignedGrant, issuerVerifier func(issuer string) (Verifier, bool), rules ScopeRules) error

VerifyDelegationChain returns nil if chain, ordered root-first (index 0) to leaf-last, is valid:

  1. each grant's signature verifies under its issuer's verifier (resolved via issuerVerifier),
  2. the root has no ParentRef,
  3. every other grant is a valid delegation of the previous one (CheckAttenuation with rules): hash-linked, issued by the parent's subject, expiring no later, and keeping every scope constraint equal or narrower.

It establishes that the leaf's authority descends, unbroken and never widened, from the root grant, with each hop signed by the principal it names. The caller still checks that the root's Issuer is a principal it trusts to grant that authority (issuerVerifier resolves any issuer it knows), and that the leaf is unexpired at the time of use (Grant.Expired, which needs a clock). A chain that does not hold, including one with an issuer issuerVerifier does not know, is an error wrapping ErrNotVerified.

func VerifyEventInclusion

func VerifyEventInclusion(root []byte, event agent.RunEvent, proof EventInclusion) error

VerifyEventInclusion returns nil if event, under the salt proof discloses, is the leaf at proof.Index in a log of proof.Size events committed by root: from the event and proof alone, no other events needed. The event must canonicalize identically to when it was added. A proof that does not hold is an error wrapping ErrNotVerified; one whose format is not EventInclusionFormat, ErrFormat; an event that cannot be canonicalized or a salt that is not agent.SaltSize bytes, ErrMalformed.

func VerifyInclusion

func VerifyInclusion(root, recordBytes []byte, proof Inclusion) error

VerifyInclusion returns nil if recordBytes, the bytes a journal stores for a record, are the leaf at proof.Index in a run of proof.Size records committed by root: checked from the bytes and the proof alone, no other records needed, and without decoding the bytes. Otherwise the error wraps ErrNotVerified.

func VerifySignature

func VerifySignature(head, sig []byte, v Verifier) error

VerifySignature returns nil if sig is a valid signature of head (see Sign) under v, and an error wrapping ErrNotVerified otherwise.

func WithGrant

func WithGrant(ctx context.Context, sg SignedGrant, signer Signer) context.Context

WithGrant binds the acting principal's signed grant and its signer to ctx. AttenuatingSubAgent reads this to mint a narrower, signed child grant on each delegation and rebind the sub-run's identity to it. Set it once at the root (the top principal's grant and key); each delegation then propagates the narrowed child grant automatically, so authority only ever shrinks down the tree.

A nil signer, or a typed nil (a nil pointer in the Signer interface), binds the grant with no signer: a delegation under it is refused with ErrConfig (recording nothing, so binding a signer and driving again continues it), and a rollback cannot verify a delegation against it.

func WithRollbackGrants added in v0.10.0

func WithRollbackGrants(ctx context.Context, signer Signer, grants ...SignedGrant) context.Context

WithRollbackGrants binds grants, with the signer the delegations minted under them signed their child grants with, as further parents a saga rollback accepts for the delegations it walks. A delegation's journaled grant is verified against the bound grant its ParentRef links to, so a saga whose delegations were minted under several grants (the root grant was rotated, or expired and was replaced, between drives) rolls back with every one of them bound: the acting grant (WithGrant) and the earlier ones here. A grant bound here is never minted from and is not the acting grant (GrantFrom does not return it): it only lets the rollback verify the delegations minted under it. It may have expired, as the rollback does not check expiry.

Each call adds to the grants an outer context bound, so grants signed with different signers (a rotated key) are bound by calling it once per signer. They reach the delegations the acting grant reaches (those of the run ctx is passed to, and of its plain sub-agents), never inside a delegation's sub-run: an AttenuatingSubAgent's sub-run, live or rolled back, is bound to its own journaled grant alone, so a grandchild still verifies against its parent. With a nil or typed-nil signer it binds nothing.

The journal is trusted, as everywhere in bide: a rollback verifies the journaled child grant's signature and narrowing, not that this delegation is the one that minted it. Whoever can write the journal could therefore plant a validly signed child grant of a rollback-only grant (one minted by another run, say) and have a compensation run under it; binding a grant here adds its children to what such a writer could use. Writing the journal is already trusted, so this is outside the threat model (see the delegation guide).

Types

type Absence

type Absence struct {
	Key   string    `json:"key"`             // the key claimed absent from the committed set
	Size  int       `json:"size"`            // number of committed keys the proof is against
	Left  *Neighbor `json:"left,omitempty"`  // committed key just below Key in sort order; nil if Key sorts before all
	Right *Neighbor `json:"right,omitempty"` // committed key just above Key in sort order; nil if Key sorts after all
}

Absence is a non-membership proof: Key is not among the Size committed keys, shown by the adjacent committed keys that bracket it. Left is nil when Key sorts before all keys; Right is nil when it sorts after all; both nil only for the empty key set.

What it discloses: the absent key, the number of distinct keys in the set (Size), and, in plain text, the one or two committed keys adjacent to it in sort order with their indices, which place them in the sorted set. For ToolUseKeys that is the ID of up to two tool calls the run did make; for PolicyUsedKeys, up to two policy digests the run did use. This is inherent to a sorted-set absence proof: the verifier must see the neighbours to check they bracket the key. It does not disclose the neighbours' journal records (their tool names, arguments, or results), and each neighbour's audit path carries hashes of further keys, which are not salted: a holder of the proof who can guess a key of the set (a policy digest is public; a tool-use ID usually is not) can confirm it against those hashes.

func ProveAbsent

func ProveAbsent(records []agent.Record, set KeySet, key string) (Absence, error)

ProveAbsent builds an absence proof for key over records in set, or errors if the key is actually present (you cannot prove absence of something that happened) or does not carry the set's prefix (it could never be present, so its absence would say nothing).

type AbsenceBundle

type AbsenceBundle struct {
	Format  string         `json:"format"`  // AbsenceFormat
	RunID   string         `json:"run_id"`  // the run whose key set the proof is against; must equal STH.RunID
	Absence Absence        `json:"absence"` // the non-membership proof
	STH     SignedTreeHead `json:"sth"`     // the signed commitment to the key set the proof is proven against
}

AbsenceBundle is the portable, anchorable form of an absence proof, mirroring ProofBundle: the proof plus the signed tree head committing the key set it is proven against. The STH commits to the key set's kind, the run, and the journal tree the set was projected from.

func ProveAbsentBundle

func ProveAbsentBundle(records []agent.Record, set KeySet, key string, sth SignedTreeHead) (AbsenceBundle, error)

ProveAbsentBundle builds an anchorable absence proof bound to sth, a signed key-set head of set (see SignAbsenceRoot). records is the run's journal: its prefix of sth.Journal.Size records must hash to sth.Journal.Root, and the key set projected from that prefix must be the one sth signs (the fidelity guard: a stale, foreign, or wrong-kind STH is rejected).

func (AbsenceBundle) Verify

func (b AbsenceBundle) Verify(v Verifier, set KeySet) error

Verify returns nil if the bundle authentically proves Absence.Key absent from the key set set of run RunID, under v (obtained out of band). It checks the STH signature, that the STH is a head of set's kind for RunID (with its source journal named), that the key carries the set's prefix, that the proof is bound to the signed size, and non-membership under the signed root. The absence holds for the journal prefix STH.Journal names; confirm from the anchor log that it is the run's latest head before reading it as "never happened in the run." A bundle that does not hold is an error wrapping ErrNotVerified; one of another format, ErrFormat.

type Alg added in v0.9.0

type Alg = agent.Alg

Alg names a signature scheme. It is agent.Alg, so the scheme an approver decision is journaled under (agent.Record.ApproverAlg) and the scheme of an audit Verifier are one type.

const (
	AlgEd25519 Alg = "ed25519"           // classical Ed25519 (small, fast, FIPS-approved)
	AlgMLDSA65 Alg = "ml-dsa-65"         // post-quantum ML-DSA-65 (FIPS 204)
	AlgHybrid  Alg = "ed25519+ml-dsa-65" // both schemes; valid only if both verify
)

The signature schemes.

type Anchor

type Anchor interface {
	Publish(ctx context.Context, runID string, sth SignedTreeHead) error
}

Anchor is the bring-your-own port for out-of-band anchoring: publish a SignedTreeHead to a trust domain separate from the app (a Certificate-Transparency-style log, a notary / timestamping service, another account's WORM store, a public ledger). Bide ships MemAnchorLog as a reference; you implement Publish against the external log you trust.

Order: one AuditedStore publishes a run's heads in increasing size. When two processes anchor the same run (around a lease handoff), a smaller head can arrive after a larger one. Both are valid commitments to prefixes of one journal, so a monitor should check consistency between a run's heads ordered by size, not by arrival.

type AnchorEntry

type AnchorEntry struct {
	Format string         `json:"format"` // AnchorEntryFormat
	Seq    int            `json:"seq"`    // the entry's position in the anchor log
	RunID  string         `json:"run_id"` // the run whose commitment was published
	STH    SignedTreeHead `json:"sth"`    // the published signed tree head
}

AnchorEntry is one published commitment recorded by a transparency log: the run it commits to, its signed tree head, and the entry's position in the anchor log.

Anchor leaves are not salted. An anchor-log proof's path hashes cover neighbouring entries, and a neighbour's sequence number and run ID may be guessable, but its signed head is not: the head's root commits to salted leaves and its Ed25519 signature needs the signing key, so a proof holder can confirm a neighbouring entry only by already holding that exact signed head, which tells them only that the head they hold was anchored there. A head published without a signature has no such entropy; do not anchor one.

type ApprovalVerdict added in v0.4.0

type ApprovalVerdict struct {
	// Need is the expected policy's k.
	Need int `json:"need"`
	// ToolName and Args are the call the approvers decided on, from the proven request.
	ToolName string          `json:"tool_name"`
	Args     json.RawMessage `json:"args,omitempty"`
	// Counted lists the approvers whose approval counted in the recount, in journal order.
	Counted []string `json:"counted"`
	// DeniedBy lists the approvers whose counted decision was a denial.
	DeniedBy []string `json:"denied_by,omitempty"`
	// Ignored lists every other disclosed decision, with the reason it did not count.
	Ignored []IgnoredDecision `json:"ignored,omitempty"`
	// Problems lists every way the evidence is inconsistent with itself or with the expected
	// policy: an omitted decision the gate read, a recount that disagrees with the gate's
	// journaled tally, a different enforced policy, or records out of order. Any problem fails
	// the verdict.
	Problems []string `json:"problems,omitempty"`
	// OK reports no Problems and at least Need approvals counted.
	OK bool `json:"ok"`
}

ApprovalVerdict is the offline result of checking an m-of-n approval gate from evidence.

func VerifyApprovals added in v0.4.0

func VerifyApprovals(actions []EvidenceAction, toolUseID string, policy agent.ApprovalPolicy, verifierFor agent.ApproverVerifierFor, logV Verifier) (ApprovalVerdict, error)

VerifyApprovals checks offline that the m-of-n gate on toolUseID held, trusting only its inputs: actions (ApprovalEvidence's output, alone or inside an EvidencePackage), the policy the auditor expects, verifierFor (each approver's key, the same resolver shape the gate uses at run time), and logV (the log operator's key, obtained out of band).

It verifies the proofs of the request, the gate's journaled tally, and the call's result, all under one signed tree head and in that journal order, then recounts the disclosed decisions with agent.TallyApprovals, the exact rule the gate ran, against the proven call's name and arguments. It reports a Problem if the evidence omits a decision the gate read, if the recount disagrees with the journaled tally, or if the gate enforced a different policy than expected. A decision signed for other arguments, another run, or another call, or under another scheme than its approver's key, does not verify and is listed in Ignored.

It returns the verdict with a nil error when the gate held (OK), and with an error wrapping ErrNotVerified when it did not: Problems, too few approvals, or evidence that lacks the request, the tally, or the result, or whose proof of one of them does not verify. An error wrapping agent.ErrConfig is an invalid expected policy, no resolver, or a policy two of whose approvers resolve to one signing key under verifierFor or one of whose approvers' verifiers reports no key identity (see agent.ApprovalPolicy.ValidateKeys); ErrFormat or ErrMalformed, a proof or a record that cannot be read. It certifies that the gate held as recorded, not that the approvers' judgment was right.

type AttenuateFunc

type AttenuateFunc func(parent Grant, subAgent string) Grant

AttenuateFunc derives a child grant from the parent grant and the delegating sub-agent's name. It sets the narrower Scope (and may set an earlier NotAfterUnix; a Subject it sets must be the sub-agent's name, or the delegation is refused); AttenuatingSubAgent fills in ParentRef and, if empty, Issuer (the parent's Subject), Subject (the sub-agent name), and NotAfterUnix (the parent's), then checks the result with CheckAttenuation before signing it, so a widening func is refused at delegation time rather than caught later by VerifyDelegationChain.

type AttenuationConfig added in v0.10.0

type AttenuationConfig struct {
	// Store is the journal the sub-runs journal to, where each child grant is recorded as
	// a durable leaf of its sub-run. Give it the store the parent and the sub-agent use, for a
	// unified, provable journal.
	Store *agent.Journal
	// Narrow derives each child grant from the parent's (see AttenuateFunc).
	Narrow AttenuateFunc
	// Rules says how each scope key may narrow; CheckAttenuation enforces it before a child
	// grant is signed.
	Rules ScopeRules
}

AttenuationConfig configures AttenuatingSubAgent.

type AuditedStore

type AuditedStore struct {
	// contains filtered or unexported fields
}

AuditedStore wraps a store so every journal write is continuously anchored: whenever a run's journal grows, it commits an RFC 6962 Merkle root over the run, signs a Signed Tree Head, and publishes it to an Anchor (an external transparency log). The caller drives the agent over a Journal on it exactly as on the store it wraps (agent.NewJournal(audit.NewAuditedStore(store, signer, anchor))) and gets a stream of signed, out-of-band commitments for free.

Anchoring is a SIDE CHANNEL: a publish failure NEVER fails the write. Failing an Insert because an STH could not be published would be dangerous: the write already succeeded, and the caller could wrongly retry a non-idempotent step. Publish errors go to the optional OnError hook instead; the journal remains the source of truth.

func NewAuditedStore

func NewAuditedStore(inner agent.Store, signer Signer, anchor Anchor) (*AuditedStore, error)

NewAuditedStore wraps inner so each journal growth is signed by signer (ed25519, ML-DSA-65, or hybrid) and published to anchor. Timestamps default to time.Now().UnixNano(); override with WithClock for tests.

It refuses a nil store or anchor, and a signer without a usable key, with an error wrapping agent.ErrConfig. Such a signer could sign nothing, and the store may not fail a write once the inner store has stored it, so the signer is refused here, before any write, rather than when the first write is anchored.

func (*AuditedStore) Format added in v0.7.0

func (a *AuditedStore) Format(f fmt.State, _ rune)

Format writes String for every verb, so no fmt verb prints the signing key.

func (*AuditedStore) Get added in v0.11.0

func (a *AuditedStore) Get(ctx context.Context, runID, name string) (agent.Entry, bool, error)

Get reads through to the inner store.

func (*AuditedStore) Insert added in v0.11.0

func (a *AuditedStore) Insert(ctx context.Context, runID, name string, data []byte) (agent.Entry, bool, error)

Insert stores the entry in the inner store, then anchors the run's tree head if the journal grew past the last anchored head. It anchors after every insert but the journal header's (a journal writes the header just before the run's first record, whose insert anchors both, so a run's anchored heads never stop at its header): also after an insert that found the entry stored (another writer's, or this writer's retry of a write whose first attempt committed but reported an error, A3), and after one that failed, which may have committed all the same. The anchoring never changes what Insert returns.

Under a context that is done it cannot anchor (the read and the publish would fail with the context), and anchoring without the caller's context could block with no deadline. If the call stored the entry, or failed (it may have committed all the same), it reports an error wrapping the context's to OnError, the signal to Reanchor the run: a run's last write, run:complete included, may be written by a drive whose context is cancelled meanwhile. An insert that stored nothing (the entry was already there) has nothing to anchor and reports nothing.

func (*AuditedStore) Load added in v0.11.0

func (a *AuditedStore) Load(ctx context.Context, runID string, after int64) iter.Seq2[agent.Entry, error]

Load reads through to the inner store.

func (*AuditedStore) OnError

func (a *AuditedStore) OnError(fn func(runID string, err error)) *AuditedStore

OnError sets a hook called when anchoring a write fails (Publish, or committing the root). Without it, anchor errors are swallowed: the write still succeeds. Returns the store.

func (*AuditedStore) Reanchor added in v0.11.0

func (a *AuditedStore) Reanchor(ctx context.Context, runID string) error

Reanchor anchors runID's journal now, if it holds records no published head covers. A failed publish is covered by the run's next write, but a run's last write has none, and a write under a context that is done is not anchored at all (see Insert): OnError is the signal to call Reanchor for that run (once the anchor is reachable again), out of band. It returns the anchoring error, and publishes nothing when the anchored head is current as far as this AuditedStore knows: it remembers the heads it published, so another AuditedStore over the same store (another process, or after a restart) publishes the current head again, which a monitor reads as the same size, not a change.

func (*AuditedStore) String added in v0.7.0

func (*AuditedStore) String() string

String returns a description of the store that does not include its signing key.

func (*AuditedStore) Unwrap added in v0.8.0

func (a *AuditedStore) Unwrap() agent.Store

Unwrap returns the wrapped store, so agent.Capability finds its optional capabilities (agent.Leaser, agent.Lister) through the wrapper: an AuditedStore has exactly the capabilities of the store it wraps. Leasing and listing touch no journal, so they bypass anchoring. It passes run IDs and names through unchanged, as Unwrap requires.

func (*AuditedStore) WithClock

func (a *AuditedStore) WithClock(now func() int64) *AuditedStore

WithClock sets a deterministic timestamp source (for tests). Returns the store for chaining.

type Consistency

type Consistency struct {
	First int      `json:"first"` // m: the earlier tree size
	Size  int      `json:"size"`  // n: the current tree size
	Path  [][]byte `json:"path"`  // proof hashes
}

Consistency is an RFC 6962 consistency proof: the first First records are an append-only prefix of the Size-record tree.

func ProveConsistency

func ProveConsistency(ctx context.Context, store *agent.Journal, runID string, first int) (Consistency, error)

ProveConsistency proves that runID's first `first` records are an append-only prefix of its current journal — i.e. nothing before `first` was changed or reordered, only appended.

type ConvergenceContent

type ConvergenceContent struct {
	Digest      string          `json:"digest"`      // the policy digest this certificate certifies (treated as opaque here)
	Certificate json.RawMessage `json:"certificate"` // the opaque, serialized convergence certificate bytes
}

ConvergenceContent is the payload of a convergence leaf: an opaque, serialized convergence certificate and the policy digest it certifies. Committing it as a journal leaf puts the convergence evidence under the same signed tree head and inclusion proofs as the policy and the actions taken under it, so an auditor can prove not merely that a policy was anchored but that the anchored policy was certified convergent in the same committed tree.

audit treats the certificate as opaque bytes and the digest as an opaque identifier, exactly as it does for a policy leaf: it does not import gsm and does not recompute either. The producer of the certificate is govern.CertifyConvergence; a rigorous verifier re-derives the certificate from the disclosed policy bytes (by rebuilding the gsm machine) and compares, so a leaf that overstates convergence is caught outside audit, not trusted here.

type CurrentGrantProof added in v0.7.0

type CurrentGrantProof struct {
	Format      string      `json:"format"`      // CurrentGrantFormat
	Leaf        ProofBundle `json:"leaf"`        // the ledger's last leaf under the presented head
	Consistency Consistency `json:"consistency"` // from the verifier's last-seen size to Leaf.STH.Size
}

CurrentGrantProof proves which earned grant is current in a ledger: Leaf proves the grant is the last leaf of a signed ledger head, and Consistency proves that head is an append-only extension of the head the verifier saw last, so a verifier that remembers its last-seen head cannot be shown an older head as current.

func ProveCurrentGrant added in v0.7.0

func ProveCurrentGrant(ctx context.Context, ledger *agent.Journal, ledgerRunID string, sth SignedTreeHead, lastSeenSize int) (CurrentGrantProof, error)

ProveCurrentGrant proves which earned grant is current in the ledger at the signed ledger head sth: an inclusion proof of the ledger's last leaf (index sth.Size-1), plus a consistency proof from lastSeenSize (the size of the ledger head the verifier saw last, 0 if none) to sth.Size. Pass the latest anchored head of the ledger run. It fails if sth is not a head of ledgerRunID's journal, or if lastSeenSize is outside [0, sth.Size].

type EarnedAuthority

type EarnedAuthority struct {
	// contains filtered or unexported fields
}

EarnedAuthority is a control loop that adjusts an agent's delegated limit from its provable track record. Authority starts at the ladder's baseline rung and is promoted one rung after promoteEvery compliant actions, capped at the top rung; an anomaly resets it to baseline at once. Each change re-issues a signed grant that is a CHILD of the root grant (ParentRef to root, every root scope constraint kept, "limit" = the current rung), so the earned limit provably never exceeds the root ceiling: every earned grant passes VerifyDelegationChain against the root with ScopeRules{"limit": NumericAtMost}, and even a buggy or compromised controller cannot widen past what the root principal authorized. Every earned grant carries the root's NotAfterUnix, so none outlives the authority it was carved from.

Revocation. A demotion must take the old, higher grant out of use at once, not when it expires. Each re-issued grant is appended to a ledger: a journal run (ledgerRunID in ledger) that holds nothing but this controller's grants, one leaf per issue (the grant's ID names its ledger index, so no two issues share a digest, even at the same rung). The CURRENT grant is the last leaf of the ledger; every earlier grant is superseded, whether the change was a promotion or a demotion. An offline verifier learns the current grant the way it learns any run's state: from the latest signed head of the ledger run in the anchor log (anchor the ledger with an AuditedStore, or sign and publish its heads), checked for consistency with the last head it saw, so the ledger cannot be rolled back to a superseded grant. ProveCurrentGrant produces the proof and VerifyCurrentGrant checks it: the grant is the leaf at index Size-1 of an authentic head of the ledger run, and that head extends the verifier's last-seen head.

It is deliberately a sequential controller, not a convergent gsm machine. Earning is temporal: how many compliant actions preceded a promotion, and whether an anomaly interleaved, is order-dependent, so it belongs to the durable/saga tier, not the order-independent governance tier (a promote event and a record event do not commute, so no convergent machine expresses it). The ENFORCEMENT of the current limit stays a convergent gsm invariant on the work machine (see examples/govern/earned-authority); the controller only decides the limit and re-issues the grant.

The asymmetry is the safety property: promotion is slow, capped, and evidence-gated; attenuation is immediate and needs no gate, because shrinking authority is always safe.

func NewEarnedAuthority

func NewEarnedAuthority(ctx context.Context, ladder []int, promoteEvery int, root SignedGrant, signer Signer, subject string, ledger *agent.Journal, ledgerRunID string) (*EarnedAuthority, error)

NewEarnedAuthority builds a controller for subject, granting the limits in ladder (rung 0 is the baseline) and promoting one rung every promoteEvery compliant actions. root is the ceiling: the top rung must not exceed root's numeric "limit" scope. signer signs as root's Subject. Each issued grant is recorded in ledgerRunID of ledger, which must hold only this controller's grants; a restarted controller continues the ledger at baseline. It issues the baseline grant immediately.

func (*EarnedAuthority) FlagAnomaly

func (e *EarnedAuthority) FlagAnomaly(ctx context.Context) (changed bool, err error)

FlagAnomaly resets authority to the baseline rung immediately, returning whether it changed. The re-issued baseline grant supersedes every earlier grant in the ledger.

func (*EarnedAuthority) Grant

func (e *EarnedAuthority) Grant() SignedGrant

Grant returns the current signed earned grant (a child of root, scope limit = the current rung).

func (*EarnedAuthority) Limit

func (e *EarnedAuthority) Limit() int

Limit returns the currently earned limit.

func (*EarnedAuthority) RecordCompliant

func (e *EarnedAuthority) RecordCompliant(ctx context.Context) (promoted bool, err error)

RecordCompliant notes one clean governed action; it promotes one rung when the streak reaches the threshold (unless already at the top), returning whether a promotion happened.

func (*EarnedAuthority) Tier

func (e *EarnedAuthority) Tier() int

Tier returns the current rung (0 is baseline).

type Ed25519Signer

type Ed25519Signer struct {
	Priv ed25519.PrivateKey // the Ed25519 private key that signs
}

Ed25519Signer signs with an Ed25519 private key.

func (Ed25519Signer) Alg

func (Ed25519Signer) Alg() Alg

Alg reports the signature scheme (AlgEd25519).

func (Ed25519Signer) Format added in v0.7.0

func (s Ed25519Signer) Format(f fmt.State, _ rune)

Format writes String for every verb, so no fmt verb prints the key.

func (Ed25519Signer) GobEncode added in v0.7.0

func (Ed25519Signer) GobEncode() ([]byte, error)

GobEncode refuses: the signer holds a private key.

func (Ed25519Signer) MarshalJSON added in v0.7.0

func (Ed25519Signer) MarshalJSON() ([]byte, error)

MarshalJSON refuses: the signer holds a private key.

func (Ed25519Signer) PublicKey added in v0.9.0

func (s Ed25519Signer) PublicKey() []byte

PublicKey returns the 32-byte Ed25519 public key, or nil if the private key has the wrong length.

func (Ed25519Signer) Sign

func (s Ed25519Signer) Sign(m []byte) ([]byte, error)

Sign returns the Ed25519 signature over m, or an error if the private key has the wrong length.

func (Ed25519Signer) String added in v0.7.0

func (Ed25519Signer) String() string

String returns a description of the signer that does not include the key.

type Ed25519Verifier

type Ed25519Verifier struct {
	Pub ed25519.PublicKey // the Ed25519 public key that verifies
}

Ed25519Verifier verifies with an Ed25519 public key.

func (Ed25519Verifier) Alg

func (Ed25519Verifier) Alg() Alg

Alg reports the signature scheme (AlgEd25519).

func (Ed25519Verifier) KeyIDs added in v0.9.0

func (v Ed25519Verifier) KeyIDs() []string

KeyIDs identifies the public key, for agent.ApproverVerifier: one entry, KeyID(AlgEd25519, Pub). A weak public key (see CheckEd25519PublicKey) verifies nothing and reports no entry, so an approval gate refuses it as a configuration error rather than seating it. This also keeps one secret to one identity: a mixed-order key A + T, which A's secret can sign for, and a non-canonical spelling of A are both refused.

func (Ed25519Verifier) PublicKey added in v0.9.0

func (v Ed25519Verifier) PublicKey() []byte

PublicKey returns the public key, or nil if it has the wrong length.

func (Ed25519Verifier) Verify

func (v Ed25519Verifier) Verify(m, sig []byte) bool

Verify reports whether sig is a valid Ed25519 signature over m. A weak public key (see CheckEd25519PublicKey: the wrong length, not canonically encoded, or not in the prime-order subgroup) verifies nothing, since a small-order key accepts forged signatures; it never panics.

type EventInclusion added in v0.7.0

type EventInclusion struct {
	Format string `json:"format"` // EventInclusionFormat
	Salt   []byte `json:"salt"`   // the proven event's salt (agent.SaltSize bytes)
	Inclusion
}

EventInclusion is an inclusion proof for one event: the event's own salt, which its leaf commits to, and the RFC 6962 audit path (the same Inclusion journal proofs use) from that leaf to the log's Root. It discloses the proven event's salt, its index, the log's size, and the sibling hashes on its path. Each sibling hash covers other events' leaves, and every leaf commits to its own random salt, so a holder of the proof cannot confirm a guess about any other event by hashing it: the salt a guess would need is disclosed only by that event's own proof.

type EventLog

type EventLog struct {
	// contains filtered or unexported fields
}

EventLog is an append-only, tamper-evident log of one run's RunEvents. Build it by Add-ing events in emission order (Agent.Stream emits them ordered from a single goroutine); then Root/Head to commit, Prove for selective disclosure, and audit.Sign to anchor. Not safe for concurrent Add — feed it from the one goroutine ranging Events.

func EventLogFromJournal

func EventLogFromJournal(ctx context.Context, store *agent.Journal, runID string) (*EventLog, error)

EventLogFromJournal builds an EventLog from runID's DURABLE journal by projecting it to the semantic events Agent.Stream re-emits on resume (agent.ReplayEvents). This is the crash-durable, resume-stable counterpart to filling an EventLog from the live stream: it is a deterministic function of the persisted journal, so its Root / STH are byte-identical before and after a crash. THIS is the artifact to anchor for a durable audit trail; the live-stream EventLog is a real-time view (and its Root shifts between a fresh run and its replay because live-only events differ). Root / Head / TreeHead / Prove / ProveConsistency then work exactly as they do on any EventLog.

Each projected event comes from one journal record, and its salt is derived from that record's random salt (journalEventSalt), so the projection needs no state beyond the journal. It errors if a source record has no agent.SaltSize salt, and refuses a journal it cannot project: one holding a redacted record (ErrRedacted) or a record whose stored bytes read two ways to JSON readers (ErrMalformed).

func LoadEventLog

func LoadEventLog(ctx context.Context, evStore EventStore, runID string) (*EventLog, error)

LoadEventLog rebuilds an EventLog from evStore's persisted leaves for runID, ready for Root / Head / TreeHead / Prove / ProveConsistency — from the store alone, no journal needed. The leaves were canonicalized, salts included, when stored, so inclusion proofs verify against the original events exactly as if the log had been built live. It refuses a leaf that is not a salted bide.audit.event-leaf.v2 leaf: its proof would have no salt to disclose.

func NewEventLog

func NewEventLog() *EventLog

NewEventLog returns an empty log.

func (*EventLog) Add

func (l *EventLog) Add(e agent.RunEvent) error

Add appends one event as a canonical, kind-tagged leaf that commits to a fresh random salt (agent.SaltSize bytes from crypto/rand; see EventInclusion). It errors if the event cannot be canonicalized (it holds a string that is not valid UTF-8, for one) or the system's random source fails.

func (*EventLog) Head

func (l *EventLog) Head() []byte

Head is the linear SHA-256 hash-chain commitment over the events (domain-separated), the event-stream analogue of audit.Head: head_0 = H(eventDomain), head_i = H(head_{i-1} || canonical(event_i)). Cheaper than Root, no selective disclosure.

func (*EventLog) Len

func (l *EventLog) Len() int

Len is the number of events committed so far.

func (*EventLog) Prove

func (l *EventLog) Prove(index int) (EventInclusion, error)

Prove returns an inclusion proof for the event at index against Root: enough to verify that one event WITHOUT revealing any other (selective disclosure over the event stream). The proof carries the event's salt and nothing about any other event beyond the hashes on its path (see EventInclusion). audit.Sign over Root and this proof compose exactly as they do for the journal.

func (*EventLog) ProveConsistency

func (l *EventLog) ProveConsistency(first int) (Consistency, error)

ProveConsistency proves the first `first` events are an append-only PREFIX of the current log — nothing observed earlier was rewritten or reordered, only appended. Verify with the shared VerifyConsistency against two published event-log roots (e.g. two STH Roots taken at different points in the run). This is the transparency-log guarantee over the event stream, the same one ProveConsistency gives over the journal.

func (*EventLog) Root

func (l *EventLog) Root() []byte

Root is the RFC 6962 Merkle root over every event added so far (empty-log root = SHA-256(); identical shape to audit.Root over the journal). Supports Prove.

func (*EventLog) TreeHead

func (l *EventLog) TreeHead(runID string, timestamp int64) TreeHead

TreeHead returns a commitment to runID's events added so far at the given timestamp, the EventLog analogue of NewTreeHead over the journal. Its Kind is TreeEvents, so a signed event head never verifies as a journal head. timestamp is Unix nanoseconds. Sign it with SignTreeHead and verify with SignedTreeHead.Verify; the signature binds kind, run, root, size, and timestamp, so an event trail gets the same anchored guarantee a journal STH gives. Inclusion proofs (Prove / VerifyEventInclusion) check against the resulting Root; Size counts events.

type EventStore

type EventStore interface {
	// Append durably records leaf at position seq (0-based, contiguous) for runID. It MUST be
	// append-only and idempotent on (runID, seq): re-appending an already-stored seq with the
	// SAME bytes is a no-op (so a retried mirror is safe), while a DIFFERENT leaf at an
	// existing seq must error (a fork / tamper attempt). A seq beyond the next position is a
	// gap and must error.
	Append(ctx context.Context, runID string, seq int, leaf []byte) error
	// Load returns every leaf for runID in seq order (0..n-1), or nil if the run is unknown.
	Load(ctx context.Context, runID string) ([][]byte, error)
}

EventStore is the bring-your-own port: append canonical event leaves to your append-only backend (a Postgres table with UNIQUE(run_id, seq) and insert-only grants, object storage with object-lock/WORM, or a log) and read them back. Bide ships MemEventStore as the in-memory default; you implement Append/Load against infrastructure you already run. A leaf holds its event's salt (see EventInclusion), so a store must keep the bytes verbatim: a proof built from the reloaded trail discloses the salt from there.

type EvidenceAction

type EvidenceAction struct {
	// Label is a short human description of the action: the tool-use id for "tool", the step name
	// for "step", the called tool's name for "call", the approver id for "approval", "approval
	// tally" for "approval-tally", and "grant <issuer> -> <subject>" for "grant".
	Label string `json:"label"`
	// Kind is the action category, one of "tool", "step", "grant", "call", "approval", or
	// "approval-tally"; the proven record must be of that kind.
	Kind EvidenceKind `json:"kind"`
	// Ref is the identifier the action was selected by: the tool-use id ("tool", "call"), the record
	// name ("step", "approval", "approval-tally"), or the grant digest ("grant").
	Ref string `json:"ref,omitempty"`
	// Bundle is the inclusion proof that this action is a committed leaf under the package's STH.
	Bundle ProofBundle `json:"bundle"`
}

EvidenceAction is one proven action inside an EvidencePackage: a ProofBundle plus the label, kind, and reference the report names it by. Verify derives all three from the proven record and fails the item if they differ, so they cannot misdescribe what was proven.

func ApprovalEvidence added in v0.4.0

func ApprovalEvidence(ctx context.Context, store *agent.Journal, runID, toolUseID string, sth SignedTreeHead) ([]EvidenceAction, error)

ApprovalEvidence builds the complete evidence for an m-of-n gated call under one STH, in this order: the model turn that requested the call (Kind "call"), every decision record the gate read (Kind "approval", from the journaled tally's Records, in journal order), the gate's terminal tally (Kind "approval-tally"), and the call's result (Kind "tool", labelled by its tool-use id as every tool action is). The result appends to EvidencePackage.Actions, where each entry verifies as a package action; drop the trailing "tool" entry if the package already carries the call.

It discloses everything the gate read, valid or not, so VerifyApprovals can recount from the same inputs and detect an omission. It errors if the call has no journaled tally (it was not m-of-n gated, or its gate has not reached an outcome), no recorded request, or no result, or if a proof cannot be built against sth. The tally is read as VerifyApprovals reads it (UnmarshalStrict), so a tally that reads two ways is ErrMalformed here too.

type EvidenceConsistency added in v0.7.0

type EvidenceConsistency struct {
	// From is an earlier signed journal head of the same run. Proof.First must equal From.Size.
	From SignedTreeHead `json:"from"`
	// Proof is the RFC 6962 consistency proof from From's tree to the package STH's tree.
	Proof Consistency `json:"proof"`
}

EvidenceConsistency proves the journal only grew between two signed heads of one run: From, an earlier head (typically taken from the anchor log), and the package STH.

type EvidenceGrants

type EvidenceGrants struct {
	// Chain is the delegation chain, root-first to leaf-last, as signed grants (may be a single grant).
	Chain []SignedGrant `json:"chain"`
	// Anchored is one ProofBundle per grant in Chain, in the same order, proving that exact signed
	// grant is committed in the signed log.
	Anchored []EvidenceAction `json:"anchored"`
}

EvidenceGrants carries the run's authority: the delegation chain as signed grants (root-first) and, per grant, a ProofBundle proving the grant leaf is anchored in the same signed log as the actions.

Note the trust boundary: the anchoring bundles verify under the package's log key, and Verify checks that Chain is exactly the grants they prove. A grant's own issuer signature is made with the ISSUER's key, which is distinct from the log key and is not carried here; verifying the issuer signatures and the attenuation chain (VerifyDelegationChain) requires the issuers' public keys, which a caller supplies from their own PKI.

type EvidenceItem

type EvidenceItem struct {
	Kind     EvidenceKind `json:"kind"`  // the item's kind: an action kind, KindRunCertificate, or KindConsistency
	Label    string       `json:"label"` // the human label for the item
	Ref      string       `json:"ref,omitempty"`
	Verified bool         `json:"verified"` // whether this item verified under the key
	Note     string       `json:"note"`     // plain-English detail, on success or failure
}

EvidenceItem is one line of an EvidenceReport: what was checked, whether it verified, and a short human note (the reason on failure, or a one-line description on success).

type EvidenceKind added in v0.9.0

type EvidenceKind string

EvidenceKind names what one item of an EvidencePackage proves.

const (
	KindCall          EvidenceKind = "call"           // the model turn that requested the gated call (its name and arguments)
	KindApproval      EvidenceKind = "approval"       // one approver decision record
	KindApprovalTally EvidenceKind = "approval-tally" // the gate's journaled terminal tally
	KindTool          EvidenceKind = "tool"           // the call's result: the action, or the denial the model received
)

Evidence kinds for an m-of-n approval, in the order ApprovalEvidence emits them.

const (
	KindStep           EvidenceKind = "step"            // a durable step's recorded value
	KindGrant          EvidenceKind = "grant"           // an anchored signed grant leaf
	KindRunCertificate EvidenceKind = "run-certificate" // the package's run certificate (a report item only)
	KindConsistency    EvidenceKind = "consistency"     // the package's consistency proof (a report item only)
)

Other evidence kinds.

type EvidenceOption

type EvidenceOption func(*evidenceOptions)

An EvidenceOption selects what an EvidencePackage includes. With no action option (WithToolCall, WithStep, or WithAllToolCalls), Evidence defaults to proving every completed tool call, so the zero-configuration package is "prove every action this run took".

func WithAllToolCalls

func WithAllToolCalls() EvidenceOption

WithAllToolCalls proves every completed tool call in the run, enumerated from its history. It is the default when no other action option is given.

func WithConsistencyFrom added in v0.7.0

func WithConsistencyFrom(earlier SignedTreeHead) EvidenceOption

WithConsistencyFrom includes an append-only consistency proof from earlier, a signed journal head of the same run taken before this package (for instance the last head an auditor saw in the anchor log), to the package's STH: nothing earlier than earlier.Size was rewritten or reordered.

func WithGrants

func WithGrants() EvidenceOption

WithGrants includes the run's authority: every anchored grant leaf, proven included in the signed log, plus the signed grants themselves as the delegation chain (in journal order, root-first for a well-formed attenuation chain).

func WithLabel

func WithLabel(label string) EvidenceOption

WithLabel sets a human-readable description of what the evidence establishes.

func WithRunCertificate

func WithRunCertificate(spec RunCertSpec) EvidenceOption

WithRunCertificate includes the proof-carrying run certificate for the whole run, certified against spec.ApprovedPolicies. It fails assembly if the run used a policy outside the allowlist (CertifyRun refuses). Evidence signs the certificate with its own signer at its own timestamp; spec.Signer and spec.TimestampNanos are ignored. The verifier checks the certificate against its own allowlist (WithApprovedPolicies), not this one.

func WithStep

func WithStep(name string) EvidenceOption

WithStep adds a proof that the durable step with this name (a Step / Parallel task) happened.

func WithToolCall

func WithToolCall(toolUseID string) EvidenceOption

WithToolCall adds a proof that the completed tool call with this tool-use id happened in the run.

type EvidencePackage

type EvidencePackage struct {
	// Format is the domain-separated format/version tag (EvidenceFormat).
	Format string `json:"format"`
	// RunID is the run this evidence is about; it must equal STH.RunID.
	RunID string `json:"run_id"`
	// Label is an optional producer-supplied description of what this evidence establishes (e.g.
	// "Q3 refund approvals"). No proof covers it; the seal attributes it to the log key holder.
	Label string `json:"label,omitempty"`
	// Alg and PublicKey name the key that signed the STH and the seal: its scheme and its encoded
	// public key (see NewVerifier), carried so a reader knows which key to obtain out of band.
	// Verify requires them to equal the verifier's; it never trusts them as the key.
	Alg       Alg    `json:"alg"`
	PublicKey []byte `json:"public_key"`

	// STH is the run's signed journal head: the commitment every action proof below is checked
	// against. One STH is built for the whole package so all proofs share one root. Its Timestamp is
	// when the package's commitment was made.
	STH SignedTreeHead `json:"sth"`

	// Actions are the material tool calls / steps proven included in the signed log. Each carries a
	// human label and the ProofBundle proving that one action is a leaf under the STH's root.
	Actions []EvidenceAction `json:"actions"`

	// Grants optionally carries the authority behind the run: the delegation chain (root-first) as
	// signed grants, plus a ProofBundle per grant proving the grant leaf is anchored in the signed
	// log. It is nil when the caller did not request grants (WithGrants).
	Grants *EvidenceGrants `json:"grants,omitempty"`

	// RunCertificate optionally carries the proof-carrying run certificate (only-approved-policies +
	// policies-convergence-certified) for the whole run. It is nil unless WithRunCertificate was set.
	RunCertificate *RunCertificate `json:"run_certificate,omitempty"`

	// Consistency optionally carries an append-only consistency proof from an earlier signed head of
	// this run's journal to STH. It is nil unless WithConsistencyFrom was set.
	Consistency *EvidenceConsistency `json:"consistency,omitempty"`

	// Signature is the seal: the log key's signature, under Alg, over the package with Signature
	// empty (see Seal). A package changed after sealing, including one with actions appended, fails to
	// verify until resealed.
	Signature []byte `json:"signature"`
}

An EvidencePackage is the portable, offline-verifiable evidence artifact for one run: a single self-contained bundle that PACKAGES a run's already-produced cryptographic proofs (a signed tree head, per-action inclusion proofs, and optionally the run certificate, the authority grant chain, and an append-only consistency proof) into one JSON document you can store, email, or hand to an auditor. Verify re-checks each one against the out-of-band public key.

Every field is verified or derived from verified data. Format must be EvidenceFormat. RunID must be the run the signed STH commits to. Each action's Kind, Label, and Ref must be what its proven record says (a tool result cannot be presented as an approval). The grant chain must be exactly the grants the anchored leaves prove. The run certificate must be for this run and tree. The consistency proof is checked from an earlier signed head of the same run to the STH. Alg and PublicKey must be the verifying key's. Label, the one field no proof covers, is covered by the seal (Signature): the log key's signature over the whole package, so nothing (Label included) can be edited, and no item added or dropped, after the producer sealed it.

It is deliberately PURE DATA: no funcs, no keys, no unexported state, so it round-trips losslessly through JSON. The verifying key must be obtained out of band (the anchor / transparency log); Verify takes it as an argument rather than trusting Alg and PublicKey.

func Evidence

func Evidence(ctx context.Context, store *agent.Journal, runID string, s Signer, timestamp int64, opts ...EvidenceOption) (EvidencePackage, error)

Evidence assembles and seals the portable, offline-verifiable EvidencePackage for runID. It reads the journal once, builds ONE signed tree head over it (signed with s at timestamp, Unix nanoseconds), then, per the options, gathers inclusion proofs for the material tool calls / steps and, optionally, the run certificate, the anchored authority grants, and an append-only consistency proof, all bound to that one STH and built for exactly the records it commits to. With no action option it proves every completed tool call in the run. It finishes with Seal(s).

It mints no new proof types: every proof is an existing audit primitive, packaged into one document a verifier re-checks offline with EvidencePackage.Verify against the out-of-band key. The signer's scheme and public key are recorded as metadata (Verify requires them to match the verifier it is given; it does not trust the embedded ones).

func (*EvidencePackage) Seal added in v0.7.0

func (e *EvidencePackage) Seal(s Signer) error

Seal signs the package with s, the log key that signed its STH, over the package's JSON encoding with Signature empty under a dedicated domain tag. Evidence seals what it builds; a caller that adds actions afterwards (for instance ApprovalEvidence's output) seals again. It refuses a signer whose scheme and key are not the ones the package names (Alg, PublicKey), and a package with invalid UTF-8 in any string, which JSON would silently rewrite.

func (EvidencePackage) Verify

Verify re-checks every proof in the package against v, a verifier for the log key obtained OUT OF BAND (the anchor / transparency log), entirely offline. It does not trust the package's embedded key: it requires it to be v's. It verifies: (0) the format tag, that Alg and PublicKey are v's scheme and key, the seal, and that the STH is an authentic journal head of RunID; (1) each action ProofBundle, confirming the action is a leaf under that signed root and that its Kind, Label, and Ref are what the proven record says; (2) each anchored grant bundle the same way, and that Chain is exactly the grants they prove; (3) the run certificate, if present, is for this run and tree and passes VerifyRun against the allowlist given with WithApprovedPolicies; and (4) the consistency proof, if present, from its authentic earlier head of this run to the STH. A package with none of (1) to (4) proves nothing and does not verify. Every signed head the package carries is held to the timestamp rule (see CheckTimestamp): against time.Now and DefaultClockSkew unless WithVerifyTime and WithClockSkew say otherwise.

It returns a structured EvidenceReport with an overall verdict and a per-item verdict. The error is nil when the package verifies (OK), and wraps ErrNotVerified when it does not (the report says why). Any other error means the package could not be checked: it is not of the format this version reads (ErrFormat), or a proof it carries cannot be read (ErrFormat, ErrMalformed). Grant ISSUER signatures and the attenuation chain are NOT checked here (they need the issuers' keys, which are not in the package); a caller verifies those with VerifyDelegationChain and its own PKI.

type EvidenceReport

type EvidenceReport struct {
	// OK is the overall verdict: true only when the package-level checks pass and every item verified.
	OK bool `json:"ok"`
	// RunID echoes the package's run id; when OK it is the run the signed STH commits to.
	RunID string `json:"run_id"`
	// Label echoes the package's optional label; when OK it is the label the log key sealed.
	Label string `json:"label,omitempty"`
	// STHVerified is true when the signed tree head is authentic under the verifying key and is a
	// journal head of RunID. Every action proof is checked against this same root.
	STHVerified bool `json:"sth_verified"`
	// Problems lists every package-level failure (format, seal, key, run binding), empty when none.
	Problems []string `json:"problems,omitempty"`
	// Items is one verdict per proven item (each action, each anchored grant, the run certificate, and
	// the consistency proof), in report order.
	Items []EvidenceItem `json:"items"`
}

EvidenceReport is the structured outcome of EvidencePackage.Verify: an overall verdict plus a per-item verdict list, so a caller (or the CLI) can print exactly which proven item held and which did not. It is the machine-readable form behind the plain-English CLI report.

type EvidenceVerifyOption added in v0.7.0

type EvidenceVerifyOption func(*evidenceVerifyOptions)

EvidenceVerifyOption supplies what the auditor, not the package, decides.

func WithApprovedPolicies added in v0.7.0

func WithApprovedPolicies(digests ...string) EvidenceVerifyOption

WithApprovedPolicies is the auditor's approved-policy allowlist, against which a package's run certificate is checked. A package that carries a run certificate fails verification without it.

func WithClockSkew added in v0.8.0

func WithClockSkew(d time.Duration) EvidenceVerifyOption

WithClockSkew sets how far past the verifier's clock a signed head's timestamp may be (DefaultClockSkew by default). A negative skew makes every timestamp check fail.

func WithVerifyTime added in v0.8.0

func WithVerifyTime(now time.Time) EvidenceVerifyOption

WithVerifyTime sets the clock EvidencePackage.Verify checks timestamps against (time.Now by default), so a package can be checked as of a given moment.

type Grant

type Grant struct {
	ID      string            `json:"id"`              // unique identifier for this grant
	Issuer  string            `json:"issuer"`          // principal that authorized the grant (resolves to the signing key)
	Subject string            `json:"subject"`         // actor the grant authorizes to act
	Scope   map[string]string `json:"scope,omitempty"` // domain-defined constraints (e.g. limits); a child keeps every one, equal or narrower
	// NotAfterUnix is the expiry in Unix seconds; zero never expires, and only a grant whose parent
	// never expires may be zero.
	NotAfterUnix int64  `json:"not_after_unix,omitempty"`
	ParentRef    string `json:"parent_ref,omitempty"` // Digest of the grant this was attenuated from; empty for a root grant
}

Grant is a signed statement by a principal (Issuer) authorizing an actor (Subject) to act within Scope until NotAfterUnix. It is the non-repudiable authority behind a governed action: the action's agent.Identity.AuthorityRef is a grant's Digest, and the grant is signed by the issuer's own key (distinct from the log's tree-head key), so an auditor attributes the authorization to the principal, not merely to the operator that runs the log.

ParentRef links a grant to the one it was attenuated from (empty for a root grant), forming a hash-linked delegation chain: a holder of a grant may mint a strictly narrower sub-grant for a sub-agent without returning to the root issuer (capability attenuation), and the chain back to the root is provable. Scope entries are constraints whose meaning is domain-defined; the chain verifier requires a child to keep every one of its parent's, unchanged or narrowed by a ScopeRule the verifier supplies (NumericAtMost is provided for limits). See CheckAttenuation.

func (Grant) Bytes

func (g Grant) Bytes() []byte

Bytes is the canonical serialization that is signed and digested: "bide.audit.grant.v2\n" followed by the grant's JSON. json.Marshal sorts map keys and emits struct fields in declaration order, so it is deterministic in-ecosystem. It is injective only over valid UTF-8 (JSON rewrites invalid bytes to U+FFFD), so SignGrant refuses, and SignedGrant.Verify rejects, a grant with invalid UTF-8 in any string.

func (Grant) Digest

func (g Grant) Digest() string

Digest is a stable SHA-256 over the grant's canonical bytes (Bytes), used as agent.Identity.AuthorityRef and as the anchoring key. It is recomputable by any verifier from the disclosed grant.

func (Grant) Expired

func (g Grant) Expired(nowUnix int64) bool

Expired reports whether the grant is past NotAfterUnix at nowUnix (Unix seconds). A zero NotAfterUnix never expires. The caller supplies the clock, so verification stays deterministic.

type HybridSigner

type HybridSigner struct {
	Ed Ed25519Signer // the Ed25519 component signer
	ML MLDSASigner   // the ML-DSA-65 component signer
}

HybridSigner signs with both schemes; the signature is only accepted if BOTH verify, so the anchor is safe as long as either scheme remains unbroken. Each component signs its own label followed by the message ("bide.hybrid.ed25519.v1\x00" || m and "bide.hybrid.mldsa65.v1\x00" || m), so a component stripped out of a hybrid signature is not a signature over m under its scheme.

func (HybridSigner) Alg

func (HybridSigner) Alg() Alg

Alg reports the signature scheme (AlgHybrid).

func (HybridSigner) Format added in v0.7.0

func (s HybridSigner) Format(f fmt.State, _ rune)

Format writes String for every verb, so no fmt verb prints either key.

func (HybridSigner) PublicKey added in v0.9.0

func (s HybridSigner) PublicKey() []byte

PublicKey returns the hybrid public key: a 4-byte big-endian length of the Ed25519 key, the Ed25519 key, then the ML-DSA-65 key. It is nil if either component has no usable key.

func (HybridSigner) Sign

func (s HybridSigner) Sign(m []byte) ([]byte, error)

Sign returns the packed pair of domain-separated Ed25519 and ML-DSA-65 signatures over m.

func (HybridSigner) String added in v0.7.0

func (HybridSigner) String() string

String returns a description of the signer that does not include either key.

type HybridVerifier

type HybridVerifier struct {
	Ed Ed25519Verifier // the Ed25519 component verifier
	ML MLDSAVerifier   // the ML-DSA-65 component verifier
}

HybridVerifier requires both component signatures to verify, each over its own label and the message.

func (HybridVerifier) Alg

func (HybridVerifier) Alg() Alg

Alg reports the signature scheme (AlgHybrid).

func (HybridVerifier) KeyIDs added in v0.9.0

func (v HybridVerifier) KeyIDs() []string

KeyIDs identifies both component keys, for agent.ApproverVerifier: the Ed25519 entry, then the ML-DSA-65 entry. They are listed separately, not as one identity of the pair, because a hybrid signature is meant to hold while either scheme holds: if one scheme breaks, the other component's key alone signs, so two approvers sharing either component are one seat. If either component reports no entry the hybrid reports none, since it can verify nothing.

func (HybridVerifier) PublicKey added in v0.9.0

func (v HybridVerifier) PublicKey() []byte

PublicKey returns the hybrid public key (see HybridSigner.PublicKey), or nil if either component has no usable key.

func (HybridVerifier) Verify

func (v HybridVerifier) Verify(m, sig []byte) bool

Verify reports whether both component signatures over m are valid.

type IgnoredDecision added in v0.4.0

type IgnoredDecision struct {
	Approver string `json:"approver"`
	Step     string `json:"step"`
	Reason   string `json:"reason"`
}

IgnoredDecision is one disclosed approver decision that did not count toward the gate.

type Inclusion

type Inclusion struct {
	Index int      `json:"index"` // the record's position in the journal
	Size  int      `json:"size"`  // the journal length the root committed to
	Path  [][]byte `json:"path"`  // sibling hashes, leaf-to-root
}

Inclusion is an RFC 6962 audit path proving one record's membership in a committed run.

func Prove

func Prove(ctx context.Context, store *agent.Journal, runID string, index int) (Inclusion, error)

Prove returns an inclusion proof for the record at index in runID's journal: enough to verify that record's stored bytes (agent.Record.Raw) against Root WITHOUT revealing any other record (selective disclosure). A redacted record cannot be proven.

type JournalExport added in v0.9.0

type JournalExport struct {
	Format  string   `json:"format"`  // JournalExportFormat
	RunID   string   `json:"run_id"`  // the run the journal is of
	Records [][]byte `json:"records"` // each record's stored bytes, in persisted order
}

A JournalExport is a run's journal as the bytes the journal stores for each record, in persisted order: the input a prover that holds no store (bide-audit prove and prove-absent) builds proofs from. Each record is carried verbatim (base64 in JSON), never re-encoded, so a proof built from an export commits to exactly the leaves a proof built from the store does, including the fields of a record this version does not know.

func ExportJournal added in v0.9.0

func ExportJournal(ctx context.Context, store *agent.Journal, runID string) (JournalExport, error)

ExportJournal returns runID's journal as a JournalExport.

func (JournalExport) Journal added in v0.9.0

func (x JournalExport) Journal() ([]agent.Record, error)

Journal decodes the exported records, each keeping its stored bytes as its Raw, so the proof builders commit to the bytes exported. A redaction tombstone decodes as a redacted record. It errors (wrapping ErrFormat or ErrMalformed) if the export is not of JournalExportFormat or a record does not decode.

type KeyFunc

type KeyFunc func(agent.Record) []string

KeyFunc extracts the absence keys a record adds to a key set: none (nil) to exclude the record, or one or more (a model turn that requests several tool calls adds one key per call). Absence is proven over the sorted set of distinct keys the KeyFunc yields over the journal.

type KeySet added in v0.7.0

type KeySet struct {
	Kind   string  // tree kind committed in the signed head, e.g. TreeToolUse
	Prefix string  // every key the set yields (and every key proven absent from it) starts with Prefix
	Key    KeyFunc // projects one journal record onto the set
}

KeySet names one absence key set: the tree Kind its signed heads commit to, the Prefix every key it yields starts with, and the KeyFunc that projects a journal onto it. Kind must start with "absence/" and be distinct per key set; Prefix must be distinct per key set.

func KeySetForKey added in v0.7.0

func KeySetForKey(key string) (KeySet, bool)

KeySetForKey returns the built-in key set a key belongs to, by its prefix.

type MLDSASigner

type MLDSASigner struct {
	Priv *mldsa.PrivateKey // the ML-DSA-65 private key that signs
}

MLDSASigner signs with an ML-DSA-65 private key. Signing is deterministic, so replaying the same tree head reproduces the same signature.

func (MLDSASigner) Alg

func (MLDSASigner) Alg() Alg

Alg reports the signature scheme (AlgMLDSA65).

func (MLDSASigner) Format added in v0.7.0

func (s MLDSASigner) Format(f fmt.State, _ rune)

Format writes String for every verb, so no fmt verb prints the key.

func (MLDSASigner) MarshalJSON added in v0.7.0

func (MLDSASigner) MarshalJSON() ([]byte, error)

MarshalJSON refuses: the signer holds a private key.

func (MLDSASigner) PublicKey added in v0.9.0

func (s MLDSASigner) PublicKey() []byte

PublicKey returns the encoded ML-DSA-65 public key, or nil if there is no key or it is of another ML-DSA parameter set.

func (MLDSASigner) Sign

func (s MLDSASigner) Sign(m []byte) ([]byte, error)

Sign returns the deterministic ML-DSA-65 signature over m, or an error if there is no key or it is of another parameter set.

func (MLDSASigner) String added in v0.7.0

func (MLDSASigner) String() string

String returns a description of the signer that does not include the key.

type MLDSAVerifier

type MLDSAVerifier struct {
	Pub *mldsa.PublicKey // the ML-DSA-65 public key that verifies
}

MLDSAVerifier verifies with an ML-DSA-65 public key.

func (MLDSAVerifier) Alg

func (MLDSAVerifier) Alg() Alg

Alg reports the signature scheme (AlgMLDSA65).

func (MLDSAVerifier) KeyIDs added in v0.9.0

func (v MLDSAVerifier) KeyIDs() []string

KeyIDs identifies the public key, for agent.ApproverVerifier: one entry, KeyID(AlgMLDSA65, Pub.Bytes()). A verifier with no key, or a key of another ML-DSA parameter set, verifies nothing (see Verify) and reports no entry, so an approval gate refuses it rather than seating it.

func (MLDSAVerifier) PublicKey added in v0.9.0

func (v MLDSAVerifier) PublicKey() []byte

PublicKey returns the encoded public key, or nil if there is none or it is not ML-DSA-65.

func (MLDSAVerifier) Verify

func (v MLDSAVerifier) Verify(m, sig []byte) bool

Verify reports whether sig is a valid ML-DSA-65 signature over m. A verifier with no key, or a key of another parameter set, verifies nothing.

type MemAnchorLog

type MemAnchorLog struct {
	// contains filtered or unexported fields
}

MemAnchorLog is a reference external transparency log: an append-only, independently Merkle-committed record of published STHs. In a real deployment the anchor lives in a DIFFERENT trust domain than the journal (a separate service / account / public log); this in-memory version is for tests and local dev. Because it keeps its OWN RFC 6962 tree over the entries, a third party can verify that the anchor log itself only grew (ProveConsistency) and that a specific STH was anchored (Prove) — the properties that make anchoring meaningful.

func NewMemAnchorLog

func NewMemAnchorLog() *MemAnchorLog

NewMemAnchorLog returns an empty transparency log.

func (*MemAnchorLog) Entries

func (l *MemAnchorLog) Entries() []AnchorEntry

Entries returns a copy of the anchored entries in order.

func (*MemAnchorLog) Len

func (l *MemAnchorLog) Len() int

Len is the number of anchored entries.

func (*MemAnchorLog) Prove

func (l *MemAnchorLog) Prove(seq int) (Inclusion, error)

Prove returns an inclusion proof that the entry at seq is in the anchor log committed by Root — i.e. that a given STH was anchored, disclosing no other entry.

func (*MemAnchorLog) ProveConsistency

func (l *MemAnchorLog) ProveConsistency(first int) (Consistency, error)

ProveConsistency proves the first `first` anchored entries are an append-only prefix of the current log — the transparency-log guarantee that no earlier anchor was rewritten.

func (*MemAnchorLog) Publish

func (l *MemAnchorLog) Publish(_ context.Context, runID string, sth SignedTreeHead) error

Publish appends an STH for runID at the next sequence. Append-only: entries are never modified or removed. It refuses a head that commits to a different run than runID, so an entry's RunID is always the run its signed head names.

func (*MemAnchorLog) Root

func (l *MemAnchorLog) Root() ([]byte, error)

Root is the RFC 6962 Merkle root over all anchored entries — the anchor log's own commitment. Publish/sign IT in turn (or expose it) so a monitor can check the log's growth.

type MemEventStore

type MemEventStore struct {
	// contains filtered or unexported fields
}

MemEventStore is the in-memory reference EventStore for tests and local dev. A real backend (Postgres, object storage) enforces the same append-only/idempotent contract with a unique constraint and insert-only permissions.

func NewMemEventStore

func NewMemEventStore() *MemEventStore

NewMemEventStore returns an empty in-memory EventStore.

func (*MemEventStore) Append

func (s *MemEventStore) Append(_ context.Context, runID string, seq int, leaf []byte) error

func (*MemEventStore) Load

func (s *MemEventStore) Load(_ context.Context, runID string) ([][]byte, error)

type Neighbor

type Neighbor struct {
	Key   string    `json:"key"`   // the committed key adjacent to the absent key
	Proof Inclusion `json:"proof"` // inclusion proof placing Key at its index in the committed key set
}

Neighbor is one committed key adjacent to an absent key, with its inclusion proof.

type PolicyContent

type PolicyContent struct {
	Digest string `json:"digest"` // the policy owner's stable identifier (treated as opaque here)
	Policy string `json:"policy"` // the serialized governed policy bytes
}

PolicyContent is the payload of a policy leaf: the serialized governed policy and its digest. Committing it as a journal leaf puts the policy itself under the same signed tree head and inclusion proofs as the actions taken under it, so an auditor can prove not merely that an action happened but that the exact policy admitting it was anchored in the same committed tree.

type PolicyConvergence

type PolicyConvergence struct {
	Digest      string      `json:"digest"`      // the used policy digest this evidence is for
	PolicyLeaf  ProofBundle `json:"policy_leaf"` // proof the policy leaf is anchored in the run STH
	Certificate ProofBundle `json:"certificate"` // proof the convergence-certificate leaf is anchored in the same tree
}

PolicyConvergence bundles the anchored evidence for one used policy: its anchored policy leaf and its anchored convergence-certificate leaf, both proven against the run STH. The verifier confirms both bundles are authentic and in the same tree, the convergence certificate certifies the policy leaf's digest, and the policy leaf's bytes hash to that digest (done in the CLI, independent of gsm), then optionally runs the oracle on the disclosed policy bytes.

type ProofBundle

type ProofBundle struct {
	Format      string         `json:"format"`       // ProofFormat
	RunID       string         `json:"run_id"`       // the run whose journal the record and STH belong to; must equal STH.RunID
	RecordBytes []byte         `json:"record_bytes"` // the single disclosed action: the bytes the journal stores for it
	Inclusion   Inclusion      `json:"inclusion"`    // its RFC 6962 audit path
	STH         SignedTreeHead `json:"sth"`          // the signed commitment it is proven against
}

A ProofBundle is a single, portable, self-describing artifact that proves ONE action happened inside a signed, committed run. It discloses the record (with its salt) as the bytes the journal stores for it, its index, the run's size and signed head, and the sibling hashes on its path, and no other record: each record's leaf commits to its own random salt, so the sibling hashes cannot be tested against a guessed neighbour (see merkle.go). It is what "produce a proof" returns: hand it (plus the signer's public key, obtained out-of-band) to an auditor and they verify it offline.

The record is carried as RecordBytes, the stored bytes verbatim, and the verifier hashes exactly those bytes: it never re-encodes a decoded record. So a record a later release wrote, with fields this release does not know, verifies here, and what a proof commits to cannot depend on how this release encodes a record. Record decodes the bytes, leniently (unknown fields are ignored), for display and for the checks that read what the record is (its kind, its name).

The trust model, stated precisely: the public key must come from OUT OF BAND (the anchor / transparency log), NOT from the bundle. Given a trusted key, Verify checks (1) the STH's signature, so the {Kind, RunID, Size, Root, TimestampNanos} commitment is authentic; (2) that the STH commits to a journal and to the bundle's RunID, so the run a verifier reports is the run the signer committed to; (3) that the inclusion proof is against that same signed size; and (4) that the record bytes are the leaf at their index under the signed root. "Proofs you verify, not logs you trust": nothing here asks the verifier to trust the producer, its database, or its logs. For full tamper-evidence the auditor also confirms the STH itself appears in the anchor log (MemAnchorLog.Prove).

func ProveApproval added in v0.4.0

func ProveApproval(ctx context.Context, store *agent.Journal, runID, step string, sth SignedTreeHead) (ProofBundle, error)

ProveApproval builds a ProofBundle proving one approver-decision record, named step, is committed in the tree sth signs. Decision records are named per decision (see agent.ApprovalTally.Records and agent.DecisionCheck.Step), so the step name identifies one exactly. The disclosed record carries the approver id and signature, so a verifier holding the approver's key can check it over agent.ApprovalDecisionBytes independently. ProveStep matches only StepValue records, so it cannot reach a decision.

func ProveConvergence

func ProveConvergence(ctx context.Context, store *agent.Journal, runID, digest string, sth SignedTreeHead) (ProofBundle, error)

ProveConvergence builds a ProofBundle proving the convergence certificate for this digest was committed in the tree sth signs. Pair it with the policy leaf's ProofBundle for the same digest to show, in one committed tree, that the anchored policy was certified convergent.

func ProveGrant

func ProveGrant(ctx context.Context, store *agent.Journal, runID, digest string, sth SignedTreeHead) (ProofBundle, error)

ProveGrant builds a ProofBundle proving the grant with this digest was committed in the tree sth signs. Pair it with an action's bundle whose Identity.AuthorityRef matches the digest to show the action ran under an in-log, issuer-signed authority.

func ProvePolicy

func ProvePolicy(ctx context.Context, store *agent.Journal, runID, digest string, sth SignedTreeHead) (ProofBundle, error)

ProvePolicy builds a ProofBundle proving the policy with this digest was committed in the tree sth signs. Pair it with an action's ProofBundle whose result embeds the same digest to show the action ran under an in-log, anchored policy; the verifier then recomputes the digest from the disclosed bytes and runs the external oracle to certify the policy converges.

func ProveRecord

func ProveRecord(ctx context.Context, store *agent.Journal, runID string, index int, sth SignedTreeHead) (ProofBundle, error)

ProveRecord builds a bundle proving the record at index is in the tree that sth commits to. The inclusion proof is built against the first sth.Size records of runID's journal (the tree the STH signed), so the bundle binds to an anchored STH rather than a freshly minted one. It fails if index is outside that tree, if the record is redacted, or if sth.Root does not match runID's journal at that size (wrong STH, or the history diverged from what was signed).

func ProveRunCertificate

func ProveRunCertificate(ctx context.Context, store *agent.Journal, runID string, sth SignedTreeHead) (ProofBundle, error)

ProveRunCertificate builds a ProofBundle proving the run certificate leaf for runID was committed in the tree sth signs. Because RecordRunCertificate grows the journal, sth here is a tree head taken AFTER the certificate was anchored (later than the run STH embedded in the certificate).

func ProveStep

func ProveStep(ctx context.Context, store *agent.Journal, runID, name string, sth SignedTreeHead) (ProofBundle, error)

ProveStep builds a ProofBundle proving that the durable step with this name (an agent.Journal.Step / Parallel task, recorded as a StepValue) is committed in the tree sth signs. It is the fan-in counterpart to ProveToolCall: after a Parallel run, each task is an independently provable record, so an auditor can prove "this specific compliance check ran and produced this result" without disclosing the other stages.

func ProveToolCall

func ProveToolCall(ctx context.Context, store *agent.Journal, runID, toolUseID string, sth SignedTreeHead) (ProofBundle, error)

ProveToolCall is the semantic form of ProveRecord: prove that a specific tool call happened, by its ToolUseID, without the caller needing to know its journal index. It resolves the completed tool-result record for toolUseID and proves it against sth. This is the ergonomic entry point ("prove the charge on run X"), not "prove record index 7".

func (ProofBundle) Record

func (b ProofBundle) Record() (agent.Record, error)

Record decodes the proven record from RecordBytes, for display and role checks. The decoding is lenient, as the journal's own: a field this version does not know is ignored. It errors (wrapping ErrMalformed) only if the bytes do not decode as a journal record.

func (ProofBundle) Verify

func (b ProofBundle) Verify(v Verifier) error

Verify returns nil if the bundle is internally consistent and authentic under v (a key obtained out-of-band, e.g. from the anchor log operator): the STH verifies under v, it is a journal head of RunID, the inclusion proof is for the signed size, and RecordBytes are the leaf at their index under the signed root. It then requires RecordBytes to decode as a record (ErrMalformed if not). A bundle that does not hold is an error wrapping ErrNotVerified; one of another format, ErrFormat. It does not check the head's timestamp; apply CheckTimestamp to b.STH for that.

type RunCertSpec

type RunCertSpec struct {
	// ApprovedPolicies is the allowlist of policy digests the run was permitted to exercise. Every
	// policy the run actually used must be a member, or CertifyRun fails: a certificate cannot be
	// issued for a run that exercised a disallowed policy. It is not written into the certificate:
	// the verifier supplies its own allowlist.
	ApprovedPolicies []string
	// Signer signs the used-policy head. It must hold the key that signed the run's journal head
	// (the log's tree-head key), so a verifier checks both heads under one out-of-band key;
	// CertifyRun refuses another. Evidence sets it to its own signer.
	Signer Signer
	// TimestampNanos stamps the used-policy head, in Unix nanoseconds. It must not be earlier than
	// the journal head's. Evidence sets it to its own timestamp.
	TimestampNanos int64
}

RunCertSpec carries the caller's inputs to CertifyRun: what the run was allowed, and how to sign the certificate's used-policy head. It is a struct so future properties (authority roots, quorum thresholds) can be added without changing the signature.

type RunCertificate

type RunCertificate struct {
	// Format is RunCertificateFormat.
	Format string `json:"format"`

	// RunID is the run this certificate is about. It must equal STH.RunID and
	// UsedPolicyAbsence.RunID, both signed.
	RunID string `json:"run_id"`

	// Properties names the asserted properties, so a reader sees the scope without decoding the
	// bindings. v1: exactly "only-approved-policies", "policies-convergence-certified".
	Properties []string `json:"properties"`

	// UsedPolicies is the sorted, distinct set of policy digests exercised by governed actions in the
	// run, recomputed from the committed leaves (PoliciesUsed). It is disclosed in full because the
	// only-approved-policies property is completeness-bearing: the verifier recomputes the key-set
	// root over this set and confirms UsedPolicyAbsence commits to it, so the set cannot omit a
	// policy that was in fact used.
	UsedPolicies []string `json:"used_policies"`

	// UsedPolicyAbsence is the signed commitment to the run's used-policy key set: a PolicyUsedKeys
	// tree head (NewAbsenceTreeHead) whose Journal is STH's tree. Its Size is the number of distinct
	// used policies and its Root is the RFC 6962 root over their sorted keys.
	UsedPolicyAbsence SignedTreeHead `json:"used_policy_absence"`

	// Convergence carries, per used policy digest and in UsedPolicies order, the anchored evidence
	// that the policy is certified convergent: the policy-leaf ProofBundle and the convergence-leaf
	// ProofBundle, both bound to the run STH below. The oracle cross-check (does the policy actually
	// converge) is performed by the verifier with a checker, as verify-convergence does; these
	// bundles establish the cryptographic root (anchored, digest-linked, same tree) that the oracle
	// check hangs off.
	Convergence []PolicyConvergence `json:"convergence"`

	// STH is the run's signed journal head: the commitment the policy and convergence bundles are
	// proven against and the journal tree the used-policy set was projected from.
	STH SignedTreeHead `json:"sth"`
}

A RunCertificate is a proof-carrying run: one portable, offline-checkable artifact asserting behavioral-property compliance over a WHOLE run, discharged from the audit package's existing primitives rather than from new cryptography. It composes what already exists (RFC 6962 inclusion proofs, signed tree heads, the policy-used absence commitment, anchored policy and convergence leaves) into one per-run statement plus a verifier, so a compliance reviewer checks a single bundle against one out-of-band public key instead of assembling the proofs by hand.

v1 asserts these properties, each dischargeable from a committed leaf:

  • only-approved-policies: every policy digest exercised by a governed action in the run is a member of the AUDITOR's approved allowlist (passed to VerifyRun; the certificate carries no allowlist of its own). This is the COMPLETENESS-bearing property: it commits WHICH policies were used, via the policy-used absence commitment (a PolicyUsedKeys tree head). The used set is exactly the sorted distinct keys that tree commits to, so a verifier recomputes its root from the disclosed set and confirms the signed head commits to it; that closes the "some other policy was quietly used" gap that disclosing only positive leaves would leave open.
  • policies-convergence-certified: for each used policy digest, an anchored convergence certificate leaf exists in the same signed tree as the policy leaf and links to its digest. The certificate is a producer claim; a rigorous verifier cross-checks it against the external oracle (bide-audit verify-run -checker), exactly as verify-convergence does, so a certificate that overstates convergence is caught outside this package.

Binding. The used-policy head is bound to this run and to this journal, not merely signed by the right key: its signed encoding commits to Kind TreePolicyUsed, to RunID, and to the journal tree (Size and Root) whose records the used set was projected from. VerifyRun requires that journal tree to be exactly STH (the certificate's signed journal head, itself a journal head of RunID). So a used-policy head from another run, from a different point in this run, or from a tool-use key set cannot stand in, and the used set is the projection of the same records the convergence bundles are proven against.

What the certificate proves, stated precisely: it proves properties of the GOVERNED, COMMITTED boundary of the run up to STH.Size records: which policies ran, that each is on the approved allowlist, and that each has an anchored convergence certificate that (with the oracle) is confirmed convergent. Composing the two properties yields "every governed state in the run was produced by an approved, oracle-certified-convergent policy," so the enforced invariant held throughout the governed boundary. It does NOT prove the model's judgment was correct, that ungoverned side effects were appropriate, or the runtime-refinement claim (the replay differential check covers the events this run took, not all inputs). It covers the journal up to STH.Size; that STH is the run's final head is a fact the anchor log supplies, not the certificate.

func CertifyRun

func CertifyRun(ctx context.Context, store *agent.Journal, runID string, sth SignedTreeHead, spec RunCertSpec) (RunCertificate, error)

CertifyRun assembles a RunCertificate for runID from its committed leaves, against the run's signed journal head sth. It confirms sth is a journal head of runID that matches the journal, recomputes the used-policy set from exactly the sth.Size records sth commits to (PoliciesUsed), confirms every used policy is in spec.ApprovedPolicies (refusing to certify a run that used a disallowed policy), signs the used-policy key-set head bound to sth's tree with spec.Signer at spec.TimestampNanos, and for each used policy assembles the anchored policy-leaf and convergence-leaf proof bundles against sth. It fails if a used policy has no anchored policy leaf or no anchored convergence leaf, so the certificate can only be issued for a run whose governed policies are fully anchored and certified.

spec.Signer must hold the key that signed sth (the log's tree-head key): the used-policy head is a second commitment over the SAME run, signed the same way, so a verifier checks both under one out-of-band key. A signer of another key or scheme is refused, and so is a used-policy timestamp earlier than sth's.

type RunVerification

type RunVerification struct {
	// OK is the overall verdict: true only when every asserted property held.
	OK bool `json:"ok"`
	// OnlyApprovedPolicies is true when every used policy is in the allowlist AND the disclosed
	// used-policy set is bound to this run's journal by the signed used-policy head (completeness).
	OnlyApprovedPolicies bool `json:"only_approved_policies"`
	// ConvergenceCertified is true when every used policy has an anchored policy leaf and convergence
	// leaf, authentic and in the same signed tree, with linking digests. It does NOT include the
	// oracle cross-check, which the caller performs separately (the CLI does, with -checker).
	ConvergenceCertified bool `json:"convergence_certified"`
	// Reasons carries human-readable detail for any property that failed (empty on full success).
	Reasons []string `json:"reasons,omitempty"`
	// Policies carries, when ConvergenceCertified holds, what the verification read from each used
	// policy's leaves, in UsedPolicies order, so a caller that cross-checks a policy (the CLI's
	// -checker runs the external oracle on it) checks exactly the bytes that were verified.
	Policies []VerifiedPolicy `json:"policies,omitempty"`
}

RunVerification is the structured outcome of VerifyRun: the overall verdict plus per-property results, so a caller (or the CLI) can report exactly which property held and which failed.

func VerifyRun

func VerifyRun(cert RunCertificate, approved []string, v Verifier) (RunVerification, error)

VerifyRun checks a RunCertificate's claims against the disclosed proofs, the auditor's own approved allowlist, and the out-of-band verifier v, entirely offline. It does NOT trust the certificate; it re-derives each property from the bundles it carries:

  1. the certificate itself: Properties is exactly the v1 list, and STH is an authentic journal head of RunID.
  2. only-approved-policies (completeness-bearing): the used-policy head is authentic under v, is a TreePolicyUsed head of RunID whose source journal is exactly STH's tree, and its Root equals the RFC 6962 root recomputed from the disclosed UsedPolicies (in the sorted, distinct order the tree commits; any other order, a duplicate, or a missing or extra digest changes the root), so the disclosed set IS what this run's journal committed; and every used policy is a member of approved. A policy that was actually used cannot be dropped from UsedPolicies without changing the root, and a head for another run or journal cannot be swapped in.
  3. policies-convergence-certified: for every used policy there is exactly one Convergence entry, in UsedPolicies order and none for an unused digest, whose policy-leaf and convergence-leaf bundles both verify under v, are in the same tree as the run STH, and whose leaves link by digest. The leaves are read with UnmarshalStrict, so a leaf that reads differently to a person than to encoding/json does not verify. The oracle cross-check of the certificate's convergence claim is left to the caller (see the CLI's -checker), on the contents returned in Policies.

It returns the per-property result with a nil error when OK, and with an error wrapping ErrNotVerified when a property failed (Reasons says which). Any other error means the certificate could not be checked: it, or a bundle it carries, is not of the format this version reads (ErrFormat), or a bundle's record or a leaf could not be read (ErrMalformed). It does not check the heads' timestamps; EvidencePackage.Verify and bide-audit verify-run do (see CheckTimestamp).

type ScopeRule added in v0.7.0

type ScopeRule func(parent, child string) bool

ScopeRule reports whether child, a changed value for one scope key, is within parent, the parent grant's value for that key.

type ScopeRules added in v0.7.0

type ScopeRules map[string]ScopeRule

ScopeRules maps a scope key to the rule that decides whether a changed value narrows it. A key with no rule can only be carried to a child unchanged.

type SignedGrant

type SignedGrant struct {
	Grant Grant  `json:"grant"` // the grant whose canonical bytes are signed
	Alg   Alg    `json:"alg"`   // signature scheme the issuer used (see signing.go)
	Sig   []byte `json:"sig"`   // issuer's signature over Grant.Bytes()
}

SignedGrant is a Grant plus the issuer's signature over its canonical bytes.

func SignGrant

func SignGrant(g Grant, s Signer) (SignedGrant, error)

SignGrant signs a grant with the issuer's key (Ed25519, ML-DSA, or hybrid, per the Signer).

func (SignedGrant) Verify

func (sg SignedGrant) Verify(v Verifier) error

Verify returns nil if the issuer's signature over the grant verifies under v. The verifier is the issuer's public key, resolved by the caller's PKI; this is non-repudiation of the authorization, separate from the log's tree-head signature. A grant signed under another scheme than v's, or whose signature does not verify, is an error wrapping ErrNotVerified; a grant with invalid UTF-8 in a string, ErrMalformed.

type SignedTreeHead

type SignedTreeHead struct {
	Format   string `json:"format"` // STHFormat
	TreeHead        // the commitment being signed
	// Alg names the signature scheme (see signing.go). It is signed with the head.
	Alg       Alg    `json:"alg"`
	Signature []byte `json:"signature"` // signature over TreeHead.canonical(Alg) under Alg
}

SignedTreeHead is a TreeHead with a signature over its canonical encoding under the scheme Alg names. The scheme is part of the signed bytes.

func SignAbsenceRoot

func SignAbsenceRoot(records []agent.Record, set KeySet, journal TreeHead, s Signer, timestamp int64) (SignedTreeHead, error)

SignAbsenceRoot commits and signs a run's key set in one call: NewAbsenceTreeHead, then SignTreeHead with s. Absence proofs verify against this separate commitment (never the journal STH). Anchor the result like any STH. timestamp is Unix nanoseconds.

func SignTreeHead

func SignTreeHead(th TreeHead, s Signer) (SignedTreeHead, error)

SignTreeHead signs th under s's scheme. Anchor the result out-of-band (this is what makes the journal tamper-evident against later rewrites). Build th with NewTreeHead, NewAbsenceTreeHead, or EventLog.TreeHead. It refuses a head of an unknown kind or shape, which no verifier accepts, and a signer without a usable key.

func (SignedTreeHead) Verify

func (sth SignedTreeHead) Verify(v Verifier) error

Verify returns nil if the STH's signature is valid under v: the head is of format STHFormat, of a known kind and shape, signed under v's scheme, and its signature over its canonical encoding (scheme included) verifies. Any change to any field invalidates it. It checks authenticity only: a caller that needs a particular tree also checks Kind and RunID (the bundle verifiers in this package do), and a caller that relies on when the head was signed applies CheckTimestamp (EvidencePackage.Verify and bide-audit do). The error wraps ErrFormat, ErrMalformed, or ErrNotVerified.

type Signer

type Signer interface {
	// Alg names the scheme.
	Alg() Alg
	// PublicKey returns the encoded public key of the signing key (see NewVerifier for the
	// encoding of each scheme), or nil if the signer holds no usable key.
	PublicKey() []byte
	// Sign returns a signature over message, or an error if the key is unusable.
	Sign(message []byte) ([]byte, error)
}

Signer produces signatures under one scheme with one key.

type TreeHead

type TreeHead struct {
	Kind  string `json:"kind"`   // which tree: TreeJournal, TreeEvents, or an absence kind
	RunID string `json:"run_id"` // the run the tree is about
	Size  int    `json:"size"`   // number of leaves committed
	Root  []byte `json:"root"`   // RFC 6962 Merkle root over those leaves
	// TimestampNanos is when the head was signed, in Unix nanoseconds (time.Now().UnixNano()); see
	// CheckTimestamp.
	TimestampNanos int64 `json:"timestamp_nanos"`
	// Journal is set on an absence key-set tree: the journal tree (of the same run) whose
	// records the key set was projected from. It is nil on a journal or event tree.
	Journal *TreeRef `json:"journal,omitempty"`
}

TreeHead is a commitment to one tree of a run at a point in time.

func NewAbsenceTreeHead added in v0.7.0

func NewAbsenceTreeHead(records []agent.Record, set KeySet, journal TreeHead, timestamp int64) (TreeHead, error)

NewAbsenceTreeHead builds the key-set tree head for set over a run's journal. journal is the run's journal tree head (NewTreeHead) and records the run's journal: the first journal.Size records must hash to journal.Root. The head commits to set.Kind, the run, the key set projected from exactly those records, and the journal tree itself, so it is bound to one run's history.

func NewTreeHead

func NewTreeHead(ctx context.Context, store *agent.Journal, runID string, timestamp int64) (TreeHead, error)

NewTreeHead builds a TreeHead committing to runID's journal at the given timestamp.

func (TreeHead) SameTree added in v0.7.0

func (th TreeHead) SameTree(o TreeHead) bool

SameTree reports whether th and o commit to the same tree: the same kind, run, size, root, and (for a key-set tree) source journal. Timestamps may differ.

type TreeRef added in v0.7.0

type TreeRef struct {
	Size int    `json:"size"` // number of leaves
	Root []byte `json:"root"` // RFC 6962 Merkle root over them
}

TreeRef names one tree of a run by its size and root.

type VerifiedPolicy added in v0.7.0

type VerifiedPolicy struct {
	Digest      string          `json:"digest"`
	Policy      string          `json:"policy"`
	Certificate json.RawMessage `json:"certificate"`
}

VerifiedPolicy is one used policy as VerifyRun read it from its anchored leaves: the policy bytes from the policy leaf and the serialized convergence certificate from the convergence leaf.

type Verifier

type Verifier interface {
	// Alg names the scheme.
	Alg() Alg
	// PublicKey returns the encoded public key (see NewVerifier), or nil if the verifier holds none.
	PublicKey() []byte
	// Verify reports whether signature is valid over message. A verifier without a usable key
	// verifies nothing; it never panics.
	Verify(message, signature []byte) bool
	// KeyIDs identifies the signing keys behind Verify (see agent.ApproverVerifier.KeyIDs and
	// KeyID), so every Verifier is an agent.ApproverVerifier. A verifier without a usable key
	// reports none.
	KeyIDs() []string
}

Verifier checks signatures under one scheme with one public key.

func NewVerifier added in v0.9.0

func NewVerifier(alg Alg, pub []byte) (Verifier, error)

NewVerifier returns the verifier of scheme alg for the encoded public key pub:

ed25519:           the 32-byte Ed25519 public key
ml-dsa-65:         the 1952-byte ML-DSA-65 public key encoding (mldsa.PublicKey.Bytes)
ed25519+ml-dsa-65: a 4-byte big-endian length of the Ed25519 key, the Ed25519 key, then the
                   ML-DSA-65 key (HybridSigner.PublicKey)

A key that is not of its scheme's form is an error wrapping ErrMalformed; a weak Ed25519 key (see CheckEd25519PublicKey) wraps ErrWeakKey as well.

func ParsePublicKey added in v0.9.0

func ParsePublicKey(s string) (Verifier, error)

ParsePublicKey reads the text form of a public key (FormatPublicKey) and returns its verifier. A bare hex string of 32 bytes is read as an Ed25519 key. Surrounding white space is ignored. Any other text is an error wrapping ErrMalformed.

func VerifierOf added in v0.9.0

func VerifierOf(s Signer) (Verifier, error)

VerifierOf returns the verifier for s's public key: NewVerifier(s.Alg(), s.PublicKey()).

Directories

Path Synopsis
Package verify is a dependency-light, standalone verifier for Bide audit proofs.
Package verify is a dependency-light, standalone verifier for Bide audit proofs.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL