One name per thing, per the HQ glossary: the module/container/image/binary/repo becomes mesh-controller, the seat the-controller, and the store+broker pair the foundation (embedded base bundles, default template and example lock renamed with their go:embed directives). No behaviour change — a pure vocabulary rename. Claude-Session: https://claude.ai/code/session_01D6qtiYU3P9jk3pnAXyAFyx
728 lines
28 KiB
Go
728 lines
28 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-controller/internal/licences/adapters"
|
|
"github.com/novox/mesh-controller/internal/secrets"
|
|
"github.com/novox/mesh-controller/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) {
|
|
// The manager holder is delivered the REFRESH token, not an access token: it is the one holder
|
|
// that refreshes rather than consumes (novox/hq ADR 0050). It is sealed to this same node's key
|
|
// with the very same anonymous box a consumer's credential is, so it rides the identical
|
|
// host-unseal-and-mount path — the host opens it, the manager module reads cleartext, and the
|
|
// module is never handed a private key. Nothing to strip on the consumer side and nothing bespoke
|
|
// on this one: the refresh token is simply the credential this particular holder receives.
|
|
managerNode, managerModule, err := l.ManagerOf(ctx, licence)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
if managerNode != "" && node == managerNode && module == managerModule {
|
|
sealed, _, ok, err := l.RefreshGrant(ctx, licence)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
if !ok {
|
|
// No refresh token adopted yet — empty, exactly as a consumer with no key. The
|
|
// declaration refuses that by name, where the module and path are both in view.
|
|
return "", nil
|
|
}
|
|
return sealed, nil
|
|
}
|
|
|
|
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)
|
|
}
|
|
|
|
// The manager holder is delivered the refresh token, not an operator-supplied access key — its
|
|
// row is fed by adoption and refresh, not by this. Skipped so an accepted value never clobbers it.
|
|
managerNode, managerModule, err := l.ManagerOf(ctx, licence)
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
|
|
sealed := 0
|
|
for _, h := range holders {
|
|
if managerNode != "" && h.Node == managerNode && h.Module == managerModule {
|
|
continue
|
|
}
|
|
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 and module that hold a licence's refresh token readably, both 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). The module is returned alongside the node because a
|
|
// node may run the manager module and a consuming module of the same licence at once, and which
|
|
// holder is delivered the refresh token turns on the module, not the node alone.
|
|
func (l *Licences) ManagerOf(ctx context.Context, licence string) (node, module string, err error) {
|
|
var mgr, mod *string
|
|
err = l.store.Pool().QueryRow(ctx,
|
|
`select manager, manager_module from licence where name = $1`, licence).Scan(&mgr, &mod)
|
|
if errors.Is(err, pgx.ErrNoRows) {
|
|
return "", "", fmt.Errorf("this mesh has no licence called %q", licence)
|
|
}
|
|
if err != nil {
|
|
return "", "", err
|
|
}
|
|
if mgr == nil {
|
|
return "", "", nil
|
|
}
|
|
if mod == nil {
|
|
return *mgr, "", nil
|
|
}
|
|
return *mgr, *mod, nil
|
|
}
|
|
|
|
// SetManager names the one node, and the module on it, that hold a licence's refresh token and
|
|
// refresh 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 module only" starts here, at the one place a manager is written.
|
|
//
|
|
// **The module is named too, and it is the holder that is delivered the refresh token.** The manager
|
|
// module must also be put on the licence as a holder (`Use`), so the plan resolves its model-access
|
|
// requirement; naming it here is what tells delivery to hand THAT holder the refresh token rather than
|
|
// an access token.
|
|
func (l *Licences) SetManager(ctx context.Context, licence, node, module string) error {
|
|
if strings.TrimSpace(node) == "" || strings.TrimSpace(module) == "" {
|
|
return errors.New("a manager needs a node and the module on it that refreshes")
|
|
}
|
|
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, manager_module = $3 where name = $1`, licence, node, module)
|
|
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 one sealed blob.
|
|
//
|
|
// **The blob is opaque here, and it is an ordinary sealed box.** It was produced where the refresh
|
|
// token was in the clear — the manager node, at adoption or after a rotation — sealed to that node's
|
|
// public sealing key with the same `crypto_box_seal` every credential uses (novox/hq ADR 0050). This
|
|
// context keeps it and delivers it without opening it: the control plane holds no private key that
|
|
// could, which is the whole point of where the carve-out draws the line.
|
|
func (l *Licences) SetRefreshGrant(ctx context.Context, licence, sealed, managerKey string) error {
|
|
if strings.TrimSpace(sealed) == "" || strings.TrimSpace(managerKey) == "" {
|
|
return errors.New("an incomplete refresh-token grant is not one to keep")
|
|
}
|
|
_, err := l.store.Pool().Exec(ctx,
|
|
`insert into refresh_grant (licence, sealed, manager_key)
|
|
values ($1, $2, $3)
|
|
on conflict (licence) do update set
|
|
sealed = excluded.sealed, manager_key = excluded.manager_key, updated_at = now()`,
|
|
licence, sealed, 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 sealed refresh token, the key it was sealed to, and whether one is
|
|
// stored.
|
|
func (l *Licences) RefreshGrant(ctx context.Context, licence string) (sealed, managerKey string, ok bool, err error) {
|
|
err = l.store.Pool().QueryRow(ctx,
|
|
`select sealed, manager_key from refresh_grant where licence = $1`, licence).
|
|
Scan(&sealed, &managerKey)
|
|
if errors.Is(err, pgx.ErrNoRows) {
|
|
return "", "", false, nil
|
|
}
|
|
if err != nil {
|
|
return "", "", false, err
|
|
}
|
|
return sealed, managerKey, 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 sealedRefresh, managerKey string
|
|
err = tx.QueryRow(ctx,
|
|
`select sealed, manager_key from refresh_grant where licence = $1`, licence).
|
|
Scan(&sealedRefresh, &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, Sealed: sealedRefresh, ManagerKey: managerKey,
|
|
})
|
|
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)
|
|
}
|
|
|
|
// The reseal-and-publish half, shared with SubmitRefresh: the new access token is sealed to every
|
|
// consumer holder that exists now, and a rotated refresh token replaces the stored sealed blob —
|
|
// never seen in the clear either way.
|
|
sealed, err := resealAndPublish(
|
|
ctx, tx, licence, result.AccessToken, result.NewSealed, result.NewManagerKey, keys)
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
|
|
if err := tx.Commit(ctx); err != nil {
|
|
return 0, err
|
|
}
|
|
return sealed, nil
|
|
}
|
|
|
|
// SubmitRefresh takes a refresh a MANAGER NODE already performed and publishes it: it seals the new
|
|
// access token to every holder and replaces the stored refresh envelope if the vendor rotated it.
|
|
//
|
|
// **This is the entry point that keeps the control plane blind to the refresh token** (novox/hq ADR
|
|
// 0050, Phase C). `Refresh` above calls an in-process VendorRefresher — which would open the at-rest
|
|
// envelope inside the control plane's own process, exactly what the carve-out forbids. So Anthropic
|
|
// registers no in-process refresher; instead its manager runtime, on the manager node, opens the
|
|
// envelope with that node's own key, calls the vendor's OAuth endpoint, and submits the *result*
|
|
// here: the new access token in the clear (which the mesh seals per holder and discards, as it does
|
|
// any accepted key) and — only if the vendor rotated it — the refresh token already re-sealed at
|
|
// rest (which the mesh stores without ever opening). The refresh token in the clear never crosses
|
|
// this boundary, because this function is never given it.
|
|
//
|
|
// It reuses the same lease, the same reseal-and-publish, and the same all-or-nothing transaction as
|
|
// `Refresh`; the only difference is where the access token came from — a module on the manager node
|
|
// rather than a plug-in in this process.
|
|
func (l *Licences) SubmitRefresh(
|
|
ctx context.Context, licence, accessToken, newSealed, newManagerKey string, keys SealingKeys,
|
|
) (int, error) {
|
|
if strings.TrimSpace(accessToken) == "" {
|
|
return 0, fmt.Errorf(
|
|
"a refresh submitted for %q carried no access token, so there is nothing to seal", licence)
|
|
}
|
|
|
|
tx, err := l.store.Pool().Begin(ctx)
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
defer func() { _ = tx.Rollback(context.WithoutCancel(ctx)) }()
|
|
|
|
// The same lease Refresh takes: a submitted refresh and an in-process one serialise rather than
|
|
// racing to publish.
|
|
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)
|
|
}
|
|
|
|
// The submitter must be a refreshable-grant licence with a manager named — the same gate Refresh
|
|
// applies, so a static key can never reach this machinery and a licence with no manager is not
|
|
// silently accepted from whoever called.
|
|
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
|
|
}
|
|
if adapter.Shape() != adapters.RefreshableGrant {
|
|
return 0, fmt.Errorf(
|
|
"%q is a %s licence; only a refreshable-grant licence has a refresh to submit", licence,
|
|
adapter.Shape())
|
|
}
|
|
if manager == nil || *manager == "" {
|
|
return 0, fmt.Errorf(
|
|
"%q has no manager named, so a refresh cannot be submitted for it. Name one:\n"+
|
|
" licence manager %s <node>", licence, licence)
|
|
}
|
|
|
|
sealed, err := resealAndPublish(ctx, tx, licence, accessToken, newSealed, newManagerKey, keys)
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
|
|
if err := tx.Commit(ctx); err != nil {
|
|
return 0, err
|
|
}
|
|
return sealed, nil
|
|
}
|
|
|
|
// resealAndPublish seals a new access token to every CONSUMER holder and, if one is given, replaces
|
|
// the stored sealed refresh token with a rotated one. It is the half `Refresh` and `SubmitRefresh`
|
|
// share: the value's source differs, what is done with it does not.
|
|
//
|
|
// **The manager holder is skipped.** It is delivered the refresh token, not an access token (`KeyFor`);
|
|
// sealing an access token into its row would be a value nothing reads, and — worse — would overwrite
|
|
// the delivery bookkeeping for the one holder whose credential is the refresh token. So the reseal
|
|
// walks consumer holders only, and the count it returns is the number of consumers a push will carry
|
|
// the new access token to.
|
|
//
|
|
// The refresh token is never in the clear here — a rotated one arrives already sealed to the manager
|
|
// node, and is stored as the opaque blob it is. A consumer's `KeyFor` reads licence_holder, so it can
|
|
// only ever deliver an access token.
|
|
func resealAndPublish(
|
|
ctx context.Context, tx pgx.Tx, licence, accessToken, newSealed, newManagerKey string,
|
|
keys SealingKeys,
|
|
) (int, error) {
|
|
var managerNode, managerModule *string
|
|
if err := tx.QueryRow(ctx,
|
|
`select manager, manager_module from licence where name = $1`, licence).
|
|
Scan(&managerNode, &managerModule); err != nil {
|
|
return 0, err
|
|
}
|
|
|
|
holders, err := holdersTx(ctx, tx, licence)
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
sealed := 0
|
|
for _, h := range holders {
|
|
if managerNode != nil && managerModule != nil &&
|
|
h.Node == *managerNode && h.Module == *managerModule {
|
|
// The manager holder receives the refresh token, not this access token. Left untouched.
|
|
continue
|
|
}
|
|
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(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 manager sealed the
|
|
// new one to its own node key before submitting — replaced here without ever being seen in the
|
|
// clear.
|
|
if newSealed != "" {
|
|
if newManagerKey == "" {
|
|
return 0, fmt.Errorf(
|
|
"the refresh returned a re-sealed refresh token for %q with no manager key", licence)
|
|
}
|
|
if _, err := tx.Exec(ctx,
|
|
`update refresh_grant set sealed = $2, manager_key = $3, updated_at = now()
|
|
where licence = $1`,
|
|
licence, newSealed, newManagerKey); 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
|
|
}
|