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

audit

package
v0.7.0 Latest Latest
Warning

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

Go to latest
Published: Sep 29, 2026 License: Apache-2.0 Imports: 25 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 lets you sign/verify the commitment. Stdlib-only (crypto/sha256, ed25519).

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 (
	KindCall          = "call"           // the model turn that requested the gated call (its name and arguments)
	KindApproval      = "approval"       // one approver decision record
	KindApprovalTally = "approval-tally" // the gate's journaled terminal tally
	KindTool          = "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.

View Source
const (
	KindStep  = "step"  // a durable step's recorded value
	KindGrant = "grant" // an anchored signed grant leaf
)

Other evidence kinds.

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

Algorithm identifiers stored in SignedTreeHead.Alg.

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 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.v2"

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.

Variables

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

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 completed tool calls, keyed "tooluse:<ToolUseID>". It proves "no tool call with this ID happened in the run."

Functions

func AbsenceRoot

func AbsenceRoot(records []agent.Record, set KeySet) []byte

AbsenceRoot is the RFC 6962 Merkle root over the run's sorted, distinct keys in set. Anyone holding the journal recomputes it to confirm a signed key-set head reflects the run.

func AttenuatingSubAgent

func AttenuatingSubAgent(name, description string, sub *agent.Agent, store agent.Durable, narrow AttenuateFunc, rules ScopeRules) 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 / RunSaga), whose run scope gives each call its own sub-run (parentRunID/toolUseID); 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.

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.NotAfter is set, child.NotAfter is set and at most parent.NotAfter (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 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.Durable, 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} || canonical(record_i)) over the records in persisted order, where canonical is agent.EncodeRecord: the bytes the store persisted for the record. 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 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.AgentEvent) 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.Durable, 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.

func PoliciesUsed

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

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.

func PolicyUsedKey

func PolicyUsedKey(r agent.Record) (string, bool)

PolicyUsedKey is the KeyFunc of PolicyUsedKeys (see ProveAbsent / ProveAbsentBundle) over governed-action leaves: a completed tool call whose result carries a policy_digest, as govern.AttestedEventTool 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 Record

func Record(log *EventLog, stream *agent.AgentStream, onEvent func(agent.AgentEvent)) (agent.Message, error)

Record drains stream through log — committing every event — while forwarding each event to onEvent (nil to skip), then returns the run's terminal Message and error (including *PendingApproval / *ResumeHalt, exactly as AgentStream.Final does). One pass gives you both the live UI feed and a committed, provable audit trail. A canonicalization failure is surfaced only if the run itself did not already fail.

func RecordConvergence

func RecordConvergence(ctx context.Context, store agent.Durable, 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.Durable, 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.Durable, 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.Durable, 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 Root

func Root(ctx context.Context, store agent.Durable, 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, priv ed25519.PrivateKey) []byte

Sign returns an Ed25519 signature over a head commitment — anchor this (store it in a separate trust domain) to make the journal tamper-evident against later rewrites.

func ToolUseKey

func ToolUseKey(r agent.Record) (string, bool)

ToolUseKey is the KeyFunc of ToolUseKeys: completed tool calls, keyed by their ToolUseID.

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.

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) (bool, error)

VerifyAbsence reports whether 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.

func VerifyAnchorInclusion

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

VerifyAnchorInclusion reports whether entry is the leaf at proof.Index in an anchor log of proof.Size entries committed by root — from the entry + proof alone.

func VerifyConsistency

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

VerifyConsistency reports whether 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 + proof alone. A rewrite or reorder of any early record fails.

func VerifyCurrentGrant added in v0.7.0

func VerifyCurrentGrant(sg SignedGrant, ledgerRunID string, p CurrentGrantProof, lastSeen *SignedTreeHead, logPub ed25519.PublicKey) (bool, error)

