why

package
v0.1.0 Latest Latest
Warning

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

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

Documentation

Overview

Package why answers "why did this happen": it loads one receipt out of the log by run and step, parses the delegation chain embedded in it, and renders the authority tree with its three verification states — verified, asserted, broken (Q12, D5) — plus any scope excess computed at read time from the raw per-hop grants (Q11, Q13).

Two disciplines govern this package:

  • Nothing is written back. The scope excess, the attenuation classification and the verification rollups rendered here are computed from the stored bytes on every read, stamped with ComparatorVersion. Raw inputs are hashed evidence; computed values live on the read path, so a comparison bug can never freeze into evidence (Q11, schema §1).
  • Nothing is invented. Receipts are pseudonymous — key thumbprints for actors, issuer plus sub-digest for the human principal (Q16, Q40) — so every human-readable name in the output comes from the local alias map (alias.go) and is an asserted label, not a cryptographic claim.

Index

Constants

View Source
const AATGrantType = "attenuating_agent_token"

AATGrantType is the draft's RFC 9396 `authorization_details` type (§3.3, registered in §10.2).

View Source
const AliasFileName = "aliases.json"

AliasFileName is the local alias map inside the log dir: a JSON object of key thumbprint -> display label.

View Source
const ComparatorVersion = "behalf.sh/attenuation/v1"

ComparatorVersion stamps every attenuation comparison and scope check this package computes. Computed values are recomputable and travel stamped with the comparator that produced them, so a comparison bug never freezes into evidence (Q11, Q13, schema §1).

View Source
const MaxConstraintDepth = 32

MaxConstraintDepth bounds how deep a nested `all`/`any` tree this comparison will walk. The draft requires a finite ceiling and RECOMMENDS 32 (§3.4); a tree past it is not rejected as a token here — behalf's resource limits are a separate, tracked question (profile §9.4) — it is simply not something this comparison can prove to be a narrowing, so it fails closed like any other unprovable subsumption.

Variables

This section is empty.

Functions

func ColorFor

func ColorFor(w io.Writer) bool

ColorFor reports whether w should get ANSI colour: a terminal, and NO_COLOR unset (no-color.org).

func CompareGrantsDetail

func CompareGrantsDetail(parent, child []Grant) (Attenuation, string, *I4Violation)

CompareGrantsDetail is CompareGrants plus the structured finding behind a `broadened` verdict, when the verdict came from the AAT draft's capability monotonicity rules (I4) and therefore names a tool and a constraint. Mint uses it to refuse with a typed error rather than a sentence; everything that only needs the classification calls CompareGrants.

Which comparison runs

Two grant vocabularies live here and the routing between them is by the RFC 9396 `type` member alone:

  • An entry of type `attenuating_agent_token` on EITHER side selects the vendored draft's own comparison — §4.5's subsumption matrix over the `tools` map, as §7 steps 4n–4p apply it (aat_i4.go). The draft defines this shape; behalf implements the draft for it.
  • Everything else keeps compareV1: behalf's own `type` + `actions` + `privileges[].limit` rules, unchanged and still stamped ComparatorVersion.

Both comparisons always run and the stricter answer wins — the draft's rules never replace behalf's for a shape the draft does not define, and a side that carries none of the other's vocabulary is a known-empty side rather than an uncomparable one.

func Render

func Render(w io.Writer, res *Result, opt Options) error

Render writes the authority tree for one receipt.

func RenderRuns

func RenderRuns(w io.Writer, rows []RunRow, opt Options) error

RenderRuns writes the runs table.

Types

type Address

type Address struct {
	RunID string
	Step  int
}

Address is a receipt's product-level address: a run id and a step.

The step is the run-relative ordinal — the receipt's zero-based position in the run view, which is log-index order filtered to the run, the authoritative order for reconstruction (Q58, Q82). It is a read-path coordinate, not a stored field: the log index is global and shifts with interleaved runs, so `run_c71e:31` names the 32nd receipt of run_c71e however the log interleaved it. The fixture runs encode the same ordinal at capture in emitter.counter (which the index projects), so the demo's step 31 is the refund.issue in both runs.

func ParseAddress

func ParseAddress(s string) (Address, error)

ParseAddress parses "<run>:<step>".

func (Address) String

func (a Address) String() string

type Aliases

type Aliases map[string]string

Aliases is the local, versioned alias map that turns key thumbprints into names on screen (Q16). It exists because the canonical actor identity is the hop's key thumbprint — keys are what the cryptography proves — while names are self-reported. Receipts therefore carry no human-readable identity at all (Q40): the human principal is issuer plus sub-digest, and the display name lives here, locally, under the operator's control. Every label this map produces is an asserted label, never evidence.

func LoadAliases

