Documentation
¶
Overview ¶
Package protocol is the Delegent authorization algebra: the effect/method lattice, body-aware classification, the subset authorization test, slip minting/narrowing, and chain verification. It is PURE — no database, no HTTP, no wall clock, no randomness. Side effects are injected (Clock, Signer, RootStore) so both the control plane and the proxy can share one decision function with zero drift.
Ported 1:1 from the TypeScript reference in src/. Behaviour is pinned by the conformance vectors in core/testdata, generated from the TS test suite.
Index ¶
- Constants
- func At(body any, path string) (any, bool)
- func Canonical(v any) []byte
- func EffectNames(mask Effect) string
- func IsBatch(body any) bool
- func MatchBody(pred map[string]any, body any) bool
- func MatchPath(pattern, path string) bool
- func MethodName(bit Method) string
- func NewKeypair() (pub string, priv ed25519.PrivateKey, err error)
- func NormalizeResource(resource string) string
- func ReceiptHash(r *Receipt, prev string) string
- func VerifyBytes(pubHex string, msg []byte, sigHex string) bool
- type Adapter
- type Caveats
- type Chain
- type ChainStatus
- type Classified
- type ClassifyRule
- type Clock
- type Decision
- type Ed25519Signer
- type Effect
- type MapRootStore
- type MatchSpec
- type Method
- type Receipt
- type Request
- type RootStore
- type Signer
- type Slip
- type SlipBody
- type VerifyResult
Constants ¶
const ReasonPossessionFailed = "proof-of-possession failed: request not signed by the bound key"
ReasonPossessionFailed is the exact VerifyChain refusal for a failed holder proof-of-possession. Exported so tooling (e.g. the CLI's structural verify, which forgives exactly this failure) can match it structurally instead of scraping prose.
Variables ¶
This section is empty.
Functions ¶
func At ¶
At reads a dotted path out of a body: "params.name" -> body.params.name. Missing or a non-object mid-path -> (nil, false).
func Canonical ¶
Canonical produces the byte-exact serialization that signatures are computed over: sorted object keys, no whitespace, JS-JSON.stringify number/string formatting. It MUST match the TypeScript canonical() byte-for-byte or no signature ever verifies across the two implementations. Its parity is pinned by core/testdata vectors generated from the TS reference.
Accepts the JSON value tree (nil, bool, string, numbers, []any/[]string, map[string]any) plus SlipBody directly. Callers building ad-hoc objects (e.g. the proxy's proof-of-possession bytes) pass a map[string]any.
func EffectNames ¶
EffectNames renders a mask as its member names joined by '+': 5 -> "read+destructive". The empty set renders "nothing". This is what Alice reads, so it is the SET, not a ceiling.
func MatchBody ¶
MatchBody checks a body predicate: every (possibly dotted) key must be present in the body with an equal value. This is what makes classification BODY-AWARE — it is the only reason a force-push can be distinguished from an ordinary ref update.
func MatchPath ¶
MatchPath matches a path against a pattern:
{name} — exactly one segment (a path parameter)
* — exactly one segment (wildcard)
** — the remainder (zero or more segments); only meaningful at the end
func MethodName ¶
MethodName renders a single method bit as its name, or "method:N" if not a known bit.
func NewKeypair ¶
func NewKeypair() (pub string, priv ed25519.PrivateKey, err error)
NewKeypair generates a fresh ed25519 keypair; pub is the raw 32-byte key, hex.
func NormalizeResource ¶
NormalizeResource decodes %xx (repeatedly), collapses '//', and resolves '.'/'..' BEFORE any prefix match happens. Path-matching bypasses are a classic auth hole: the string we match must be the string that will be requested.
func ReceiptHash ¶
ReceiptHash computes the canonical hash of one receipt folded with the prior chain hash. Field order and separators are fixed, and Scopes are sorted first, so neither scope ordering nor field concatenation can change the hash for the same logical receipt. It deliberately excludes Hash/Sig/PrevHash themselves — prev IS the prior receipt's hash.
Types ¶
type Adapter ¶
type Adapter struct {
Vendor string `json:"vendor"`
Version string `json:"version"`
Classify []ClassifyRule `json:"classify"`
Default struct {
Effect string `json:"effect"`
} `json:"default"`
}
type Caveats ¶
type Caveats struct {
Effects *Effect
Methods *Method
Scopes *[]string
Ceiling *[]string
Resources *[]string
Budget *float64
Exp *int64
Depth *int
}
Caveats attenuate a parent slip. A nil field means "inherit the parent" (no change); a set field is intersected/clamped against the parent — never widened.
type Chain ¶
type Chain []Slip
Chain is a slip plus all its ancestors, root first. It travels together.
func Narrow ¶
func Narrow(parent Chain, c Caveats, childPub string, parentSigner Signer, nonce string) (Chain, []string, error)
Narrow mints a strictly-weaker child slip. Offline, no authority contacted. Every caveat is clamped against the folded parent; widening attempts are reported, not hidden. The nonce is injected (core holds no randomness).
type ChainStatus ¶
type ChainStatus struct {
Verified bool `json:"verified"`
Count int `json:"count"`
BrokenAt string `json:"broken_at,omitempty"` // receipt ID where verification first failed
Reason string `json:"reason,omitempty"`
Unsigned int `json:"unsigned,omitempty"` // count of unsigned receipts (soft)
}
ChainStatus is the verdict of walking one principal's receipt chain: whether it is intact, how many receipts were checked, and — on the first break — which receipt failed and why. Unsigned counts receipts that carry no signature (a legitimate fail-soft mint), which is a soft warning rather than a hard chain break.
func VerifyReceiptChain ¶
func VerifyReceiptChain(receipts []Receipt, pub string) ChainStatus
VerifyReceiptChain walks receipts oldest→newest (chain order for ONE principal) and returns the first break, if any. pub is that principal's public key (hex).
Per receipt, in order:
- linkage: PrevHash must equal the prior receipt's Hash ("" for the first) — a mismatch means a receipt was dropped or reordered.
- integrity: the recomputed hash must equal the stored Hash — a mismatch means a field was altered.
- authenticity: a non-empty Sig must verify under pub. An empty Sig is a legitimate unsigned receipt (fail-soft mint): counted and skipped, never a hard break, as long as its hash and linkage hold.
type Classified ¶
type Classified struct {
Action string
Effect Effect
Method Method
Scopes []string
Resource string
Cost float64
Unknown bool // true => action not in the adapter, defaulted (fail closed)
}
Classified is the danger assessment of a Request against an adapter.
func Classify ¶
func Classify(a Adapter, r Request) Classified
Classify assigns an effect/method/scopes to a Request. A JSON-RPC batch is classified as the UNION of its elements — one unclassified element contributes the UNKNOWN bit and poisons the whole array, so a batch is allowed in full or not at all.
type ClassifyRule ¶
type ClassifyRule struct {
ID string `json:"id,omitempty"`
Match *MatchSpec `json:"match,omitempty"` // nil => a section-comment entry, matches nothing
Effect string `json:"effect"`
Method *string `json:"method,omitempty"` // demo-style rules carry the method here
Scopes []string `json:"scopes"`
Meters []string `json:"meters,omitempty"`
}
type Clock ¶
type Clock interface{ NowMillis() int64 }
Clock is injected so core never reads the wall clock.
type Decision ¶
Decision is the result of Authorize. Reason is set only when Allow is false, and IS the audit trail — it must name the specific thing that was refused.
func Authorize ¶
func Authorize(e SlipBody, c Classified) Decision
Authorize is the whole enforcement decision: the classified request's effect/method/ scopes/resource/cost must all fit inside the effective slip. It is SUBSET, not ≤ — an effect the grant does not contain is not "too high", it is simply absent, and no ordering can smuggle it in. The deny reasons ARE the audit trail, so they name the specific thing refused.
type Ed25519Signer ¶
type Ed25519Signer struct {
// contains filtered or unexported fields
}
Ed25519Signer is a local, in-process Signer. In production the control plane swaps this for a KMS-backed Signer implementing the same interface.
func NewEd25519Signer ¶
func NewEd25519Signer(priv ed25519.PrivateKey) Ed25519Signer
func (Ed25519Signer) Public ¶
func (s Ed25519Signer) Public() string
type Effect ¶
type Effect uint
Effect is a bitmask SET (a child effect set folds with the parent by AND). Danger is not a line: "may notify humans" and "may irreversibly destroy" are independent axes, so a single ≤ ceiling cannot express "may comment, may not delete". As a set it can. UNKNOWN is a bit no slip may ever hold, so an unclassified action is denied structurally rather than by a numeric accident.
const ( EffectRead Effect = 1 // observes state EffectWrite Effect = 2 // changes state, reversibly EffectDestructive Effect = 4 // changes state, irreversibly EffectSpends Effect = 8 // costs the principal money EffectExternal Effect = 16 // affects the outside world (email, public post) EffectUnknown Effect = 32 // unclassified — NO slip may ever hold this bit )
func EffectByName ¶
EffectByName maps a lowercase effect name to its bit. Unknown name -> (0, false), which callers treat as fail-closed (the UNKNOWN bit).
type MapRootStore ¶
MapRootStore is a trivial in-memory RootStore.
func (MapRootStore) IssuerPubKey ¶
func (m MapRootStore) IssuerPubKey(iss string) (string, bool)
type Method ¶
type Method uint
Method is a bitmask SET, same lattice discipline as Effect — never a ranked scale.
func MethodByName ¶
MethodByName maps an HTTP method name to its bit. Unknown -> (0, false).
type Receipt ¶
type Receipt struct {
ID string `json:"id"`
Principal string `json:"principal"`
Handle string `json:"handle,omitempty"`
Tool string `json:"tool,omitempty"` // the tool or action decided on
Decision string `json:"decision"` // "grant" | "deny" | "flag"
Reason string `json:"reason,omitempty"` //
Effect string `json:"effect,omitempty"` // rendered effect names, e.g. "read+write"
Scopes []string `json:"scopes,omitempty"` //
OverAsk bool `json:"over_ask,omitempty"` //
CreatedAt int64 `json:"created_at"` // unix ms
// PrevHash/Hash/Sig form the chain: Hash = H(canonical fields ‖ PrevHash), Sig =
// ed25519(rootKey, Hash). An empty Sig marks a fail-soft unsigned mint.
PrevHash string `json:"prev_hash,omitempty"`
Hash string `json:"hash"`
Sig string `json:"sig,omitempty"`
}
Receipt is one decision record in a principal's tamper-evident chain: what was decided (grant/deny/flag), for which tool and scopes, when — hash-chained per principal and signed by the principal's root key. This is the protocol-level shape: any party holding a chain of these and the principal's public key can verify integrity, ordering, and authenticity with VerifyReceiptChain — no platform required.
type Request ¶
Request is what gets classified: an action name (or HTTP method), a resource id (or URL path), an optional metered amount, and a body that is either an object (map[string]any) or a JSON-RPC batch ([]any).
type RootStore ¶
RootStore resolves a named root issuer ("root:alice") to its public key. Only named roots may anchor a chain; intermediate links are keyed by raw hex public keys.
type Signer ¶
type Signer interface {
Public() string // issuer public key, hex (raw 32 bytes)
Sign(msg []byte) (string, error) // detached signature, hex
}
Signer produces detached ed25519 signatures over arbitrary bytes and exposes the issuer public key (hex). A KMS-backed implementation satisfies the same interface.
type Slip ¶
type Slip struct {
Body SlipBody `json:"body"`
Sig string `json:"sig"` // ed25519(issuer_priv, canonical(body)), hex
}
Slip is a SlipBody plus its issuer's signature over Canonical(body).
type SlipBody ¶
type SlipBody struct {
V int `json:"v"`
Iss string `json:"iss"` // issuer public key (hex) or a root name ("root:alice")
Aud string `json:"aud"` // BOUND to this agent's public key (hex)
Vendor string `json:"vendor"` //
Effects Effect `json:"effects"`
Methods Method `json:"methods"`
Scopes []string `json:"scopes"`
Ceiling []string `json:"ceiling"` // scopes the holder may pull later without per-request approval
Resources []string `json:"resources"`
Budget float64 `json:"budget"` // USD
Exp int64 `json:"exp"` // unix ms
Depth int `json:"depth"` // remaining sub-delegations
Nonce string `json:"nonce"`
}
SlipBody is a signed statement of limits, bound to one agent's public key. It is NOT a credential. The json tags are load-bearing: Canonical signs over these exact keys, so they must match the TS field names byte-for-byte.
func Fold ¶
Fold collapses a chain into its effective slip. Widening is impossible BY CONSTRUCTION: a malicious child claiming every effect bit is simply ignored, because {read,write,destructive,…} & {read} = {read}. We do not validate-and-reject widening; the fold makes it a no-op. Attempts are reported via onAnomaly (may be nil), never hidden. Every field folds by the operation its TYPE demands: sets intersect, ordered scalars take the min. Identity fields (v/iss/aud/vendor/nonce) keep the root's.
type VerifyResult ¶
VerifyResult is the outcome of VerifyChain; Effective/Anomalies valid when OK, Reason set when not. Reasons are specific — they ARE the audit trail.
func VerifyChain ¶
func VerifyChain(chain Chain, callerPub, callerSig string, reqBytes []byte, roots RootStore, now int64) VerifyResult
VerifyChain checks a chain end to end, cheapest first, fail fast: trusted root, link continuity, every signature, caller binding, proof-of-possession, expiry, and depth.
Source Files
¶
Directories
¶
| Path | Synopsis |
|---|---|
|
cmd
|
|
|
delegent-proto
command
Command delegent-proto is the Delegent protocol in a terminal: mint, attenuate, inspect, and verify capability slips (chains), and hash/sign/verify tamper-evident receipt chains.
|
Command delegent-proto is the Delegent protocol in a terminal: mint, attenuate, inspect, and verify capability slips (chains), and hash/sign/verify tamper-evident receipt chains. |