Model access is vendor-agnostic: rename provider→vendor, add adapter seam (Phase A)
ADR 0050 Phase A. Rename the licence's `provider` field to `vendor` — the inventory already uses "provider" for which node answers a brokered provision, and one word must not carry two facts — and route the licence layer's sealing and delivery through a per-vendor adapter selected by that field. The rename touches the Go struct/params/SQL in internal/licences, the operator CLI, and the schema: 0001 (the consolidated schema) now creates the column as `vendor`; a new guarded 0002 renames it on a database that predates the change, and is a no-op on a fresh one. The adapter (internal/licences/adapters) has a `shape` and the two verbs a static-key vendor needs — accept (the generic anonymous-box seal) and deliver (the sealed blob unchanged). refresh/identity/usage are named as optional capability interfaces so the refreshable-grant seam exists before its code. A registry maps vendor→shape (anthropic→static-key for now, with a Phase-B TODO to swap it to refreshable-grant); an unknown vendor is refused clearly. Behaviour is unchanged from the operator's view except the field name. Claude-Session: https://claude.ai/code/session_01LrgweAeERJYBg88c5cKDzF
This commit is contained in:
@@ -20,7 +20,7 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/jackc/pgx/v5"
|
||||
"github.com/novox/mesh-control/internal/secrets"
|
||||
"github.com/novox/mesh-control/internal/licences/adapters"
|
||||
"github.com/novox/mesh-control/internal/store"
|
||||
)
|
||||
|
||||
@@ -63,10 +63,14 @@ func (l *Licences) Ready(ctx context.Context, within time.Duration) error {
|
||||
|
||||
// A Licence is one way to reach a model, under the name a person calls it.
|
||||
type Licence struct {
|
||||
Name string
|
||||
Provider string
|
||||
Serves map[string]any
|
||||
Added time.Time
|
||||
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.
|
||||
@@ -79,9 +83,9 @@ type Holder struct {
|
||||
}
|
||||
|
||||
// Add records a licence under the operator's own name for it.
|
||||
func (l *Licences) Add(ctx context.Context, name, provider string, serves map[string]any) error {
|
||||
if strings.TrimSpace(name) == "" || strings.TrimSpace(provider) == "" {
|
||||
return errors.New("a licence needs a name and a provider")
|
||||
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{}
|
||||
@@ -91,16 +95,16 @@ func (l *Licences) Add(ctx context.Context, name, provider string, serves map[st
|
||||
return err
|
||||
}
|
||||
_, err = l.store.Pool().Exec(ctx,
|
||||
`insert into licence (name, provider, serves) values ($1, $2, $3)
|
||||
on conflict (name) do update set provider = excluded.provider, serves = excluded.serves`,
|
||||
name, provider, body)
|
||||
`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, provider, serves, added_at from licence order by name`)
|
||||
`select name, vendor, serves, added_at from licence order by name`)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -110,7 +114,7 @@ func (l *Licences) All(ctx context.Context) ([]Licence, error) {
|
||||
for rows.Next() {
|
||||
var one Licence
|
||||
var body []byte
|
||||
if err := rows.Scan(&one.Name, &one.Provider, &body, &one.Added); err != nil {
|
||||
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 {
|
||||
@@ -194,6 +198,12 @@ func (l *Licences) Chosen(ctx context.Context, node, module string) (string, 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,
|
||||
@@ -205,9 +215,32 @@ func (l *Licences) KeyFor(ctx context.Context, licence, node, module string) (st
|
||||
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)
|
||||
|
||||
@@ -225,6 +258,14 @@ func (l *Licences) Accept(ctx context.Context, licence, value string, keys Seali
|
||||
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
|
||||
@@ -248,7 +289,7 @@ func (l *Licences) Accept(ctx context.Context, licence, value string, keys Seali
|
||||
"%s has no sealing key, so nothing can be sealed to it — it joins again to get one",
|
||||
h.Node)
|
||||
}
|
||||
made, err := secrets.Accept(value, key, key)
|
||||
made, err := adapter.Accept(value, key, key)
|
||||
if err != nil {
|
||||
return sealed, err
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user