func LoadAliases(logDir string) (Aliases, error)

LoadAliases reads logDir/aliases.json over the built-in demo names. A missing file is not an error: the map is a display convenience, and an unnamed key renders as its own thumbprint.

func (Aliases) Label

func (a Aliases) Label(jkt string) string

Label returns the display label for a key thumbprint, falling back to a short form of the thumbprint itself — an honest "we do not know this key" rather than a blank.

type Attenuation

type Attenuation string

Attenuation is the read-time comparison of one hop's grants against its parent's (Q13). `unknown` is a first-class outcome: vocabularies the AAT invariants cannot compare are recorded and flagged, never swallowed — and since D8.7 they hold the hop at `asserted`, because an invariant that could not be checked is not one that held. The comparator itself is unchanged by that decision; it answers, and internal/aat decides what the answer is worth.

const (
	AttenuationUnchanged  Attenuation = "unchanged"
	AttenuationAttenuated Attenuation = "attenuated"
	AttenuationBroadened  Attenuation = "broadened"
	AttenuationUnknown    Attenuation = "unknown"
)

func CompareGrants

func CompareGrants(parent, child []Grant) (Attenuation, string)

CompareGrants compares a child hop's grants against its parent's and returns the classification plus a human-readable reason for the non-obvious ones.

This is a pure read-time computation over the raw stored grants. Nothing it produces is written back to the record (Q11).

type Credential

type Credential struct {
	Issuer   string   `json:"issuer"`
	Kind     string   `json:"kind"`
	ID       string   `json:"id"`
	Exp      int64    `json:"exp"`
	JKT      string   `json:"jkt"`
	AuthTime int64    `json:"auth_time"`
	AMR      []string `json:"amr"`
}

Credential is the canonical per-hop credential reference — never the token itself (Q23).

type Grant

type Grant struct {
	Type       string      `json:"type"`
	Actions    []string    `json:"actions"`
	Locations  []string    `json:"locations"`
	Datatypes  []string    `json:"datatypes"`
	Identifier string      `json:"identifier"`
	Privileges []Privilege `json:"privileges"`
	// Intent is a named behalf extension on the RFC 9396 object: the
	// human's words for what was delegated ("resolve ticket 4417").
	Intent string `json:"intent"`
	// Tools is the AAT draft's own profile of RFC 9396 (§3.3): a map of tool
	// name to argument constraint set, carried on an entry whose Type is
	// AATGrantType. It is held as raw bytes rather than a decoded map so that
	// a malformed `tools` member cannot abort the projection of the fields
	// beside it — the draft's shape is read, and validated, in aat_i4.go.
	Tools json.RawMessage `json:"tools"`
	Raw   json.RawMessage `json:"-"`
}

Grant is one RFC 9396 authorization_details object, read out of a hop verbatim. Raw keeps the exact stored bytes so nothing is normalized away: the fields below are a read-time projection for comparison, never a rewrite of the record (Q11).

type Hop

type Hop struct {
	Depth        int
	MaxDepth     int
	ParHash      string
	JKT          string // RFC 7638 thumbprint of the hop's cnf.jwk (Q16)
	JTI          string
	Exp          int64
	Grants       []Grant
	Credential   Credential
	RootBinding  *RootBinding
	Verification Verification
	Carriage     string
	// StoredFlag is the attenuation_flag as captured (schema §7); Computed
	// and ComputedReason are this read's comparison against the parent hop
	// and are never written back (Q11).
	StoredFlag     string
	Computed       Attenuation
	ComputedReason string
}

Hop is one delegation hop as rendered: the stored per-hop fields plus the read-time comparison against its parent.

type I4Violation

type I4Violation struct {
	Tool        string
	Argument    string
	ParentType  string
	DerivedType string
	Detail      string
}

I4Violation names the specific §4.5 rule a hop broke, so a caller — Mint above all — can refuse with something more precise than a sentence. Tool is always set; Argument is empty when the finding is tool-level (a tool the parent never granted, a key set that changed shape); ParentType and DerivedType are the two `constraint_type` values when both are known.

func (*I4Violation) String

func (v *I4Violation) String() string

String renders the violation the way a refusal should read.

type Limit

type Limit struct {
	Amount   string `json:"amount"`
	Currency string `json:"currency"`
}

Limit is a decimal ceiling on an operation. Amount is kept as the verbatim decimal string — it is rendered exactly as captured and compared as an exact rational, never through a float.

type Options

type Options struct {
	Color   bool
	Aliases Aliases
}

Options controls rendering. Colour is opt-in and off by default so piped and captured output is plain text.

type Privilege

type Privilege struct {
	Operation string `json:"operation"`
	Limit     *Limit `json:"limit"`
}

