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:
2026-09-06 23:49:12 +02:00
parent 671fb4f8f3
commit ddb41baaf4
6 changed files with 245 additions and 25 deletions
@@ -1,10 +1,10 @@
-- 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*,
-- novox/hq ADR 0024. Not an anonymous credential hanging off a vendor: *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
-- **Many to many.** One vendor 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.
@@ -12,8 +12,10 @@ 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,
-- Which company sells it: anthropic, openai, a model the mesh runs itself. Selects the adapter
-- that runs this licence's lifecycle (novox/hq ADR 0050). Named `vendor`, not `provider`: the
-- inventory already uses "provider" for which node answers a brokered provision.
vendor 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,
@@ -0,0 +1,22 @@
-- Vendor, not provider.
--
-- novox/hq ADR 0050 renames the licence's vendor field from `provider` to `vendor`. "provider" is
-- already the word the inventory uses for *which node answers a brokered provision*; reusing it for
-- *which company sells this licence* would collide two unrelated facts on one word. The consolidated
-- schema (0001) now creates the column as `vendor`; this migration carries an existing database the
-- same distance.
--
-- **Guarded so it is a no-op on a fresh database.** A database created after 0001 was updated
-- already has `vendor` and no `provider` column, and PostgreSQL has no `RENAME COLUMN IF EXISTS` --
-- so the rename is wrapped in an explicit existence check. On a database that predates the rename
-- the `provider` column is present and is renamed; on a fresh one nothing is done, and both end with
-- exactly the same schema.
do $$
begin
if exists (
select 1 from information_schema.columns
where table_name = 'licence' and column_name = 'provider'
) then
alter table licence rename column provider to vendor;
end if;
end $$;