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
261 lines
8.6 KiB
Go
261 lines
8.6 KiB
Go
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
|
|
}
|