Files
mesh-controller/internal/licences/licences.go
T
jschoubben 2e33c5e80e model access B: refreshable-grant machinery — manager, at-rest refresh token, refresh flow
The ADR 0050 carve-out, built generic and vendor-neutral. A refreshable-grant
licence records one manager node; that node holds the refresh token encrypted at
rest, access tokens are still sealed per holder, and the refresh token is never in
a holder's delivery. Bounded on the three stated axes: refreshable-grant vendors
only, the refresh token only, the manager node only. Anthropic's actual OAuth
refresh stays a Phase-C plug-in behind a clean seam.

- New at-rest crypto (secrets.SealAtRest/OpenAtRest): envelope encryption distinct
  from the per-holder anonymous-box seal. The refresh token is under a symmetric
  data key (secretbox); the data key is wrapped to the manager node's public
  sealing key. The database alone holds ciphertext and a wrapped key with no
  private half to open either — only the manager node reads it back.

- Refreshable-grant adapter dispatch: anthropic is now refreshable-grant,
  anthropic-api-key the static-key second case. The adapter implements the
  Refresher seam by delegating to an injected VendorRefresher (the Phase-C plug,
  none shipped). static-key is untouched. The type assertion to Refresher is what
  gates the carve-out to refreshable-grant vendors.

- Refresh lease/rotate/publish flow (Licences.Refresh): a transaction-scoped
  advisory lock is the single-refresher lease; the new access token comes from the
  vendor refresh, is sealed per holder (secrets.Seal, as Accept does) and delivered
  on the next push — doc 13's reseal-and-publish half, all-or-nothing. The refresh
  token stays put, re-encrypted at rest only if the vendor rotated it.

- Manager and refresh_grant schema: consolidated into migrations/0001 and carried
  by a new incremental 0003 (the dual-write rule).

- 17 new tests, including the four security checks: KeyFor never carries the
  refresh token, a static key has no manager and cannot be refreshed, the at-rest
  token needs the manager's key, and a refresh delivers a new sealed access token.

Claude-Session: https://claude.ai/code/session_01LrgweAeERJYBg88c5cKDzF
2026-09-07 00:23:35 +02:00

568 lines
20 KiB
Go

