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,235 @@
|
||||
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 <provider> <name> [--serves '{"model":"..."}']`)
|
||||
}
|
||||
provider, 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, provider, 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, provider, 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.Provider)
|
||||
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()
|
||||
|
||||
inv, err := openInventory(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer inv.Close()
|
||||
|
||||
sealed, err := held.Accept(ctx, name, value, func(node string) (string, error) {
|
||||
return inv.SealingKeyOf(ctx, node)
|
||||
})
|
||||
if err != nil {
|
||||
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
|
||||
}
|
||||
@@ -25,6 +25,7 @@ import (
|
||||
"github.com/novox/mesh-control/internal/catalogue"
|
||||
"github.com/novox/mesh-control/internal/identity"
|
||||
"github.com/novox/mesh-control/internal/inventory"
|
||||
"github.com/novox/mesh-control/internal/licences"
|
||||
"github.com/novox/mesh-control/internal/link"
|
||||
"github.com/novox/mesh-control/internal/overlay"
|
||||
"github.com/novox/mesh-control/internal/store"
|
||||
@@ -45,6 +46,7 @@ var held = []struct {
|
||||
}{
|
||||
{inventory.Name, inventory.Migrations},
|
||||
{identity.Name, identity.Migrations},
|
||||
{licences.Name, licences.Migrations},
|
||||
}
|
||||
|
||||
func main() {
|
||||
@@ -69,6 +71,8 @@ func run() error {
|
||||
return buildCommand(ctx, args[1:])
|
||||
case "builder":
|
||||
return builderCommand(ctx, args[1:])
|
||||
case "licence":
|
||||
return licenceCommand(ctx, args[1:])
|
||||
case "rotate":
|
||||
return rotateCommand(ctx, args[1:])
|
||||
case "builds":
|
||||
@@ -144,6 +148,7 @@ func usage() {
|
||||
build <repository> [--ref R] have a build machine build it, and record what came out
|
||||
builds [<module>] what has been built lately, and what came of it
|
||||
builder issue <name> a broker account for a build machine, scoped to build work
|
||||
licence add|list|use|key model access, under the name a person calls it
|
||||
rotate <provision> [--consumer <n>] a new credential for every holder, both ends at once
|
||||
pin <node> <provision> <from> which node this one gets a provision from
|
||||
unpin <node> <provision> put that question back
|
||||
@@ -1048,6 +1053,15 @@ func planFor(ctx context.Context, inv *inventory.Inventory, nodeName string) (ca
|
||||
if err != nil {
|
||||
return catalogue.Resolution{}, nil, err
|
||||
}
|
||||
|
||||
// What this mesh can answer with a record rather than a machine, and which record each of
|
||||
// this node's modules was put on. Read across a context boundary by name, which is what
|
||||
// crossing one is allowed to carry (novox/hq ADR 0008).
|
||||
world.Licences, world.Using, err = licencesFor(ctx, nodeName)
|
||||
if err != nil {
|
||||
return catalogue.Resolution{}, nil, err
|
||||
}
|
||||
|
||||
resolved, err := catalogue.Resolve(shelf, assigned,
|
||||
catalogue.Node{Name: nodeName, Site: site, Capabilities: capabilities,
|
||||
At: onNetwork[nodeName]}, world)
|
||||
@@ -1059,6 +1073,17 @@ func planFor(ctx context.Context, inv *inventory.Inventory, nodeName string) (ca
|
||||
// password a provider is told to create is the one its consumer was given — and sealed to
|
||||
// this node before it was ever written down, so nothing between here and there can read it.
|
||||
for i, n := range resolved.Needs {
|
||||
if n.ByRecord {
|
||||
// Answered by something the mesh holds, so there is no pair-wise secret between two
|
||||
// machines. Its key was supplied by a person and sealed to this node then; the mesh
|
||||
// discarded the plaintext and cannot make another.
|
||||
sealed, err := keyFor(ctx, n.From, nodeName, n.For)
|
||||
if err != nil {
|
||||
return catalogue.Resolution{}, nil, err
|
||||
}
|
||||
resolved.Needs[i].Sealed = sealed
|
||||
continue
|
||||
}
|
||||
secret, err := inv.SecretFor(ctx, n.Name, nodeName, n.From)
|
||||
if err != nil {
|
||||
// Said rather than skipped. A machine that resolves cleanly and receives no
|
||||
@@ -2315,3 +2340,79 @@ func builderCommand(ctx context.Context, args []string) error {
|
||||
fmt.Println("This is the only time it is shown.")
|
||||
return nil
|
||||
}
|
||||
|
||||
// openLicences connects to the context that holds which model access exists and who may use it.
|
||||
func openLicences(ctx context.Context) (*licences.Licences, error) {
|
||||
held, err := licences.Open(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := held.Ready(ctx, 30*time.Second); err != nil {
|
||||
held.Close()
|
||||
return nil, err
|
||||
}
|
||||
return held, nil
|
||||
}
|
||||
|
||||
// licencesFor is what this node can be answered with by record, and what it was put on.
|
||||
//
|
||||
// A mesh with no licences at all is the ordinary case and must not be an error: every existing
|
||||
// mesh is one, and a control plane that refused to plan because nobody had bought an API key
|
||||
// would be unusable for the thing it already does.
|
||||
func licencesFor(ctx context.Context, node string) (
|
||||
map[string][]catalogue.Record, map[string]map[string]catalogue.Record, error) {
|
||||
|
||||
held, err := openLicences(ctx)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
defer held.Close()
|
||||
|
||||
all, err := held.All(ctx)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
if len(all) == 0 {
|
||||
return nil, nil, nil
|
||||
}
|
||||
|
||||
offered := map[string][]catalogue.Record{}
|
||||
byName := map[string]catalogue.Record{}
|
||||
for _, one := range all {
|
||||
record := catalogue.Record{Name: one.Name, Serves: one.Serves}
|
||||
offered[licences.Provision] = append(offered[licences.Provision], record)
|
||||
byName[one.Name] = record
|
||||
}
|
||||
|
||||
using := map[string]map[string]catalogue.Record{}
|
||||
for _, one := range all {
|
||||
holders, err := held.HoldersOf(ctx, one.Name)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
for _, h := range holders {
|
||||
if h.Node != node {
|
||||
continue
|
||||
}
|
||||
if using[h.Module] == nil {
|
||||
using[h.Module] = map[string]catalogue.Record{}
|
||||
}
|
||||
using[h.Module][licences.Provision] = byName[one.Name]
|
||||
}
|
||||
}
|
||||
return offered, using, nil
|
||||
}
|
||||
|
||||
// keyFor is the licence key sealed to one machine, for one module.
|
||||
//
|
||||
// **Empty is not an error here.** The mesh discarded the plaintext when it was supplied, so a
|
||||
// holder recorded afterwards genuinely has no key — and the declaration refuses that by name,
|
||||
// where the module and the path are both in view, rather than here.
|
||||
func keyFor(ctx context.Context, licence, node, module string) (string, error) {
|
||||
held, err := openLicences(ctx)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
defer held.Close()
|
||||
return held.KeyFor(ctx, licence, node, module)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user