Files
mesh-catalog/modules/claude-licence-manager/cmd/manager/manager.go
T
jschoubben b1da6344a8 Join 11: merge origin/main (the licence manager in parts, issue 1409)
The conflict in the old cmd/claude-licence-manager/tools_test.go resolves to both sides: main
moved it to cmd/manager, and the join's count of binding.changed.<consumer> under its family
goes into the moved TestResyncSaysEachOnce. The whole-module test's runtime refused
binding.changed.<consumer> because the manifest names the family binding.changed: it now
accepts a key that extends a named family by further parts, as A-134 says the event.

Builds: design 47 G A-134

Claude-Session: https://claude.ai/code/session_01NcbgfGKaxw984XyAPfLm4h
2026-11-12 14:05:51 +01:00

1005 lines
39 KiB
Go

package main
// The Anthropic licence manager's rules (novox/hq ADR 0183, ADR 0206, design 39), written against what it
// is handed — a store, the vendor, a way to ask a node, its own state, a way to emit — so every rule is
// tested without a bus, a database or the vendor.
//
// - A licence is an account, learned from what the nodes report (`holdings`, the agent module's state).
// A report with a refresh token the manager does not hold is a candidate.
// - The secret is asked for, sealed to this module's key, never published.
// - Adopting is refreshing: newest login first, once per account; a failure adopts nothing.
// - What each consumer should hold is this module's `bindings` state, with a generation that grows with
// every rotation and switch; the consumer fetches its token by asking `current`.
// - One rotation source: every exchange with the vendor runs under a lease.
import (
"context"
"crypto/sha256"
"encoding/hex"
"encoding/json"
"errors"
"fmt"
"os"
"regexp"
"sort"
"strings"
"sync"
"time"
oc "git.novox.be/novox/mesh-contracts/operator-and-conversation/go/seats/operatorchannel"
. "claude-licence-manager/internal/licences"
)
// Fingerprint names a token without being one: the agent module's own fingerprint, so the two compare.
func Fingerprint(s string) string {
sum := sha256.Sum256([]byte(s))
return "sha256:" + hex.EncodeToString(sum[:])[:16]
}
// Identity is the account a grant belongs to, as the agent's own state file names it.
type Identity struct {
AccountUUID string `json:"accountUuid"`
EmailAddress string `json:"emailAddress,omitempty"`
OrganizationUUID string `json:"organizationUuid,omitempty"`
}
// Holdings is one node's report, as the agent module writes it to its `holdings` state (ADR 0206).
type Holdings struct {
Node string `json:"node"`
Identity *Identity `json:"identity"`
Kind string `json:"kind"`
Refresh struct {
Present bool `json:"present"`
Fingerprint string `json:"fingerprint"`
} `json:"refresh"`
ChangedAt string `json:"changedAt"`
// Generation is the binding generation the node last applied (novox/hq issue 243).
Generation int64 `json:"generation"`
}
// Cause is why a consumer's binding changed and who or what asked for it (novox/hq issue 625): Why is the verb
// called or the rule that fired, By the caller the runtime proved (stdio.CallerOf) or this module itself.
type Cause struct {
Why string
By string
}
// Itself names this module where it moved a binding by a rule of its own, with no caller.
const Itself = "claude-licence-manager"
type causeKey struct{}
// WithCause is ctx carrying why what runs in it changes a binding, and who asked.
func WithCause(ctx context.Context, c Cause) context.Context {
return context.WithValue(ctx, causeKey{}, c)
}
// CauseOf is the cause ctx carries, or "a rule of this module's own" when none: never empty, so a record always
// says something, and a change nobody named a cause for reads as this module's own.
func CauseOf(ctx context.Context) Cause {
if c, ok := ctx.Value(causeKey{}).(Cause); ok && c.By != "" {
return c
}
return Cause{Why: "a rule of this module's own", By: Itself}
}
// record says one change of what a consumer holds, the same fields in two places (novox/hq issue 625): a line in
// the journal and an entry of the audit. The bus hears it as binding.changed (§21); `licence.bound`, `released` and
// `rotated` are said no more (B16). Never a token.
func (m *Manager) record(ctx context.Context, what, line string, detail map[string]any) error {
if err := m.Store.Audit(ctx, what, detail); err != nil {
m.Log("%s, but %v", line, err)
return err
}
m.Log("%s", line)
return nil
}
// BindingChanged is the event of a consumer's binding written or deleted, said as `binding.changed.<consumer>`: the consumer is
// its further part, the one form of G's seat events (novox/hq design 47 G A-134), so a reader adopts it.
const BindingChanged = "binding.changed"
// changed says that one consumer's key of `bindings` was written or deleted (§21, B7): its licence (empty on a
// release) and that licence's key, kind, generation, the licence before, why, by and when. Never a token.
func (m *Manager) changed(b Binding, from string) {
key := ""
if b.Licence != "" {
key = KeyFor(b.Licence)
}
_ = m.Emit(BindingChanged+"."+b.Consumer, map[string]any{"consumer": b.Consumer, "licence": b.Licence, "key": key,
"kind": b.Kind, "generation": b.Generation, "from": from, "why": b.Why, "by": b.By,
"at": m.Now().UTC().Format(time.RFC3339Nano)})
}
// switchedFields are a switch's fields, the same in the event licence.switched and the audit entry switched (§27).
func switchedFields(from, to string, consumers []string, c Cause) map[string]any {
return map[string]any{"from": from, "to": to, "consumers": consumers, "by": c.By, "why": c.Why}
}
// moved records a consumer bound or switched, after its key is written: in the journal and the audit (a switch with
// §27's fields), and as binding.changed.
func (m *Manager) moved(ctx context.Context, b Binding, from string, c Cause) error {
what, line := "bound", fmt.Sprintf("bound %s to %s", b.Consumer, b.Licence)
detail := map[string]any{"consumer": b.Consumer, "licence": b.Licence, "from": from, "generation": b.Generation,
"why": c.Why, "by": c.By}
if from != "" && from != b.Licence {
what, line = "switched", fmt.Sprintf("switched %s from %s to %s", b.Consumer, from, b.Licence)
detail = switchedFields(from, b.Licence, []string{b.Consumer}, c)
}
err := m.record(ctx, what, fmt.Sprintf("%s (generation %d): %s; asked by %s", line, b.Generation, c.Why, c.By), detail)
m.changed(b, from)
return err
}
// GrantAnswer is what a node's `claude_code_grant` answers: a login sealed to the key given, or nothing.
type GrantAnswer struct {
Sealed *SealedBox `json:"sealed"`
Identity *Identity `json:"identity"`
Fingerprint string `json:"fingerprint"`
}
// Manager is the rules and what they are handed.
type Manager struct {
Store Store
Vendor Vendor
// Grants is the vault, which holds every licence's grant and access token (§15, §16).
Grants Grants
Keys KeyPair
// AskGrant asks a node's agent module for the grant a login left there, sealed to publicKey. An error
// is the node not answering; a nil Sealed is no login waiting.
AskGrant func(ctx context.Context, node, publicKey string) (GrantAnswer, error)
// Reported is the binding generation a node last reported applying (B11), 0 when none is known.
Reported func(node string) int64
Emit func(event string, body map[string]any) error
// Tell tells the operator one notice that needs no answer, through the seat operator-channel's tell (§28).
Tell func(ctx context.Context, headline string, lines []string) error
Now func() time.Time
Log func(format string, args ...any)
// Holder names this process in a lease, so a second run is told apart.
Holder string
// Settings are those in force, read again from SettingsFile at every reading (settings.go); an empty SettingsFile
// keeps them.
Settings Settings
SettingsFile string
settingsMu sync.RWMutex
// Conditions is the state `conditions` (design 39 bus-and-switch §9), and Say says one condition on the operator
// channel; either nil says none.
Conditions State
Say func(c oc.Condition) error
conditionsMu sync.Mutex
}
const leaseFor = 2 * time.Minute
// NameFor is a licence's name: the account's address where it has one, else its id.
func NameFor(id Identity) string {
if e := strings.TrimSpace(id.EmailAddress); e != "" {
return e
}
return id.AccountUUID
}
// ---- learning licences from what the nodes hold ------------------------------------------------------
// Candidate is a report the manager should try.
type Candidate struct {
Node string
Identity Identity
Fingerprint string
ChangedAt int64
}
// CandidatesIn is the candidates in a set of reports by account, newest login first (ADR 0206 §2, §4). A
// report without an identity is not one: a grant is filed under its account or not at all.
func (m *Manager) CandidatesIn(ctx context.Context, reports []Holdings) (map[string][]Candidate, error) {
out := map[string][]Candidate{}
for _, r := range reports {
if !r.Refresh.Present || r.Refresh.Fingerprint == "" || r.Identity == nil || r.Identity.AccountUUID == "" {
continue
}
settled, err := m.Store.Outcome(ctx, r.Refresh.Fingerprint)
if err != nil {
return nil, err
}
if settled != "" {
continue // adopted, dead, skipped, refused or gone: settled once
}
held, err := m.Store.LicenceForAccount(ctx, r.Identity.AccountUUID)
if err != nil {
return nil, err
}
if held != nil && held.RefreshFingerprint == r.Refresh.Fingerprint {
continue
}
var changed int64
if t, err := time.Parse(time.RFC3339Nano, r.ChangedAt); err == nil {
changed = t.UnixMilli()
}
// The latest login wins: one no newer than the grant held is not a newer login.
if held != nil && changed <= held.AdoptedAt {
continue
}
out[r.Identity.AccountUUID] = append(out[r.Identity.AccountUUID],
Candidate{Node: r.Node, Identity: *r.Identity, Fingerprint: r.Refresh.Fingerprint, ChangedAt: changed})
}
for _, list := range out {
sort.SliceStable(list, func(i, j int) bool { return list[i].ChangedAt > list[j].ChangedAt })
}
return out, nil
}
// Consider every report (ADR 0206): for each account with candidates, under that account's lease, try them
// newest first; the first that refreshes is adopted and the rest are settled as skipped without being
// exchanged. Answers what was adopted.
func (m *Manager) Consider(ctx context.Context, reports []Holdings) ([]string, error) {
// **Never a generation a node has already passed** (novox/hq issue 243, B11): each consumer's count is its own,
// and moves past the number its node reported applying.
floor := m.floorIn(reports)
byAccount, err := m.CandidatesIn(ctx, reports)
if err != nil {
return nil, err
}
accounts := make([]string, 0, len(byAccount))
for a := range byAccount {
accounts = append(accounts, a)
}
sort.Strings(accounts)
// A node reporting an account the manager already holds, and bound to nothing, is bound to it
// (ADR 0206 §7) — whenever its report arrives, not only when the licence is adopted: a node whose own
// login is older than the one adopted is never a candidate, and would otherwise never be bound.
if err := m.bindReporters(ctx, reports, floor); err != nil {
return nil, err
}
var adopted []string
for _, account := range accounts {
list := byAccount[account]
key := "account." + account
ok, err := m.Store.Lease(ctx, key, m.Holder, leaseFor)
if err != nil || !ok {
continue
}
for i, c := range list {
name, err := m.adoptOne(ctx, c, reports, floor)
if err != nil {
m.Log("adopting %s's login failed: %v", c.Node, err)
continue
}
if name != "" {
adopted = append(adopted, name)
for _, rest := range list[i+1:] {
_ = m.Store.RecordOutcome(ctx, rest.Fingerprint, rest.Node, account, Skipped, c.Node+"'s newer login was adopted first")
}
break
}
}
_ = m.Store.Unlease(ctx, key, m.Holder)
}
return adopted, nil
}
// bindReporters binds each reporting node that is bound to nothing to the licence its account already has.
func (m *Manager) bindReporters(ctx context.Context, reports []Holdings, floor func(string) int64) error {
for _, rep := range reports {
if rep.Identity == nil || rep.Identity.AccountUUID == "" {
continue
}
if b, err := m.Store.Binding(ctx, rep.Node); err != nil || b != nil {
if err != nil {
return err
}
continue
}
l, err := m.Store.LicenceForAccount(ctx, rep.Identity.AccountUUID)
if err != nil {
return err
}
if l == nil {
continue
}
c := Cause{Why: rep.Node + " reported the account of " + l.Name + " and was bound to nothing", By: Itself}
b, err := m.Store.Bind(ctx, rep.Node, l.Name, Move{Kind: l.Kind, Why: c.Why, By: c.By, Floor: floor})
if err != nil {
return err
}
if err := m.moved(ctx, b, "", c); err != nil {
return err
}
}
return nil
}
func (m *Manager) adoptOne(ctx context.Context, c Candidate, reports []Holdings, floor func(string) int64) (string, error) {
answer, err := m.AskGrant(ctx, c.Node, m.Keys.PublicKey)
if err != nil {
// Not answering is not an answer: asked again on the next pass.
m.Log("%s did not hand over its login: %v", c.Node, err)
return "", nil
}
if answer.Sealed == nil {
return "", m.Store.RecordOutcome(ctx, c.Fingerprint, c.Node, c.Identity.AccountUUID, Gone, "no login was waiting when asked")
}
plain, err := Open(*answer.Sealed, m.Keys.PrivateKey)
if err != nil {
return "", m.Store.RecordOutcome(ctx, c.Fingerprint, c.Node, c.Identity.AccountUUID, Refused, "the grant did not open with this module's key")
}
var offered FullGrant
if err := json.Unmarshal([]byte(plain), &offered); err != nil || offered.RefreshToken == "" {
return "", m.Store.RecordOutcome(ctx, c.Fingerprint, c.Node, c.Identity.AccountUUID, Refused, "the grant holds no refresh token")
}
if Fingerprint(offered.RefreshToken) != c.Fingerprint {
m.Log("%s's login changed while it was asked for; waiting for its next report", c.Node)
return "", nil
}
// A new licence whose key another licence already has is refused before its grant is exchanged
// (design 39 bus-and-switch §10).
if held, err := m.Store.LicenceForAccount(ctx, c.Identity.AccountUUID); err != nil {
return "", err
} else if held == nil {
taken, err := m.keyTaken(ctx, NameFor(c.Identity))
if err != nil {
return "", err
}
if taken != "" {
why := "its key " + KeyFor(NameFor(c.Identity)) + " is the key of the licence " + taken
_ = m.Store.RecordOutcome(ctx, c.Fingerprint, c.Node, c.Identity.AccountUUID, Refused, why)
if err := m.Store.Audit(ctx, "refused", map[string]any{"node": c.Node, "licence": NameFor(c.Identity), "reason": why}); err != nil {
return "", err
}
_ = m.Emit("licence.refused", map[string]any{"node": c.Node, "account": NameFor(c.Identity), "reason": why})
return "", nil
}
}
// Adopting is refreshing (ADR 0206 §4): the exchange is the check, and from here this module is the
// only holder of a live refresh token for the account.
r := m.Vendor.Refresh(ctx, offered)
if !r.OK {
_ = m.Store.RecordOutcome(ctx, c.Fingerprint, c.Node, c.Identity.AccountUUID, Dead, fmt.Sprintf("%d %s", r.Status, r.Reason))
if err := m.Store.Audit(ctx, "refused", map[string]any{"node": c.Node, "account": c.Identity.AccountUUID, "status": r.Status, "reason": r.Reason}); err != nil {
return "", err
}
_ = m.Emit("licence.refused", map[string]any{"node": c.Node, "account": NameFor(c.Identity),
"reason": fmt.Sprintf("the login's refresh token did not refresh (%d)", r.Status)})
m.Log("%s's login for %s did not refresh: %d %s", c.Node, NameFor(c.Identity), r.Status, r.Reason)
return "", nil
}
// The identity guard, on two sources (ADR 0206 §8).
if r.Account != "" && r.Account != c.Identity.AccountUUID {
_ = m.Store.RecordOutcome(ctx, c.Fingerprint, c.Node, c.Identity.AccountUUID, Refused,
"the node says "+c.Identity.AccountUUID+", the vendor says "+r.Account)
if err := m.Store.Audit(ctx, "refused", map[string]any{"node": c.Node, "reported": c.Identity.AccountUUID, "vendor": r.Account}); err != nil {
return "", err
}
_ = m.Emit("licence.refused", map[string]any{"node": c.Node, "account": NameFor(c.Identity),
"reason": "the account the node reported is not the one the vendor answered for"})
return "", nil
}
held, err := m.Store.LicenceForAccount(ctx, c.Identity.AccountUUID)
if err != nil {
return "", err
}
now := m.Now().UnixMilli()
l := Licence{Name: NameFor(c.Identity), Kind: "subscription", AccountUUID: c.Identity.AccountUUID,
Email: c.Identity.EmailAddress, OrganizationUUID: c.Identity.OrganizationUUID, AdoptedFrom: c.Node}
if held != nil {
// A newer login keeps what the key holds beside its grant: its latest reading (§2, §24).
l.Name, l.NotifiedAt, l.Generation, l.Usage = held.Name, held.NotifiedAt, held.Generation, held.Usage
if l.Email == "" {
l.Email = held.Email
}
if l.OrganizationUUID == "" {
l.OrganizationUUID = held.OrganizationUUID
}
}
if err := m.keep(ctx, &l, r.Grant); err != nil {
return "", err
}
l.AdoptedAt = max(now, c.ChangedAt)
if err := m.Store.SaveLicence(ctx, l); err != nil {
return "", err
}
why := "a new licence"
if held != nil {
why = "a newer login"
}
_ = m.Store.RecordOutcome(ctx, c.Fingerprint, c.Node, c.Identity.AccountUUID, Adopted, why)
if err := m.Store.Audit(ctx, "adopted", map[string]any{"licence": l.Name, "node": c.Node, "account": c.Identity.AccountUUID,
"vendorNamedAccount": r.Account != ""}); err != nil {
return "", err
}
// A login moves its node (ADR 0209): the node this login came from is bound to its licence —
// switched, if it was bound to another. Then every node reporting this account and bound to nothing.
// Who asked is the person who logged in there; the mesh knows them only as that login (novox/hq issue 625).
cause := Cause{Why: "a login to " + l.Name + " on " + c.Node + " (ADR 0209)", By: Itself}
ctx = WithCause(ctx, cause)
// The consumers already bound to it take the new grant first, so each consumer this adoption moves is said once
// (§21).
if err := m.publishAdvance(ctx, l, floor); err != nil {
return "", err
}
if b, err := m.Store.Binding(ctx, c.Node); err != nil {
return "", err
} else if b == nil || b.Licence != l.Name {
moved, err := m.Store.Bind(ctx, c.Node, l.Name, Move{Kind: l.Kind, Why: cause.Why, By: cause.By, Floor: floor})
if err != nil {
return "", err
}
from := ""
if b != nil {
from = b.Licence
}
if err := m.moved(ctx, moved, from, cause); err != nil {
return "", err
}
}
for _, rep := range reports {
if rep.Identity == nil || rep.Identity.AccountUUID != c.Identity.AccountUUID {
continue
}
if b, err := m.Store.Binding(ctx, rep.Node); err != nil || b != nil {
continue
}
bc := Cause{Why: rep.Node + " reports the account of " + l.Name + ", logged in on " + c.Node + ", and was bound to nothing", By: Itself}
if b, err := m.Store.Bind(ctx, rep.Node, l.Name, Move{Kind: l.Kind, Why: bc.Why, By: bc.By, Floor: floor}); err == nil {
if err := m.moved(ctx, b, "", bc); err != nil {
return "", err
}
}
}
_ = m.Emit("licence.adopted", map[string]any{"licence": l.Name, "from": c.Node, "replaced": held != nil})
m.Log("adopted %s from %s (%s)", l.Name, c.Node, why)
return l.Name, nil
}
// keep hands a grant to the vault at the licence's next generation, the grant first and then its access (design 39
// bus-and-switch §15), and keeps on the licence its fingerprint and expiries alone: no token once it returns.
func (m *Manager) keep(ctx context.Context, l *Licence, g FullGrant) error {
grant, _ := json.Marshal(g)
if err := m.handOver(ctx, l, string(grant), accessOf(g)); err != nil {
return err
}
l.RefreshFingerprint = Fingerprint(g.RefreshToken)
l.AccessExpiresAt = g.ExpiresAt
if g.RefreshTokenExpiresAt != nil {
l.RefreshExpiresAt = *g.RefreshTokenExpiresAt
}
l.Failures = 0
l.RotatedAt = m.Now().UnixMilli()
return nil
}
// accessOf is a subscription's `access` secret: its access token and expiry, never its refresh token (§15).
func accessOf(g FullGrant) string {
access, _ := json.Marshal(map[string]any{"accessToken": g.AccessToken, "expiresAt": g.ExpiresAt,
"refreshTokenExpiresAt": g.RefreshTokenExpiresAt, "scopes": g.Scopes,
"subscriptionType": g.SubscriptionType, "rateLimitTier": g.RateLimitTier})
return string(access)
}
// handOver puts a licence's two secrets in the vault at its next generation, the grant first (§15).
func (m *Manager) handOver(ctx context.Context, l *Licence, grant, access string) error {
generation := l.Generation + 1
for _, part := range []struct{ name, value string }{{PartGrant, grant}, {PartAccess, access}} {
if err := m.Grants.Put(ctx, KeyFor(l.Name), part.name, generation, part.value); err != nil {
return fmt.Errorf("the vault did not take the %s of %s: %w", part.name, l.Name, err)
}
}
l.Generation = generation
return nil
}
// grantOf reads a subscription's grant from the vault for one act (§16); the caller drops it when the act ends.
func (m *Manager) grantOf(ctx context.Context, l Licence) (FullGrant, error) {
var g FullGrant
plain, err := m.Grants.Get(ctx, KeyFor(l.Name), PartGrant)
if err != nil {
return g, fmt.Errorf("the grant of %s cannot be read from the vault: %w", l.Name, err)
}
return g, json.Unmarshal([]byte(plain), &g)
}
// publishAdvance gives every consumer of a licence a new generation, with the cause ctx carries, so the node says why
// it fetched again (novox/hq issue 625).
func (m *Manager) publishAdvance(ctx context.Context, l Licence, floor func(string) int64) error {
c := CauseOf(ctx)
advanced, err := m.Store.Advance(ctx, l.Name, Move{Kind: l.Kind, Why: c.Why, By: c.By, Floor: floor})
for _, b := range advanced {
m.changed(b, b.Licence)
}
return err
}
// floorIn is the floor of each consumer's generation (B11): the higher of what its node reported in reports and what
// Reported says it last applied. A worker's node is the part of its name before the first dot (ADR 0183 §19).
func (m *Manager) floorIn(reports []Holdings) func(string) int64 {
return func(consumer string) int64 {
node, _, _ := strings.Cut(consumer, ".")
var f int64
for _, r := range reports {
if r.Node == node {
f = max(f, r.Generation)
}
}
if m.Reported != nil {
f = max(f, m.Reported(node))
}
return f
}
}
// ---- keeping grants alive ----------------------------------------------------------------------------
// Due says whether a licence needs a refresh now: near expiry, or older than the cadence.
func Due(l Licence, now time.Time, s Settings) bool {
if l.Kind != "subscription" || l.Generation == 0 {
return false
}
ms := now.UnixMilli()
if l.AccessExpiresAt != 0 && l.AccessExpiresAt-ms < s.Floor.Milliseconds() {
return true
}
return l.RotatedAt == 0 || ms-l.RotatedAt >= s.Cadence.Milliseconds()
}
// Rotate refreshes one licence under its lease (design 39 §3). A second run started together finds the
// lease live and does nothing; one started just after finds a fresh grant and is not due. force refreshes
// whatever the age — the seat's `refresh` verb.
func (m *Manager) Rotate(ctx context.Context, name string, force bool) (map[string]any, error) {
key := "licence." + KeyFor(name)
ok, err := m.Store.Lease(ctx, key, m.Holder, leaseFor)
if err != nil {
return nil, err
}
if !ok {
return map[string]any{"licence": name, "refreshed": false, "reason": "another run holds its lease"}, nil
}
defer func() { _ = m.Store.Unlease(ctx, key, m.Holder) }()
l, err := m.Store.Licence(ctx, name)
if err != nil {
return nil, err
}
if l == nil {
return map[string]any{"licence": name, "refreshed": false, "reason": "no such licence"}, nil
}
if l.Kind != "subscription" || l.Generation == 0 {
return map[string]any{"licence": name, "refreshed": false, "reason": "an API key does not refresh"}, nil
}
now := m.Now()
if !force && !Due(*l, now, m.settings()) {
return map[string]any{"licence": name, "refreshed": false, "reason": "not due"}, nil
}
g, err := m.grantOf(ctx, *l)
if err != nil {
return nil, err
}
r := m.Vendor.Refresh(ctx, g)
g = FullGrant{}
if !r.OK {
l.Failures++
m.Log("%s did not refresh (%d in a row): %d %s", name, l.Failures, r.Status, r.Reason)
if err := m.Store.Audit(ctx, "failed", map[string]any{"licence": name, "status": r.Status, "reason": r.Reason, "failures": l.Failures}); err != nil {
return nil, err
}
m.notify(l, fmt.Sprintf("refresh failed %d time(s) in a row: %d", l.Failures, r.Status), l.Failures >= m.settings().FailuresToNotify)
if err := m.Store.SaveLicence(ctx, *l); err != nil {
return nil, err
}
return map[string]any{"licence": name, "refreshed": false, "reason": fmt.Sprintf("%d %s", r.Status, r.Reason), "failures": l.Failures}, nil
}
if err := m.keep(ctx, l, r.Grant); err != nil {
return nil, err
}
if l.RefreshExpiresAt != 0 {
left := time.Duration(l.RefreshExpiresAt-now.UnixMilli()) * time.Millisecond
m.notify(l, fmt.Sprintf("its refresh token expires in %.1f day(s): a person must log in again", left.Hours()/24),
left < m.settings().RefreshWarn)
}
if err := m.Store.SaveLicence(ctx, *l); err != nil {
return nil, err
}
c := CauseOf(ctx)
if force {
c.Why = "a refresh asked for: " + c.Why
} else {
c = Cause{Why: "due: older than its cadence or near its expiry", By: Itself}
if by := CauseOf(ctx); by.By != Itself {
c.By = by.By
}
}
ctx = WithCause(ctx, Cause{Why: "a rotation of " + name + " (" + c.Why + ")", By: c.By})
if err := m.record(ctx, "rotated", fmt.Sprintf("rotated %s, its token now expiring %s: %s; asked by %s", name,
time.UnixMilli(l.AccessExpiresAt).UTC().Format(time.RFC3339), c.Why, c.By),
map[string]any{"licence": name, "expiresAt": l.AccessExpiresAt, "why": c.Why, "by": c.By}); err != nil {
return nil, err
}
if err := m.publishAdvance(ctx, *l, m.floorIn(nil)); err != nil {
return nil, err
}
return map[string]any{"licence": name, "refreshed": true, "expiresAt": time.UnixMilli(l.AccessExpiresAt).UTC().Format(time.RFC3339)}, nil
}
// notify emits `licence.failing` at most once per cooldown (design 39 §3); it carries no secret.
func (m *Manager) notify(l *Licence, why string, when bool) {
if !when {
return
}
now := m.Now().UnixMilli()
if l.NotifiedAt != 0 && now-l.NotifiedAt < m.settings().Cooldown.Milliseconds() {
return
}
l.NotifiedAt = now
_ = m.Emit("licence.failing", map[string]any{"licence": l.Name, "why": why})
}
// RotateDue refreshes every licence that is due.
func (m *Manager) RotateDue(ctx context.Context) ([]map[string]any, error) {
all, err := m.Store.Licences(ctx)
if err != nil {
return nil, err
}
var out []map[string]any
for _, l := range all {
if Due(l, m.Now(), m.settings()) {
r, err := m.Rotate(ctx, l.Name, false)
if err != nil {
return out, err
}
out = append(out, r)
}
}
return out, nil
}
// ReadUsage reads every subscription's usage, on the manager's cadence (§24): each reading written, the rule applied
// after it (§26), and the conditions said (§9).
func (m *Manager) ReadUsage(ctx context.Context) error {
_, err := m.ReadUsageOf(ctx, "", false)
return err
}
// Read is one reading as licences_read_usage answers it (§38): the licence, the reading or why it failed, and what §26
// did or, with dry, would do.
type Read struct {
Licence string `json:"licence"`
Reading *UsageReading `json:"reading,omitempty"`
Error string `json:"readError,omitempty"`
Switches []Switch `json:"switches"`
}
// ReadUsageOf reads the usage of one subscription, or every one when licence is empty, as §24 does, and applies §26
// after each reading unless dry; dry answers what §26 would do (§32, §38).
func (m *Manager) ReadUsageOf(ctx context.Context, licence string, dry bool) ([]Read, error) {
if err := m.ReadSettings(ctx); err != nil {
return nil, err
}
all, err := m.Store.Licences(ctx)
if err != nil {
return nil, err
}
out := []Read{}
for _, l := range all {
if l.Kind != "subscription" || l.Generation == 0 || (licence != "" && l.Name != licence) {
continue
}
// The reading takes the licence's grant under its lease, so it never reads a grant a refresh is replacing (§4,
// §16); a grant the vault does not answer is a failed reading like a vendor's refusal (§31).
key := "licence." + KeyFor(l.Name)
leased, err := m.Store.Lease(ctx, key, m.Holder, leaseFor)
if err != nil {
return out, err
}
if !leased {
out = append(out, Read{Licence: l.Name, Error: "another run holds its lease", Switches: []Switch{}})
continue
}
unlease := func() { _ = m.Store.Unlease(ctx, key, m.Holder) }
if fresh, err := m.Store.Licence(ctx, l.Name); err == nil && fresh != nil {
l = *fresh
}
var raw map[string]any
g, err := m.grantOf(ctx, l)
if err == nil {
raw, err = m.Vendor.Usage(ctx, g.AccessToken)
}
g = FullGrant{}
if err == nil && raw == nil {
err = errors.New("the usage endpoint answered nothing")
}
if err != nil {
// A failed reading keeps the last percentages and writes the vendor's status and reason (§24).
kept := UsageReading{}
if l.Usage != nil {
kept = *l.Usage
}
kept.ReadError = err.Error()
l.Usage = &kept
err := m.Store.SaveLicence(ctx, l)
unlease()
if err != nil {
return out, err
}
m.Log("reading the usage of %s failed: %v", l.Name, err)
out = append(out, Read{Licence: l.Name, Error: kept.ReadError, Switches: []Switch{}})
continue
}
reading := FlattenUsage(raw)
if err := m.Store.RecordUsage(ctx, l.Name, m.Now().UnixMilli(), reading, raw); err != nil {
unlease()
return out, err
}
// The latest reading beside its licence, so licences answers from one key (§2).
latest := reading
latest.ReadAt = m.Now().UTC().Format(time.RFC3339Nano)
l.Usage = &latest
err = m.Store.SaveLicence(ctx, l)
unlease()
if err != nil {
return out, err
}
_ = m.Emit("usage.read", map[string]any{"licence": l.Name, "sessionPct": reading.SessionPct,
"sessionResetsAt": reading.SessionResetsAt, "weeklyPct": reading.WeeklyPct,
"weeklyResetsAt": reading.WeeklyResetsAt, "sonnetPct": reading.SonnetPct,
"sonnetResetsAt": reading.SonnetResetsAt})
// On each reading, a licence at its threshold moves every consumer bound to it (§26).
acts, err := m.ApplyRule(ctx, l.Name, false, dry)
if acts == nil {
acts = []Switch{}
}
out = append(out, Read{Licence: l.Name, Reading: &latest, Switches: acts})
if err != nil {
return out, err
}
}
// What the readings and the settings now hold is said, each condition only when what it says changed (§9).
return out, m.SayConditions(ctx, false)
}
// ---- the seat's verbs --------------------------------------------------------------------------------
// Current is which licence a consumer is bound to and its health, never the token (design 39 bus-and-switch §18, B7,
// B19): the licence, its key (the name of its secrets in the vault), kind, generation, health and accessExpiresAt, and
// for a subscription the account's identity the agent module points its account file at. Nil when it is bound to
// nothing.
func (m *Manager) Current(ctx context.Context, consumer string) (map[string]any, error) {
b, err := m.Store.Binding(ctx, consumer)
if err != nil || b == nil {
return nil, err
}
l, err := m.Store.Licence(ctx, b.Licence)
if err != nil || l == nil {
return nil, err
}
out := map[string]any{"licence": l.Name, "key": KeyFor(l.Name), "kind": l.Kind, "generation": b.Generation,
"health": Health(*l, m.Now(), m.settings())}
if l.AccessExpiresAt != 0 {
out["accessExpiresAt"] = time.UnixMilli(l.AccessExpiresAt).UTC().Format(time.RFC3339)
}
if l.AccountUUID != "" {
out["identity"] = Identity{AccountUUID: l.AccountUUID, EmailAddress: l.Email, OrganizationUUID: l.OrganizationUUID}
}
return out, nil
}
// Health is a licence's health (B19): `failing` once its refresh failed failures_to_notify times in a row, `expiring`
// when its refresh token expires within refresh_warn_days, else `ok`.
func Health(l Licence, now time.Time, s Settings) string {
switch {
case l.Failures >= s.FailuresToNotify:
return "failing"
case l.RefreshExpiresAt != 0 && l.RefreshExpiresAt-now.UnixMilli() < s.RefreshWarn.Milliseconds():
return "expiring"
}
return "ok"
}
// BindingsOf is every binding, or the one of consumer when one is named (B10): a list either way, unpaged, each with
// its licence's key beside the §3 fields (B25).
func (m *Manager) BindingsOf(ctx context.Context, consumer string) ([]Binding, error) {
all, err := m.Store.Bindings(ctx)
if err != nil {
return nil, err
}
out := []Binding{}
for _, b := range all {
if consumer == "" || b.Consumer == consumer {
b.Key = KeyFor(b.Licence)
out = append(out, b)
}
}
return out, nil
}
// Bind binds or switches a consumer: a person's act (ADR 0183), told to the consumer as a new generation, and
// recorded with the cause ctx carries — the verb and its proven caller (novox/hq issue 625).
func (m *Manager) Bind(ctx context.Context, consumer, licence string) (map[string]any, error) {
l, err := m.Store.Licence(ctx, licence)
if err != nil {
return nil, err
}
if l == nil {
return nil, fmt.Errorf("there is no licence %s; `licences` lists them", licence)
}
before, err := m.Store.Binding(ctx, consumer)
if err != nil {
return nil, err
}
c := CauseOf(ctx)
b, err := m.Store.Bind(ctx, consumer, licence, Move{Kind: l.Kind, Why: c.Why, By: c.By, Floor: m.floorIn(nil)})
if err != nil {
return nil, err
}
from := ""
if before != nil {
from = before.Licence
}
if err := m.moved(ctx, b, from, c); err != nil {
return nil, err
}
if from != "" && from != licence {
_ = m.Emit("licence.switched", switchedFields(from, licence, []string{consumer}, c)) // one per act (§27, §36)
}
return map[string]any{"consumer": consumer, "licence": licence, "generation": b.Generation, "from": from}, nil
}
// Release unbinds a consumer; its node keeps its last token, which expires within hours.
func (m *Manager) Release(ctx context.Context, consumer string) (map[string]any, error) {
was, err := m.Store.Binding(ctx, consumer)
if err != nil {
return nil, err
}
if ok, err := m.Store.Unbind(ctx, consumer); err != nil || !ok {
return map[string]any{"consumer": consumer, "released": false, "reason": "it was bound to nothing"}, err
}
c := CauseOf(ctx)
if err := m.record(ctx, "released", fmt.Sprintf("released %s from %s: %s; asked by %s", consumer, was.Licence, c.Why, c.By),
map[string]any{"consumer": consumer, "licence": was.Licence, "generation": was.Generation, "why": c.Why, "by": c.By}); err != nil {
return nil, err
}
m.changed(Binding{Consumer: consumer, Generation: was.Generation, Why: c.Why, By: c.By}, was.Licence)
return map[string]any{"consumer": consumer, "released": true, "was": was.Licence}, nil
}
var licenceName = regexp.MustCompile(`^[A-Za-z0-9@._-]+$`)
// apiKeyShape is what an Anthropic API key looks like. A key of another shape is refused before it is kept,
// so a file that is not a key (a credential, a private key) is never adopted as one (novox/hq ADR 0266).
var apiKeyShape = regexp.MustCompile(`^sk-ant-[A-Za-z0-9_-]{20,300}$`)
// AdoptKey adopts an API key from a file on this node — never an argument (design 39 §6).
func (m *Manager) AdoptKey(ctx context.Context, name, file string) (map[string]any, error) {
raw, err := os.ReadFile(file)
if err != nil {
return nil, err
}
return m.adoptKey(ctx, name, strings.TrimSpace(string(raw)), "a file on "+m.Holder)
}
// AdoptSealedKey adopts an API key sealed to this module's public key by a node's agent module (ADR 0209):
// the key crosses the bus only sealed, on request/reply.
func (m *Manager) AdoptSealedKey(ctx context.Context, name string, box SealedBox, from string) (map[string]any, error) {
key, err := Open(box, m.Keys.PrivateKey)
if err != nil {
return nil, err
}
return m.adoptKey(ctx, name, strings.TrimSpace(key), from)
}
func (m *Manager) adoptKey(ctx context.Context, name, key, from string) (map[string]any, error) {
if !licenceName.MatchString(name) {
return nil, fmt.Errorf("%q is not a licence name: letters, digits and @._-", name)
}
if key == "" {
return nil, errors.New("the key is empty")
}
if !apiKeyShape.MatchString(key) {
// Never the content: what was handed may be a secret of another kind.
return nil, errors.New("what was handed over is not an Anthropic API key (sk-ant-…): nothing adopted")
}
held, err := m.Store.Licence(ctx, name)
if err != nil {
return nil, err
}
if held != nil && held.Kind != "api-key" {
return nil, fmt.Errorf("%s is a subscription; an API key needs a name of its own", name)
}
taken, err := m.keyTaken(ctx, name)
if err != nil {
return nil, err
}
if taken != "" {
why := "its key " + KeyFor(name) + " is the key of the licence " + taken
if err := m.Store.Audit(ctx, "refused", map[string]any{"licence": name, "kind": "api-key", "from": from, "reason": why}); err != nil {
return nil, err
}
_ = m.Emit("licence.refused", map[string]any{"licence": name, "from": from, "reason": why})
return nil, fmt.Errorf("%s is refused: %s", name, why)
}
l := Licence{Name: name, Kind: "api-key", AdoptedAt: m.Now().UnixMilli(), AdoptedFrom: from}
if held != nil {
l.Generation, l.Usage = held.Generation, held.Usage
}
if err := m.handOver(ctx, &l, key, key); err != nil {
return nil, err
}
if err := m.Store.SaveLicence(ctx, l); err != nil {
return nil, err
}
c := CauseOf(ctx)
if err := m.Store.Audit(ctx, "adopted", map[string]any{"licence": name, "kind": "api-key", "from": from, "why": c.Why, "by": c.By}); err != nil {
return nil, err
}
m.Log("adopted the API key %s from %s; asked by %s", name, from, c.By)
ctx = WithCause(ctx, Cause{Why: "the API key " + name + " adopted from " + from, By: c.By})
if err := m.publishAdvance(ctx, l, m.floorIn(nil)); err != nil {
return nil, err
}
_ = m.Emit("licence.adopted", map[string]any{"licence": name, "from": from, "replaced": held != nil})
return map[string]any{"licence": name, "kind": "api-key", "adopted": true, "fingerprint": Fingerprint(key)}, nil
}
// Licences is each licence as a person reads it (design 39 bus-and-switch §34, §47, B19, B40): today's fields, its
// latest usage with its reset times, its room or why it is not known, whether it is the target a full licence's
// consumers move to next, when and from where it was adopted, and the conditions open about it. Never a token.
func (m *Manager) Licences(ctx context.Context) ([]map[string]any, error) {
all, err := m.Store.Licences(ctx)
if err != nil {
return nil, err
}
bindings, err := m.Store.Bindings(ctx)
if err != nil {
return nil, err
}
open, err := KeptConditions(m.Conditions)
if err != nil {
return nil, err
}
s := m.settings()
stamp := func(ms int64) any {
if ms == 0 {
return nil
}
return time.UnixMilli(ms).UTC().Format(time.RFC3339)
}
targets := map[string]bool{}
for _, l := range all {
if len(consumersOf(l.Name, bindings)) > 0 {
if to, _ := m.targetFor(l, all, s); to != "" {
targets[to] = true
}
}
}
out := []map[string]any{}
for _, l := range all {
bound := consumersOf(l.Name, bindings)
if bound == nil {
bound = []string{}
}
account := l.Email
if account == "" {
account = l.AccountUUID
}
var room any = m.roomWords(l, s)
if r, known := Room(l, m.Now(), s); known {
room = r
}
conditions := []map[string]any{}
prefix := "licence." + KeyFor(l.Name) + "."
for _, c := range open {
if strings.HasPrefix(c.Body.Key, prefix) {
conditions = append(conditions, map[string]any{"key": c.Body.Key, "severity": c.Body.Severity,
"headline": c.Body.Headline, "since": c.Since})
}
}
out = append(out, map[string]any{"name": l.Name, "kind": l.Kind, "account": account,
"organisation": l.OrganizationUUID, "accessExpiresAt": stamp(l.AccessExpiresAt),
"refreshExpiresAt": stamp(l.RefreshExpiresAt), "rotatedAt": stamp(l.RotatedAt), "failures": l.Failures,
"bound": bound, "usage": l.Usage, "room": room, "target": targets[l.Name], "adoptedAt": stamp(l.AdoptedAt),
"adoptedFrom": l.AdoptedFrom, "conditions": conditions})
}
return out, nil
}