Files
jschoubben ddb41baaf4 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
2026-09-06 23:49:12 +02:00

311 lines
10 KiB
Go

package licences
import (
"context"
"strings"
"testing"
)
// These run against a real PostgreSQL, like every other context's. `make check` raises one.
func fresh(t *testing.T) (*Licences, context.Context) {
t.Helper()
return ForTest(t), t.Context()
}
func aKey(t *testing.T) string { return ASealingKey(t) }
// The mesh does not keep a key it cannot seal to somebody, because keeping it for later means
// keeping it readably — which is the whole thing this refuses to do.
func TestAKeyWithNobodyToSealItToIsRefused(t *testing.T) {
held, ctx := fresh(t)
if err := held.Add(ctx, "personal", "anthropic", nil); err != nil {
t.Fatal(err)
}
_, err := held.Accept(ctx, "personal", "sk-test", func(string) (string, error) {
return "", nil
})
if err == nil {
t.Fatal("a key was taken with nobody to seal it to, so it was kept in the open")
}
if !strings.Contains(err.Error(), "consumer on it first") {
t.Fatalf("the refusal does not say what to do: %v", err)
}
}
// Sealed to each holder, and the plaintext discarded.
func TestAKeyIsSealedToEachHolderAndNotKept(t *testing.T) {
held, ctx := fresh(t)
if err := held.Add(ctx, "personal", "anthropic", map[string]any{"model": "a-model"}); err != nil {
t.Fatal(err)
}
for _, node := range []string{"workstation", "laptop"} {
if err := held.Use(ctx, "personal", node, "assistant"); err != nil {
t.Fatal(err)
}
}
keys := map[string]string{"workstation": aKey(t), "laptop": aKey(t)}
const value = "sk-the-operators-own-key"
sealed, err := held.Accept(ctx, "personal", value, func(node string) (string, error) {
return keys[node], nil
})
if err != nil {
t.Fatal(err)
}
if sealed != 2 {
t.Fatalf("%d holder(s) were sealed to, and there are two", sealed)
}
first, err := held.KeyFor(ctx, "personal", "workstation", "assistant")
if err != nil {
t.Fatal(err)
}
second, err := held.KeyFor(ctx, "personal", "laptop", "assistant")
if err != nil {
t.Fatal(err)
}
if first == "" || second == "" {
t.Fatal("a holder was left with no key")
}
// Sealed to different machines, so the blobs differ even though the key is one key. Two
// identical blobs would mean one of them was sealed to a machine that cannot open it.
if first == second {
t.Fatal("both holders were given the same blob, so one of them cannot open it")
}
// And nowhere in the open. This is the argument, not a detail.
for _, blob := range []string{first, second} {
if strings.Contains(blob, value) {
t.Fatal("the key is in the stored value in the open")
}
}
}
// A holder recorded after the key was supplied has none, and the mesh cannot make one.
//
// Reported rather than hidden: a machine that resolves cleanly and receives nothing fails later,
// somewhere that names neither the licence nor the mesh.
func TestAHolderAddedAfterTheKeyHasNone(t *testing.T) {
held, ctx := fresh(t)
if err := held.Add(ctx, "personal", "anthropic", nil); err != nil {
t.Fatal(err)
}
if err := held.Use(ctx, "personal", "workstation", "assistant"); err != nil {
t.Fatal(err)
}
key := aKey(t)
if _, err := held.Accept(ctx, "personal", "sk-test", func(string) (string, error) {
return key, nil
}); err != nil {
t.Fatal(err)
}
if err := held.Use(ctx, "personal", "laptop", "assistant"); err != nil {
t.Fatal(err)
}
later, err := held.KeyFor(ctx, "personal", "laptop", "assistant")
if err != nil {
t.Fatal(err)
}
if later != "" {
t.Fatal("a holder added after the key was discarded was somehow given one")
}
// And the first holder still has theirs — a new holder must not disturb an existing one.
first, err := held.KeyFor(ctx, "personal", "workstation", "assistant")
if err != nil {
t.Fatal(err)
}
if first == "" {
t.Fatal("adding a holder took the key away from one that had it")
}
}
// Taking a consumer off a licence takes its copy of the key with it.
func TestReleasingAConsumerTakesItsKey(t *testing.T) {
held, ctx := fresh(t)
if err := held.Add(ctx, "personal", "anthropic", nil); err != nil {
t.Fatal(err)
}
if err := held.Use(ctx, "personal", "workstation", "assistant"); err != nil {
t.Fatal(err)
}
key := aKey(t)
if _, err := held.Accept(ctx, "personal", "sk-test", func(string) (string, error) {
return key, nil
}); err != nil {
t.Fatal(err)
}
if err := held.StopUsing(ctx, "personal", "workstation", "assistant"); err != nil {
t.Fatal(err)
}
holders, err := held.HoldersOf(ctx, "personal")
if err != nil {
t.Fatal(err)
}
if len(holders) != 0 {
t.Fatalf("a released consumer is still a holder: %+v", holders)
}
}
// A licence nobody recorded is not a licence somebody can be put on.
func TestUsingALicenceThatDoesNotExistIsRefused(t *testing.T) {
held, ctx := fresh(t)
err := held.Use(ctx, "invented", "workstation", "assistant")
if err == nil {
t.Fatal("a consumer was put on a licence this mesh has never heard of")
}
// Named, in words a person can act on. The database's own foreign-key message is true and
// mentions a constraint rather than a licence, which sends somebody reading a schema instead
// of typing the name they meant.
if !strings.Contains(err.Error(), `"invented"`) {
t.Fatalf("the refusal does not name the licence: %v", err)
}
}
// Two sessions on one machine, each on its own licence (novox/hq work breakdown 1.2).
//
// **The case ADR 0026 creates.** The control-plane node runs its own node session and the mesh's,
// so a machine is no longer a usable answer to *whose licence is this*. `14-model-access.md`
// records that gap as "a consumer that is not a machine", and the question this answers is whether
// it needs a new consumer identity or whether the existing one already distinguishes them.
//
// It does. The two sessions are two modules — the same mechanism started in different context
// roots (ADR 0026), and a context root is what a module delivers — so `(node, module)` names them
// apart without a schema knowing anything about sessions.
func TestTwoSessionsOnOneMachineHoldDifferentLicences(t *testing.T) {
held, ctx := fresh(t)
for _, l := range []struct{ name, vendor string }{
{"personal", "anthropic"},
{"company", "anthropic"},
} {
if err := held.Add(ctx, l.name, l.vendor, map[string]any{"model": "a-model"}); err != nil {
t.Fatal(err)
}
}
// Both on the machine that holds the control plane.
const machine = "anchor"
if err := held.Use(ctx, "personal", machine, "node-session"); err != nil {
t.Fatal(err)
}
if err := held.Use(ctx, "company", machine, "mesh-session"); err != nil {
t.Fatal(err)
}
// Each is asked for separately, and neither answer is the other's.
node, err := held.Chosen(ctx, machine, "node-session")
if err != nil {
t.Fatal(err)
}
mesh, err := held.Chosen(ctx, machine, "mesh-session")
if err != nil {
t.Fatal(err)
}
if node != "personal" || mesh != "company" {
t.Fatalf("the node session is on %q and the mesh session on %q; expected personal and company",
node, mesh)
}
// And the keys are separate too, which is the half that matters: a machine-wide answer would
// hand both sessions whichever key was sealed last.
sealing := aKey(t)
for _, l := range []struct{ name, value string }{
{"personal", "sk-the-operators-own-key"},
{"company", "sk-the-companys-key"},
} {
if _, err := held.Accept(ctx, l.name, l.value, func(string) (string, error) {
return sealing, nil
}); err != nil {
t.Fatal(err)
}
}
first, err := held.KeyFor(ctx, "personal", machine, "node-session")
if err != nil {
t.Fatal(err)
}
second, err := held.KeyFor(ctx, "company", machine, "mesh-session")
if err != nil {
t.Fatal(err)
}
if first == "" || second == "" {
t.Fatal("a session on a licence was given no key")
}
if first == second {
t.Fatal("both sessions were given the same sealed key, so the machine answered rather " +
"than the session")
}
}
// A session put on no licence is not silently given the machine's other one.
func TestASessionOnNoLicenceIsAnsweredWithNothing(t *testing.T) {
held, ctx := fresh(t)
if err := held.Add(ctx, "personal", "anthropic", map[string]any{"model": "a-model"}); err != nil {
t.Fatal(err)
}
if err := held.Use(ctx, "personal", "anchor", "node-session"); err != nil {
t.Fatal(err)
}
chosen, err := held.Chosen(ctx, "anchor", "mesh-session")
if err != nil {
t.Fatal(err)
}
if chosen != "" {
t.Fatalf("a session nobody put on a licence was answered with %q, which belongs to "+
"something else on the same machine", chosen)
}
}
// Two sessions on one machine and the SAME licence are still two holders.
//
// **Written because a fault injection stayed silent.** The test above puts them on different
// licences, so the licence alone tells them apart and the module argument is never load-bearing —
// removing it from the query changed nothing and every assertion still passed. This is the case
// that needs `(node, module)` to be the identity: releasing one session must leave the other,
// and a machine-shaped answer would take both.
func TestReleasingOneSessionLeavesTheOtherOnTheSameMachine(t *testing.T) {
held, ctx := fresh(t)
if err := held.Add(ctx, "company", "anthropic", map[string]any{"model": "a-model"}); err != nil {
t.Fatal(err)
}
const machine = "anchor"
for _, session := range []string{"node-session", "mesh-session"} {
if err := held.Use(ctx, "company", machine, session); err != nil {
t.Fatal(err)
}
}
if _, err := held.Accept(ctx, "company", "sk-the-companys-key", func(string) (string, error) {
return aKey(t), nil
}); err != nil {
t.Fatal(err)
}
if err := held.StopUsing(ctx, "company", machine, "node-session"); err != nil {
t.Fatal(err)
}
gone, err := held.Chosen(ctx, machine, "node-session")
if err != nil {
t.Fatal(err)
}
if gone != "" {
t.Errorf("the released session is still on %q", gone)
}
kept, err := held.Chosen(ctx, machine, "mesh-session")
if err != nil {
t.Fatal(err)
}
if kept != "company" {
t.Fatal("releasing one session took the other's licence with it, so the machine was " +
"released rather than the session")
}
key, err := held.KeyFor(ctx, "company", machine, "mesh-session")
if err != nil {
t.Fatal(err)
}
if key == "" {
t.Fatal("the session that was kept lost its key")
}
}