claude-licence-manager holds the anthropic-licence-manager seat: it reads every node's holdings state, adopts a login it does not hold by refreshing it (newest first, once per account), keeps each grant alive under a lease, publishes what each consumer should hold as its bindings state with a generation, and answers current sealed to the consumer's key. Postgres store prepared by a run-once step; grants encrypted with the vault's key. claude-code reports what its node holds (fingerprints and account, never a token), hands its grant over only when the manager asks, watches its binding and fetches the token on a newer generation, and writes access-token-only. Its ask now reads the runtime's answer as a value and addresses seats as seats.
264 lines
7.7 KiB
Go
264 lines
7.7 KiB
Go
package main
|
|
|
|
// The manager's store (novox/hq ADR 0183, design 39 §1): licences with their grants encrypted, bindings
|
|
// with a generation, what became of each login it was offered, usage readings, and the audit. One
|
|
// interface, two implementations — postgres for the mesh (pgstore.go), memory for the tests — so every
|
|
// rule is tested without a database, and the database is asked only to keep rows.
|
|
|
|
import (
|
|
"context"
|
|
"sort"
|
|
"sync"
|
|
"time"
|
|
)
|
|
|
|
// Licence is one licence: an account, or an API key.
|
|
type Licence struct {
|
|
Name string `json:"name"`
|
|
Kind string `json:"kind"` // subscription | api-key
|
|
AccountUUID string `json:"accountUuid,omitempty"`
|
|
Email string `json:"email,omitempty"`
|
|
OrganizationUUID string `json:"organizationUuid,omitempty"`
|
|
Sealed string `json:"-"` // the grant or the key, encrypted at rest
|
|
RefreshFingerprint string `json:"-"`
|
|
AccessExpiresAt int64 `json:"accessExpiresAt,omitempty"`
|
|
RefreshExpiresAt int64 `json:"refreshExpiresAt,omitempty"`
|
|
Failures int `json:"failures"`
|
|
NotifiedAt int64 `json:"-"`
|
|
AdoptedAt int64 `json:"adoptedAt"`
|
|
RotatedAt int64 `json:"rotatedAt,omitempty"`
|
|
}
|
|
|
|
// Binding is one consumer's binding and the generation it was last given (ADR 0206).
|
|
type Binding struct {
|
|
Consumer string `json:"consumer"`
|
|
Licence string `json:"licence"`
|
|
Generation int64 `json:"generation"`
|
|
}
|
|
|
|
// Outcome is what became of a login the manager was offered, by its refresh token's fingerprint.
|
|
type Outcome string
|
|
|
|
const (
|
|
Adopted Outcome = "adopted"
|
|
Dead Outcome = "dead"
|
|
Skipped Outcome = "skipped"
|
|
Refused Outcome = "refused"
|
|
Gone Outcome = "gone"
|
|
)
|
|
|
|
// UsageRow is one usage reading.
|
|
type UsageRow struct {
|
|
Licence string `json:"licence"`
|
|
At int64 `json:"at"`
|
|
Reading UsageReading `json:"reading"`
|
|
}
|
|
|
|
// Store is what the manager keeps.
|
|
type Store interface {
|
|
Licences(ctx context.Context) ([]Licence, error)
|
|
Licence(ctx context.Context, name string) (*Licence, error)
|
|
LicenceForAccount(ctx context.Context, account string) (*Licence, error)
|
|
SaveLicence(ctx context.Context, l Licence) error
|
|
// Lease takes a lease for d, or answers false while another holder's is live.
|
|
Lease(ctx context.Context, key, holder string, d time.Duration) (bool, error)
|
|
Unlease(ctx context.Context, key, holder string) error
|
|
Bindings(ctx context.Context) ([]Binding, error)
|
|
Binding(ctx context.Context, consumer string) (*Binding, error)
|
|
// Bind binds (or switches) a consumer at the next generation.
|
|
Bind(ctx context.Context, consumer, licence string) (Binding, error)
|
|
Unbind(ctx context.Context, consumer string) (bool, error)
|
|
// Advance gives every consumer of a licence a new generation: what a rotation is to them.
|
|
Advance(ctx context.Context, licence string) ([]Binding, error)
|
|
Outcome(ctx context.Context, fingerprint string) (Outcome, error)
|
|
RecordOutcome(ctx context.Context, fingerprint, node, account string, o Outcome, why string) error
|
|
RecordUsage(ctx context.Context, licence string, at int64, r UsageReading, raw map[string]any) error
|
|
Usage(ctx context.Context, licence string, limit int) ([]UsageRow, error)
|
|
Audit(ctx context.Context, what string, detail map[string]any) error
|
|
Close()
|
|
}
|
|
|
|
// MemoryStore is the tests' store.
|
|
type MemoryStore struct {
|
|
mu sync.Mutex
|
|
now func() time.Time
|
|
rows map[string]Licence
|
|
binds map[string]Binding
|
|
leases map[string]struct {
|
|
holder string
|
|
until time.Time
|
|
}
|
|
outcomes map[string]Outcome
|
|
usage []UsageRow
|
|
Audits []map[string]any
|
|
generation int64
|
|
}
|
|
|
|
// NewMemoryStore is an empty store whose leases age by now.
|
|
func NewMemoryStore(now func() time.Time) *MemoryStore {
|
|
return &MemoryStore{now: now, rows: map[string]Licence{}, binds: map[string]Binding{},
|
|
leases: map[string]struct {
|
|
holder string
|
|
until time.Time
|
|
}{}, outcomes: map[string]Outcome{}}
|
|
}
|
|
|
|
func (m *MemoryStore) Licences(context.Context) ([]Licence, error) {
|
|
m.mu.Lock()
|
|
defer m.mu.Unlock()
|
|
out := make([]Licence, 0, len(m.rows))
|
|
for _, l := range m.rows {
|
|
out = append(out, l)
|
|
}
|
|
sort.Slice(out, func(i, j int) bool { return out[i].Name < out[j].Name })
|
|
return out, nil
|
|
}
|
|
|
|
func (m *MemoryStore) Licence(_ context.Context, name string) (*Licence, error) {
|
|
m.mu.Lock()
|
|
defer m.mu.Unlock()
|
|
if l, ok := m.rows[name]; ok {
|
|
return &l, nil
|
|
}
|
|
return nil, nil
|
|
}
|
|
|
|
func (m *MemoryStore) LicenceForAccount(_ context.Context, account string) (*Licence, error) {
|
|
m.mu.Lock()
|
|
defer m.mu.Unlock()
|
|
for _, l := range m.rows {
|
|
if l.AccountUUID == account {
|
|
return &l, nil
|
|
}
|
|
}
|
|
return nil, nil
|
|
}
|
|
|
|
func (m *MemoryStore) SaveLicence(_ context.Context, l Licence) error {
|
|
m.mu.Lock()
|
|
defer m.mu.Unlock()
|
|
m.rows[l.Name] = l
|
|
return nil
|
|
}
|
|
|
|
func (m *MemoryStore) Lease(_ context.Context, key, holder string, d time.Duration) (bool, error) {
|
|
m.mu.Lock()
|
|
defer m.mu.Unlock()
|
|
if held, ok := m.leases[key]; ok && held.until.After(m.now()) && held.holder != holder {
|
|
return false, nil
|
|
}
|
|
m.leases[key] = struct {
|
|
holder string
|
|
until time.Time
|
|
}{holder, m.now().Add(d)}
|
|
return true, nil
|
|
}
|
|
|
|
func (m *MemoryStore) Unlease(_ context.Context, key, holder string) error {
|
|
m.mu.Lock()
|
|
defer m.mu.Unlock()
|
|
if m.leases[key].holder == holder {
|
|
delete(m.leases, key)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (m *MemoryStore) Bindings(context.Context) ([]Binding, error) {
|
|
m.mu.Lock()
|
|
defer m.mu.Unlock()
|
|
out := make([]Binding, 0, len(m.binds))
|
|
for _, b := range m.binds {
|
|
out = append(out, b)
|
|
}
|
|
sort.Slice(out, func(i, j int) bool { return out[i].Consumer < out[j].Consumer })
|
|
return out, nil
|
|
}
|
|
|
|
func (m *MemoryStore) Binding(_ context.Context, consumer string) (*Binding, error) {
|
|
m.mu.Lock()
|
|
defer m.mu.Unlock()
|
|
if b, ok := m.binds[consumer]; ok {
|
|
return &b, nil
|
|
}
|
|
return nil, nil
|
|
}
|
|
|
|
func (m *MemoryStore) Bind(_ context.Context, consumer, licence string) (Binding, error) {
|
|
m.mu.Lock()
|
|
defer m.mu.Unlock()
|
|
m.generation++
|
|
b := Binding{Consumer: consumer, Licence: licence, Generation: m.generation}
|
|
m.binds[consumer] = b
|
|
return b, nil
|
|
}
|
|
|
|
func (m *MemoryStore) Unbind(_ context.Context, consumer string) (bool, error) {
|
|
m.mu.Lock()
|
|
defer m.mu.Unlock()
|
|
_, ok := m.binds[consumer]
|
|
delete(m.binds, consumer)
|
|
return ok, nil
|
|
}
|
|
|
|
func (m *MemoryStore) Advance(_ context.Context, licence string) ([]Binding, error) {
|
|
m.mu.Lock()
|
|
defer m.mu.Unlock()
|
|
var out []Binding
|
|
for c, b := range m.binds {
|
|
if b.Licence != licence {
|
|
continue
|
|
}
|
|
m.generation++
|
|
b.Generation = m.generation
|
|
m.binds[c] = b
|
|
out = append(out, b)
|
|
}
|
|
sort.Slice(out, func(i, j int) bool { return out[i].Consumer < out[j].Consumer })
|
|
return out, nil
|
|
}
|
|
|
|
func (m *MemoryStore) Outcome(_ context.Context, fp string) (Outcome, error) {
|
|
m.mu.Lock()
|
|
defer m.mu.Unlock()
|
|
return m.outcomes[fp], nil
|
|
}
|
|
|
|
func (m *MemoryStore) RecordOutcome(_ context.Context, fp, _, _ string, o Outcome, _ string) error {
|
|
m.mu.Lock()
|
|
defer m.mu.Unlock()
|
|
m.outcomes[fp] = o
|
|
return nil
|
|
}
|
|
|
|
func (m *MemoryStore) RecordUsage(_ context.Context, licence string, at int64, r UsageReading, _ map[string]any) error {
|
|
m.mu.Lock()
|
|
defer m.mu.Unlock()
|
|
m.usage = append(m.usage, UsageRow{Licence: licence, At: at, Reading: r})
|
|
return nil
|
|
}
|
|
|
|
func (m *MemoryStore) Usage(_ context.Context, licence string, limit int) ([]UsageRow, error) {
|
|
m.mu.Lock()
|
|
defer m.mu.Unlock()
|
|
var out []UsageRow
|
|
for i := len(m.usage) - 1; i >= 0 && len(out) < limit; i-- {
|
|
if licence == "" || m.usage[i].Licence == licence {
|
|
out = append(out, m.usage[i])
|
|
}
|
|
}
|
|
return out, nil
|
|
}
|
|
|
|
func (m *MemoryStore) Audit(_ context.Context, what string, detail map[string]any) error {
|
|
m.mu.Lock()
|
|
defer m.mu.Unlock()
|
|
d := map[string]any{"what": what}
|
|
for k, v := range detail {
|
|
d[k] = v
|
|
}
|
|
m.Audits = append(m.Audits, d)
|
|
return nil
|
|
}
|
|
|
|
func (m *MemoryStore) Close() {}
|