From 1c032997210ca51721bf48d66ba1e6be7db21bbf Mon Sep 17 00:00:00 2001 From: jochen Date: Wed, 11 Nov 2026 23:01:43 +0100 Subject: [PATCH] M-2: the manager's manifest asks for its kept data by template MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Eight bus-state entries in the typed long form; the old state and the reads of claude-code.holdings go, and so does the run-once prepare, since nothing writes the tables again. The database and grant-key stay as the carry-over's read-only source. The manifest declares its new events, its consumption of holdings-changed, the operator channel it uses, the verbs it invokes, six settings and its eight tools. Builds: design 39 bus-and-switch §1, §9, §21, §23, §27, §33, §37-§44, A3, B9, B16 Claude-Session: https://claude.ai/code/session_01NcbgfHb9d4T9nq5Q6WqdiJ --- .../cmd/claude-licence-manager/main.go | 22 --- .../claude-licence-manager/manifest_test.go | 50 +++++ .../files/settings.json | 8 +- modules/claude-licence-manager/module.json | 178 +++++++++++++++--- 4 files changed, 207 insertions(+), 51 deletions(-) create mode 100644 modules/claude-licence-manager/cmd/claude-licence-manager/manifest_test.go diff --git a/modules/claude-licence-manager/cmd/claude-licence-manager/main.go b/modules/claude-licence-manager/cmd/claude-licence-manager/main.go index 0d966153b..5ca869a5f 100644 --- a/modules/claude-licence-manager/cmd/claude-licence-manager/main.go +++ b/modules/claude-licence-manager/cmd/claude-licence-manager/main.go @@ -3,9 +3,6 @@ // `anthropic-licence-manager` seat's verbs and, beside them, runs long: 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. // -// `claude-licence-manager prepare` is the preparation step (ADR 0135): the host runs it once before the -// version that needs it, with the module's words and no bus, and it brings the store's schema to shape. -// // stdout is the MCP channel; everything this module says, it says on stderr. package main @@ -32,14 +29,6 @@ func say(format string, args ...any) { } func main() { - if len(os.Args) > 1 && os.Args[1] == "prepare" { - if err := prepare(); err != nil { - say("preparing the store failed: %v", err) - os.Exit(1) - } - say("the store's schema is what this version needs") - return - } go daemon() if err := stdio.Serve("", tools()); err != nil { say("%v", err) @@ -47,17 +36,6 @@ func main() { } } -func prepare() error { - ctx, cancel := context.WithTimeout(context.Background(), time.Minute) - defer cancel() - store, err := PgStoreFromEnv(ctx) - if err != nil { - return err - } - defer store.Close() - return store.Migrate(ctx) -} - // ---- what the binary is handed ----------------------------------------------------------------------- var ( diff --git a/modules/claude-licence-manager/cmd/claude-licence-manager/manifest_test.go b/modules/claude-licence-manager/cmd/claude-licence-manager/manifest_test.go new file mode 100644 index 000000000..e087078ab --- /dev/null +++ b/modules/claude-licence-manager/cmd/claude-licence-manager/manifest_test.go @@ -0,0 +1,50 @@ +package main + +import ( + "encoding/json" + "os" + "path/filepath" + "testing" +) + +// design 39 bus-and-switch §1: the module moves to the new form whole — no `reads`, no old `state`, no `prepare` +// step — and asks for each piece of its kept data by template. +func TestTheManifestKeepsNoOldForm(t *testing.T) { + raw, err := os.ReadFile(filepath.Join(moduleDir, "module.json")) + if err != nil { + t.Fatal(err) + } + var m map[string]json.RawMessage + if err := json.Unmarshal(raw, &m); err != nil { + t.Fatal(err) + } + for _, old := range []string{"reads", "state"} { + if _, ok := m[old]; ok { + t.Errorf("the manifest keeps the old section %q", old) + } + } + var resources []struct { + ID string `json:"id"` + } + _ = json.Unmarshal(m["resources"], &resources) + for _, r := range resources { + if r.ID == "prepare" { + t.Error("the manifest keeps the run-once step prepare") + } + } + var requires []json.RawMessage + _ = json.Unmarshal(m["requires"], &requires) + kept := map[string]string{} + for _, r := range requires { + var e struct{ Name, Template string } + if json.Unmarshal(r, &e) == nil { + kept[e.Name] = e.Template + } + } + for name, template := range map[string]string{"licences": "state", "bindings": "state", "leases": "state", + "offered": "state", "usage": "log", "audit": "log", "carried": "state", "conditions": "state"} { + if kept[name] != template { + t.Errorf("bus-state entry %s is %q, want template %s", name, kept[name], template) + } + } +} diff --git a/modules/claude-licence-manager/files/settings.json b/modules/claude-licence-manager/files/settings.json index 5d4aed520..fc6df9b3a 100644 --- a/modules/claude-licence-manager/files/settings.json +++ b/modules/claude-licence-manager/files/settings.json @@ -3,5 +3,11 @@ "floor_minutes": 60, "failures_to_notify": 3, "cooldown_hours": 24, - "refresh_warn_days": 3 + "refresh_warn_days": 3, + "switch_auto": true, + "switch_at_pct": 99, + "room_below_pct": 90, + "warn_at_pct": 90, + "usage_minutes": 5, + "usage_stale_minutes": 15 } diff --git a/modules/claude-licence-manager/module.json b/modules/claude-licence-manager/module.json index a55da31e2..00869aaac 100644 --- a/modules/claude-licence-manager/module.json +++ b/modules/claude-licence-manager/module.json @@ -4,7 +4,74 @@ "slug": "licmgr", "requires": [ "postgres-database", - "secret" + "secret", + { + "name": "licences", + "storage": "key-value", + "template": "state", + "options": { + "history": 1 + } + }, + { + "name": "bindings", + "storage": "key-value", + "template": "state", + "options": { + "history": 8 + } + }, + { + "name": "leases", + "storage": "key-value", + "template": "state", + "options": { + "history": 1, + "key-ttl": true + } + }, + { + "name": "offered", + "storage": "key-value", + "template": "state", + "options": { + "history": 1, + "key-ttl": true + } + }, + { + "name": "usage", + "storage": "stream", + "template": "log", + "options": { + "max-age-days": 35, + "max-mib": 256 + } + }, + { + "name": "audit", + "storage": "stream", + "template": "log", + "options": { + "max-mib": 64 + } + }, + { + "name": "carried", + "storage": "key-value", + "template": "state", + "options": { + "history": 1 + } + }, + { + "name": "conditions", + "storage": "key-value", + "template": "state", + "options": { + "history": 1 + } + } ], "contributes": { "postgres-database": { @@ -24,6 +91,14 @@ "grant-key": "${dir:state}/grant.key" } }, + "uses": [ + "operator-channel" + ], + "invokes": [ + "seat:operator-channel.tell", + "seat:mesh-vault.licence-put", + "seat:mesh-vault.get" + ], "seats": [ { "name": "anthropic-licence-manager", @@ -63,18 +138,14 @@ "emits": [ "licence.adopted", "licence.refused", - "licence.bound", "licence.switched", - "licence.released", - "licence.rotated", "licence.failing", - "usage.read" + "usage.read", + "binding.changed", + "holdings.asked" ], - "state": [ - "bindings" - ], - "reads": [ - "claude-code.holdings" + "consumes": [ + "claude-code.holdings-changed" ], "config": { "cadence_minutes": { @@ -111,6 +182,52 @@ "minimum": 0, "trusted": true, "default": 3 + }, + "switch_auto": { + "type": "boolean", + "description": "Whether the manager moves every consumer off a licence whose session or weekly usage reaches switch_at_pct, to the licence with the most room; off, it still raises its no-room and near-limit conditions.", + "trusted": true, + "default": true + }, + "switch_at_pct": { + "type": "number", + "description": "The session or weekly usage of a licence, in percent, at which its consumers are moved to the licence with the most room.", + "minimum": 1, + "maximum": 100, + "trusted": true, + "default": 99 + }, + "room_below_pct": { + "type": "number", + "description": "What both the session and the weekly usage of a licence must be below, in percent, for it to be a target of a switch.", + "minimum": 1, + "maximum": 100, + "trusted": true, + "default": 90 + }, + "warn_at_pct": { + "type": "number", + "description": "The session or weekly usage of a licence with consumers, in percent, at which it is warned about once.", + "minimum": 1, + "maximum": 100, + "trusted": true, + "default": 90 + }, + "usage_minutes": { + "type": "number", + "description": "How often each subscription's usage is read, in minutes.", + "minimum": 1, + "maximum": 60, + "trusted": true, + "default": 5 + }, + "usage_stale_minutes": { + "type": "number", + "description": "How old a subscription's last successful usage reading may grow before its room is not known and it is said, in minutes.", + "minimum": 5, + "maximum": 180, + "trusted": true, + "default": 15 } }, "resources": [ @@ -133,28 +250,33 @@ "type": "file", "path": "${dir:state}/settings.json", "mode": "0600", - "takes-config": ["cadence_minutes", "floor_minutes", "failures_to_notify", "cooldown_hours", "refresh_warn_days"], + "takes-config": [ + "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" - }, - { - "id": "prepare", - "type": "process", - "name": "claude-licence-manager-prepare", - "artifact": "code", - "run": [ - "./claude-licence-manager", - "prepare" - ], - "run-once": true, - "env": { - "DATABASE_URL_FILE": "${dir:state}/database.url" - }, - "restart-on": [ - "database-url" - ] } ], + "tools": [ + "licences_switches", + "licences_read_usage", + "licences_switch_now", + "licences_settings", + "licences_kept", + "licences_resync", + "licences_conditions", + "licences_carried" + ], "build": { "artifacts": [ {