Model access is a provision answered by a record, not a machine
novox/hq ADR 0024, gaps 1 and 2. The user's stated requirement, and the first thing here that no machine can answer: a hosted model is on nobody's node and is reached over the public internet, so the rule that refuses two ends sharing no private network must not apply to it. A licence is a named thing and the name is the operator's — *the personal account*, *the organisation's* — because the whole point is saying which one a given consumer uses, and an anonymous credential hanging off a provider cannot be said. Many to many, so deliberately not a claim: two machines sharing an account is ordinary rather than a collision. Gap 2 is the missing verb, *accept*: take a value somebody supplied, seal it to each holder, discard the plaintext. With the consequence stated rather than hidden — a holder recorded after the key was supplied has no key and the mesh cannot make one, so it is refused by name with the remedy, not silently handed an empty file. Refusal is felt, as the record warns: a mesh holding three ways to reach a model refuses every consumer that has not chosen. So the refusal names the candidates and the exact command. Being right is not the same as being usable. Gaps 3 and 4 — a consumer that is not a machine, and switching as a reaction rather than a declaration — remain gaps. Half-building them would put a conditional in the declaration language, which is what ADR 0024 says plainly to avoid. Its own context, with its own store and its own credential: a licence is a different aggregate from anything inventory owns, and it refers to nodes by name because that is what crossing a context boundary may carry.
This commit is contained in:
@@ -0,0 +1,274 @@
|
||||
// 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/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
|
||||
Provider 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, 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")
|
||||
}
|
||||
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, provider, serves) values ($1, $2, $3)
|
||||
on conflict (name) do update set provider = excluded.provider, serves = excluded.serves`,
|
||||
name, provider, 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`)
|
||||
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.Provider, &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.
|
||||
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
|
||||
}
|
||||
return *sealed, nil
|
||||
}
|
||||
|
||||
// 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")
|
||||
}
|
||||
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 := secrets.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
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
-- A licence is a named thing, and the name is the operator's.
|
||||
--
|
||||
-- novox/hq ADR 0024. Not an anonymous credential hanging off a provider: *the personal account*,
|
||||
-- *the organisation's account* are names a person uses, and the mesh has to use them too, because
|
||||
-- the whole point is saying WHICH ONE a given consumer uses.
|
||||
--
|
||||
-- **Many to many.** One provider has several licences; one licence serves several consumers. So it
|
||||
-- is deliberately not a claim — claims are for things only one holder may have, and two machines
|
||||
-- sharing an account is the ordinary case rather than a collision.
|
||||
|
||||
create table licence (
|
||||
-- The operator's name for it. The primary key, because that is what a person types and what a
|
||||
-- consumer is pinned to.
|
||||
name text primary key,
|
||||
-- Which service it is for: anthropic, openai, a model the mesh runs itself.
|
||||
provider text not null,
|
||||
-- What a consumer needs to know that is not secret -- a base URL, a model name. The key is
|
||||
-- never here.
|
||||
serves jsonb not null default '{}'::jsonb,
|
||||
added_at timestamptz not null default now()
|
||||
);
|
||||
|
||||
-- Who holds it, and the key sealed to them.
|
||||
--
|
||||
-- **The node is a name, not a foreign key.** It lives in another context and this one may not join
|
||||
-- across that boundary (novox/hq ADR 0008); a name is the published identifier and is what
|
||||
-- crossing a context boundary is allowed to carry.
|
||||
create table licence_holder (
|
||||
licence text not null references licence(name) on delete cascade,
|
||||
node text not null,
|
||||
module text not null,
|
||||
|
||||
-- Sealed to that node's key. Null until a key has been supplied while this holder existed --
|
||||
-- which is a real state and not an error: the mesh discarded the plaintext, so it cannot seal
|
||||
-- to a holder that arrived afterwards, and saying so is better than delivering nothing.
|
||||
sealed text,
|
||||
node_key text,
|
||||
|
||||
added_at timestamptz not null default now(),
|
||||
primary key (licence, node, module)
|
||||
);
|
||||
Reference in New Issue
Block a user