P2: the licence manager is four parts of one manifest (issue 1409)
mesh/delivery merged: source.main moved to novox/mesh-catalog@6d000647de2d
mesh/check being carried to cff1005422c1 on 6d000647de2d | from f9dcbc295723

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
This commit is contained in:
2026-11-12 12:08:03 +01:00
parent ce9f984363
commit cff1005422
41 changed files with 1433 additions and 822 deletions
+4 -1
View File
@@ -43,7 +43,10 @@ dist/
/modules/claude-code/claude-code
/modules/claude-code/cmd/claude-code/claude-code
/modules/claude-licence-manager/claude-licence-manager
/modules/claude-licence-manager/cmd/claude-licence-manager/claude-licence-manager
/modules/claude-licence-manager/cmd/manager/manager
/modules/claude-licence-manager/cmd/tools/tools
/modules/claude-licence-manager/manager
/modules/claude-licence-manager/tools
/modules/clipmenu/clipmenu-tools
/modules/clipmenu/cmd/clipmenu-tools/clipmenu-tools
/modules/cups/cmd/cups-tools/cups-tools
+15 -9
View File
@@ -5,9 +5,12 @@ rotation source, its consumers moved off a licence near its limit by the manager
ADR 0206, design 39 and its milestone bus-and-switch). Every grant and access token lives in the vault; this module
keeps none.
It runs as an account of its own (`runs-as claude-licence-manager`, design 39 bus-and-switch C1), in a runtime of its
own, never in the machine's tool runner: its service part (the conditions judged at its start, the usage timer, the switch
rule) and its tools beside it, so every state, log and secret below is issued to its own account. Its
It is one manifest of four parts (design 39 parts §1): the manager bundle (`manager`, `cmd/manager`), which runs long
(the conditions judged at its start, the usage timer, the switch rule) and answers the seat's verbs and every act; the
tools bundle (`tools`, `cmd/tools`), which answers the reading tools and writes nothing, so they answer while the
manager fails; the settings file; and the module's account with its directory. Both bundles run as an account of its
own (`runs-as claude-licence-manager`, design 39 bus-and-switch C1), in a runtime of its own, never in the machine's
tool runner, so every state, log and secret below is issued to its own account. Its
account is issued what its manifest names: the asks under `invokes` (the vault, the operator channel, mesh-config and
a node's `claude_code_grant`), the events under `emits` and `consumes`.
@@ -71,10 +74,11 @@ health, never the token) — through the console as `anthropic-licence-manager.<
## Its tools
`licences_switches` (every switch and why), `licences_read_usage` (read now, `dry`), `licences_switch_now` (apply the
rule now, even with `switch_auto` off), `licences_settings` (each value, its default and where it was set),
`licences_kept` (every entry it keeps, how full against its cap), `licences_resync` (say every binding, holdings.asked
and each condition again, and hand each access token to the vault again), and `licences_conditions`.
Read by the tools bundle: `licences_switches` (every switch and why), `licences_settings` (each value, its default
and where it was set), `licences_kept` (every entry it keeps, how full against its cap) and `licences_conditions`.
Acting, by the manager bundle: `licences_read_usage` (read now, `dry`), `licences_switch_now` (apply the rule now, even
with `switch_auto` off) and `licences_resync` (say every binding, holdings.asked and each condition again, and hand
each access token to the vault again).
## Settings
@@ -90,7 +94,9 @@ Through mesh-config, each `trusted`, read again at every reading: `cadence_minut
## Code and tests
Go, one binary (`cmd/claude-licence-manager`): the seat's verbs, the tools and the daemon in one launched bundle. The
Go, one Go module of two binaries: `cmd/manager` (`claude-licence-manager`: the seat's verbs, the acting tools and the
daemon) and `cmd/tools` (`claude-licence-manager-tools`: the reading tools), sharing the package `internal/licences`
(the layouts of the kept data and the log entries, the key form, the entries' names and the settings). The
sealed box is `claude-code`'s own format, byte for byte — the two modules carry the same `seal.go` — and a test opens
one sealed by the TypeScript agent module the Go one replaced, so the format is the one already on the machines.
@@ -98,7 +104,7 @@ one sealed by the TypeScript agent module the Go one replaced, so the format is
# the whole module on a real bus:
go test -tags integration ./...
The whole tests (`whole_integration_test.go`) start the built binary as its runtime does, its kept data in a real
The whole tests (`cmd/manager/whole_integration_test.go`) start the built binaries as their runtime does, its kept data in a real
nats-server. The vault, the agent modules, the
operator channel and the vendor are played beside it; a condition is read through the operator channel's client.
@@ -1,375 +0,0 @@
package main
// The module's own tools (design 39 bus-and-switch §37 to §44, B40): beside the seat's verbs, they read everything the
// manager keeps and start by hand everything it does by itself.
import (
"context"
"encoding/json"
"errors"
"fmt"
"slices"
"sort"
"strings"
"time"
stdio "git.novox.be/novox/mesh-sdk/go"
)
// tool is one of the module's own tools.
func tool(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: 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 + " tool", By: stdio.CallerOf(a)})
return run(ctx, m, a)
}}
}
func boolean(description string) map[string]any {
return map[string]any{"type": "boolean", "description": description}
}
func number(description string) map[string]any {
return map[string]any{"type": "number", "description": description}
}
func moduleTools() []stdio.Tool {
licence := str("a licence, as `licences` names it; absent for all")
dry := boolean("answer what the rule would do, and do nothing")
return []stdio.Tool{
tool("licences_switches", "Every switch of a consumer and why, newest first, from the audit: when, from and to which licence, the consumers, by whom and why, and for a switch by usage the reading that moved them.",
map[string]any{"licence": licence, "consumer": str("a consumer; absent for all"),
"since": str("a time, RFC 3339: no switch before it"), "limit": number("how many (default 20)")},
func(ctx context.Context, m *Manager, a map[string]any) (any, error) {
l, _ := a["licence"].(string)
c, _ := a["consumer"].(string)
since, err := sinceOf(a)
if err != nil {
return nil, err
}
return m.Switches(strings.TrimSpace(l), strings.TrimSpace(c), since, limitOf(a, 20, 2000))
}),
tool("licences_read_usage", "Read usage now, for one subscription or every one, as the manager does on its cadence; then apply the switch rule unless dry. Answers each reading and what the rule did or would do.",
map[string]any{"licence": licence, "dry": dry},
func(ctx context.Context, m *Manager, a map[string]any) (any, error) {
l, _ := a["licence"].(string)
d, _ := a["dry"].(bool)
return m.ReadUsageOf(ctx, strings.TrimSpace(l), d)
}),
tool("licences_switch_now", "Apply the switch rule now from the latest readings, for one licence or all, even with switch_auto off since a person asked; with dry, answer the moves and make none.",
map[string]any{"licence": licence, "dry": dry},
func(ctx context.Context, m *Manager, a map[string]any) (any, error) {
l, _ := a["licence"].(string)
d, _ := a["dry"].(bool)
acts, err := m.ApplyRule(ctx, strings.TrimSpace(l), true, d)
if acts == nil {
acts = []Switch{}
}
return acts, err
}),
tool("licences_settings", "The settings in force: each one's value, its default, where it was set (mesh, node or the default, as mesh-config answers it) and why it is invalid when it is.",
nil, func(ctx context.Context, m *Manager, _ map[string]any) (any, error) {
return m.SettingsInForce(ctx)
}),
tool("licences_kept", "Every entry the manager keeps on the bus: a state's keys, or a log's subjects with their last entry; with key, that key's value or that subject's last entry; and how full each is against its cap.",
map[string]any{"entry": str("one of licences, bindings, leases, offered, usage, audit, carried, conditions; absent for all"),
"key": str("a key of a state, or a subject of a log")},
func(ctx context.Context, m *Manager, a map[string]any) (any, error) {
e, _ := a["entry"].(string)
k, _ := a["key"].(string)
return m.Kept(strings.TrimSpace(e), strings.TrimSpace(k))
}),
tool("licences_resync", "Recover: say binding.changed again for every binding, ask every node what it holds, say again each condition held, and hand each licence's access token to the vault again. Answers what it said.",
nil, func(ctx context.Context, m *Manager, _ map[string]any) (any, error) {
return m.Resync(ctx)
}),
tool("licences_conditions", "The conditions the manager said and holds: each key of its state conditions with its body and since.",
nil, func(_ context.Context, m *Manager, _ map[string]any) (any, error) {
return m.KeptConditions()
}),
}
}
// Switches are the audit's switches, newest first: of a licence (from or to it) and a consumer when named, none before
// since, at most limit (§37).
func (m *Manager) Switches(licence, consumer string, since time.Time, limit int) ([]map[string]any, error) {
bus, ok := m.Store.(*BusStore)
if !ok {
return nil, errors.New("the audit is not kept on the bus")
}
info, err := bus.AuditLog.Info()
if err != nil || info.Last == 0 {
return []map[string]any{}, err
}
var all []stdio.LogEntry
for next := uint64(1); next != 0; {
page, err := bus.AuditLog.Read(next, "switched", 0)
if err != nil {
return nil, err
}
all = append(all, page.Entries...)
next = page.Next
}
out := []map[string]any{}
for i := len(all) - 1; i >= 0 && len(out) < limit; i-- {
var e map[string]any
if json.Unmarshal(all[i].Value, &e) != nil {
continue
}
at, _ := time.Parse(time.RFC3339Nano, fmt.Sprint(e["at"]))
if at.Before(since) {
continue
}
if licence != "" && e["from"] != licence && e["to"] != licence {
continue
}
consumers, _ := e["consumers"].([]any)
if consumer != "" && !slices.Contains(consumers, any(consumer)) {
continue
}
s := map[string]any{"at": e["at"], "from": e["from"], "to": e["to"], "consumers": e["consumers"],
"by": e["by"], "why": e["why"]}
if _, ok := e["window"]; ok {
s["reading"] = map[string]any{"window": e["window"], "pct": e["pct"], "resetsAt": e["resetsAt"],
"room": e["room"]}
}
out = append(out, s)
}
return out, nil
}
// SettingAnswer is one setting as licences_settings answers it (§40).
type SettingAnswer struct {
Setting string `json:"setting"`
Value any `json:"value"`
Default any `json:"default"`
Level string `json:"level"`
Invalid string `json:"invalid,omitempty"`
}
// SettingsInForce is each setting in force, its default, where mesh-config says it was set, and why it is invalid
// when it is (§40, B18).
func (m *Manager) SettingsInForce(ctx context.Context) ([]SettingAnswer, error) {
levels := map[string]string{}
if m.ConfigLevels != nil {
got, err := m.ConfigLevels(ctx)
if err != nil {
return nil, fmt.Errorf("mesh-config did not say where each setting was set: %w", err)
}
levels = got
}
s := m.settings()
held, err := m.Judge(ctx)
if err != nil {
return nil, err
}
values, defaults := s.Values(), Defaults.Values()
var out []SettingAnswer
for k, v := range values {
a := SettingAnswer{Setting: k, Value: v, Default: defaults[k], Level: levels[k]}
if a.Level == "" {
a.Level = "default"
}
if c, ok := held["settings."+k+".invalid"]; ok {
a.Invalid = c.Summary
}
out = append(out, a)
}
sort.Slice(out, func(i, j int) bool { return out[i].Setting < out[j].Setting })
return out, nil
}
// kept are the entries the manager keeps on the bus, by their local names in the manifest (§2 to §9).
func (m *Manager) kept() (map[string]keptState, map[string]keptLog) {
states, logs := map[string]keptState{}, map[string]keptLog{}
if bus, ok := m.Store.(*BusStore); ok {
states["licences"], states["bindings"] = bus.LicencesState, bus.BindingsState
states["leases"], states["offered"] = bus.LeasesState, bus.OfferedState
logs["usage"], logs["audit"] = bus.UsageLog, bus.AuditLog
}
if m.Carried != nil {
states["carried"] = m.Carried
}
if m.Conditions != nil {
states["conditions"] = m.Conditions
}
return states, logs
}
// Kept answers one entry the manager keeps, or every one (§41): a state's keys and their count, a log's subjects with
// their last entry and its fill against its cap; with key, one key's value or one subject's last entry.
func (m *Manager) Kept(entry, key string) (map[string]any, error) {
states, logs := m.kept()
out := map[string]any{}
names := []string{}
for n := range states {
names = append(names, n)
}
for n := range logs {
names = append(names, n)
}
sort.Strings(names)
if entry != "" && !slices.Contains(names, entry) {
return nil, fmt.Errorf("the manager keeps no entry %q: it keeps %s", entry, strings.Join(names, ", "))
}
for _, n := range names {
if entry != "" && n != entry {
continue
}
if st, ok := states[n]; ok {
a, err := keptStateAnswer(st, key)
if err != nil {
return nil, fmt.Errorf("%s: %w", n, err)
}
out[n] = a
continue
}
a, err := keptLogAnswer(logs[n], key)
if err != nil {
return nil, fmt.Errorf("%s: %w", n, err)
}
out[n] = a
}
return out, nil
}
func keptStateAnswer(st keptState, key string) (map[string]any, error) {
keys, err := st.Keys()
if err != nil {
return nil, err
}
sort.Strings(keys)
a := map[string]any{"kind": "state", "keys": keys, "fill": map[string]any{"keys": len(keys), "cap": "none declared"}}
if key != "" {
e, err := st.Get(key)
if err != nil {
return nil, err
}
if e == nil {
a["value"] = nil
} else {
a["value"] = e.Value
}
}
return a, nil
}
func keptLogAnswer(l keptLog, key string) (map[string]any, error) {
info, err := l.Info()
if err != nil {
return nil, err
}
a := map[string]any{"kind": "log", "fill": map[string]any{"entries": info.Entries, "bytes": info.Bytes,
"maxBytes": info.MaxBytes}}
if key != "" {
e, err := l.Last(key)
if err != nil {
return nil, err
}
a["last"] = e
return a, nil
}
newest, err := l.Newest("")
if err != nil {
return nil, err
}
subjects := map[string]any{}
for _, e := range newest {
subjects[e.Key] = e
}
a["subjects"] = subjects
return a, nil
}
// Resync says again what a consumer, the vault or a node may have missed (§42): binding.changed for every binding,
// holdings.asked, each condition held, and licence-put of each licence's access token. Answers what it said.
func (m *Manager) Resync(ctx context.Context) (map[string]any, error) {
bindings, err := m.Store.Bindings(ctx)
if err != nil {
return nil, err
}
said := map[string]any{}
consumers := []string{}
for _, b := range bindings {
m.changed(b, b.Licence)
consumers = append(consumers, b.Consumer)
}
said["binding.changed"] = consumers
if err := m.AskHoldings(CauseOf(ctx).By); err != nil {
return said, fmt.Errorf("holdings.asked could not be said: %w", err)
}
said["holdings.asked"] = true
if err := m.SayConditions(ctx, true); err != nil {
return said, err
}
conditions, err := m.KeptConditions()
if err != nil {
return said, err
}
keys := []string{}
for _, c := range conditions {
keys = append(keys, c.Body.Key)
}
said["conditions"] = keys
licences, err := m.Store.Licences(ctx)
if err != nil {
return said, err
}
put := []string{}
var errs []error
for _, l := range licences {
if l.Generation == 0 {
continue
}
key := KeyFor(l.Name)
// The access token is built from the grant, which the vault hands the manager; it hands a licence's access
// token only to a machine a binding names (§17), and the manager's machine may have none.
access, err := m.Grants.Get(ctx, key, PartGrant)
if err == nil && l.Kind == "subscription" {
var g FullGrant
if err = json.Unmarshal([]byte(access), &g); err == nil {
access = accessOf(g)
}
}
if err == nil {
err = m.Grants.Put(ctx, key, PartAccess, l.Generation, access)
}
access = ""
if err != nil {
errs = append(errs, fmt.Errorf("the access token of %s: %w", l.Name, err))
continue
}
put = append(put, l.Name)
}
said["licence-put"] = put
return said, errors.Join(errs...)
}
// KeptConditions is each key of the state conditions with its body and since (§43), by key.
func (m *Manager) KeptConditions() ([]KeptCondition, error) {
out := []KeptCondition{}
if m.Conditions == nil {
return out, nil
}
keys, err := m.Conditions.Keys()
if err != nil {
return nil, err
}
sort.Strings(keys)
for _, k := range keys {
e, err := m.Conditions.Get(k)
if err != nil {
return nil, err
}
var c KeptCondition
if e == nil || json.Unmarshal(e.Value, &c) != nil {
continue
}
out = append(out, c)
}
return out, nil
}
@@ -13,43 +13,22 @@ import (
"time"
stdio "git.novox.be/novox/mesh-sdk/go"
. "claude-licence-manager/internal/licences"
)
// keptState is a state this module keeps, as mesh-sdk's KeptState answers it.
type keptState interface {
Get(key string) (*stdio.StateEntry, error)
Put(key string, value any) (uint64, error)
PutTTL(key string, value any, ttl time.Duration) (uint64, error)
Create(key string, value any) (uint64, error)
CreateTTL(key string, value any, ttl time.Duration) (uint64, error)
Delete(key string) error
Keys() ([]string, error)
History(key string) ([]stdio.StateRevision, error)
}
// BindingState is the value of a consumer's key in `bindings` (§3, B22: the form 0 its readers decode): no secret,
// only what it should hold, which generation, since when, why and by whom.
type BindingState struct {
Licence string `json:"licence"`
Kind string `json:"kind"`
Generation int64 `json:"generation"`
Since string `json:"since,omitempty"`
Why string `json:"why,omitempty"`
By string `json:"by,omitempty"`
}
// BusStore keeps the manager's data in the bus's own storage.
type BusStore struct {
LicencesState keptState
BindingsState keptState
LeasesState keptState
OfferedState keptState
UsageLog keptLog
AuditLog keptLog
LicencesState State
BindingsState State
LeasesState State
OfferedState State
UsageLog Log
AuditLog Log
Now func() time.Time
}
func (s *BusStore) get(st keptState, key string, into any) (bool, error) {
func (s *BusStore) get(st State, key string, into any) (bool, error) {
e, err := st.Get(key)
if err != nil || e == nil || len(e.Value) == 0 || string(e.Value) == "null" {
return false, err
@@ -185,16 +164,10 @@ func (s *BusStore) Advance(ctx context.Context, licence string, m Move) ([]Bindi
// ---- leases and offers (§4, §5) ---------------------------------------------------------------------------------
// leaseValue is a lease's key in `leases`: its holder and until when.
type leaseValue struct {
Holder string `json:"holder"`
Until string `json:"until"`
}
// Lease takes a lease by creating its key with a lifetime of d (§4): a create fails while another holds it, the bus's
// own compare-and-set. A key its own holder holds is taken again.
func (s *BusStore) Lease(_ context.Context, key, holder string, d time.Duration) (bool, error) {
v := leaseValue{Holder: holder, Until: s.Now().Add(d).UTC().Format(time.RFC3339)}
v := LeaseValue{Holder: holder, Until: s.Now().Add(d).UTC().Format(time.RFC3339)}
_, err := s.LeasesState.CreateTTL(key, v, d)
if err == nil {
return true, nil
@@ -202,7 +175,7 @@ func (s *BusStore) Lease(_ context.Context, key, holder string, d time.Duration)
if !errors.Is(err, stdio.ErrChanged) {
return false, err
}
var held leaseValue
var held LeaseValue
if ok, gerr := s.get(s.LeasesState, key, &held); gerr != nil || !ok || held.Holder != holder {
return false, gerr
}
@@ -212,7 +185,7 @@ func (s *BusStore) Lease(_ context.Context, key, holder string, d time.Duration)
// Unlease deletes a lease its holder holds; another's is left to it or its lifetime.
func (s *BusStore) Unlease(_ context.Context, key, holder string) error {
var held leaseValue
var held LeaseValue
if ok, err := s.get(s.LeasesState, key, &held); err != nil || !ok || held.Holder != holder {
return err
}
@@ -222,20 +195,11 @@ func (s *BusStore) Unlease(_ context.Context, key, holder string) error {
// offeredFor is how long the outcome of an offered grant is kept (§5): longer than a refresh token lives.
const offeredFor = 90 * 24 * time.Hour
// offeredValue is a grant's key in `offered`: what became of it, where and when (§5, B13: every outcome).
type offeredValue struct {
Node string `json:"node"`
Account string `json:"account"`
Outcome Outcome `json:"outcome"`
Why string `json:"why"`
At string `json:"at"`
}
// OfferedKey is a fingerprint's key in `offered`: through the licence key form (B13).
func OfferedKey(fingerprint string) string { return KeyFor(fingerprint) }
func (s *BusStore) Outcome(_ context.Context, fingerprint string) (Outcome, error) {
var v offeredValue
var v OfferedValue
if ok, err := s.get(s.OfferedState, OfferedKey(fingerprint), &v); err != nil || !ok {
return "", err
}
@@ -243,37 +207,17 @@ func (s *BusStore) Outcome(_ context.Context, fingerprint string) (Outcome, erro
}
func (s *BusStore) RecordOutcome(_ context.Context, fingerprint, node, account string, o Outcome, why string) error {
return s.putOffered(fingerprint, offeredValue{Node: node, Account: account, Outcome: o, Why: why,
return s.putOffered(fingerprint, OfferedValue{Node: node, Account: account, Outcome: o, Why: why,
At: s.Now().UTC().Format(time.RFC3339)}, offeredFor)
}
func (s *BusStore) putOffered(fingerprint string, v offeredValue, ttl time.Duration) error {
func (s *BusStore) putOffered(fingerprint string, v OfferedValue, ttl time.Duration) error {
_, err := s.OfferedState.PutTTL(OfferedKey(fingerprint), v, ttl)
return err
}
// ---- the logs (§6, §7) ------------------------------------------------------------------------------------------
// keptLog is a log this module keeps, as mesh-sdk's KeptLog answers it.
type keptLog interface {
Append(key string, value any) (uint64, error)
Read(from uint64, keys string, limit int) (*stdio.LogPage, error)
Last(key string) (*stdio.LogEntry, error)
Info() (*stdio.LogInfo, error)
Newest(keys string) ([]stdio.LogEntry, error)
}
// UsageEntry is one entry of the log `usage` (§6): when, the flattened reading and the vendor's answer whole (ADR 0054).
type UsageEntry struct {
At string `json:"at"`
Reading UsageReading `json:"reading"`
Raw map[string]any `json:"raw,omitempty"`
}
// AuditKinds are the subjects of the log `audit` (§7); an act of another kind is kept under `drift`.
var AuditKinds = []string{"adopted", "refused", "failed", "rotated", "bound", "switched", "released", "drift", "carried",
"settings"}
func (s *BusStore) RecordUsage(_ context.Context, licence string, at int64, r UsageReading, raw map[string]any) error {
_, err := s.UsageLog.Append(KeyFor(licence), UsageEntry{At: time.UnixMilli(at).UTC().Format(time.RFC3339Nano),
Reading: r, Raw: raw})
@@ -324,7 +268,7 @@ func (s *BusStore) Usage(ctx context.Context, licence string, since time.Time, l
// newest is up to limit entries of keys, newest first: read back from the log's last sequence, a window that doubles
// until it holds limit entries or reaches the first.
func (s *BusStore) newest(l keptLog, keys string, limit int) ([]stdio.LogEntry, error) {
func (s *BusStore) newest(l Log, keys string, limit int) ([]stdio.LogEntry, error) {
info, err := l.Info()
if err != nil || info.Last == 0 {
return nil, err
@@ -11,6 +11,8 @@ import (
"time"
stdio "git.novox.be/novox/mesh-sdk/go"
. "claude-licence-manager/internal/licences"
)
// memState is a state of the bus as the runtime answers it, in memory: values, their past values, keys' lifetimes.
@@ -205,7 +207,7 @@ func TestALeaseAndAnOfferAreKeptByTheirKeysAndLifetimes(t *testing.T) {
if strings.ContainsAny(fp, ":") || !strings.HasPrefix(fp, "sha256_") {
t.Fatalf("the offered key %q is not in the key form", fp)
}
var v offeredValue
var v OfferedValue
_ = json.Unmarshal(mm.offered.values[fp], &v)
if v.Outcome != Dead || v.Node != "laptop" || v.Account != "acct-1" || v.Why == "" || v.At == "" {
t.Fatalf("offered %+v", v)
@@ -17,20 +17,13 @@ import (
occ "git.novox.be/novox/mesh-contracts/operator-and-conversation/go/clients/operatorchannelclient"
oc "git.novox.be/novox/mesh-contracts/operator-and-conversation/go/seats/operatorchannel"
stdio "git.novox.be/novox/mesh-sdk/go"
)
// Publisher is the name the manager says its conditions under, and the id of their subject (B17).
const Publisher = "claude-licence-manager"
. "claude-licence-manager/internal/licences"
)
// HeadlineLength is the operator channel's limit on a headline; a longer one is cut, ending in "…" (B39).
const HeadlineLength = 60
// KeptCondition is the value of a key of `conditions` (§9): the condition said raised, and since when.
type KeptCondition struct {
Body oc.Condition `json:"body"`
Since string `json:"since"`
}
// SayCondition publishes a condition on the operator channel's condition accept, under the manager's name, in the form
// its client encodes (ADR 0437 §9).
func SayCondition(c oc.Condition) error {
@@ -89,6 +82,7 @@ func machines(n int) string {
func (m *Manager) Judge(ctx context.Context) (map[string]oc.Condition, error) {
held := map[string]oc.Condition{}
s := m.settings()
reasons := s.Reasons()
for _, o := range s.OutOfBound {
bound := fmt.Sprintf("at least %g", o.Min)
if o.HasMax {
@@ -96,7 +90,7 @@ func (m *Manager) Judge(ctx context.Context) (map[string]oc.Condition, error) {
}
key := "settings." + o.Setting + ".invalid"
held[key] = condition(key, occ.SeverityWarning, o.Setting+" is outside its bound",
fmt.Sprintf("%s is %g, outside its bound; its default %g is used", o.Setting, o.Value, o.Default),
reasons[o.Setting],
fmt.Sprintf("set %s %s", o.Setting, bound),
fmt.Sprintf("%s is set to %g; the licence manager uses its default %g until it is fixed.", o.Setting,
o.Value, o.Default), o.Setting+" is within its bound again")
@@ -107,7 +101,7 @@ func (m *Manager) Judge(ctx context.Context) (map[string]oc.Condition, error) {
continue
}
held[key] = condition(key, occ.SeverityWarning, p.Setting+" is above switch_at_pct",
fmt.Sprintf("%s is %g, above switch_at_pct %g; no consumer is moved", p.Setting, p.Value, p.SwitchAt),
reasons[p.Setting],
fmt.Sprintf("set %s at or below switch_at_pct (%g)", p.Setting, p.SwitchAt),
fmt.Sprintf("%s is %g, above switch_at_pct %g; the licence manager moves no consumer until it is fixed.",
p.Setting, p.Value, p.SwitchAt), p.Setting+" is at or below switch_at_pct again")
@@ -172,7 +166,7 @@ func (m *Manager) judgeLicence(l Licence, licences []Licence, bindings []Binding
if len(consumers) > 0 {
needs = list + " are not moved off it while its usage is unknown"
}
why := l.Usage.readError()
why := l.Usage.ReadErrorWords()
held[key+"usage-unread"] = condition(key+"usage-unread", occ.SeverityWarning,
short(l.Name)+"'s usage cannot be read",
fmt.Sprintf("the usage of %s cannot be read: %s", l.Name, why), needs,
@@ -182,14 +176,6 @@ func (m *Manager) judgeLicence(l Licence, licences []Licence, bindings []Binding
return held
}
// readError is the vendor's reason a reading failed, or that none was given.
func (u *UsageReading) readError() string {
if u == nil || u.ReadError == "" {
return "no reason given"
}
return u.ReadError
}
// unread is whether subscription l's latest successful reading is older than usage_stale_minutes (§31), or, never
// read successfully, its adoption is (B38); and the words naming since when.
func (m *Manager) unread(l Licence, s Settings) (string, bool) {
@@ -12,6 +12,8 @@ import (
occ "git.novox.be/novox/mesh-contracts/operator-and-conversation/go/clients/operatorchannelclient"
oc "git.novox.be/novox/mesh-contracts/operator-and-conversation/go/seats/operatorchannel"
. "claude-licence-manager/internal/licences"
)
// saying gives mm's manager a state `conditions` and records each condition it says, in the form the operator
@@ -0,0 +1,24 @@
package main
// A licence's key taken by another (design 39 bus-and-switch §10, B6); the key form itself is the shared package's.
import (
"context"
. "claude-licence-manager/internal/licences"
)
// keyTaken names the licence other than name whose key is name's, or "" when none is.
func (m *Manager) keyTaken(ctx context.Context, name string) (string, error) {
all, err := m.Store.Licences(ctx)
if err != nil {
return "", err
}
key := KeyFor(name)
for _, l := range all {
if l.Name != name && KeyFor(l.Name) == key {
return l.Name, nil
}
}
return "", nil
}
@@ -5,23 +5,9 @@ import (
"strings"
"testing"
"time"
)
// design 39 bus-and-switch §10, B6.
func TestALicencesKeyForm(t *testing.T) {
for name, key := range map[string]string{
"jane@example.org": "jane_at_example.org",
"Jane.Doe@Example.ORG": "jane.doe_at_example.org",
"0c1d2e3f-4a5b-6c7d-8e9f-a0b1c2d3e4f5": "0c1d2e3f-4a5b-6c7d-8e9f-a0b1c2d3e4f5",
"team key_1": "team_key_1",
"a+b/c:d": "a_b_c_d",
"ops-key": "ops-key",
} {
if got := KeyFor(name); got != key {
t.Errorf("KeyFor(%q) = %q, want %q", name, got, key)
}
}
}
. "claude-licence-manager/internal/licences"
)
// design 39 bus-and-switch §10: an adoption whose key another licence has is refused, in the audit and as an event.
func TestAnAdoptionWhoseKeyIsTakenIsRefused(t *testing.T) {
@@ -1,9 +1,10 @@
// claude-licence-manager (novox/hq ADR 0183, ADR 0206, design 39): one binary, launched by a 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). Its service part 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. Its tools part, the
// seat's verbs and the module's tools, is served beside it and acts only through that one running manager.
// 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
@@ -20,6 +21,8 @@ import (
"time"
stdio "git.novox.be/novox/mesh-sdk/go"
. "claude-licence-manager/internal/licences"
)
// Seat is the role this module holds.
@@ -68,10 +71,9 @@ func manager() (*Manager, error) {
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(),
Carried: stdio.State("carried"),
Grants: VaultGrants{Keys: keys},
Keys: keys,
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})
@@ -94,34 +96,10 @@ func manager() (*Manager, error) {
SettingsFile: os.Getenv("MESH_LICENCE_SETTINGS"),
Conditions: stdio.State("conditions"),
Say: SayCondition,
ConfigLevels: func(context.Context) (map[string]string, error) { return configLevels(node) },
}
return built, nil
}
// configLevels asks mesh-config where each of the manager's own settings was set on its node (§40, B18): the source
// of each resolved value, by its key.
func configLevels(node string) (map[string]string, error) {
raw, err := stdio.Ask("seat:mesh-config.config", map[string]any{"action": "list", "module": Publisher, "node": node})
if err != nil {
return nil, err
}
var answer struct {
Resolved []struct {
Key string `json:"key"`
Source string `json:"source"`
} `json:"resolved"`
}
if err := json.Unmarshal(raw, &answer); err != nil {
return nil, fmt.Errorf("mesh-config's answer: %w", err)
}
levels := map[string]string{}
for _, r := range answer.Resolved {
levels[r.Key] = r.Source
}
return levels, 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
@@ -27,6 +27,8 @@ import (
"time"
oc "git.novox.be/novox/mesh-contracts/operator-and-conversation/go/seats/operatorchannel"
. "claude-licence-manager/internal/licences"
)
// Fingerprint names a token without being one: the agent module's own fingerprint, so the two compare.
@@ -137,8 +139,6 @@ type GrantAnswer struct {
type Manager struct {
Store Store
Vendor Vendor
// Carried is the state `carried`, the carry-over's record, which nothing writes again (design 39 parts §10).
Carried keptState
// Grants is the vault, which holds every licence's grant and access token (§15, §16).
Grants Grants
Keys KeyPair
@@ -161,12 +161,9 @@ type Manager struct {
settingsMu sync.RWMutex
// Conditions is the state `conditions` (design 39 bus-and-switch §9), and Say says one condition on the operator
// channel; either nil says none.
Conditions keptState
Conditions State
Say func(c oc.Condition) error
conditionsMu sync.Mutex
// ConfigLevels is where mesh-config says each setting was set on the manager's node (mesh, node or default), by
// setting (§40, B18); nil answers every one as the default.
ConfigLevels func(ctx context.Context) (map[string]string, error)
}
const leaseFor = 2 * time.Minute
@@ -952,7 +949,7 @@ func (m *Manager) Licences(ctx context.Context) ([]map[string]any, error) {
if err != nil {
return nil, err
}
open, err := m.KeptConditions()
open, err := KeptConditions(m.Conditions)
if err != nil {
return nil, err
}
@@ -10,6 +10,8 @@ import (
"sync"
"testing"
"time"
. "claude-licence-manager/internal/licences"
)
var t0 = time.Date(2026, 10, 4, 12, 0, 0, 0, time.UTC)
@@ -2,12 +2,16 @@ package main
import (
"encoding/json"
"fmt"
"os"
"path/filepath"
"regexp"
"slices"
"sort"
"strings"
"testing"
. "claude-licence-manager/internal/licences"
)
// design 39 bus-and-switch §1: the module moves to the new form whole — no `reads`, no old `state`, no `prepare`
@@ -174,7 +178,9 @@ func TestTheManagerRunsAsItsOwnAccount(t *testing.T) {
if !made {
t.Errorf("the manager does not make its account: no user resource named %s", Publisher)
}
files, _ := filepath.Glob("*.go")
files, _ := filepath.Glob("../*/*.go")
shared, _ := filepath.Glob("../../internal/*/*.go")
files = append(files, shared...)
asked := regexp.MustCompile(`stdio\.Ask\("([^"]+)"`)
for _, f := range files {
if strings.HasSuffix(f, "_test.go") {
@@ -268,3 +274,104 @@ func TestTheCarryOverHasLeft(t *testing.T) {
t.Fatal(err)
}
}
// design 39 parts §1 to §4, §6 to §8: the module is four parts in one manifest. Its artifacts are exactly the manager
// bundle, the tools bundle and the templates archive, with their words; its resources exactly the account, its
// directory and the settings file, in that order; no process or container resource, no run-once step, no bus account
// of its own and no runtime-health; its tools are the four reading ones and the three acting ones; files/ holds only
// the settings template.
func TestTheModuleIsFourParts(t *testing.T) {
raw, err := os.ReadFile(filepath.Join(moduleDir, "module.json"))
if err != nil {
t.Fatal(err)
}
var m map[string]any
if err := json.Unmarshal(raw, &m); err != nil {
t.Fatal(err)
}
same := func(what string, got, want any) {
t.Helper()
g, _ := json.Marshal(got)
w, _ := json.Marshal(want)
if string(g) != string(w) {
t.Errorf("%s is %s, want %s", what, g, w)
}
}
build, _ := m["build"].(map[string]any)
same("build.artifacts", build["artifacts"], []map[string]any{
{"name": "manager", "kind": "bundle", "language": "go", "system": "arch", "from": "cmd/manager",
"binary": "claude-licence-manager", "loads": []string{"claude-licence-manager"},
"env": map[string]string{"MESH_LICENCE_STATE": "${dir:state}", "MESH_LICENCE_SETTINGS": "${dir:state}/settings.json"}},
{"name": "tools", "kind": "bundle", "language": "go", "system": "arch", "from": "cmd/tools",
"binary": "claude-licence-manager-tools", "loads": []string{"claude-licence-manager-tools"}},
{"name": "files", "kind": "archive", "from": "files"},
})
same("resources", m["resources"], []map[string]any{
{"id": "account", "type": "user", "name": Publisher, "shell": "/usr/bin/nologin", "home": "/var/lib/" + Publisher},
{"id": "state", "type": "directory", "mode": "0700", "owner": Publisher, "place": ".", "unknown": "program"},
{"id": "settings", "type": "file", "path": "${dir:state}/settings.json", "mode": "0600", "owner": Publisher,
"takes-config": []string{"cadence_minutes", "floor_minutes", "failures_to_notify", "cooldown_hours",
"refresh_warn_days", "switch_auto", "switch_at_pct", "room_below_pct", "warn_at_pct", "usage_minutes",
"usage_stale_minutes"}, "merge": "json", "template": "files/settings.json"},
})
tools, _ := m["tools"].([]any)
var names []string
for _, n := range tools {
names = append(names, fmt.Sprint(n))
}
sort.Strings(names)
same("tools", names, []string{"licences_conditions", "licences_kept", "licences_read_usage", "licences_resync",
"licences_settings", "licences_switch_now", "licences_switches"})
same("runs-as", m["runs-as"], Publisher)
same("secrets-owner", m["secrets-owner"], Publisher)
same("own-secrets", m["own-secrets"], map[string]string{"broker": "${dir:state}/broker"})
if _, ok := m["runtime-health"]; ok {
t.Error("the manifest adds runtime-health: the node-engine's judgement that the runtime is alive is its health")
}
var walk func(path string, v any)
walk = func(path string, v any) {
switch v := v.(type) {
case map[string]any:
for k, e := range v {
if k == "bus" || k == "run-once" {
t.Errorf("the manifest says %s at %s", k, path)
}
if k == "type" && (e == "process" || e == "container") {
t.Errorf("the manifest has a %v resource at %s", e, path)
}
walk(path+"."+k, e)
}
case []any:
for i, e := range v {
walk(fmt.Sprintf("%s[%d]", path, i), e)
}
}
}
walk("", m)
files, _ := os.ReadDir(filepath.Join(moduleDir, "files"))
var templates []string
for _, f := range files {
templates = append(templates, f.Name())
}
same("files/", templates, []string{"settings.json"})
}
// design 39 parts §2, §4: the manager bundle registers the seat's ten verbs under the seat and the three acting tools
// under the module, and nothing more.
func TestTheManagerRegistersTheVerbsAndTheActs(t *testing.T) {
var names []string
for _, tool := range tools() {
names = append(names, tool.Name)
}
sort.Strings(names)
var want []string
for _, v := range []string{"licences", "bindings", "bind", "switch", "release", "refresh", "usage", "adopt",
"current", "public-key"} {
want = append(want, Seat+"."+v)
}
want = append(want, "licences_read_usage", "licences_resync", "licences_switch_now")
sort.Strings(want)
if !slices.Equal(names, want) {
t.Fatalf("the manager bundle registers %v, not %v", names, want)
}
}
@@ -5,7 +5,11 @@ package main
// failed failures_to_notify times in a row and whose refresh token has not expired. An API key's room is never known,
// so it is never a target.
import "time"
import (
"time"
. "claude-licence-manager/internal/licences"
)
// Room is a licence's room at now, and whether it is known.
func Room(l Licence, now time.Time, s Settings) (float64, bool) {
@@ -6,6 +6,8 @@ import (
"strings"
"testing"
"time"
. "claude-licence-manager/internal/licences"
)
// vendorUsage is the vendor's usage answer as it reads: three windows, each with its reset time.
@@ -0,0 +1,45 @@
package main
// The manager's settings (design 39 §8, design 39 bus-and-switch §33): today's five and the six of the switch, each
// declared in the manifest's `config` with its default and bound, set through mesh-config and written into the
// settings file. They are read again at every reading, so a change takes effect at the next one, and each change of a
// value read is written in the audit under `settings`.
import (
"context"
"fmt"
"reflect"
. "claude-licence-manager/internal/licences"
)
// settings are those in force.
func (m *Manager) settings() Settings {
m.settingsMu.RLock()
defer m.settingsMu.RUnlock()
return m.Settings
}
// ReadSettings reads the settings file again, at every reading (§33): each value that changed is written in the audit
// under `settings`, its value before and now, and the settings read are those in force from then on.
func (m *Manager) ReadSettings(ctx context.Context) error {
if m.SettingsFile == "" {
return nil
}
next := SettingsFrom(m.SettingsFile)
m.settingsMu.Lock()
before := m.Settings
m.Settings = next
m.settingsMu.Unlock()
was, now := before.Values(), next.Values()
changed := map[string]any{}
for k, v := range now {
if !reflect.DeepEqual(was[k], v) {
changed[k] = map[string]any{"from": was[k], "to": v}
}
}
if len(changed) == 0 {
return nil
}
return m.record(ctx, "settings", fmt.Sprintf("%d setting(s) changed: %v", len(changed), changed), changed)
}
@@ -0,0 +1,33 @@
package main
import (
"context"
"os"
"path/filepath"
"testing"
)
// design 39 bus-and-switch §33: the settings are read again at every reading, and each change of a value read is
// written once in the audit under settings.
func TestASettingChangedIsReadAtTheNextReadingAndAudited(t *testing.T) {
mm := newMesh(t)
ctx := context.Background()
mm.m.SettingsFile = filepath.Join(t.TempDir(), "settings.json")
_ = os.WriteFile(mm.m.SettingsFile, []byte(`{"switch_at_pct": 99}`), 0o600)
if err := mm.m.ReadUsage(ctx); err != nil || len(mm.audit.under("settings")) != 0 {
t.Fatalf("an unchanged file audited %v (%v)", mm.audit.under("settings"), err)
}
_ = os.WriteFile(mm.m.SettingsFile, []byte(`{"switch_at_pct": 95, "usage_minutes": 10}`), 0o600)
_ = mm.m.ReadUsage(ctx)
_ = mm.m.ReadUsage(ctx)
if s := mm.m.settings(); s.SwitchAt != 95 || s.UsageEvery.Minutes() != 10 {
t.Fatalf("in force %+v", s)
}
got := mm.audit.under("settings")
if len(got) != 1 {
t.Fatalf("audited %v", got)
}
if c, _ := got[0]["switch_at_pct"].(map[string]any); c["from"] != 99.0 || c["to"] != 95.0 {
t.Fatalf("audited %v", got[0])
}
}
@@ -7,29 +7,9 @@ package main
import (
"context"
"time"
)
// Licence is one licence: an account, or an API key. Its form is the value of the state `licences` (design 39
// bus-and-switch §2): never a secret, which the vault holds (§15).
type Licence struct {
Name string `json:"name"`
Kind string `json:"kind"` // subscription | api-key
AccountUUID string `json:"account,omitempty"`
Email string `json:"address,omitempty"`
OrganizationUUID string `json:"organisation,omitempty"`
RefreshFingerprint string `json:"refreshFingerprint,omitempty"`
AccessExpiresAt int64 `json:"accessExpiresAt,omitempty"`
RefreshExpiresAt int64 `json:"refreshExpiresAt,omitempty"`
Failures int `json:"failures"`
NotifiedAt int64 `json:"notifiedAt,omitempty"`
AdoptedAt int64 `json:"adoptedAt"`
AdoptedFrom string `json:"adoptedFrom,omitempty"`
RotatedAt int64 `json:"rotatedAt,omitempty"`
// Generation is the generation of the licence's secrets in the vault (§2, §15).
Generation int64 `json:"generation,omitempty"`
// Usage is the latest reading of the licence's usage (§2, §24).
Usage *UsageReading `json:"usage,omitempty"`
}
. "claude-licence-manager/internal/licences"
)
// Binding is one consumer's binding (ADR 0206, §3): the licence, kind and generation it should hold, since when, and
// why and by whom it was moved last.
@@ -60,17 +40,6 @@ func (m Move) floor(consumer string) int64 {
return m.Floor(consumer)
}
// Outcome is what became of a login the manager was offered, by its refresh token's fingerprint.
type Outcome string
const (
Adopted Outcome = "adopted"
Dead Outcome = "dead"
Skipped Outcome = "skipped"
Refused Outcome = "refused"
Gone Outcome = "gone"
)
// UsageRow is one usage reading.
type UsageRow struct {
Licence string `json:"licence"`
@@ -1,6 +1,8 @@
package main
import "testing"
import (
"testing"
)
func TestARefreshThatDoesNotRotateKeepsTheRefreshToken(t *testing.T) {
prev := FullGrant{AccessToken: "a", RefreshToken: "r", ExpiresAt: 1}
@@ -12,6 +12,8 @@ import (
"fmt"
"sort"
"strings"
. "claude-licence-manager/internal/licences"
)
// ByUsage is who moves a consumer by the rule (§26).
@@ -6,6 +6,8 @@ import (
"strings"
"testing"
"time"
. "claude-licence-manager/internal/licences"
)
// read is a licence's latest reading, read at t0.
@@ -0,0 +1,129 @@
package main
// The module's acting tools (design 39 bus-and-switch §38, §39, §42; design 39 parts §4): registered by the manager
// bundle beside the seat's verbs, they start by hand what the manager does by itself. The tools that only read are the
// tools bundle's (cmd/tools).
import (
"context"
"encoding/json"
"errors"
"fmt"
"sort"
"strings"
stdio "git.novox.be/novox/mesh-sdk/go"
. "claude-licence-manager/internal/licences"
)
// tool is one of the module's own tools.
func tool(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: 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 + " tool", By: stdio.CallerOf(a)})
return run(ctx, m, a)
}}
}
func boolean(description string) map[string]any {
return map[string]any{"type": "boolean", "description": description}
}
func number(description string) map[string]any {
return map[string]any{"type": "number", "description": description}
}
func moduleTools() []stdio.Tool {
licence := str("a licence, as `licences` names it; absent for all")
dry := boolean("answer what the rule would do, and do nothing")
return []stdio.Tool{
tool("licences_read_usage", "Read usage now, for one subscription or every one, as the manager does on its cadence; then apply the switch rule unless dry. Answers each reading and what the rule did or would do.",
map[string]any{"licence": licence, "dry": dry},
func(ctx context.Context, m *Manager, a map[string]any) (any, error) {
l, _ := a["licence"].(string)
d, _ := a["dry"].(bool)
return m.ReadUsageOf(ctx, strings.TrimSpace(l), d)
}),
tool("licences_switch_now", "Apply the switch rule now from the latest readings, for one licence or all, even with switch_auto off since a person asked; with dry, answer the moves and make none.",
map[string]any{"licence": licence, "dry": dry},
func(ctx context.Context, m *Manager, a map[string]any) (any, error) {
l, _ := a["licence"].(string)
d, _ := a["dry"].(bool)
acts, err := m.ApplyRule(ctx, strings.TrimSpace(l), true, d)
if acts == nil {
acts = []Switch{}
}
return acts, err
}),
tool("licences_resync", "Recover: say binding.changed again for every binding, ask every node what it holds, say again each condition held, and hand each licence's access token to the vault again. Answers what it said.",
nil, func(ctx context.Context, m *Manager, _ map[string]any) (any, error) {
return m.Resync(ctx)
}),
}
}
// Resync says again what a consumer, the vault or a node may have missed (§42): binding.changed for every binding,
// holdings.asked, each condition held, and licence-put of each licence's access token. Answers what it said.
func (m *Manager) Resync(ctx context.Context) (map[string]any, error) {
bindings, err := m.Store.Bindings(ctx)
if err != nil {
return nil, err
}
said := map[string]any{}
consumers := []string{}
for _, b := range bindings {
m.changed(b, b.Licence)
consumers = append(consumers, b.Consumer)
}
said["binding.changed"] = consumers
if err := m.AskHoldings(CauseOf(ctx).By); err != nil {
return said, fmt.Errorf("holdings.asked could not be said: %w", err)
}
said["holdings.asked"] = true
if err := m.SayConditions(ctx, true); err != nil {
return said, err
}
keys, err := m.Conditions.Keys()
if err != nil {
return said, err
}
sort.Strings(keys)
said["conditions"] = keys
licences, err := m.Store.Licences(ctx)
if err != nil {
return said, err
}
put := []string{}
var errs []error
for _, l := range licences {
if l.Generation == 0 {
continue
}
key := KeyFor(l.Name)
// The access token is built from the grant, which the vault hands the manager; it hands a licence's access
// token only to a machine a binding names (§17), and the manager's machine may have none.
access, err := m.Grants.Get(ctx, key, PartGrant)
if err == nil && l.Kind == "subscription" {
var g FullGrant
if err = json.Unmarshal([]byte(access), &g); err == nil {
access = accessOf(g)
}
}
if err == nil {
err = m.Grants.Put(ctx, key, PartAccess, l.Generation, access)
}
access = ""
if err != nil {
errs = append(errs, fmt.Errorf("the access token of %s: %w", l.Name, err))
continue
}
put = append(put, l.Name)
}
said["licence-put"] = put
return said, errors.Join(errs...)
}
@@ -3,11 +3,11 @@ package main
import (
"context"
"encoding/json"
"errors"
"os"
"strings"
"testing"
"time"
. "claude-licence-manager/internal/licences"
)
// stdTool is one served verb or tool's run.
@@ -105,32 +105,6 @@ func TestUsageIsBoundedBySinceAndLimit(t *testing.T) {
}
}
// design 39 bus-and-switch §37/AC1: a switch by usage and one by a person are answered, newest first, with their
// causes, a switch by usage with the reading that moved them; licence and consumer narrow the answer.
func TestSwitchesAnswersBothCauses(t *testing.T) {
mm := fourOnA(t)
ctx := context.Background()
_, _ = mm.m.ApplyRule(ctx, "", false, false)
mm.now = t0.Add(time.Minute)
_, _ = mm.m.Bind(WithCause(ctx, Cause{Why: "the switch verb", By: "a person"}), "ace", "c")
run := served(t, mm)["licences_switches"]
got := answer(t, run, nil).([]any)
if len(got) != 2 {
t.Fatalf("switches %v", got)
}
person, usage := got[0].(map[string]any), got[1].(map[string]any)
if person["by"] != "a person" || person["why"] != "the switch verb" || person["to"] != "c" || person["reading"] != nil ||
usage["by"] != ByUsage || usage["reading"].(map[string]any)["window"] != "weekly" || usage["from"] != "a" {
t.Fatalf("switches %v", got)
}
if got := answer(t, run, map[string]any{"licence": "c"}).([]any); len(got) != 1 {
t.Fatalf("of c %v", got)
}
if got := answer(t, run, map[string]any{"consumer": "g14"}).([]any); len(got) != 1 || got[0].(map[string]any)["by"] != ByUsage {
t.Fatalf("of g14 %v", got)
}
}
// design 39 bus-and-switch §38/AC1, §24, §32: reading usage now writes the reading and applies the rule, and with dry
// reads, answers the moves and moves nothing.
func TestReadUsageNowAppliesTheRuleUnlessDry(t *testing.T) {
@@ -165,95 +139,6 @@ func TestSwitchNowRunsWithSwitchAutoOff(t *testing.T) {
}
}
// design 39 bus-and-switch §40/AC1, B18: a value set at node level is answered with its level, the rest with the
// default, each with its value and default, and an invalid one saying why.
func TestSettingsAnswersTheLevel(t *testing.T) {
mm := newMesh(t)
mm.m.Settings.SwitchAt, mm.m.Settings.WarnAt = 80, 85
mm.m.ConfigLevels = func(context.Context) (map[string]string, error) {
return map[string]string{"switch_at_pct": "node", "warn_at_pct": "mesh", "cadence_minutes": "default"}, nil
}
got := answer(t, served(t, mm)["licences_settings"], nil).([]any)
by := map[string]map[string]any{}
for _, s := range got {
f := s.(map[string]any)
by[f["setting"].(string)] = f
}
if len(by) != len(Defaults.Values()) || by["switch_at_pct"]["level"] != "node" || by["switch_at_pct"]["value"] != 80.0 ||
by["switch_at_pct"]["default"] != 99.0 || by["usage_minutes"]["level"] != "default" ||
!strings.Contains(fmtAny(by["room_below_pct"]["invalid"]), "above switch_at_pct 80") ||
!strings.Contains(fmtAny(by["warn_at_pct"]["invalid"]), "85") || by["switch_auto"]["invalid"] != nil {
t.Fatalf("settings %v", by)
}
mm.m.ConfigLevels = func(context.Context) (map[string]string, error) { return nil, errors.New("no responders") }
if _, err := served(t, mm)["licences_settings"](nil); err == nil {
t.Fatal("mesh-config not answering is answered as the default")
}
}
func fmtAny(v any) string { s, _ := v.(string); return s }
// design 39 bus-and-switch §41/AC1, §20: every bus-state entry the manifest asks for is read by licences_kept, with its
// keys or subjects, one key's value and its fill; and no entry holds a token.
func TestKeptReadsEveryEntryOfTheManifest(t *testing.T) {
raw, err := os.ReadFile("../../module.json")
if err != nil {
t.Fatal(err)
}
var m struct {
Requires []json.RawMessage `json:"requires"`
Logs []json.RawMessage `json:"logs"`
}
_ = json.Unmarshal(raw, &m)
// The logs are declared in the logs section (C3), each read as a log.
for name := range logsSection(m.Logs) {
r, _ := json.Marshal(map[string]string{"name": name, "template": "log"})
m.Requires = append(m.Requires, r)
}
mm := newMesh(t)
ctx := context.Background()
_, _ = saying(mm)
mm.m.Carried = newMemState(func() time.Time { return mm.now })
_, _ = mm.m.Consider(ctx, []Holdings{mm.login("laptop", "rt-secret-1", true, t0)})
_ = mm.m.ReadUsage(ctx)
run := served(t, mm)["licences_kept"]
all := answer(t, run, nil).(map[string]any)
entries := 0
for _, r := range m.Requires {
var e struct {
Name string `json:"name"`
Template string `json:"template"`
}
if json.Unmarshal(r, &e) != nil || e.Template == "" {
continue
}
entries++
got, ok := all[e.Name].(map[string]any)
if !ok || got["fill"] == nil || got["kind"] != map[string]string{"state": "state", "log": "log"}[e.Template] {
t.Errorf("licences_kept does not read %s as a %s: %v", e.Name, e.Template, all[e.Name])
}
}
if entries != 8 || len(all) != entries {
t.Fatalf("the manifest asks for %d entries, licences_kept reads %d", entries, len(all))
}
one := answer(t, run, map[string]any{"entry": "licences", "key": KeyFor(licence1)}).(map[string]any)
if one["licences"].(map[string]any)["value"] == nil {
t.Fatalf("one key %v", one)
}
if subjects := all["usage"].(map[string]any)["subjects"].(map[string]any); subjects[KeyFor(licence1)] == nil {
t.Fatalf("usage subjects %v", subjects)
}
if _, err := run(map[string]any{"entry": "tables"}); err == nil {
t.Fatal("an entry not kept is answered")
}
everything, _ := json.Marshal(all)
for _, token := range []string{"rt-secret", "rt-1", "at-1", "local-"} {
if strings.Contains(string(everything), token) {
t.Fatalf("a kept entry holds %s", token)
}
}
}
// design 39 bus-and-switch §42/AC1: resync says binding.changed once per binding, holdings.asked once, each condition
// once, and licence-put of each licence's access once.
func TestResyncSaysEachOnce(t *testing.T) {
@@ -276,25 +161,3 @@ func TestResyncSaysEachOnce(t *testing.T) {
t.Fatalf("said %v, conditions %d, answered %v", mm.events, len(*said), got)
}
}
// design 39 bus-and-switch §43/AC1: each key of conditions is answered with its body and since.
func TestConditionsAnswersEachKey(t *testing.T) {
mm := fourOnA(t)
ctx := context.Background()
_, _ = saying(mm)
_ = mm.store.SaveLicence(ctx, sub("a", read(40, 95)))
mm.m.Settings.RoomBelow = 100
_ = mm.m.SayConditions(ctx, false)
got := answer(t, served(t, mm)["licences_conditions"], nil).([]any)
var keys []string
for _, c := range got {
f := c.(map[string]any)
if f["since"] == "" || f["body"].(map[string]any)["headline"] == "" {
t.Fatalf("condition %v", f)
}
keys = append(keys, f["body"].(map[string]any)["key"].(string))
}
if strings.Join(keys, ",") != "licence.a.near-limit,settings.room_below_pct.invalid" {
t.Fatalf("keys %v", keys)
}
}
@@ -5,6 +5,8 @@ import (
"encoding/json"
"strings"
"testing"
. "claude-licence-manager/internal/licences"
)
// design 39 bus-and-switch §15, §16, §20: a rotation reads the grant from the vault and hands it back, the grant first
@@ -14,6 +14,8 @@ import (
"os"
"strings"
"time"
. "claude-licence-manager/internal/licences"
)
// The public Claude Code client's id: not a secret, and a hard-won constant — a metadata URL in its place
@@ -168,19 +170,6 @@ func (v liveVendor) Usage(ctx context.Context, accessToken string) (map[string]a
return out, nil
}
// UsageReading is the licence-grain reading (ADR 0054), flattened from the vendor's windows (design 39 bus-and-switch
// §24), with, as the licence keeps it (§2), when it was last read successfully and why the latest reading failed.
type UsageReading struct {
SessionPct *float64 `json:"sessionPct"`
SessionResetsAt string `json:"sessionResetsAt,omitempty"`
WeeklyPct *float64 `json:"weeklyPct"`
WeeklyResetsAt string `json:"weeklyResetsAt,omitempty"`
SonnetPct *float64 `json:"sonnetPct"`
SonnetResetsAt string `json:"sonnetResetsAt,omitempty"`
ReadAt string `json:"readAt,omitempty"`
ReadError string `json:"readError,omitempty"`
}
// FlattenUsage reads the session, weekly and sonnet windows, each with its reset time (§24).
func FlattenUsage(u map[string]any) UsageReading {
window := func(k string) (*float64, string) {
@@ -37,6 +37,8 @@ import (
occ "git.novox.be/novox/mesh-contracts/operator-and-conversation/go/clients/operatorchannelclient"
oc "git.novox.be/novox/mesh-contracts/operator-and-conversation/go/seats/operatorchannel"
"git.novox.be/novox/mesh-sdk/go/testbus"
. "claude-licence-manager/internal/licences"
)
const (
@@ -157,14 +159,16 @@ type said struct {
// mesh is the bus, the state directory and the parts beside the manager, kept across its starts.
type mesh struct {
t *testing.T
ctx context.Context
nc *nats.Conn
js jetstream.JetStream
bin string
dir string
vendor *vendor
own issued // what the manager's own runtime is issued from its manifest (C1)
t *testing.T
ctx context.Context
nc *nats.Conn
js jetstream.JetStream
bin string
// toolsBin is the tools bundle's binary.
toolsBin string
dir string
vendor *vendor
own issued // what the manager's own runtime is issued from its manifest (C1)
mu sync.Mutex
said []said
@@ -188,11 +192,14 @@ func newWholeMesh(t *testing.T) *mesh {
ctx, cancel := context.WithCancel(context.Background())
t.Cleanup(cancel)
dir := t.TempDir()
bin := filepath.Join(dir, "claude-licence-manager")
build := exec.Command("go", "build", "-o", bin, ".")
build.Stderr = os.Stderr
if err := build.Run(); err != nil {
t.Fatalf("the manager does not build: %v", err)
// The module's two bundles, each loaded by the same runtime (design 39 parts §2, §3).
bin, toolsBin := filepath.Join(dir, "claude-licence-manager"), filepath.Join(dir, "claude-licence-manager-tools")
for out, from := range map[string]string{bin: ".", toolsBin: "../tools"} {
build := exec.Command("go", "build", "-o", out, from)
build.Stderr = os.Stderr
if err := build.Run(); err != nil {
t.Fatalf("%s does not build: %v", from, err)
}
}
nc, err := nats.Connect(testbus.URL(t))
if err != nil {
@@ -209,9 +216,20 @@ func newWholeMesh(t *testing.T) *mesh {
t.Fatal(err)
}
}
// Each log with the cap its logs section names (C3), as the bus module creates it.
raw, err := os.ReadFile(filepath.Join(moduleDir, "module.json"))
if err != nil {
t.Fatal(err)
}
var manifest struct {
Logs []json.RawMessage `json:"logs"`
}
_ = json.Unmarshal(raw, &manifest)
bounds := logsSection(manifest.Logs)
for l := range own.Logs {
mib, _ := bounds[l]["max-mib"].(float64)
if _, err := js.CreateStream(ctx, jetstream.StreamConfig{Name: "LOG_" + Publisher + "_" + l,
Subjects: []string{"mesh.log." + Publisher + "." + l + ".>"}}); err != nil {
Subjects: []string{"mesh.log." + Publisher + "." + l + ".>"}, MaxBytes: int64(mib) << 20}); err != nil {
t.Fatal(err)
}
}
@@ -219,7 +237,7 @@ func newWholeMesh(t *testing.T) *mesh {
if err := os.WriteFile(filepath.Join(dir, "settings.json"), []byte("{}"), 0o600); err != nil {
t.Fatal(err)
}
m := &mesh{t: t, ctx: ctx, nc: nc, js: js, bin: bin, dir: dir, vendor: newVendor(t), own: own,
m := &mesh{t: t, ctx: ctx, nc: nc, js: js, bin: bin, toolsBin: toolsBin, dir: dir, vendor: newVendor(t), own: own,
news: make(chan struct{}, 1), secrets: map[string]string{}, calls: map[string][32]byte{},
callArgs: map[string]map[string]any{}, logins: map[string]FullGrant{}}
sub, err := nc.Subscribe("mesh.seat.operator-channel.accept.condition.>", func(msg *nats.Msg) {
@@ -318,7 +336,14 @@ type started struct {
// start starts the manager as its runtime launches it, with the module's words (ADR 0192).
func (m *mesh) start() *started {
m.t.Helper()
cmd := exec.CommandContext(m.ctx, m.bin)
return m.load(m.bin)
}
// load starts one bundle's binary as the module's runtime loads it, with the module's words, which the runtime hands
// to every bundle of the module (ADR 0192).
func (m *mesh) load(bin string) *started {
m.t.Helper()
cmd := exec.CommandContext(m.ctx, bin)
cmd.Env = append(os.Environ(), "MESH_LICENCE_STATE="+filepath.Join(m.dir, "state"),
"MESH_LICENCE_SETTINGS="+filepath.Join(m.dir, "settings.json"), "MESH_NODE=novox", "MESH_MODULE="+Publisher,
"MESH_ANTHROPIC_TOKEN_ENDPOINT="+m.vendor.server.URL+"/token", "MESH_ANTHROPIC_USAGE_ENDPOINT="+m.vendor.server.URL+"/usage")
@@ -911,3 +936,71 @@ func TestANewLoginIsAdoptedListedAndATarget(t *testing.T) {
t.Errorf("laptop is bound %+v", b)
}
}
// design 39 parts §3, §10, §18: with the manager bundle not loaded (as when it refuses to start), the tools bundle,
// loaded by the same runtime, answers licences_kept with every entry the module keeps and licences_conditions with
// each condition kept, and says, writes and asks nothing while it does.
func TestTheReadingToolsAnswerWithoutTheManager(t *testing.T) {
m := newWholeMesh(t)
m.seed()
carried, err := m.js.KeyValue(m.ctx, Publisher+"_carried")
if err != nil {
t.Fatal(err)
}
if _, err := carried.Put(m.ctx, "postgres", []byte(`{"at":"2026-11-12T09:44:59Z"}`)); err != nil {
t.Fatal(err)
}
seqs := func() map[string]uint64 {
out := map[string]uint64{}
for s := range m.own.States {
st, err := m.js.Stream(m.ctx, "KV_"+Publisher+"_"+s)
if err != nil {
t.Fatal(err)
}
i, _ := st.Info(m.ctx)
out[s] = i.State.LastSeq
}
for l := range m.own.Logs {
st, err := m.js.Stream(m.ctx, "LOG_"+Publisher+"_"+l)
if err != nil {
t.Fatal(err)
}
i, _ := st.Info(m.ctx)
out["log "+l] = i.State.LastSeq
}
return out
}
before := seqs()
tools := m.load(m.toolsBin)
var all map[string]map[string]any
if err := json.Unmarshal(tools.must("licences_kept", nil), &all); err != nil {
t.Fatal(err)
}
if len(all) != len(m.own.States)+len(m.own.Logs) || len(all) != 8 {
t.Fatalf("licences_kept answers %d entries without the manager, not 8: %v", len(all), all)
}
for name, e := range all {
if e["fill"] == nil {
t.Errorf("licences_kept answers %s without its fill: %v", name, e)
}
}
var one map[string]map[string]any
if err := json.Unmarshal(tools.must("licences_kept", map[string]any{"entry": "carried", "key": "postgres"}), &one); err != nil {
t.Fatal(err)
}
if v, _ := one["carried"]["value"].(map[string]any); v["at"] != "2026-11-12T09:44:59Z" {
t.Errorf("the carry-over's record is answered as %v", one)
}
if keys, _ := all["bindings"]["keys"].([]any); len(keys) != len(consumers) {
t.Errorf("bindings answers %v", all["bindings"])
}
tools.must("licences_conditions", nil)
if after := seqs(); fmt.Sprint(after) != fmt.Sprint(before) {
t.Errorf("the tools bundle wrote: before %v, after %v", before, after)
}
m.mu.Lock()
defer m.mu.Unlock()
if len(m.said) != 0 || len(m.conditions) != 0 {
t.Errorf("the tools bundle said or asked %v and %v", m.said, m.conditions)
}
}
@@ -0,0 +1,70 @@
// claude-licence-manager-tools (design 39 parts §3): the licence manager's tools bundle, loaded by the module's runtime
// of its own beside the manager bundle. It answers the module's four reading tools and writes nothing: it reads the
// module's kept data with the runtime's bus-state calls and its two logs with the runtime's log calls, each answered
// as the module whose bundle asks, and asks mesh-config where each setting was set. The runtime serves the tools of
// every bundle of a module that loaded, so these answer while the manager bundle fails to load or stops.
//
// stdout is the MCP channel; everything this bundle says, it says on stderr.
package main
import (
"context"
"encoding/json"
"fmt"
"os"
stdio "git.novox.be/novox/mesh-sdk/go"
"claude-licence-manager/internal/licences"
)
func main() {
if err := stdio.Serve("", fromTheRuntime().Tools()); err != nil {
fmt.Fprintf(os.Stderr, "[claude-licence-manager-tools] %v\n", err)
os.Exit(1)
}
}
// fromTheRuntime is the reader of the module's kept data through its runtime, and of its settings file by the word
// the manager bundle gives the module (MESH_LICENCE_SETTINGS; ADR 0192: the runtime hands a module's words to all
// its bundles).
func fromTheRuntime() *Reader {
node, _ := os.Hostname()
if n := os.Getenv("MESH_NODE"); n != "" {
node = n
}
r := &Reader{States: map[string]licences.State{}, Logs: map[string]licences.Log{},
SettingsFile: os.Getenv("MESH_LICENCE_SETTINGS"),
ConfigLevels: func(context.Context) (map[string]string, error) { return configLevels(node) }}
for _, name := range licences.States {
r.States[name] = stdio.State(name)
}
for _, name := range licences.Logs {
r.Logs[name] = stdio.Log(name)
}
return r
}
// configLevels asks mesh-config where each of the module's own settings was set on its node (bus-and-switch §40,
// B18): the source of each resolved value, by its key.
func configLevels(node string) (map[string]string, error) {
raw, err := stdio.Ask("seat:mesh-config.config", map[string]any{"action": "list", "module": licences.Publisher,
"node": node})
if err != nil {
return nil, err
}
var answer struct {
Resolved []struct {
Key string `json:"key"`
Source string `json:"source"`
} `json:"resolved"`
}
if err := json.Unmarshal(raw, &answer); err != nil {
return nil, fmt.Errorf("mesh-config's answer: %w", err)
}
levels := map[string]string{}
for _, r := range answer.Resolved {
levels[r.Key] = r.Source
}
return levels, nil
}
@@ -0,0 +1,260 @@
package main
// The module's four reading tools (design 39 parts §3; bus-and-switch §37, §40, §41, §43): they read everything the
// manager keeps and answer it, and write nothing. Every act is the manager bundle's (parts §4).
import (
"context"
"encoding/json"
"fmt"
"slices"
"sort"
"strings"
"time"
stdio "git.novox.be/novox/mesh-sdk/go"
"claude-licence-manager/internal/licences"
)
// Reader reads the module's kept data and its settings, as the module whose bundle asks.
type Reader struct {
// States and Logs are the module's kept states and logs, by their names in the manifest.
States map[string]licences.State
Logs map[string]licences.Log
// SettingsFile is the settings file the node-engine writes, read again at every call (bus-and-switch §33).
SettingsFile string
// ConfigLevels is where mesh-config says each setting was set on the manager's node (mesh, node or default), by
// setting (§40, B18).
ConfigLevels func(ctx context.Context) (map[string]string, error)
}
func str(description string) map[string]any {
return map[string]any{"type": "string", "description": description}
}
func number(description string) map[string]any {
return map[string]any{"type": "number", "description": description}
}
// 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)
}
// Tools are the four reading tools, registered under the module (parts §3).
func (r *Reader) Tools() []stdio.Tool {
licence := str("a licence, as `licences` names it; absent for all")
return []stdio.Tool{
{Name: "licences_switches", Description: "Every switch of a consumer and why, newest first, from the audit: when, from and to which licence, the consumers, by whom and why, and for a switch by usage the reading that moved them.",
Input: map[string]any{"licence": licence, "consumer": str("a consumer; absent for all"),
"since": str("a time, RFC 3339: no switch before it"), "limit": number("how many (default 20)")},
Run: func(a map[string]any) (any, error) {
l, _ := a["licence"].(string)
c, _ := a["consumer"].(string)
since, err := sinceOf(a)
if err != nil {
return nil, err
}
return r.Switches(strings.TrimSpace(l), strings.TrimSpace(c), since, limitOf(a, 20, 2000))
}},
{Name: "licences_settings", Description: "The settings in force: each one's value, its default, where it was set (mesh, node or the default, as mesh-config answers it) and why it is invalid when it is.",
Run: func(map[string]any) (any, error) { return r.SettingsInForce(context.Background()) }},
{Name: "licences_kept", Description: "Every entry the manager keeps on the bus: a state's keys, or a log's subjects with their last entry; with key, that key's value or that subject's last entry; and how full each is against its cap.",
Input: map[string]any{"entry": str("one of licences, bindings, leases, offered, usage, audit, carried, conditions; absent for all"),
"key": str("a key of a state, or a subject of a log")},
Run: func(a map[string]any) (any, error) {
e, _ := a["entry"].(string)
k, _ := a["key"].(string)
return r.Kept(strings.TrimSpace(e), strings.TrimSpace(k))
}},
{Name: "licences_conditions", Description: "The conditions the manager said and holds: each key of its state conditions with its body and since.",
Run: func(map[string]any) (any, error) {
return licences.KeptConditions(r.States[licences.EntryConditions])
}},
}
}
// Switches are the audit's switches, newest first: of a licence (from or to it) and a consumer when named, none before
// since, at most limit (§37).
func (r *Reader) Switches(licence, consumer string, since time.Time, limit int) ([]map[string]any, error) {
audit, ok := r.Logs[licences.EntryAudit]
if !ok {
return nil, fmt.Errorf("the module keeps no log %s", licences.EntryAudit)
}
info, err := audit.Info()
if err != nil || info.Last == 0 {
return []map[string]any{}, err
}
var all []stdio.LogEntry
for next := uint64(1); next != 0; {
page, err := audit.Read(next, "switched", 0)
if err != nil {
return nil, err
}
all = append(all, page.Entries...)
next = page.Next
}
out := []map[string]any{}
for i := len(all) - 1; i >= 0 && len(out) < limit; i-- {
var e map[string]any
if json.Unmarshal(all[i].Value, &e) != nil {
continue
}
at, _ := time.Parse(time.RFC3339Nano, fmt.Sprint(e["at"]))
if at.Before(since) {
continue
}
if licence != "" && e["from"] != licence && e["to"] != licence {
continue
}
consumers, _ := e["consumers"].([]any)
if consumer != "" && !slices.Contains(consumers, any(consumer)) {
continue
}
s := map[string]any{"at": e["at"], "from": e["from"], "to": e["to"], "consumers": e["consumers"],
"by": e["by"], "why": e["why"]}
if _, ok := e["window"]; ok {
s["reading"] = map[string]any{"window": e["window"], "pct": e["pct"], "resetsAt": e["resetsAt"],
"room": e["room"]}
}
out = append(out, s)
}
return out, nil
}
// SettingAnswer is one setting as licences_settings answers it (§40).
type SettingAnswer struct {
Setting string `json:"setting"`
Value any `json:"value"`
Default any `json:"default"`
Level string `json:"level"`
Invalid string `json:"invalid,omitempty"`
}
// SettingsInForce is each setting in force, read from the settings file, its default, where mesh-config says it was
// set, and why it is invalid when it is (§40, B18).
func (r *Reader) SettingsInForce(ctx context.Context) ([]SettingAnswer, error) {
levels := map[string]string{}
if r.ConfigLevels != nil {
got, err := r.ConfigLevels(ctx)
if err != nil {
return nil, fmt.Errorf("mesh-config did not say where each setting was set: %w", err)
}
levels = got
}
s := licences.SettingsFrom(r.SettingsFile)
reasons := s.Reasons()
values, defaults := s.Values(), licences.Defaults.Values()
var out []SettingAnswer
for k, v := range values {
a := SettingAnswer{Setting: k, Value: v, Default: defaults[k], Level: levels[k], Invalid: reasons[k]}
if a.Level == "" {
a.Level = "default"
}
out = append(out, a)
}
sort.Slice(out, func(i, j int) bool { return out[i].Setting < out[j].Setting })
return out, nil
}
// Kept answers one entry the module keeps, or every one (§41): a state's keys and their count, a log's subjects with
// their last entry and its fill against its cap; with key, one key's value or one subject's last entry.
func (r *Reader) Kept(entry, key string) (map[string]any, error) {
out := map[string]any{}
names := []string{}
for n := range r.States {
names = append(names, n)
}
for n := range r.Logs {
names = append(names, n)
}
sort.Strings(names)
if entry != "" && !slices.Contains(names, entry) {
return nil, fmt.Errorf("the manager keeps no entry %q: it keeps %s", entry, strings.Join(names, ", "))
}
for _, n := range names {
if entry != "" && n != entry {
continue
}
if st, ok := r.States[n]; ok {
a, err := stateAnswer(st, key)
if err != nil {
return nil, fmt.Errorf("%s: %w", n, err)
}
out[n] = a
continue
}
a, err := logAnswer(r.Logs[n], key)
if err != nil {
return nil, fmt.Errorf("%s: %w", n, err)
}
out[n] = a
}
return out, nil
}
func stateAnswer(st licences.State, key string) (map[string]any, error) {
keys, err := st.Keys()
if err != nil {
return nil, err
}
sort.Strings(keys)
a := map[string]any{"kind": "state", "keys": keys, "fill": map[string]any{"keys": len(keys), "cap": "none declared"}}
if key != "" {
e, err := st.Get(key)
if err != nil {
return nil, err
}
if e == nil {
a["value"] = nil
} else {
a["value"] = e.Value
}
}
return a, nil
}
func logAnswer(l licences.Log, key string) (map[string]any, error) {
info, err := l.Info()
if err != nil {
return nil, err
}
a := map[string]any{"kind": "log", "fill": map[string]any{"entries": info.Entries, "bytes": info.Bytes,
"maxBytes": info.MaxBytes}}
if key != "" {
e, err := l.Last(key)
if err != nil {
return nil, err
}
a["last"] = e
return a, nil
}
newest, err := l.Newest("")
if err != nil {
return nil, err
}
subjects := map[string]any{}
for _, e := range newest {
subjects[e.Key] = e
}
a["subjects"] = subjects
return a, nil
}
@@ -0,0 +1,340 @@
package main
import (
"context"
"encoding/json"
"errors"
"os"
"path/filepath"
"regexp"
"slices"
"sort"
"strings"
"testing"
"time"
oc "git.novox.be/novox/mesh-contracts/operator-and-conversation/go/seats/operatorchannel"
stdio "git.novox.be/novox/mesh-sdk/go"
"claude-licence-manager/internal/licences"
)
// runtime is a fixture of the runtime's bus-state and log calls (design 39 parts §3): it answers every read from what
// a test seeds, and fails the test on any write or append the tools bundle makes.
type runtime struct {
t *testing.T
states map[string]map[string]json.RawMessage
logs map[string][]stdio.LogEntry
}
func newRuntime(t *testing.T) *runtime {
rt := &runtime{t: t, states: map[string]map[string]json.RawMessage{}, logs: map[string][]stdio.LogEntry{}}
for _, s := range licences.States {
rt.states[s] = map[string]json.RawMessage{}
}
for _, l := range licences.Logs {
rt.logs[l] = nil
}
return rt
}
// seedState and seedLog keep what the manager wrote before the tools were asked.
func (rt *runtime) seedState(state, key string, v any) {
raw, _ := json.Marshal(v)
rt.states[state][key] = raw
}
func (rt *runtime) seedLog(log, key string, v any) {
raw, _ := json.Marshal(v)
rt.logs[log] = append(rt.logs[log], stdio.LogEntry{Sequence: uint64(len(rt.logs[log]) + 1), Key: key, Value: raw})
}
func (rt *runtime) reader() *Reader {
r := &Reader{States: map[string]licences.State{}, Logs: map[string]licences.Log{}}
for s := range rt.states {
r.States[s] = keptState{rt, s}
}
for l := range rt.logs {
r.Logs[l] = keptLog{rt, l}
}
return r
}
type keptState struct {
rt *runtime
name string
}
func (s keptState) wrote(how, key string) error {
s.rt.t.Errorf("the tools bundle %s %s.%s: it writes nothing", how, s.name, key)
return errors.New("refused: the tools bundle writes nothing")
}
func (s keptState) Get(key string) (*stdio.StateEntry, error) {
v, ok := s.rt.states[s.name][key]
if !ok {
return nil, nil
}
return &stdio.StateEntry{Key: key, Value: v, Revision: 1}, nil
}
func (s keptState) Put(key string, _ any) (uint64, error) { return 0, s.wrote("put", key) }
func (s keptState) PutTTL(key string, _ any, _ time.Duration) (uint64, error) {
return 0, s.wrote("put", key)
}
func (s keptState) Create(key string, _ any) (uint64, error) { return 0, s.wrote("created", key) }
func (s keptState) CreateTTL(key string, _ any, _ time.Duration) (uint64, error) {
return 0, s.wrote("created", key)
}
func (s keptState) Delete(key string) error { return s.wrote("deleted", key) }
func (s keptState) Keys() ([]string, error) {
keys := []string{}
for k := range s.rt.states[s.name] {
keys = append(keys, k)
}
sort.Strings(keys)
return keys, nil
}
func (s keptState) History(key string) ([]stdio.StateRevision, error) { return nil, nil }
type keptLog struct {
rt *runtime
name string
}
func (l keptLog) Append(key string, _ any) (uint64, error) {
l.rt.t.Errorf("the tools bundle appended to %s.%s: it writes nothing", l.name, key)
return 0, errors.New("refused: the tools bundle writes nothing")
}
func (l keptLog) Read(from uint64, keys string, _ int) (*stdio.LogPage, error) {
page := &stdio.LogPage{Last: uint64(len(l.rt.logs[l.name]))}
for _, e := range l.rt.logs[l.name] {
if e.Sequence >= from && (keys == "" || e.Key == keys) {
page.Entries = append(page.Entries, e)
}
}
return page, nil
}
func (l keptLog) Last(key string) (*stdio.LogEntry, error) {
var last *stdio.LogEntry
for _, e := range l.rt.logs[l.name] {
if e.Key == key {
last = &e
}
}
return last, nil
}
func (l keptLog) Info() (*stdio.LogInfo, error) {
n := uint64(len(l.rt.logs[l.name]))
return &stdio.LogInfo{Entries: n, Last: n, First: min(n, 1), MaxBytes: 64 << 20}, nil
}
func (l keptLog) Newest(string) ([]stdio.LogEntry, error) {
by := map[string]stdio.LogEntry{}
for _, e := range l.rt.logs[l.name] {
by[e.Key] = e
}
out := []stdio.LogEntry{}
for _, e := range by {
out = append(out, e)
}
return out, nil
}
// run is one tool's answer as JSON, decoded into a generic form.
func run(t *testing.T, r *Reader, name string, args map[string]any) any {
t.Helper()
for _, tool := range r.Tools() {
if tool.Name != name {
continue
}
got, err := tool.Run(args)
if err != nil {
t.Fatalf("%s %v answered %v", name, args, err)
}
raw, _ := json.Marshal(got)
var v any
_ = json.Unmarshal(raw, &v)
return v
}
t.Fatalf("the tools bundle registers no %s", name)
return nil
}
// design 39 parts §3, §4: the tools bundle registers exactly the four reading tools; its code emits nothing,
// subscribes to no event, asks no heartbeat, calls neither the vendor nor the vault, and asks nothing but mesh-config.
func TestTheToolsBundleRegistersTheFourReadingToolsAndActsOnNothing(t *testing.T) {
var names []string
for _, tool := range newRuntime(t).reader().Tools() {
names = append(names, tool.Name)
}
sort.Strings(names)
if want := []string{"licences_conditions", "licences_kept", "licences_settings", "licences_switches"}; !slices.Equal(names, want) {
t.Fatalf("the tools bundle registers %v, not %v", names, want)
}
files, _ := filepath.Glob("*.go")
acts := regexp.MustCompile(`stdio\.(Emit|Subscribe|SeatPublish|Publish|Heartbeat|Watch)\b|\.(Put|PutTTL|Create|CreateTTL|Delete|Append)\(|os\.(WriteFile|Create|OpenFile)|http\.`)
asked := regexp.MustCompile(`stdio\.Ask\("([^"]+)"`)
for _, f := range files {
if strings.HasSuffix(f, "_test.go") {
continue
}
src, err := os.ReadFile(f)
if err != nil {
t.Fatal(err)
}
for _, m := range acts.FindAllString(string(src), -1) {
t.Errorf("%s calls %s: the tools bundle acts on nothing", f, m)
}
for _, m := range asked.FindAllStringSubmatch(string(src), -1) {
if m[1] != "seat:mesh-config.config" {
t.Errorf("%s asks %s: the tools bundle asks only mesh-config", f, m[1])
}
}
}
}
// design 39 bus-and-switch §37/AC1, design 39 parts §3: a switch by usage and one by a person are answered, newest
// first, with their causes, a switch by usage with the reading that moved them; licence and consumer narrow the
// answer; nothing is written.
func TestSwitchesAnswersBothCauses(t *testing.T) {
rt := newRuntime(t)
rt.seedLog(licences.EntryAudit, "switched", map[string]any{"at": "2026-11-12T10:00:00Z", "from": "a", "to": "b",
"consumers": []string{"ace", "g14"}, "by": "usage", "why": "a at 99 %", "window": "weekly", "pct": 99,
"resetsAt": "2026-11-14T09:00:00Z", "room": 60})
rt.seedLog(licences.EntryAudit, "bound", map[string]any{"at": "2026-11-12T10:00:30Z", "consumer": "ace"})
rt.seedLog(licences.EntryAudit, "switched", map[string]any{"at": "2026-11-12T10:01:00Z", "from": "b", "to": "c",
"consumers": []string{"ace"}, "by": "a person", "why": "the switch verb"})
r := rt.reader()
got := run(t, r, "licences_switches", nil).([]any)
if len(got) != 2 {
t.Fatalf("switches %v", got)
}
person, usage := got[0].(map[string]any), got[1].(map[string]any)
if person["by"] != "a person" || person["why"] != "the switch verb" || person["to"] != "c" || person["reading"] != nil ||
usage["by"] != "usage" || usage["reading"].(map[string]any)["window"] != "weekly" || usage["from"] != "a" {
t.Fatalf("switches %v", got)
}
if got := run(t, r, "licences_switches", map[string]any{"licence": "c"}).([]any); len(got) != 1 {
t.Fatalf("of c %v", got)
}
if got := run(t, r, "licences_switches", map[string]any{"consumer": "g14"}).([]any); len(got) != 1 ||
got[0].(map[string]any)["by"] != "usage" {
t.Fatalf("of g14 %v", got)
}
if got := run(t, r, "licences_switches", map[string]any{"since": "2026-11-12T10:00:30Z"}).([]any); len(got) != 1 {
t.Fatalf("since %v", got)
}
}
// design 39 bus-and-switch §40/AC1, B18, design 39 parts §3: a value set at node level is answered with its level, the
// rest with the default, each with its value and default, and an invalid one saying why, read from the settings file;
// mesh-config not answering is an error, never the default.
func TestSettingsAnswersTheLevel(t *testing.T) {
r := newRuntime(t).reader()
r.SettingsFile = filepath.Join(t.TempDir(), "settings.json")
if err := os.WriteFile(r.SettingsFile, []byte(`{"switch_at_pct": 80, "warn_at_pct": 85, "usage_minutes": 900}`), 0o600); err != nil {
t.Fatal(err)
}
r.ConfigLevels = func(context.Context) (map[string]string, error) {
return map[string]string{"switch_at_pct": "node", "warn_at_pct": "mesh", "cadence_minutes": "default"}, nil
}
by := map[string]map[string]any{}
for _, s := range run(t, r, "licences_settings", nil).([]any) {
f := s.(map[string]any)
by[f["setting"].(string)] = f
}
str := func(v any) string { s, _ := v.(string); return s }
if len(by) != len(licences.Defaults.Values()) || by["switch_at_pct"]["level"] != "node" ||
by["switch_at_pct"]["value"] != 80.0 || by["switch_at_pct"]["default"] != 99.0 ||
by["usage_minutes"]["level"] != "default" || by["usage_minutes"]["value"] != 5.0 ||
!strings.Contains(str(by["usage_minutes"]["invalid"]), "outside its bound") ||
!strings.Contains(str(by["room_below_pct"]["invalid"]), "above switch_at_pct 80") ||
!strings.Contains(str(by["warn_at_pct"]["invalid"]), "85") || by["switch_auto"]["invalid"] != nil {
t.Fatalf("settings %v", by)
}
r.ConfigLevels = func(context.Context) (map[string]string, error) { return nil, errors.New("no responders") }
for _, tool := range r.Tools() {
if tool.Name == "licences_settings" {
if _, err := tool.Run(nil); err == nil {
t.Fatal("mesh-config not answering is answered as the default")
}
}
}
}
// design 39 bus-and-switch §41/AC1, design 39 parts §3, §10: every bus-state entry the manifest asks for is read by
// licences_kept, with its keys or subjects, one key's value and its fill, the carry-over's record among them; an
// entry not kept is refused; nothing is written.
func TestKeptReadsEveryEntryOfTheManifest(t *testing.T) {
raw, err := os.ReadFile("../../module.json")
if err != nil {
t.Fatal(err)
}
var m struct {
Requires []struct {
Name string `json:"name"`
Template string `json:"template"`
} `json:"requires"`
Logs []struct {
Name string `json:"name"`
} `json:"logs"`
}
if err := json.Unmarshal(raw, &m); err != nil {
t.Fatal(err)
}
rt := newRuntime(t)
rt.seedState(licences.EntryLicences, "a_at_example.org", licences.Licence{Name: "a@example.org", Kind: "subscription"})
rt.seedState(licences.EntryCarried, "postgres", map[string]any{"at": "2026-11-12T09:44:59Z"})
rt.seedLog(licences.EntryUsage, "a_at_example.org", licences.UsageEntry{At: "2026-11-12T10:00:00Z"})
r := rt.reader()
all := run(t, r, "licences_kept", nil).(map[string]any)
kinds := map[string]string{}
for _, e := range m.Requires {
kinds[e.Name] = e.Template
}
for _, l := range m.Logs {
kinds[l.Name] = "log"
}
for name, kind := range kinds {
got, ok := all[name].(map[string]any)
if !ok || got["fill"] == nil || got["kind"] != kind {
t.Errorf("licences_kept does not read %s as a %s: %v", name, kind, all[name])
}
}
if len(kinds) != 8 || len(all) != len(kinds) {
t.Fatalf("the manifest asks for %d entries, licences_kept reads %d", len(kinds), len(all))
}
one := run(t, r, "licences_kept", map[string]any{"entry": "carried", "key": "postgres"}).(map[string]any)
if v, _ := one["carried"].(map[string]any)["value"].(map[string]any); v["at"] != "2026-11-12T09:44:59Z" {
t.Fatalf("the carry-over's record %v", one)
}
if subjects := all["usage"].(map[string]any)["subjects"].(map[string]any); subjects["a_at_example.org"] == nil {
t.Fatalf("usage subjects %v", subjects)
}
if _, err := r.Kept("tables", ""); err == nil {
t.Fatal("an entry not kept is answered")
}
}
// design 39 bus-and-switch §43/AC1, design 39 parts §3: each key of conditions is answered with its body and since.
func TestConditionsAnswersEachKey(t *testing.T) {
rt := newRuntime(t)
for _, k := range []string{"settings.room_below_pct.invalid", "licence.a.near-limit"} {
rt.seedState(licences.EntryConditions, k, licences.KeptCondition{Body: oc.Condition{Key: k, Headline: "h"},
Since: "2026-11-12T10:00:00Z"})
}
var keys []string
for _, c := range run(t, rt.reader(), "licences_conditions", nil).([]any) {
f := c.(map[string]any)
if f["since"] == "" || f["body"].(map[string]any)["headline"] == "" {
t.Fatalf("condition %v", f)
}
keys = append(keys, f["body"].(map[string]any)["key"].(string))
}
if strings.Join(keys, ",") != "licence.a.near-limit,settings.room_below_pct.invalid" {
t.Fatalf("keys %v", keys)
}
}
@@ -0,0 +1,179 @@
package licences
// The layouts of the module's kept data and of its log entries (design 39 bus-and-switch §2 to §9; design 39 parts
// §5): what the manager bundle writes and the tools bundle reads, declared once here, so the reading tools never read
// a form the manager no longer writes.
import (
"encoding/json"
"sort"
"time"
oc "git.novox.be/novox/mesh-contracts/operator-and-conversation/go/seats/operatorchannel"
stdio "git.novox.be/novox/mesh-sdk/go"
)
// Publisher is the name the manager says its conditions under, and the id of their subject (B17).
const Publisher = "claude-licence-manager"
// The names of the entries the module keeps on the bus, as its manifest asks for them (§2 to §9).
const (
EntryLicences = "licences"
EntryBindings = "bindings"
EntryLeases = "leases"
EntryOffered = "offered"
EntryUsage = "usage"
EntryAudit = "audit"
EntryCarried = "carried"
EntryConditions = "conditions"
)
// States are the module's kept states, and Logs its two logs, by their names in the manifest.
var (
States = []string{EntryLicences, EntryBindings, EntryLeases, EntryOffered, EntryCarried, EntryConditions}
Logs = []string{EntryUsage, EntryAudit}
)
// State is a state the module keeps, as mesh-sdk's KeptState answers it.
type State interface {
Get(key string) (*stdio.StateEntry, error)
Put(key string, value any) (uint64, error)
PutTTL(key string, value any, ttl time.Duration) (uint64, error)
Create(key string, value any) (uint64, error)
CreateTTL(key string, value any, ttl time.Duration) (uint64, error)
Delete(key string) error
Keys() ([]string, error)
History(key string) ([]stdio.StateRevision, error)
}
// Log is a log the module keeps, as mesh-sdk's KeptLog answers it.
type Log interface {
Append(key string, value any) (uint64, error)
Read(from uint64, keys string, limit int) (*stdio.LogPage, error)
Last(key string) (*stdio.LogEntry, error)
Info() (*stdio.LogInfo, error)
Newest(keys string) ([]stdio.LogEntry, error)
}
// Licence is one licence: an account, or an API key. Its form is the value of the state `licences` (design 39
// bus-and-switch §2): never a secret, which the vault holds (§15).
type Licence struct {
Name string `json:"name"`
Kind string `json:"kind"` // subscription | api-key
AccountUUID string `json:"account,omitempty"`
Email string `json:"address,omitempty"`
OrganizationUUID string `json:"organisation,omitempty"`
RefreshFingerprint string `json:"refreshFingerprint,omitempty"`
AccessExpiresAt int64 `json:"accessExpiresAt,omitempty"`
RefreshExpiresAt int64 `json:"refreshExpiresAt,omitempty"`
Failures int `json:"failures"`
NotifiedAt int64 `json:"notifiedAt,omitempty"`
AdoptedAt int64 `json:"adoptedAt"`
AdoptedFrom string `json:"adoptedFrom,omitempty"`
RotatedAt int64 `json:"rotatedAt,omitempty"`
// Generation is the generation of the licence's secrets in the vault (§2, §15).
Generation int64 `json:"generation,omitempty"`
// Usage is the latest reading of the licence's usage (§2, §24).
Usage *UsageReading `json:"usage,omitempty"`
}
// UsageReading is the licence-grain reading (ADR 0054), flattened from the vendor's windows (design 39 bus-and-switch
// §24), with, as the licence keeps it (§2), when it was last read successfully and why the latest reading failed.
type UsageReading struct {
SessionPct *float64 `json:"sessionPct"`
SessionResetsAt string `json:"sessionResetsAt,omitempty"`
WeeklyPct *float64 `json:"weeklyPct"`
WeeklyResetsAt string `json:"weeklyResetsAt,omitempty"`
SonnetPct *float64 `json:"sonnetPct"`
SonnetResetsAt string `json:"sonnetResetsAt,omitempty"`
ReadAt string `json:"readAt,omitempty"`
ReadError string `json:"readError,omitempty"`
}
// ReadErrorWords is the vendor's reason a reading failed, or that none was given.
func (u *UsageReading) ReadErrorWords() string {
if u == nil || u.ReadError == "" {
return "no reason given"
}
return u.ReadError
}
// BindingState is the value of a consumer's key in `bindings` (§3, B22: the form 0 its readers decode): no secret,
// only what it should hold, which generation, since when, why and by whom.
type BindingState struct {
Licence string `json:"licence"`
Kind string `json:"kind"`
Generation int64 `json:"generation"`
Since string `json:"since,omitempty"`
Why string `json:"why,omitempty"`
By string `json:"by,omitempty"`
}
// LeaseValue is a lease's key in `leases`: its holder and until when.
type LeaseValue struct {
Holder string `json:"holder"`
Until string `json:"until"`
}
// OfferedValue is a grant's key in `offered`: what became of it, where and when (§5, B13: every outcome).
type OfferedValue struct {
Node string `json:"node"`
Account string `json:"account"`
Outcome Outcome `json:"outcome"`
Why string `json:"why"`
At string `json:"at"`
}
// Outcome is what became of a login the manager was offered, by its refresh token's fingerprint.
type Outcome string
const (
Adopted Outcome = "adopted"
Dead Outcome = "dead"
Skipped Outcome = "skipped"
Refused Outcome = "refused"
Gone Outcome = "gone"
)
// UsageEntry is one entry of the log `usage` (§6): when, the flattened reading and the vendor's answer whole (ADR 0054).
type UsageEntry struct {
At string `json:"at"`
Reading UsageReading `json:"reading"`
Raw map[string]any `json:"raw,omitempty"`
}
// AuditKinds are the subjects of the log `audit` (§7); an act of another kind is kept under `drift`.
var AuditKinds = []string{"adopted", "refused", "failed", "rotated", "bound", "switched", "released", "drift", "carried",
"settings"}
// KeptCondition is the value of a key of `conditions` (§9): the condition said raised, and since when.
type KeptCondition struct {
Body oc.Condition `json:"body"`
Since string `json:"since"`
}
// KeptConditions is each key of the state conditions with its body and since (bus-and-switch §9, §43), by key; a
// nil state holds none.
func KeptConditions(conditions State) ([]KeptCondition, error) {
out := []KeptCondition{}
if conditions == nil {
return out, nil
}
keys, err := conditions.Keys()
if err != nil {
return nil, err
}
sort.Strings(keys)
for _, k := range keys {
e, err := conditions.Get(k)
if err != nil {
return nil, err
}
var c KeptCondition
if e == nil || json.Unmarshal(e.Value, &c) != nil {
continue
}
out = append(out, c)
}
return out, nil
}
@@ -1,12 +1,9 @@
package main
package licences
// A licence's key form (design 39 bus-and-switch §10, B6): the name every verb and tool takes and answers, written
// in the characters a key of the bus takes. The key is the store's alone.
// in the characters a key of the bus takes.
import (
"context"
"strings"
)
import "strings"
// KeyFor is a licence's key: its name in lower case, each `@` written as `_at_`, and every other character
// outside letters, digits, `-` and `.` written as `_`.
@@ -24,18 +21,3 @@ func KeyFor(name string) string {
}
return b.String()
}
// keyTaken names the licence other than name whose key is name's, or "" when none is.
func (m *Manager) keyTaken(ctx context.Context, name string) (string, error) {
all, err := m.Store.Licences(ctx)
if err != nil {
return "", err
}
key := KeyFor(name)
for _, l := range all {
if l.Name != name && KeyFor(l.Name) == key {
return l.Name, nil
}
}
return "", nil
}
@@ -1,13 +1,28 @@
package main
package licences
import (
"context"
"encoding/json"
"os"
"path/filepath"
"testing"
)
// design 39 bus-and-switch §10, B6.
func TestALicencesKeyForm(t *testing.T) {
for name, key := range map[string]string{
"jane@example.org": "jane_at_example.org",
"Jane.Doe@Example.ORG": "jane.doe_at_example.org",
"0c1d2e3f-4a5b-6c7d-8e9f-a0b1c2d3e4f5": "0c1d2e3f-4a5b-6c7d-8e9f-a0b1c2d3e4f5",
"team key_1": "team_key_1",
"a+b/c:d": "a_b_c_d",
"ops-key": "ops-key",
} {
if got := KeyFor(name); got != key {
t.Errorf("KeyFor(%q) = %q, want %q", name, got, key)
}
}
}
// design 39 bus-and-switch §33/AC1: each setting's default is the manifest's, a value at its bound is taken, and one
// outside its bound or absent keeps the default.
func TestEachSettingsDefaultAndBound(t *testing.T) {
@@ -72,28 +87,3 @@ func TestAnInvalidPairIsFound(t *testing.T) {
t.Fatalf("invalid %+v", got)
}
}
// design 39 bus-and-switch §33: the settings are read again at every reading, and each change of a value read is
// written once in the audit under settings.
func TestASettingChangedIsReadAtTheNextReadingAndAudited(t *testing.T) {
mm := newMesh(t)
ctx := context.Background()
mm.m.SettingsFile = filepath.Join(t.TempDir(), "settings.json")
_ = os.WriteFile(mm.m.SettingsFile, []byte(`{"switch_at_pct": 99}`), 0o600)
if err := mm.m.ReadUsage(ctx); err != nil || len(mm.audit.under("settings")) != 0 {
t.Fatalf("an unchanged file audited %v (%v)", mm.audit.under("settings"), err)
}
_ = os.WriteFile(mm.m.SettingsFile, []byte(`{"switch_at_pct": 95, "usage_minutes": 10}`), 0o600)
_ = mm.m.ReadUsage(ctx)
_ = mm.m.ReadUsage(ctx)
if s := mm.m.settings(); s.SwitchAt != 95 || s.UsageEvery.Minutes() != 10 {
t.Fatalf("in force %+v", s)
}
got := mm.audit.under("settings")
if len(got) != 1 {
t.Fatalf("audited %v", got)
}
if c, _ := got[0]["switch_at_pct"].(map[string]any); c["from"] != 99.0 || c["to"] != 95.0 {
t.Fatalf("audited %v", got[0])
}
}
@@ -1,16 +1,13 @@
package main
package licences
// The manager's settings (design 39 §8, design 39 bus-and-switch §33): today's five and the six of the switch, each
// declared in the manifest's `config` with its default and bound, set through mesh-config and written into the
// settings file. They are read again at every reading, so a change takes effect at the next one, and each change of a
// value read is written in the audit under `settings`.
// The module's settings (design 39 §8, design 39 bus-and-switch §33, B37; design 39 parts §5): today's five and the six
// of the switch, each declared in the manifest's `config` with its default and bound, set through mesh-config and
// written into the settings file; with their defaults, bounds and validity, read by both bundles.
import (
"context"
"encoding/json"
"fmt"
"os"
"reflect"
"time"
)
@@ -142,33 +139,18 @@ func (s Settings) Invalid() []Invalid {
return out
}
// settings are those in force.
func (m *Manager) settings() Settings {
m.settingsMu.RLock()
defer m.settingsMu.RUnlock()
return m.Settings
}
// ReadSettings reads the settings file again, at every reading (§33): each value that changed is written in the audit
// under `settings`, its value before and now, and the settings read are those in force from then on.
func (m *Manager) ReadSettings(ctx context.Context) error {
if m.SettingsFile == "" {
return nil
// Reasons are why each invalid setting is invalid, by setting, in the words its condition's summary says (§33, B37): a
// setting outside its bound first, then a setting of §33's pairs above switch_at_pct.
func (s Settings) Reasons() map[string]string {
out := map[string]string{}
for _, o := range s.OutOfBound {
out[o.Setting] = fmt.Sprintf("%s is %g, outside its bound; its default %g is used", o.Setting, o.Value, o.Default)
}
next := SettingsFrom(m.SettingsFile)
m.settingsMu.Lock()
before := m.Settings
m.Settings = next
m.settingsMu.Unlock()
was, now := before.Values(), next.Values()
changed := map[string]any{}
for k, v := range now {
if !reflect.DeepEqual(was[k], v) {
changed[k] = map[string]any{"from": was[k], "to": v}
for _, p := range s.Invalid() {
if _, ok := out[p.Setting]; !ok {
out[p.Setting] = fmt.Sprintf("%s is %g, above switch_at_pct %g; no consumer is moved", p.Setting, p.Value,
p.SwitchAt)
}
}
if len(changed) == 0 {
return nil
}
return m.record(ctx, "settings", fmt.Sprintf("%d setting(s) changed: %v", len(changed), changed), changed)
return out
}
+13 -2
View File
@@ -262,11 +262,11 @@
"build": {
"artifacts": [
{
"name": "code",
"name": "manager",
"kind": "bundle",
"language": "go",
"system": "arch",
"from": "cmd/claude-licence-manager",
"from": "cmd/manager",
"binary": "claude-licence-manager",
"loads": [
"claude-licence-manager"
@@ -276,6 +276,17 @@
"MESH_LICENCE_SETTINGS": "${dir:state}/settings.json"
}
},
{
"name": "tools",
"kind": "bundle",
"language": "go",
"system": "arch",
"from": "cmd/tools",
"binary": "claude-licence-manager-tools",
"loads": [
"claude-licence-manager-tools"
]
},
{
"name": "files",
"kind": "archive",