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
130 lines
4.5 KiB
Go
130 lines
4.5 KiB
Go
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...)
|
|
}
|