Files
mesh-controller/cmd/mesh-control/licence.go
T
jschoubben 8e0c22fc2e licences: submit-refresh, the module-produced refresh entry point
Phase C of model-access (ADR 0050). Refresh above calls an in-process
VendorRefresher, which would open the at-rest envelope inside the control
plane's own process. Anthropic must not: its refresh runs on the manager
node. So add SubmitRefresh, the companion that publishes a refresh a
manager node already performed -- it is given only the new access token in
the clear (sealed per holder, as any accepted key) and an opaque re-sealed
refresh envelope (stored unopened). The refresh token in the clear never
crosses this boundary. The reseal-and-publish half is extracted and shared
with Refresh, so the sealing logic is one implementation.

CLI: licence grant (print the opaque envelope), set-grant (store a
module-produced envelope -- adoption), submit-refresh (access token +
optional rotated envelope). Tests defend that the manager alone opens the
refresh token and the control plane never holds it in the clear.

Claude-Session: https://claude.ai/code/session_01LrgweAeERJYBg88c5cKDzF
2026-09-07 01:00:27 +02:00

532 lines
17 KiB
Go

package main
import (
"bufio"
"context"
"encoding/json"
"errors"
"flag"
"fmt"
"io"
"os"
"strings"
"github.com/novox/mesh-control/internal/secrets"
)
// 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|manager|grant|set-grant|refresh|submit-refresh|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 "manager":
return licenceManager(ctx, args[1:])
case "grant":
return licenceGrant(ctx, args[1:])
case "set-grant":
return licenceSetGrant(ctx, args[1:])
case "refresh":
return licenceRefresh(ctx, args[1:])
case "submit-refresh":
return licenceSubmitRefresh(ctx, args[1:])
case "forget":
return licenceForget(ctx, args[1:])
}
return fmt.Errorf(
"licence %q; it is add, list, use, release, key, manager, grant, set-grant, refresh, "+
"submit-refresh 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
}
// licenceManager names the one node that holds a refreshable-grant licence's refresh token readably
// and refreshes it centrally (novox/hq ADR 0050).
//
// **Only a refreshable-grant licence has one.** A static-key licence has no refresh token to hold, so
// naming a manager for it is refused where the mistake is made rather than kept as a field that means
// nothing — the absent manager is part of what keeps a static key from ever holding a value readably
// at rest.
func licenceManager(ctx context.Context, args []string) error {
if len(args) != 2 {
return errors.New("licence manager <name> <node>")
}
name, node := args[0], args[1]
held, err := openLicences(ctx)
if err != nil {
return err
}
defer held.Close()
if err := held.SetManager(ctx, name, node); err != nil {
return err
}
fmt.Printf("%s holds and refreshes %s.\n"+
" Its refresh token is kept encrypted at rest, readable by %s alone — no other node, and "+
"not this database on its own.\n", node, name, node)
return nil
}
// envelopeJSON is the wire shape of a refresh-token at-rest envelope on this command surface: the
// three opaque parts of secrets.AtRest and nothing else.
//
// **Every field of it is ciphertext or a public key.** `token` is the refresh token under a data
// key, `wrapped_key` is that data key sealed to the manager node, `manager_key` is the manager's
// public sealing key. None of them is the refresh token in the clear — which is why this surface may
// print one out (`grant`) and read one in (`set-grant`, `submit-refresh`) without the control plane
// ever holding a refresh token it could read. The manager runtime, on the manager node, is the only
// place these open (novox/hq ADR 0050, Phase C).
type envelopeJSON struct {
Token string `json:"token"`
WrappedKey string `json:"wrapped_key"`
ManagerKey string `json:"manager_key"`
}
func (e envelopeJSON) atRest() secrets.AtRest {
return secrets.AtRest{Token: e.Token, WrappedKey: e.WrappedKey, ManagerKey: e.ManagerKey}
}
func envelopeOf(a secrets.AtRest) envelopeJSON {
return envelopeJSON{Token: a.Token, WrappedKey: a.WrappedKey, ManagerKey: a.ManagerKey}
}
// readEnvelope reads an at-rest envelope from a file or standard input as JSON.
func readEnvelope(from string) (envelopeJSON, error) {
var raw []byte
var err error
if from != "" {
raw, err = os.ReadFile(from)
} else {
raw, err = readAllStdin()
}
if err != nil {
return envelopeJSON{}, err
}
var env envelopeJSON
if err := json.Unmarshal(raw, &env); err != nil {
return envelopeJSON{}, fmt.Errorf("the refresh-token envelope is not JSON: %w", err)
}
if env.Token == "" || env.WrappedKey == "" || env.ManagerKey == "" {
return envelopeJSON{}, errors.New(
"an at-rest envelope is {token, wrapped_key, manager_key}, and one part is missing")
}
return env, nil
}
func readAllStdin() ([]byte, error) {
reader := bufio.NewReader(os.Stdin)
return io.ReadAll(reader)
}
// licenceGrant prints a licence's refresh-token envelope, so the manager runtime can fetch the
// opaque thing it will open on the manager node (novox/hq ADR 0050, Phase C).
//
// **What is printed is ciphertext.** The envelope is the refresh token sealed at rest to the manager
// node's key; it opens nowhere but that node. Printing it here is how the manager runtime — which
// does not read this database directly — is handed the envelope to open, and it discloses nothing a
// copy of the store did not already hold.
func licenceGrant(ctx context.Context, args []string) error {
if len(args) != 1 {
return errors.New("licence grant <name>")
}
name := args[0]
held, err := openLicences(ctx)
if err != nil {
return err
}
defer held.Close()
at, ok, err := held.RefreshGrant(ctx, name)
if err != nil {
return err
}
if !ok {
// Said, not printed as an empty object: a licence with no grant and a failed read must not
// look the same to whatever parses this.
return fmt.Errorf(
"%q has no refresh token stored; its manager adopts one first with `licence set-grant %s`",
name, name)
}
out, err := json.Marshal(envelopeOf(at))
if err != nil {
return err
}
fmt.Println(string(out))
return nil
}
// licenceSetGrant stores a refresh-token envelope the manager runtime produced — adoption, and the
// re-seal after a rotation done outside this process (novox/hq ADR 0050, Phase C).
//
// **It takes an envelope, never a refresh token.** The manager node reads the operator's refresh
// token, seals it at rest to its own key, and hands the sealed envelope here. So the one moment a
// refresh token is in the clear is on the manager node, never in the control plane — the same bound
// the whole carve-out keeps. This surface refuses anything that is not a complete envelope rather
// than storing half of one.
func licenceSetGrant(ctx context.Context, args []string) error {
set := flag.NewFlagSet("licence set-grant", flag.ContinueOnError)
from := set.String("file", "", "read the envelope 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 set-grant <name> [--file <path>]")
}
name := positionals[0]
env, err := readEnvelope(*from)
if err != nil {
return err
}
held, err := openLicences(ctx)
if err != nil {
return err
}
defer held.Close()
if err := held.SetRefreshGrant(ctx, name, env.atRest()); err != nil {
return err
}
fmt.Printf("%s now holds a refresh token for %s, encrypted at rest and readable by that node "+
"alone.\n the control plane stored the envelope without opening it\n",
"the manager", name)
return nil
}
// licenceSubmitRefresh publishes a refresh a MANAGER NODE already performed: the new access token is
// sealed to every holder, and a rotated refresh token replaces the stored envelope (novox/hq ADR
// 0050, Phase C).
//
// **This is the boundary the invariant rests on.** The manager runtime, on the manager node, opened
// the at-rest envelope with that node's key, called the vendor's OAuth endpoint, and produced this:
// the new access token in the clear, and — only if the vendor rotated it — the refresh token already
// re-sealed at rest. This reads exactly those two things and no refresh token in the clear ever
// reaches it, because it is never given one. The access token is sealed per holder and discarded,
// as any accepted key is; the rotated envelope is stored opaque.
func licenceSubmitRefresh(ctx context.Context, args []string) error {
set := flag.NewFlagSet("licence submit-refresh", flag.ContinueOnError)
accessFrom := set.String("access-file", "",
"read the new access token from a file instead of standard input")
grantFrom := set.String("grant-file", "",
"the rotated refresh-token envelope, if the vendor rotated it; omit if it did not")
positionals, err := parseAround(set, args)
if err != nil {
return err
}
if len(positionals) != 1 {
return errors.New(
"licence submit-refresh <name> [--access-file <path>] [--grant-file <path>]")
}
name := positionals[0]
var accessToken string
if *accessFrom != "" {
raw, err := os.ReadFile(*accessFrom)
if err != nil {
return err
}
accessToken = strings.TrimSpace(string(raw))
} else {
raw, err := readAllStdin()
if err != nil {
return err
}
accessToken = strings.TrimSpace(string(raw))
}
if accessToken == "" {
return errors.New("no access token was given, so there is nothing to seal")
}
// The rotated envelope is optional: absent, the stored refresh token is left exactly as it was.
var rotated *secrets.AtRest
if *grantFrom != "" {
env, err := readEnvelope(*grantFrom)
if err != nil {
return err
}
at := env.atRest()
rotated = &at
}
open, err := openStores(ctx)
if err != nil {
return err
}
defer open.Close()
held, err := open.Licences(ctx)
if err != nil {
return err
}
inv := open.inventory
sealed, err := held.SubmitRefresh(ctx, name, accessToken, rotated, func(node string) (string, error) {
return inv.SealingKeyOf(ctx, node)
})
if err != nil {
return err
}
rotatedNote := "the refresh token was left with its manager unchanged"
if rotated != nil {
rotatedNote = "the rotated refresh token replaced the stored envelope, still readable by the " +
"manager node alone"
}
fmt.Printf("submitted a refresh for %s: a new access token sealed to %d holder(s), and %s.\n"+
" run `push` to deliver it\n", name, sealed, rotatedNote)
return nil
}
// licenceRefresh mints a new access token for a refreshable-grant licence and seals it to every
// holder (novox/hq ADR 0050). The refresh token stays with the manager and is never delivered.
//
// The vendor's actual refresh is a plug-in this build does not ship (Phase C), so here this reports
// that plainly rather than pretending to have refreshed.
func licenceRefresh(ctx context.Context, args []string) error {
if len(args) != 1 {
return errors.New("licence refresh <name>")
}
name := args[0]
open, err := openStores(ctx)
if err != nil {
return err
}
defer open.Close()
held, err := open.Licences(ctx)
if err != nil {
return err
}
inv := open.inventory
sealed, err := held.Refresh(ctx, name, func(node string) (string, error) {
return inv.SealingKeyOf(ctx, node)
})
if err != nil {
return err
}
fmt.Printf("refreshed %s: a new access token sealed to %d holder(s), and the refresh token left "+
"with its manager.\n run `push` to deliver it\n", name, sealed)
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
}