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
246 lines
7.4 KiB
Go
246 lines
7.4 KiB
Go
package main
|
|
|
|
import (
|
|
"bufio"
|
|
"context"
|
|
"encoding/json"
|
|
"errors"
|
|
"flag"
|
|
"fmt"
|
|
"os"
|
|
"strings"
|
|
)
|
|
|
|
// licenceCommand is everything about model access the mesh holds.
|
|
//
|
|
// **A licence is a named thing and the name is the operator's** (novox/hq ADR 0024). *The personal
|
|
// account*, *the organisation's account* — those 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.
|
|
func licenceCommand(ctx context.Context, args []string) error {
|
|
if len(args) == 0 {
|
|
return errors.New("licence add|list|use|release|key|forget")
|
|
}
|
|
switch args[0] {
|
|
case "add":
|
|
return licenceAdd(ctx, args[1:])
|
|
case "list":
|
|
return licenceList(ctx)
|
|
case "use":
|
|
return licenceUse(ctx, args[1:], true)
|
|
case "release":
|
|
return licenceUse(ctx, args[1:], false)
|
|
case "key":
|
|
return licenceKey(ctx, args[1:])
|
|
case "forget":
|
|
return licenceForget(ctx, args[1:])
|
|
}
|
|
return fmt.Errorf("licence %q; it is add, list, use, release, key or forget", args[0])
|
|
}
|
|
|
|
func licenceAdd(ctx context.Context, args []string) error {
|
|
set := flag.NewFlagSet("licence add", flag.ContinueOnError)
|
|
// What a consumer must know that is not secret — a base URL, a model name. Never the key.
|
|
serves := set.String("serves", "",
|
|
"JSON a consumer must know that is not secret, such as a base URL or a model")
|
|
positionals, err := parseAround(set, args)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if len(positionals) != 2 {
|
|
return errors.New(`licence add <vendor> <name> [--serves '{"model":"..."}']`)
|
|
}
|
|
vendor, name := positionals[0], positionals[1]
|
|
|
|
values := map[string]any{}
|
|
if strings.TrimSpace(*serves) != "" {
|
|
if err := json.Unmarshal([]byte(*serves), &values); err != nil {
|
|
return fmt.Errorf("--serves is not JSON: %w", err)
|
|
}
|
|
}
|
|
|
|
held, err := openLicences(ctx)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer held.Close()
|
|
if err := held.Add(ctx, name, vendor, values); err != nil {
|
|
return err
|
|
}
|
|
fmt.Printf("%s (%s) recorded. Nothing uses it yet, and it has no key:\n"+
|
|
" licence use %s <node> <module>\n licence key %s\n", name, vendor, name, name)
|
|
return nil
|
|
}
|
|
|
|
func licenceList(ctx context.Context) error {
|
|
held, err := openLicences(ctx)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer held.Close()
|
|
|
|
all, err := held.All(ctx)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if len(all) == 0 {
|
|
// Said, not printed as nothing: an empty list and a failed read must never look the same.
|
|
fmt.Println("this mesh holds no licences")
|
|
return nil
|
|
}
|
|
for _, one := range all {
|
|
holders, err := held.HoldersOf(ctx, one.Name)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
fmt.Printf("%s (%s)\n", one.Name, one.Vendor)
|
|
if len(holders) == 0 {
|
|
fmt.Printf(" nobody uses it\n")
|
|
}
|
|
for _, h := range holders {
|
|
// Whether it has a key is the question somebody is actually asking, so it is said
|
|
// per holder rather than per licence: the key was sealed to the holders that existed
|
|
// when it was supplied, and one recorded afterwards has none.
|
|
state := "has no key — supply it again with `licence key " + one.Name + "`"
|
|
if h.Sealed != "" {
|
|
state = "has a key"
|
|
}
|
|
fmt.Printf(" %s on %s: %s\n", h.Module, h.Node, state)
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func licenceUse(ctx context.Context, args []string, using bool) error {
|
|
verb := "use"
|
|
if !using {
|
|
verb = "release"
|
|
}
|
|
if len(args) != 3 {
|
|
return fmt.Errorf("licence %s <name> <node> <module>", verb)
|
|
}
|
|
name, node, module := args[0], args[1], args[2]
|
|
|
|
held, err := openLicences(ctx)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer held.Close()
|
|
|
|
if !using {
|
|
if err := held.StopUsing(ctx, name, node, module); err != nil {
|
|
return err
|
|
}
|
|
fmt.Printf("%s on %s no longer uses %s. Its copy of the key goes on the next push\n",
|
|
module, node, name)
|
|
return nil
|
|
}
|
|
if err := held.Use(ctx, name, node, module); err != nil {
|
|
return err
|
|
}
|
|
fmt.Printf("%s on %s uses %s.\n", module, node, name)
|
|
// The consequence, said now rather than discovered as a machine that resolves and receives
|
|
// nothing: the mesh discarded the plaintext, so a holder added after the key was supplied has
|
|
// no key and the mesh cannot make one.
|
|
sealed, err := held.KeyFor(ctx, name, node, module)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if sealed == "" {
|
|
fmt.Printf(" It has no key yet — the mesh discarded the plaintext when it was supplied "+
|
|
"and cannot seal another. Supply it again:\n licence key %s\n", name)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// licenceKey is the *accept* verb novox/hq ADR 0024 names as missing.
|
|
//
|
|
// Take a value, seal it to each holder, and discard the plaintext. Every other credential the
|
|
// mesh handles 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.
|
|
func licenceKey(ctx context.Context, args []string) error {
|
|
set := flag.NewFlagSet("licence key", flag.ContinueOnError)
|
|
// A file rather than an argument, by default. A key on a command line is a key in shell
|
|
// history and in every process listing taken while it ran.
|
|
from := set.String("file", "", "read the key from a file instead of standard input")
|
|
positionals, err := parseAround(set, args)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if len(positionals) != 1 {
|
|
return errors.New("licence key <name> [--file <path>]")
|
|
}
|
|
name := positionals[0]
|
|
|
|
var value string
|
|
if *from != "" {
|
|
raw, err := os.ReadFile(*from)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
value = strings.TrimSpace(string(raw))
|
|
} else {
|
|
fmt.Fprintln(os.Stderr, "reading the key from standard input; it is not echoed anywhere")
|
|
reader := bufio.NewReader(os.Stdin)
|
|
line, err := reader.ReadString('\n')
|
|
if err != nil && line == "" {
|
|
return fmt.Errorf("nothing was given on standard input: %w", err)
|
|
}
|
|
value = strings.TrimSpace(line)
|
|
}
|
|
|
|
held, err := openLicences(ctx)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer held.Close()
|
|
|
|
open, err := openStores(ctx)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer open.Close()
|
|
inv := open.inventory
|
|
|
|
sealed, err := held.Accept(ctx, name, value, func(node string) (string, error) {
|
|
return inv.SealingKeyOf(ctx, node)
|
|
})
|
|
if err != nil {
|
|
if sealed > 0 {
|
|
// Some holders got it and some did not, and the person holding the key is the only
|
|
// one who can finish the job. Saying how far it got is the difference between running
|
|
// this again knowing what it will do and running it hoping.
|
|
return fmt.Errorf(
|
|
"%w\n\n%d holder(s) were sealed before this. Running `licence key %s` again "+
|
|
"with the same key seals the rest and changes nothing for those already done",
|
|
err, sealed, name)
|
|
}
|
|
return err
|
|
}
|
|
// Not echoed back, ever. What is stored is unreadable by whoever holds it, the mesh included,
|
|
// and printing the value here would put the one copy that matters on a terminal.
|
|
fmt.Printf("sealed to %d holder(s). The mesh has discarded the key and cannot read it back\n",
|
|
sealed)
|
|
fmt.Printf(" run `push` to deliver it\n")
|
|
return nil
|
|
}
|
|
|
|
func licenceForget(ctx context.Context, args []string) error {
|
|
if len(args) != 1 {
|
|
return errors.New("licence forget <name>")
|
|
}
|
|
held, err := openLicences(ctx)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer held.Close()
|
|
if err := held.Forget(ctx, args[0]); err != nil {
|
|
return err
|
|
}
|
|
// Said plainly, because the mesh cannot do it and pretending otherwise is worse than useless:
|
|
// a licence outliving its holder is a live credential nobody is watching.
|
|
fmt.Printf("%s is forgotten, and every record of who held it with it.\n"+
|
|
" The key itself is not the mesh's to revoke — do that where the licence was bought\n",
|
|
args[0])
|
|
return nil
|
|
}
|