protocol

package module
v0.2.0 Latest Latest
Warning

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

Go to latest
Published: Jul 21, 2026 License: Apache-2.0 Imports: 15 Imported by: 0

README

delegent.dev/protocol

The reference implementation of the Delegent protocol: consent-bound, attenuable, audience-bound capability slips for AI-agent authority — plus the tamper-evident receipt chains that audit every decision made under them.

This library is the math. It has zero dependencies (Go standard library only) and holds no state, no I/O, and no keys of its own — any party can verify a chain or a receipt trail with nothing but this package and a public key. The Delegent gateway and the hosted platform at delegent.dev build on it; so can anyone else.

The model in five sentences

An operator's root key signs a slip: a statement of limits — scopes, effects, budget, expiry, sub-delegation depth — bound to one agent's public key (aud). The agent may narrow its slip into a strictly-weaker child bound to another agent's key, offline, without contacting anyone; the signed chain travels with the request. A verifier folds the chain — every link is intersected, so no child can ever exceed its parent — and checks signatures, linkage, expiry, depth, and the caller's proof of possession of the bound key, which makes a stolen chain useless. Every decision taken under a slip is recorded as a receipt, hash-chained per principal and signed by the root key, so dropped, reordered, or altered records are detectable by anyone holding the public key. Revocation is enforced wherever the chain is redeemed — the issuer's gateway — so it is local and instant.

Library

import protocol "delegent.dev/protocol"

pub, priv, _ := protocol.NewKeypair()
slip, _ := protocol.SignSlip(protocol.SlipBody{ /* limits */ }, protocol.NewEd25519Signer(priv))
chain, anomalies, _ := protocol.Narrow(parent, caveats, childPub, parentSigner, nonce)
res := protocol.VerifyChain(chain, callerPub, callerSig, reqBytes, roots, now)
status := protocol.VerifyReceiptChain(receipts, rootPub)

Key entry points: SignSlip, Narrow, Fold, VerifyChain, Authorize, Classify, ReceiptHash, VerifyReceiptChain, Canonical. Conformance vectors live in testdata/.

CLI

cmd/delegent-proto is the protocol in a terminal — the same operations, file-shaped:

go install delegent.dev/protocol/cmd/delegent-proto@latest

delegent-proto keygen                                  # {"pub": …, "priv": …}
delegent-proto mint --priv K --iss root:alice --aud AGENT_PUB \
  --vendor github --scopes repos:read,repos:write --effects read,write \
  --ceiling repos:admin --methods GET,POST --resources '/repos/*' \
  --budget 10 --ttl-minutes 60 --depth 2 > chain.json
delegent-proto attenuate --chain chain.json --priv AGENT_PRIV \
  --aud REMOTE_PUB --scopes repos:read --ceiling repos:read --budget 2 > child.json
delegent-proto inspect --chain child.json              # per-link view + folded limits
delegent-proto verify --chain child.json --root root:alice=ROOT_PUB \
  --priv REMOTE_PRIV                             # full verify (proof-of-possession)
delegent-proto verify --chain child.json --root root:alice=ROOT_PUB
                                                 # structural verify (auditor's view)
delegent-proto hash-receipts --receipts raw.json --priv ROOT_PRIV > signed.json
                                                 # chain + SIGN receipts (omit --priv to
                                                 # hash only; stale sigs are stripped loudly)
delegent-proto verify-receipts --receipts signed.json --pub ROOT_PUB

Exit code 0 = verified; non-zero = refused, with the specific reason on stderr — the reasons are the audit trail.

License

Apache-2.0 — see LICENSE.

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

View Source
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

func At(body any, path string) (any, bool)

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

func Canonical(v any) []byte

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

func EffectNames(mask Effect) string

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 IsBatch

func IsBatch(body any) bool

IsBatch reports whether a body is a JSON-RPC batch (an array).

func MatchBody

func MatchBody(pred map[string]any, body any) bool

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

func MatchPath(pattern, path string) bool

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

func MethodName(bit Method) string

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

func NormalizeResource(resource string) string

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

func ReceiptHash(r *Receipt, prev string) string

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.

func VerifyBytes

func VerifyBytes(pubHex string, msg []byte, sigHex string) bool

VerifyBytes checks a detached ed25519 signature. Malformed key/sig -> false (fail closed).

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

type Decision struct {
	Allow  bool
	Reason string
}

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

func (Ed25519Signer) Sign

func (s Ed25519Signer) Sign(msg []byte) (string, error)

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

func EffectByName(name string) (Effect, bool)

EffectByName maps a lowercase effect name to its bit. Unknown name -> (0, false), which callers treat as fail-closed (the UNKNOWN bit).

type MapRootStore

type MapRootStore map[string]string

MapRootStore is a trivial in-memory RootStore.

func (MapRootStore) IssuerPubKey

func (m MapRootStore) IssuerPubKey(iss string) (string, bool)

type MatchSpec

type MatchSpec struct {
	Action *string        `json:"action,omitempty"`
	Method *string        `json:"method,omitempty"`
	Path   *string        `json:"path,omitempty"`
	Body   map[string]any `json:"body,omitempty"`
}

type Method

type Method uint

Method is a bitmask SET, same lattice discipline as Effect — never a ranked scale.

const (
	MethodGET    Method = 1
	MethodPOST   Method = 2
	MethodPUT    Method = 4
	MethodPATCH  Method = 8
	MethodDELETE Method = 16
)

func MethodByName

func MethodByName(name string) (Method, bool)

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

type Request struct {
	Action   string
	Resource string
	Amount   float64
	Body     any
}

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

type RootStore interface {
	IssuerPubKey(iss string) (string, bool)
}

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

func SignSlip

func SignSlip(body SlipBody, signer Signer) (Slip, error)

SignSlip signs a slip body over its canonical encoding.

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

func Fold(chain Chain, onAnomaly func(string)) SlipBody

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

type VerifyResult struct {
	OK        bool
	Effective SlipBody
	Anomalies []string
	Reason    string
}

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.

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.

Jump to

Keyboard shortcuts

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