VerifyCurrentGrant checks that sg is the current earned grant of the ledger run ledgerRunID, under logPub (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 logPub 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.

func VerifyDelegationChain

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

VerifyDelegationChain verifies a delegation chain ordered root-first (index 0) to leaf-last:

  1. each grant's signature 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).

func VerifyEventInclusion

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

VerifyEventInclusion reports whether 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. It errors if the event cannot be canonicalized or proof.Salt is not agent.SaltSize bytes.

func VerifyInclusion

func VerifyInclusion(root []byte, record agent.Record, proof Inclusion) (bool, error)

VerifyInclusion reports whether record is the leaf at proof.Index in a run of proof.Size records committed by root — checked from record + proof alone, no other records needed.

func VerifySignature

func VerifySignature(head, sig []byte, pub ed25519.PublicKey) bool

VerifySignature reports whether sig is a valid signature of head under pub. A public key of the wrong length is reported as not verifying rather than panicking.

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.

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 {
	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(pub ed25519.PublicKey, set KeySet) (bool, error)

Verify reports whether the bundle authentically proves Absence.Key absent from the key set set of run RunID, under pub (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."

func (AbsenceBundle) VerifyWith

func (b AbsenceBundle) VerifyWith(v Verifier, set KeySet) (bool, error)

VerifyWith is the scheme-agnostic form of Verify.

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 {
	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, logPub ed25519.PublicKey) (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 logPub (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 does not verify and is listed in Ignored.

It returns an error, not a verdict, when the evidence cannot be evaluated: an invalid policy, a missing request, tally, or result, or one of those whose proof does not verify. 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 Subject and an earlier NotAfter); AttenuatingSubAgent fills in ParentRef and, if empty, Issuer (the parent's Subject), Subject (the sub-agent name), and NotAfter (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 AuditedStore

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

AuditedStore wraps a Durable so every durable step 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 writes the agent loop exactly as before — audit.NewAuditedStore(store, priv, anchor) is a drop-in Durable — and gets a stream of signed, out-of-band commitments for free.

Anchoring is a SIDE CHANNEL: a publish failure NEVER fails the durable step. Failing Do because an STH could not be published would be dangerous — the underlying 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.Durable, priv ed25519.PrivateKey, anchor Anchor) *AuditedStore

NewAuditedStore wraps inner so each journal growth is signed with priv and published to anchor. Timestamps default to time.Now().UnixNano(); override with WithClock for tests.

func (*AuditedStore) Do

func (a *AuditedStore) Do(ctx context.Context, runID, name string, fn func(context.Context) (agent.Record, error)) (agent.Record, error)

Do runs the step on the inner store, then (if the journal grew) anchors the new tree head.

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) History

func (a *AuditedStore) History(ctx context.Context, runID string) ([]agent.Record, error)

History delegates unchanged.

func (*AuditedStore) OnError

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

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

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) 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      // m — the earlier tree size
	Size  int      // n — the current tree size
	Path  [][]byte // 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.Durable, 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 {
	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.Durable, 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 NotAfter, 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/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.Durable, 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() string

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) 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() string

Alg reports the signature scheme (AlgEd25519).

func (Ed25519Verifier) Verify

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

Verify reports whether sig is a valid Ed25519 signature over m. A public key of the wrong length verifies nothing (it never panics).

type EventInclusion added in v0.7.0

type EventInclusion struct {
	Salt []byte // 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 AgentEvents. 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.Durable, 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.

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.AgentEvent) 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 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. 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 string `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.Durable, 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"). The result appends to EvidencePackage.Actions; 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.

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     string `json:"kind"`  // "tool", "step", "grant", "call", "approval", "approval-tally", "run-certificate", or "consistency"
	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 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 the given approved-policy allowlist. It fails assembly if the run used a policy outside the allowlist (CertifyRun refuses). 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"`
	// PublicKeyHex is the hex-encoded ed25519 public key that produced the STH and the seal, carried
	// so a reader knows which key to obtain out of band. Verify requires it to equal the key it is
	// given; it never trusts it as the key.
	PublicKeyHex string `json:"public_key_hex"`

	// 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 ed25519 signature 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. PublicKeyHex must be the verifying key. 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 PublicKeyHex.

func Evidence

func Evidence(ctx context.Context, store agent.Durable, runID string, priv ed25519.PrivateKey, 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 priv at timestamp), 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(priv).

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 public key. The public key is derived from priv and recorded as metadata (Verify requires it to match the key it is given; it does not trust the embedded one).

func (*EvidencePackage) Seal added in v0.7.0

func (e *EvidencePackage) Seal(priv ed25519.PrivateKey) error

Seal signs the package with priv, 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 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 pub, an ed25519 public key obtained OUT OF BAND (the anchor / transparency log), entirely offline. It does not trust the package's embedded key. It verifies: (0) the format tag, the seal, that PublicKeyHex is pub, 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.

It returns a structured EvidenceReport with an overall bool and a per-item verdict. A false OK with notes is a well-formed-but-invalid package; an error means a bundle could not be canonicalized (a malformed artifact). 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.

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
	NotAfter  int64             `json:"not_after,omitempty"`  // unix-seconds expiry; zero never expires, and only a grant whose parent never expires may be zero
	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 NotAfter. 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. 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, domain-separated SHA-256 over the grant, 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(now int64) bool

Expired reports whether the grant is past NotAfter at time now (unix seconds). A zero NotAfter 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.

func (HybridSigner) Alg

func (HybridSigner) Alg() string

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) Sign

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

Sign returns the packed pair of 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.

func (HybridVerifier) Alg

func (HybridVerifier) Alg() string

Alg reports the signature scheme (AlgHybrid).

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      // the record's position in the journal
	Size  int      // the journal length the root committed to
	Path  [][]byte // 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.Durable, 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 against Root WITHOUT revealing any other record (selective disclosure).

type KeyFunc

type KeyFunc func(agent.Record) (string, bool)

KeyFunc extracts an absence key from a record, returning false to exclude the record from the key set. Absence is proven over the sorted set of distinct keys the KeyFunc yields.

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() string

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) Sign

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

Sign returns the deterministic ML-DSA-65 signature over m.

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() string

Alg reports the signature scheme (AlgMLDSA65).

func (MLDSAVerifier) Verify

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

