Builds: design 39 bus-and-switch §37, §12 and the lead's answer C5 (hq 45ee2475).
277 lines
9.0 KiB
Go
277 lines
9.0 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])
|
|
}},
|
|
}
|
|
}
|
|
|
|
// carriedForm gives an entry carried over from the old table `audit` (§12) the fields of §37: the old manager wrote a
|
|
// switch as `consumer` (one), `licence` (the one moved to), `from` and `by`, and no `consumers` or `to`.
|
|
func carriedForm(e map[string]any) {
|
|
if _, ok := e["to"]; !ok {
|
|
if l, ok := e["licence"]; ok {
|
|
e["to"] = l
|
|
}
|
|
}
|
|
if _, ok := e["consumers"]; !ok {
|
|
if c, ok := e["consumer"]; ok {
|
|
e["consumers"] = []any{c}
|
|
}
|
|
}
|
|
}
|
|
|
|
// 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
|
|
}
|
|
carriedForm(e)
|
|
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
|
|
}
|