Files
mesh-catalog/modules/claude-licence-manager/cmd/manager/main.go
T
jschoubben cff1005422
mesh/delivery merged: source.main moved to novox/mesh-catalog@6d000647de2d
mesh/check being carried to cff1005422c1 on 6d000647de2d | from f9dcbc295723
P2: the licence manager is four parts of one manifest (issue 1409)
The one bundle that did everything becomes the manager bundle, which runs long and answers the seat's ten verbs and
the three acting tools, and a tools bundle of its own that answers the four reading tools and writes nothing, so they
answer while the manager fails to load or stops. Both import the module's one package for the layouts of the kept
data and the log entries, the key form, the entries' names and the settings with their validity; the settings file
and the account with its directory stay as they are.

Builds: design 39 parts §1, §2, §3, §4, §5, §6, §7, §8
2026-11-12 12:08:03 +01:00

419 lines
16 KiB
Go

// claude-licence-manager (novox/hq ADR 0183, ADR 0206, design 39): the licence manager's manager bundle (design 39
// parts §2), loaded by the module's runtime of its own under its own account (`runs-as claude-licence-manager`, design
// 39 bus-and-switch C1), never by the machine's tool runner, and speaking MCP to it over stdio through the Go SDK (ADR
// 0193, ADR 0198). It runs long (the conditions judged at its start, the usage timer, the switch rule): it watches what
// every node reports holding and adopts a login it does not hold, keeps every grant alive under a lease, and reads
// usage. It answers the seat's ten verbs and the module's three acting tools, and is the only part that writes the
// module's kept data and logs; the reading tools are the tools bundle's (cmd/tools, parts §3, §4).
//
// stdout is the MCP channel; everything this module says, it says on stderr.
package main
import (
"context"
"encoding/json"
"errors"
"fmt"
"os"
"path/filepath"
"strings"
"sync"
"time"
stdio "git.novox.be/novox/mesh-sdk/go"
. "claude-licence-manager/internal/licences"
)
// Seat is the role this module holds.
const Seat = "anthropic-licence-manager"
func say(format string, args ...any) {
fmt.Fprintf(os.Stderr, "[claude-licence-manager] "+format+"\n", args...)
}
func main() {
go daemon()
if err := stdio.Serve("", tools()); err != nil {
say("%v", err)
os.Exit(1)
}
}
// ---- what the binary is handed -----------------------------------------------------------------------
var (
built *Manager
buildMu sync.Mutex
)
// manager builds the rules' dependencies once, from the module's words (ADR 0192): file paths, never
// values. A failure is said and tried again on the next call, so a key pair that cannot be made yet is not fatal.
func manager() (*Manager, error) {
buildMu.Lock()
defer buildMu.Unlock()
if built != nil {
return built, nil
}
dir := os.Getenv("MESH_LICENCE_STATE")
if dir == "" {
return nil, errors.New("MESH_LICENCE_STATE is not set: the mesh renders it for this module")
}
keys, err := keypair(dir, say)
if err != nil {
return nil, err
}
node, _ := os.Hostname()
if n := os.Getenv("MESH_NODE"); n != "" {
node = n
}
built = &Manager{
Store: &BusStore{LicencesState: stdio.State("licences"), BindingsState: stdio.State("bindings"),
LeasesState: stdio.State("leases"), OfferedState: stdio.State("offered"), UsageLog: stdio.Log("usage"),
AuditLog: stdio.Log("audit"), Now: time.Now},
Vendor: LiveVendor(),
Grants: VaultGrants{Keys: keys},
Keys: keys,
AskGrant: func(_ context.Context, node, publicKey string) (GrantAnswer, error) {
var a GrantAnswer
raw, err := stdio.Ask("claude-code.claude_code_grant@"+node, map[string]any{"public_key": publicKey})
if err != nil {
return a, err
}
return a, json.Unmarshal(raw, &a)
},
Reported: heard.Of,
Emit: func(event string, body map[string]any) error { return stdio.Emit(event, body) },
Tell: func(_ context.Context, headline string, lines []string) error {
_, err := stdio.Ask("seat:operator-channel.tell", map[string]any{"subject": Seat, "headline": headline,
"lines": lines})
return err
},
Now: time.Now,
Log: say,
Holder: fmt.Sprintf("%s:%d", node, os.Getpid()),
Settings: SettingsFrom(os.Getenv("MESH_LICENCE_SETTINGS")),
SettingsFile: os.Getenv("MESH_LICENCE_SETTINGS"),
Conditions: stdio.State("conditions"),
Say: SayCondition,
}
return built, nil
}
// keypair is this module's own, made once in its state directory; the private half never leaves it.
// Written whole, then linked into place, so a second process reads the first's key and never half of it. A pair it
// finds and cannot read (another account's, left before the module ran as its own) is made anew in its place and said
// in its journal (design 39 bus-and-switch C2): every use passes the public key per ask, so nothing depends on the old.
func keypair(dir string, log func(string, ...any)) (KeyPair, error) {
file := filepath.Join(dir, "manager-key.json")
if _, err := os.Stat(file); errors.Is(err, os.ErrNotExist) {
if err := writeKeyPair(dir, file, false); err != nil {
return KeyPair{}, err
}
}
k, err := readKeyPair(file)
if err == nil {
return k, nil
}
log("the key pair %s cannot be read (%v); a new one is made in its place", file, err)
if err := writeKeyPair(dir, file, true); err != nil {
return KeyPair{}, err
}
return readKeyPair(file)
}
func readKeyPair(file string) (KeyPair, error) {
raw, err := os.ReadFile(file)
if err != nil {
return KeyPair{}, err
}
var k KeyPair
if err := json.Unmarshal(raw, &k); err != nil {
return KeyPair{}, err
}
if k.PublicKey == "" || k.PrivateKey == "" {
return KeyPair{}, errors.New("it holds no whole key pair")
}
return k, nil
}
// writeKeyPair writes a new pair whole beside file, then links it into place, or, replacing, renames it over the one
// that cannot be read.
func writeKeyPair(dir, file string, replace bool) error {
k, err := GenerateKeyPair()
if err != nil {
return err
}
raw, _ := json.Marshal(k)
tmp := filepath.Join(dir, fmt.Sprintf(".manager-key.%d.json", os.Getpid()))
if err := os.WriteFile(tmp, raw, 0o600); err != nil {
return err
}
if replace {
return os.Rename(tmp, file)
}
_ = os.Link(tmp, file) // fails when another made it first, which is right
_ = os.Remove(tmp)
return nil
}
// ---- the long-running half ---------------------------------------------------------------------------
// heard is what every node last said it holds (ADR 0206, design 39 bus-and-switch §23), by node.
var heard = &Heard{}
// judgedAtStart judges every condition anew at the start, before the manager asks what the nodes hold and starts its
// timers: raised again each it holds, cleared each key of `conditions` it no longer holds, carry-over-failed among
// them when present (design 39 parts §11, bus-and-switch §9). It waits for nothing to be carried over.
func judgedAtStart() {
<-stdio.Served()
m, err := manager()
if err != nil {
say("the conditions cannot be judged at the start: %v", err)
return
}
if err := m.SayConditions(context.Background(), true); err != nil {
say("saying the conditions at the start: %v", err)
}
}
func daemon() {
judgedAtStart()
var passing sync.Mutex
pass := func() {
passing.Lock() // one pass at a time: each account is leased, and a pass is cheap
defer passing.Unlock()
m, err := manager()
if err != nil {
say("not ready: %v", err)
return
}
adopted, err := m.Consider(context.Background(), heard.All())
if err != nil {
say("considering the reports failed: %v", err)
}
if len(adopted) > 0 {
say("adopted %s", strings.Join(adopted, ", "))
}
}
// What every node holds (ADR 0206, design 39 bus-and-switch §23): each `holdings-changed` heard is that node's
// latest report and starts a pass; once heard, every node is asked to say it again (B12: at every start, after the
// conditions are judged). Asked again until it answers — the channel to the runtime opens as the bundle starts.
go func() {
waits := []time.Duration{2 * time.Second, 5 * time.Second, 10 * time.Second, 30 * time.Second}
for attempt := 0; ; attempt++ {
err := stdio.Subscribe(HoldingsChanged, func(e stdio.Envelope) error {
if _, err := heard.Take(e.Body); err != nil {
say("%v", err)
return nil
}
go pass()
return nil
})
if err == nil {
say("hearing what the nodes hold")
if m, err := manager(); err != nil {
say("not ready to ask what the nodes hold: %v", err)
} else if err := m.AskHoldings(Itself); err != nil {
say("asking what the nodes hold could not be said: %v", err)
}
return
}
pause := time.Minute
if attempt < len(waits) {
pause = waits[attempt]
}
say("what the nodes hold cannot be heard yet (%v); asking again in %s", err, pause)
time.Sleep(pause)
}
}()
refresh := time.NewTicker(time.Minute)
// Every usage_minutes, read again at every reading (§24, §33): the manager's one timer of its own readings.
every := Defaults.UsageEvery
if m, err := manager(); err == nil {
every = m.settings().UsageEvery
}
usage := time.NewTimer(every)
for {
select {
case <-refresh.C:
if m, err := manager(); err == nil {
out, err := m.RotateDue(context.Background())
for _, r := range out {
b, _ := json.Marshal(r)
say("%s", b)
}
if err != nil {
say("refreshing failed: %v", err)
}
}
pass() // a login whose node did not answer last time is asked again
case <-usage.C:
if m, err := manager(); err == nil {
if err := m.ReadUsage(context.Background()); err != nil {
say("reading usage failed: %v", err)
}
every = m.settings().UsageEvery
}
usage.Reset(every)
}
}
}
// ---- the seat's verbs --------------------------------------------------------------------------------
func text(a map[string]any, k string) (string, error) {
v, _ := a[k].(string)
if strings.TrimSpace(v) == "" {
return "", fmt.Errorf("%s is required", k)
}
return strings.TrimSpace(v), nil
}
// verb is one of the seat's verbs: listed as `<seat>.<verb>`, so the runtime serves it on the seat's subject.
// What it changes is recorded as asked by its caller, as the runtime proved it (novox/hq issues 365 and 625).
func verb(name, description string, input map[string]any, run func(ctx context.Context, m *Manager, a map[string]any) (any, error)) stdio.Tool {
return stdio.Tool{Name: Seat + "." + name, Description: description, Input: input,
Run: func(a map[string]any) (any, error) {
m, err := manager()
if err != nil {
return nil, err
}
ctx := WithCause(context.Background(), Cause{Why: "the " + name + " verb", By: stdio.CallerOf(a)})
return run(ctx, m, a)
}}
}
// sinceOf is the argument since, RFC 3339; absent is the zero time.
func sinceOf(a map[string]any) (time.Time, error) {
v, _ := a["since"].(string)
if strings.TrimSpace(v) == "" {
return time.Time{}, nil
}
t, err := time.Parse(time.RFC3339Nano, strings.TrimSpace(v))
if err != nil {
return time.Time{}, fmt.Errorf("since is not a time in RFC 3339: %w", err)
}
return t, nil
}
// limitOf is the argument limit: def when absent or not positive, at most most.
func limitOf(a map[string]any, def, most int) int {
limit := def
if v, ok := a["limit"].(float64); ok && v > 0 {
limit = int(v)
}
return min(limit, most)
}
func str(description string) map[string]any {
return map[string]any{"type": "string", "description": description}
}
func two(a map[string]any, k1, k2 string) (string, string, error) {
v1, err1 := text(a, k1)
v2, err2 := text(a, k2)
return v1, v2, errors.Join(err1, err2)
}
func tools() []stdio.Tool {
consumer := str("the node's name")
return append([]stdio.Tool{
verb("licences", "Every licence the manager holds — account, kind, when its token and its refresh token expire, failures in a row, which consumers are bound to it, its latest usage with its reset times, its room or why it is not known, whether it is where a full licence's consumers move next, when and from where it was adopted, and its open conditions. Never a token.",
nil, func(ctx context.Context, m *Manager, _ map[string]any) (any, error) { return m.Licences(ctx) }),
verb("bindings", "Which licence each consumer (a node's agent, by the node's name) is bound to, and the generation it was last given; with consumer, that consumer's alone.",
map[string]any{"consumer": str("a consumer; absent for every one")},
func(ctx context.Context, m *Manager, a map[string]any) (any, error) {
c, _ := a["consumer"].(string)
return m.BindingsOf(ctx, strings.TrimSpace(c))
}),
verb("bind", "Bind a consumer — a node's agent, by the node's name — to a licence. Its node fetches the licence's token at once.",
map[string]any{"consumer": consumer, "licence": str("a licence, as `licences` names it")},
func(ctx context.Context, m *Manager, a map[string]any) (any, error) {
c, l, err := two(a, "consumer", "licence")
if err != nil {
return nil, err
}
return m.Bind(ctx, c, l)
}),
verb("switch", "Move a consumer to another licence. Its node fetches the new licence's token at once and points the agent's account at it. A login on a node does this by itself (ADR 0209); this is for moving one without a login, or back.",
map[string]any{"consumer": consumer, "licence": str("the licence to move to")},
func(ctx context.Context, m *Manager, a map[string]any) (any, error) {
c, l, err := two(a, "consumer", "licence")
if err != nil {
return nil, err
}
return m.Bind(ctx, c, l)
}),
verb("release", "Unbind a consumer. Its node keeps its last token, which expires within hours.",
map[string]any{"consumer": consumer},
func(ctx context.Context, m *Manager, a map[string]any) (any, error) {
c, err := text(a, "consumer")
if err != nil {
return nil, err
}
return m.Release(ctx, c)
}),
verb("refresh", "Refresh a licence now, or every due licence when none is named; under each licence's lease, so it never races the daemon. Answers the outcome, never a token.",
map[string]any{"licence": str("a licence; absent for every due one")},
func(ctx context.Context, m *Manager, a map[string]any) (any, error) {
if l, _ := a["licence"].(string); l != "" {
return m.Rotate(ctx, l, true)
}
return m.RotateDue(ctx)
}),
verb("usage", "Usage readings, the latest first, for every licence or one: the flattened reading with its three reset times, none before since, at most limit (default 20, at most 2000), and with raw the vendor's answer whole.",
map[string]any{"licence": str("a licence; absent for all"), "since": str("a time, RFC 3339: no reading before it"),
"limit": map[string]any{"type": "number", "description": "how many readings (default 20, at most 2000)"},
"raw": map[string]any{"type": "boolean", "description": "also the vendor's answer whole"}},
func(ctx context.Context, m *Manager, a map[string]any) (any, error) {
l, _ := a["licence"].(string)
since, err := sinceOf(a)
if err != nil {
return nil, err
}
raw, _ := a["raw"].(bool)
return m.Store.Usage(ctx, l, since, limitOf(a, 20, 2000), raw)
}),
verb("adopt", "Adopt an API key — never as an argument: from a file on the manager's node (`file`), or sealed to the manager's `public-key` by a node's claude-code (`sealed`; its `claude_code_add_api_key` does this). Subscriptions are adopted from the nodes' logins by themselves.",
map[string]any{"name": str("the licence's name"), "file": str("a file on the manager's node holding the key"),
"sealed": map[string]any{"type": "object", "description": "the key sealed to the manager's public key"},
"from": str("which node it came from, for the audit")},
func(ctx context.Context, m *Manager, a map[string]any) (any, error) {
n, err := text(a, "name")
if err != nil {
return nil, err
}
if raw, ok := a["sealed"]; ok && raw != nil {
b, _ := json.Marshal(raw)
var box SealedBox
if err := json.Unmarshal(b, &box); err != nil {
return nil, err
}
from, _ := a["from"].(string)
return m.AdoptSealedKey(ctx, n, box, "a node: "+from)
}
f, err := text(a, "file")
if err != nil {
return nil, errors.New("adopt takes a `file` on the manager's node, or a `sealed` key")
}
return m.AdoptKey(ctx, n, f)
}),
verb("public-key", "The manager's public key, PEM: what a node seals an API key or a login to before handing it over.",
nil, func(_ context.Context, m *Manager, _ map[string]any) (any, error) {
return map[string]any{"public_key": m.Keys.PublicKey}, nil
}),
verb("current", "For a consumer's agent module: which licence it is bound to, the licence's key, kind and generation, its health (ok, failing or expiring) and when its access token expires; never the token, which the vault hands. Null when it is bound to nothing.",
map[string]any{"consumer": consumer},
func(ctx context.Context, m *Manager, a map[string]any) (any, error) {
c, err := text(a, "consumer")
if err != nil {
return nil, err
}
return m.Current(ctx, c)
}),
}, moduleTools()...)
}