Verify reports whether sig is a valid ML-DSA-65 signature over m.

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 {
	RunID     string         `json:"run_id"`    // the run whose journal the record and STH belong to; must equal STH.RunID
	Record    agent.Record   `json:"record"`    // the single disclosed action
	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), 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. This turns the audit package's separate pieces (a record, its inclusion path, a signed tree head) into one thing you can marshal to JSON, store, email, or publish.

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, Timestamp} 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 disclosed record is the leaf at its 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.Durable, 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.Durable, 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.Durable, 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.Durable, 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.Durable, 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, 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.Durable, 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.Durable, runID, name string, sth SignedTreeHead) (ProofBundle, error)

ProveStep builds a ProofBundle proving that the durable step with this name (an agent.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.Durable, 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) Verify

func (b ProofBundle) Verify(pub ed25519.PublicKey) (bool, error)

Verify reports whether the bundle is internally consistent and authentic under pub (a key obtained out-of-band, e.g. from the anchor log operator). It returns false, not an error, for a well-formed-but-invalid proof; an error indicates the record could not be canonicalized.

func (ProofBundle) VerifyWith

func (b ProofBundle) VerifyWith(v Verifier) (bool, error)

VerifyWith is the scheme-agnostic form of Verify: it authenticates the STH under any Verifier (ed25519, ML-DSA, or hybrid), then binds and checks the inclusion proof.

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
}

RunCertSpec carries the caller's assertion inputs to CertifyRun. v1 needs only the approved allowlist; it is a struct so future properties (authority roots, quorum thresholds) can be added without changing the signature.

type RunCertificate

type RunCertificate struct {
	// 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.Durable, runID string, sth SignedTreeHead, spec RunCertSpec, priv ed25519.PrivateKey, timestamp int64) (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 priv, 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.

priv is the same signing key that produced 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 public key. timestamp stamps the used-policy head.

type RunVerification

type RunVerification struct {
	// OK is the overall verdict: true only when every asserted property held.
	OK bool
	// 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
	// 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
	// Reasons carries human-readable detail for any property that failed (empty on full success).
	Reasons []string
	// 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
}

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, pub ed25519.PublicKey) (RunVerification, error)

VerifyRun checks a RunCertificate's claims against the disclosed proofs, the auditor's own approved allowlist, and the out-of-band public key pub, 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 pub, 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 pub, 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.

A false OK with populated Reasons means a well-formed-but-invalid certificate; an error means a bundle could not be canonicalized or a leaf could not be read (a malformed artifact).

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   string `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) (bool, error)

Verify checks the issuer's signature over the grant. 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.

type SignedTreeHead

type SignedTreeHead struct {
	TreeHead         // the commitment being signed
	Signature []byte `json:"signature"` // signature over TreeHead.canonical() under the scheme named by Alg
	// Alg names the signature scheme (see signing.go). Empty means "ed25519", so heads produced
	// by the ed25519-only SignTreeHead / Verify path stay compact. Agility-aware producers set
	// it explicitly (SignTreeHeadWith).
	Alg string `json:"alg,omitempty"`
}

SignedTreeHead is a TreeHead with a signature over its canonical encoding.

func SignAbsenceRoot

func SignAbsenceRoot(records []agent.Record, set KeySet, journal TreeHead, priv ed25519.PrivateKey, timestamp int64) (SignedTreeHead, error)

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

func SignTreeHead

func SignTreeHead(th TreeHead, priv ed25519.PrivateKey) SignedTreeHead

SignTreeHead signs a TreeHead with an ed25519 key. 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: a head of an unknown kind never verifies.

func SignTreeHeadWith

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

SignTreeHeadWith signs a TreeHead under any scheme (ed25519, ML-DSA, or the hybrid of both), tagging the result with the signer's algorithm. Anchor the result out-of-band as usual.

func (SignedTreeHead) Verify

func (sth SignedTreeHead) Verify(pub ed25519.PublicKey) bool

Verify reports whether the STH's signature is valid under pub. Any change to any field invalidates it. This is the ed25519 fast path; it accepts an STH with Alg empty or "ed25519" and rejects any other scheme (use VerifyWith for those). It checks authenticity only: a caller that needs a particular tree also checks Kind and RunID (the bundle verifiers in this package do).

func (SignedTreeHead) VerifyWith

func (sth SignedTreeHead) VerifyWith(v Verifier) bool

VerifyWith reports whether the STH's signature is valid under v, requiring the STH's algorithm to match the verifier's (an empty Alg is treated as ed25519). Use this for ML-DSA or hybrid STHs; Verify remains the ed25519-only convenience.

type Signer

type Signer interface {
	Alg() string
	Sign(message []byte) ([]byte, error)
}

Signer produces a signature over a message under a named algorithm.

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
	Timestamp int64  `json:"timestamp"` // caller-supplied (e.g. time.Now().UnixNano())
	// 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.Durable, 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
	Policy      string
	Certificate json.RawMessage
}

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() string
	Verify(message, signature []byte) bool
}

Verifier checks a signature over a message under a named algorithm.

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