Privilege is one per-operation constraint inside a grant.

type Result

type Result struct {
	Address  Address
	LogIndex uint64
	LeafHash string
	// Payload is the exact stored payload span — the signed bytes,
	// untouched (the span rule).
	Payload []byte

	ReceiptID  string
	Kind       string
	CapturedAt string
	Operation  string
	Target     string
	Outcome    string
	// Amount is the operation's amount as captured when the surface reported
	// a decimal, and the read-time rendering of `amount_cents` when it
	// reported minor units (see amountOf). It is what the scope check
	// compares.
	Amount   string
	Currency string
	ActorJKT string

	// StoredAttribution is the receipt-level rollup as captured (§8).
	StoredAttribution string
	AttributionClass  string

	Chain  []Hop
	Excess *ScopeExcess
	// VerifiedHops of TotalHops is the "chain intact for N of M hops" line.
	VerifiedHops int
	TotalHops    int
}

Result is everything one `behalf why` render needs.

func FromPayload

func FromPayload(addr Address, logIndex uint64, leafHash string, payload []byte) (*Result, error)

FromPayload builds the same model Load builds, from a receipt payload the caller has already read out of the log.

It exists for readers that walk a whole run rather than one address — the HTML export — and would otherwise re-open the index and re-parse the checkpoint once per step. Everything Load computes is computed here too: the attenuation deltas and the scope check are read-time comparisons over the stored grants and are recomputed on every read, never cached and never written back (Q11).

The payload must already have been checked against its indexed leaf hash — tlog.BundleReader.Payload does that, and Load reaches it that way. A caller that skips the check renders bytes the index does not vouch for.

func Load

func Load(ctx context.Context, logDir string, addr Address) (*Result, error)

Load resolves addr against the log in logDir and builds the render model. The index supplies the run view and the leaf hash; the payload bytes come from the log's own entry bundles and are checked against that leaf hash before anything is read out of them.

type RootBinding

type RootBinding struct {
	Nonce      string `json:"nonce"`
	DeviceJKT  string `json:"device_jkt"`
	IDTokenRef string `json:"id_token_ref"`
}

RootBinding is the depth-0 OIDC nonce-thumbprint binding (D5).

type RunRow

type RunRow struct {
	RunID   string
	Started string // the first captured_at in the run, verbatim
	// Status is the run's outcome: `ok` unless some receipt records a
	// failed operation. Two successful runs both reading `ok` is the
	// point of the listing, not a gap in it — an error tracker shows
	// nothing here, and the divergence that matters is only visible to
	// `behalf diff`.
	//
	// Run *completeness* (Q82) is deliberately not this column: it is
	// marked by a session-end receipt, the frozen kind enum has no such
	// kind yet, so on v1 data every run would read `open` forever. That
	// belongs in the listing once the kind exists, not before.
	Status string
	// Actions counts the action-family receipts — the denominator of the
	// attribution metric (Q86).
	Actions int64
	// Actor names the human at the root of the delegation chain — who the
	// run was carried out on behalf of, which is the question the product
	// exists to answer. It is a display label off the local alias map
	// (Q16): asserted, never evidence.
	Actor string
	// Attribution is the run's weakest attribution, rendered from the
	// stored per-hop verification states (Q12, Q86).
	Attribution string
}

RunRow is one line of `behalf runs`: a run, and how much of it is attributable.

func ListRuns

func ListRuns(ctx context.Context, logDir string, aliases Aliases) ([]RunRow, error)

ListRuns builds the `behalf runs` table from the index, reading receipt payloads only where the stored rollup already says something is unverified — that is the only case that needs the per-hop detail behind the summary.

type ScopeExcess

type ScopeExcess struct {
	Operation         string
	Limit             string // the tightest delegated ceiling, verbatim
	Currency          string
	Amount            string // what the operation actually did, verbatim
	ComparatorVersion string
}

ScopeExcess is a read-time finding: the operation exceeded the ceiling the chain delegated. It is computed from the raw per-hop authorization_details against the operation every time it is rendered, stamped with the comparator version, and never stored — a computed delta that froze into evidence would be a computation bug preserved forever (Q11, Q13).

func CheckScope

func CheckScope(hops []Hop, operation, amount string) *ScopeExcess

CheckScope compares operation/amount against the tightest ceiling any hop of the chain placed on that operation. It returns nil when the operation is inside the delegated ceiling, when no hop constrained it, or when the values are not comparable — behalf records, it does not enforce.

type Verification

type Verification struct {
	Status      string `json:"status"`
	Method      string `json:"method"`
	EvidenceRef string `json:"evidence_ref"`
}

Verification is the stored per-hop three-state (Q12).

Jump to

Keyboard shortcuts

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