// Package licences is the context that holds which model access exists and who may use it.
//
// novox/hq ADR 0024. It is the first provision answered by a **record rather than a node**: a
// hosted model is on nobody's machine, is reached over the public internet, and the rule that
// refuses two ends sharing no private network must not apply to it.
//
// It owns its store exclusively (novox/hq ADR 0008): a database called `licences`, reached with a
// credential no other context holds — including `inventory`, in the same process. It refers to
// nodes by name, which is what crossing a context boundary is allowed to carry.
package licences
import (
"context"
"embed"
"encoding/json"
"errors"
"fmt"
"sort"
"strings"
"time"
"github.com/jackc/pgx/v5"
"github.com/novox/mesh-control/internal/licences/adapters"
"github.com/novox/mesh-control/internal/secrets"
"github.com/novox/mesh-control/internal/store"
)
// Name is what this context is called: its database and its credential are named after it.
const Name = "licences"
// Provision is what a module requires in order to be given one.
//
// One name for all of them, because *which* licence is the operator's choice per consumer rather
// than something a module asks for — a module that required `anthropic` by name could never be
// moved onto a mesh-hosted model without editing it.
const Provision = "model-access"
//go:embed migrations/*.sql
var files embed.FS
// Migrations are this context's schema changes, in order.
func Migrations() ([]store.Migration, error) {
return store.LoadMigrations(files, "migrations")
}
// Licences is this context, holding the store it exclusively owns.
type Licences struct{ store *store.Store }
// Open connects to the licence store.
func Open(ctx context.Context) (*Licences, error) {
s, err := store.Open(ctx, Name)
if err != nil {
return nil, err
}
return &Licences{store: s}, nil
}
func (l *Licences) Close() { l.store.Close() }
// Ready waits for the database to answer.
func (l *Licences) Ready(ctx context.Context, within time.Duration) error {
return l.store.Ready(ctx, within)
}
// A Licence is one way to reach a model, under the name a person calls it.
type Licence struct {
Name string
// Vendor is which company sells this licence, and selects the adapter that runs its lifecycle
// (novox/hq ADR 0050). Named `vendor`, not `provider`: the inventory already uses "provider"
// for *which node answers a brokered provision*, and one word must not carry two unrelated
// facts.
Vendor string
Serves map[string]any
Added time.Time
}
// A Holder is one consumer using a licence, and whether it has been given the key.
type Holder struct {
Licence string
Node string
Module string
// Sealed is empty when no key has been supplied since this holder was recorded.
Sealed string
}
// Add records a licence under the operator's own name for it.
func (l *Licences) Add(ctx context.Context, name, vendor string, serves map[string]any) error {
if strings.TrimSpace(name) == "" || strings.TrimSpace(vendor) == "" {
return errors.New("a licence needs a name and a vendor")
}
if serves == nil {
serves = map[string]any{}
}
body, err := json.Marshal(serves)
if err != nil {
return err
}
_, err = l.store.Pool().Exec(ctx,
`insert into licence (name, vendor, serves) values ($1, $2, $3)
on conflict (name) do update set vendor = excluded.vendor, serves = excluded.serves`,
name, vendor, body)
return err
}
// All is every licence this mesh knows about.
func (l *Licences) All(ctx context.Context) ([]Licence, error) {
rows, err := l.store.Pool().Query(ctx,
`select name, vendor, serves, added_at from licence order by name`)
if err != nil {
return nil, err
}
defer rows.Close()
var out []Licence
for rows.Next() {
var one Licence
var body []byte
if err := rows.Scan(&one.Name, &one.Vendor, &body, &one.Added); err != nil {
return nil, err
}
if err := json.Unmarshal(body, &one.Serves); err != nil {
return nil, err
}
out = append(out, one)
}
return out, rows.Err()
}
// Forget removes a licence, and with it every record of who held it.
//
// **A licence outliving its holder is a live credential nobody is watching** (ADR 0024). This is
// the other direction and has the same shape: what the mesh no longer grants, it stops naming.
// The key itself is not the mesh's to revoke — that is done where the licence was bought, and
// saying so is more use than pretending otherwise.
func (l *Licences) Forget(ctx context.Context, name string) error {
tag, err := l.store.Pool().Exec(ctx, `delete from licence where name = $1`, name)
if err != nil {
return err
}
if tag.RowsAffected() == 0 {
return fmt.Errorf("this mesh has no licence called %q", name)
}
return nil
}
// Use records that a consumer holds a licence.
//
// Recorded before any key exists, deliberately. Who uses what is a decision; the key is a value
// somebody supplies afterwards, and often by a different person.
func (l *Licences) Use(ctx context.Context, licence, node, module string) error {
_, err := l.store.Pool().Exec(ctx,
`insert into licence_holder (licence, node, module) values ($1, $2, $3)
on conflict (licence, node, module) do nothing`, licence, node, module)
if err != nil && strings.Contains(err.Error(), "licence_holder_licence_fkey") {
return fmt.Errorf("this mesh has no licence called %q", licence)
}
return err
}
// StopUsing takes a consumer off a licence, and its sealed key with it.
func (l *Licences) StopUsing(ctx context.Context, licence, node, module string) error {
_, err := l.store.Pool().Exec(ctx,
`delete from licence_holder where licence = $1 and node = $2 and module = $3`,
licence, node, module)
return err
}
// HoldersOf is every consumer using a licence.
func (l *Licences) HoldersOf(ctx context.Context, licence string) ([]Holder, error) {
rows, err := l.store.Pool().Query(ctx,
`select licence, node, module, coalesce(sealed, '') from licence_holder
where licence = $1 order by node, module`, licence)
if err != nil {
return nil, err
}
defer rows.Close()
var out []Holder
for rows.Next() {
var h Holder
if err := rows.Scan(&h.Licence, &h.Node, &h.Module, &h.Sealed); err != nil {
return nil, err
}
out = append(out, h)
}
return out, rows.Err()
}
// Chosen is the licence a consumer was put on, empty if it was put on none.
func (l *Licences) Chosen(ctx context.Context, node, module string) (string, error) {
var name string
err := l.store.Pool().QueryRow(ctx,
`select licence from licence_holder where node = $1 and module = $2`, node, module).
Scan(&name)
if errors.Is(err, pgx.ErrNoRows) {
return "", nil
}
return name, err
}
// KeyFor is the sealed key for one holder, empty if none has been supplied since it was recorded.
//
// What is stored is what is delivered, routed through the vendor's adapter so a refreshable-grant
// vendor can strip its refresh token here in Phase B (novox/hq ADR 0050). For a static-key vendor
// that step is the identity — the sealed blob is what the holder receives — so this is unchanged
// for today's vendors. An unregistered vendor is not consulted: a static-key blob delivers as it is,
// and a licence whose key was accepted at all necessarily had a registered adapter.
func (l *Licences) KeyFor(ctx context.Context, licence, node, module string) (string, error) {
var sealed *string
err := l.store.Pool().QueryRow(ctx,
`select sealed from licence_holder where licence = $1 and node = $2 and module = $3`,
licence, node, module).Scan(&sealed)
if errors.Is(err, pgx.ErrNoRows) || sealed == nil {
return "", nil
}
if err != nil {
return "", err
}
if adapter, err := l.adapterFor(ctx, licence); err == nil {
return adapter.Deliver(*sealed), nil
}
return *sealed, nil
}
// vendorOf reads a licence's vendor, the field that selects its adapter.
func (l *Licences) vendorOf(ctx context.Context, licence string) (string, error) {
var vendor string
err := l.store.Pool().QueryRow(ctx,
`select vendor from licence where name = $1`, licence).Scan(&vendor)
if errors.Is(err, pgx.ErrNoRows) {
return "", fmt.Errorf("this mesh has no licence called %q", licence)
}
return vendor, err
}
// adapterFor is the adapter a licence's vendor selects (novox/hq ADR 0050).
func (l *Licences) adapterFor(ctx context.Context, licence string) (adapters.Adapter, error) {
vendor, err := l.vendorOf(ctx, licence)
if err != nil {
return nil, err
}
return adapters.For(vendor)
}
// SealingKeys is what Accept needs: each holder's node and the key to seal to it.
type SealingKeys func(node string) (string, error)
// Accept takes a key somebody supplied, seals it to every holder, and discards the plaintext.
//
// **The missing verb** (ADR 0024). Every credential the mesh handles otherwise it generated
// itself; an API key arrives from a person, and a mesh that kept operator-supplied keys readably
// is the arrangement this project measured and rejected.
//
// **It seals to the holders that exist now.** A holder recorded afterwards has no key, and the
// mesh cannot make one — it discarded the only copy. That is reported rather than hidden: the
// remedy is to supply the key again, which is a thing a person can do, and delivering nothing
// while reporting success is not.
func (l *Licences) Accept(ctx context.Context, licence, value string, keys SealingKeys) (int, error) {
if strings.TrimSpace(value) == "" {
return 0, errors.New("an empty key is not a key")
}
// The vendor selects the adapter that seals it (novox/hq ADR 0050). A static-key vendor's accept
// is the generic seal; the dispatch is what lets a refreshable-grant vendor do otherwise in
// Phase B without this layer changing. An unknown vendor is refused here, before any key is
// touched.
adapter, err := l.adapterFor(ctx, licence)
if err != nil {
return 0, err
}
holders, err := l.HoldersOf(ctx, licence)
if err != nil {
return 0, err
}
if len(holders) == 0 {
// Refused rather than stored for later, because storing it for later means storing it
// readably — which is the whole thing this refuses to do.
return 0, fmt.Errorf(
"nothing uses %q yet, and the mesh does not keep a key it cannot seal to somebody. "+
"Put a consumer on it first, then supply the key", licence)
}
sealed := 0
for _, h := range holders {
key, err := keys(h.Node)
if err != nil {
return sealed, err
}
if key == "" {
return sealed, fmt.Errorf(
"%s has no sealing key, so nothing can be sealed to it — it joins again to get one",
h.Node)
}
made, err := adapter.Accept(value, key, key)
if err != nil {
return sealed, err
}
if _, err := l.store.Pool().Exec(ctx,
`update licence_holder set sealed = $4, node_key = $5
where licence = $1 and node = $2 and module = $3`,
h.Licence, h.Node, h.Module, made.ForConsumer, key); err != nil {
return sealed, err
}
sealed++
}
return sealed, nil
}
// ManagerOf is the node that holds a licence's refresh token readably, empty if none is named.
//
// Empty for every static-key licence, which has nothing to refresh, and for a refreshable-grant one
// before its manager is set (novox/hq ADR 0050).
func (l *Licences) ManagerOf(ctx context.Context, licence string) (string, error) {
var manager *string
err := l.store.Pool().QueryRow(ctx,
`select manager from licence where name = $1`, licence).Scan(&manager)
if errors.Is(err, pgx.ErrNoRows) {
return "", fmt.Errorf("this mesh has no licence called %q", licence)
}
if err != nil {
return "", err
}
if manager == nil {
return "", nil
}
return *manager, nil
}
// SetManager names the one node that holds a licence's refresh token and refreshes it centrally.
//
// **Only a refreshable-grant licence has one.** A static-key licence has no refresh token, so naming
// a manager for it is refused rather than kept — the absent manager is part of what keeps a static
// key from ever growing a value something holds readably at rest (novox/hq ADR 0050). The bound
// "the manager node only" starts here, at the one place a manager is written.
func (l *Licences) SetManager(ctx context.Context, licence, node string) error {
if strings.TrimSpace(node) == "" {
return errors.New("a manager needs a node")
}
vendor, err := l.vendorOf(ctx, licence)
if err != nil {
return err
}
adapter, err := adapters.For(vendor)
if err != nil {
return err
}
if adapter.Shape() != adapters.RefreshableGrant {
return fmt.Errorf(
"%q is a %s licence; only a refreshable-grant licence has a manager, because only it "+
"has a refresh token to hold", licence, adapter.Shape())
}
tag, err := l.store.Pool().Exec(ctx,
`update licence set manager = $2 where name = $1`, licence, node)
if err != nil {
return err
}
if tag.RowsAffected() == 0 {
return fmt.Errorf("this mesh has no licence called %q", licence)
}
return nil
}
// SetRefreshGrant stores, or replaces, a licence's refresh token as its at-rest envelope.
//
// **The envelope is opaque here.** It was produced by the manager node — the only place the refresh
// token is ever in the clear (novox/hq ADR 0050, Phase C) — and this context keeps it and forwards
// it to a refresh without opening it. The control plane holds no key that could, which is the whole
// point of where the carve-out draws the line.
func (l *Licences) SetRefreshGrant(ctx context.Context, licence string, at secrets.AtRest) error {
if at.Token == "" || at.WrappedKey == "" || at.ManagerKey == "" {
return errors.New("an incomplete refresh-token envelope is not one to keep")
}
_, err := l.store.Pool().Exec(ctx,
`insert into refresh_grant (licence, token, wrapped_key, manager_key)
values ($1, $2, $3, $4)
on conflict (licence) do update set
token = excluded.token, wrapped_key = excluded.wrapped_key,
manager_key = excluded.manager_key, updated_at = now()`,
licence, at.Token, at.WrappedKey, at.ManagerKey)
if err != nil && strings.Contains(err.Error(), "refresh_grant_licence_fkey") {
return fmt.Errorf("this mesh has no licence called %q", licence)
}
return err
}
// RefreshGrant is a licence's refresh token as its at-rest envelope, and whether one is stored.
func (l *Licences) RefreshGrant(ctx context.Context, licence string) (secrets.AtRest, bool, error) {
var at secrets.AtRest
err := l.store.Pool().QueryRow(ctx,
`select token, wrapped_key, manager_key from refresh_grant where licence = $1`, licence).
Scan(&at.Token, &at.WrappedKey, &at.ManagerKey)
if errors.Is(err, pgx.ErrNoRows) {
return secrets.AtRest{}, false, nil
}
if err != nil {
return secrets.AtRest{}, false, err
}
return at, true, nil
}
// Refresh mints a new access token for a refreshable-grant licence, seals it to every holder, and
// leaves the refresh token where it is — re-encrypted at rest if the vendor rotated it too.
//
// **The lease.** One refresh of a licence at a time, held as a transaction-scoped advisory lock on
// the licence: two refreshes serialise rather than both minting a token and racing to publish. This
// is doc 13's single-actor rotation lease, expressed against the database that is the source of
// truth (novox/hq ADR 0003) rather than reinvented.
//
// **It reuses rotation's reseal-and-publish, not its value source.** doc 13's rotate discards a
// mesh-minted secret and regenerates it; here the new access token comes from the vendor refresh
// instead, and is then sealed per holder (secrets.Seal, exactly as Accept does) and delivered on the
// next push — the same publish path any credential change takes. The refresh token is never sealed
// to a holder, so `KeyFor` cannot deliver it.
//
// **All or nothing.** The reseal, the grant replacement and the lease are one transaction: a refresh
// that cannot finish leaves every holder on the token it had and the stored grant untouched — a
// licence that has not refreshed, which is far better than one half refreshed (doc 13).
//
// The vendor refresh itself is the injected VendorRefresher (novox/hq ADR 0050, Phase C); with none
// plugged in, the adapter refuses here and nothing is changed.
func (l *Licences) Refresh(ctx context.Context, licence string, keys SealingKeys) (int, error) {
tx, err := l.store.Pool().Begin(ctx)
if err != nil {
return 0, err
}
defer func() { _ = tx.Rollback(context.WithoutCancel(ctx)) }()
// The lease. Released when the transaction ends, either way.
if _, err := tx.Exec(ctx,
`select pg_advisory_xact_lock(hashtext($1)::bigint)`, licence); err != nil {
return 0, fmt.Errorf("cannot take the refresh lease on %q: %w", licence, err)
}
var vendor string
var manager *string
err = tx.QueryRow(ctx,
`select vendor, manager from licence where name = $1`, licence).Scan(&vendor, &manager)
if errors.Is(err, pgx.ErrNoRows) {
return 0, fmt.Errorf("this mesh has no licence called %q", licence)
}
if err != nil {
return 0, err
}
adapter, err := adapters.For(vendor)
if err != nil {
return 0, err
}
refresher, ok := adapter.(adapters.Refresher)
if !ok {
// The type assertion is what gates the carve-out to refreshable-grant vendors: a static key
// is not a Refresher, so it can never reach the machinery that holds a token readably.
return 0, fmt.Errorf(
"%q is a %s licence and cannot be refreshed; only a refreshable-grant licence has a "+
"refresh token", licence, adapter.Shape())
}
if manager == nil || *manager == "" {
return 0, fmt.Errorf(
"%q has no manager named, so there is no node to refresh it. Name one:\n"+
" licence manager %s <node>", licence, licence)
}
var at secrets.AtRest
err = tx.QueryRow(ctx,
`select token, wrapped_key, manager_key from refresh_grant where licence = $1`, licence).
Scan(&at.Token, &at.WrappedKey, &at.ManagerKey)
if errors.Is(err, pgx.ErrNoRows) {
return 0, fmt.Errorf(
"%q has no refresh token stored yet; its manager %s adopts one first "+
"(novox/hq ADR 0050, Phase C)", licence, *manager)
}
if err != nil {
return 0, err
}
result, err := refresher.Refresh(ctx,
adapters.RefreshInput{Licence: licence, Manager: *manager, AtRest: at})
if err != nil {
return 0, err
}
if strings.TrimSpace(result.AccessToken) == "" {
return 0, fmt.Errorf(
"the refresh produced no access token for %q, so nothing was resealed", licence)
}
// Reseal the new access token to the holders that exist now — the same set Accept seals to — and
// keep no readable copy.
holders, err := holdersTx(ctx, tx, licence)
if err != nil {
return 0, err
}
sealed := 0
for _, h := range holders {
key, err := keys(h.Node)
if err != nil {
return 0, err
}
if key == "" {
return 0, fmt.Errorf(
"%s has no sealing key, so the new access token cannot be sealed to it", h.Node)
}
blob, err := secrets.Seal(key, []byte(result.AccessToken))
if err != nil {
return 0, err
}
if _, err := tx.Exec(ctx,
`update licence_holder set sealed = $4, node_key = $5
where licence = $1 and node = $2 and module = $3`,
h.Licence, h.Node, h.Module, blob, key); err != nil {
return 0, err
}
sealed++
}
// The refresh token stays put unless the vendor rotated it, in which case the refresher returned
// it already re-encrypted at rest — replaced here without ever being seen in the clear.
if result.NewAtRest != nil {
if result.NewAtRest.Token == "" || result.NewAtRest.WrappedKey == "" ||
result.NewAtRest.ManagerKey == "" {
return 0, fmt.Errorf(
"the refresh returned an incomplete re-sealed refresh token for %q", licence)
}
if _, err := tx.Exec(ctx,
`update refresh_grant set token = $2, wrapped_key = $3, manager_key = $4, updated_at = now()
where licence = $1`,
licence, result.NewAtRest.Token, result.NewAtRest.WrappedKey,
result.NewAtRest.ManagerKey); err != nil {
return 0, err
}
}
if err := tx.Commit(ctx); err != nil {
return 0, err
}
return sealed, nil
}
// holdersTx reads a licence's holders inside a transaction, so the reseal set is consistent under
// the refresh lease.
func holdersTx(ctx context.Context, tx pgx.Tx, licence string) ([]Holder, error) {
rows, err := tx.Query(ctx,
`select licence, node, module, coalesce(sealed, '') from licence_holder
where licence = $1 order by node, module`, licence)
if err != nil {
return nil, err
}
defer rows.Close()
var out []Holder
for rows.Next() {
var h Holder
if err := rows.Scan(&h.Licence, &h.Node, &h.Module, &h.Sealed); err != nil {
return nil, err
}
out = append(out, h)
}
return out, rows.Err()
}
// Names is every licence's name, sorted — what a refusal lists when a consumer has not chosen.
func Names(all []Licence) []string {
out := make([]string, 0, len(all))
for _, one := range all {
out = append(out, one.Name)
}
sort.Strings(out)
return out
}