V-4: the vault's first-view tools cover the licence bindings, and its README says the licence secrets (issue 1409)

vault_first_views lists the licence bindings' first view beside the
assignments'; vault_first_view_again asks the one its new argument `of`
names, and both when it is absent.

Builds: design 39 bus-and-switch §52, §15 to §17, B21, B42

Claude-Session: https://claude.ai/code/session_01NcbgfHb9d4T9nq5Q6WqdiJ
This commit is contained in:
2026-11-12 00:42:53 +01:00
parent 3b92e02862
commit 9cf5c1ddbd
3 changed files with 118 additions and 13 deletions
@@ -168,25 +168,48 @@ func leaverKey(key string) (string, bool) {
return node, ok && node != ""
}
// viewTools are the read tool of the first view and its re-read, as every reader has them (design 25 § 12.2).
// firstViews are the vault's first views begun, by the owner's state each follows (`<owner>.<state>`): the assigner's
// assignments and the licence manager's bindings (design 39 bus-and-switch B21, B42).
func firstViews() ([]string, map[string]*views.Follower) {
all := map[string]*views.Follower{}
if assignmentsView != nil {
all[assignerModule+".assignments"] = assignmentsView
}
if licenceBindingsView != nil {
all["claude-licence-manager.bindings"] = licenceBindingsView
}
names := make([]string, 0, len(all))
for n := range all {
names = append(names, n)
}
sort.Strings(names)
return names, all
}
// viewTools are the read tool of the first views and their re-read, as every reader has them (design 25 § 12.2; design
// 39 bus-and-switch B21, B42).
func viewTools(env *Env) []toolDef {
return []toolDef{
{Name: FirstViewsTool,
Description: "The vault's first view of the assigner's assignments: the tool it asks, the revision its picture holds, when it " +
"asked and was answered, how many asks (and by hand), and whether it is waiting.",
Description: "The vault's first views, of the assigner's assignments and of the licence manager's bindings: for each, the tool " +
"it asks, the revision its picture holds, when it asked and was answered, how many asks (and by hand), and whether it is waiting.",
run: func(string, map[string]any) (any, error) {
if assignmentsView == nil {
return nil, fmt.Errorf("the vault reads the assigner's assignments only when it runs as the module")
names, all := firstViews()
if len(names) == 0 {
return nil, fmt.Errorf("the vault keeps its first views only when it runs as the module")
}
return map[string]any{"first-views": []views.Status{assignmentsView.Status()}}, nil
out := []views.Status{}
for _, n := range names {
out = append(out, all[n].Status())
}
return map[string]any{"first-views": out}, nil
}},
{Name: FirstViewAgainTool,
Description: "Ask the first view of the assigner's assignments once more by hand, with why, recorded in the vault's log.",
Input: map[string]any{"why": str("why the first view is asked again")},
Description: "Ask a first view once more by hand, with why, recorded in the vault's log: of names it by its owner's state " +
"(mesh-delivery-assigner.assignments or claude-licence-manager.bindings); absent, both are asked again.",
Input: map[string]any{"why": str("why the first view is asked again"),
"of": str("the first view, by its owner's state: mesh-delivery-assigner.assignments or claude-licence-manager.bindings; absent for both")},
run: func(caller string, a map[string]any) (any, error) {
if assignmentsView == nil {
return nil, fmt.Errorf("the vault reads the assigner's assignments only when it runs as the module")
}
why := strings.TrimSpace(arg(a, "why"))
if why == "" {
return nil, fmt.Errorf("say why: a first view asked by hand keeps its why")
@@ -194,8 +217,23 @@ func viewTools(env *Env) []toolDef {
if err := fromAGrantedCaller(caller, FirstViewAgainTool); err != nil {
return nil, err
}
say("the first view of the assigner's assignments is asked again by %s: %s", caller, why)
return map[string]any{"asked-again": true}, assignmentsView.Again(context.Background(), why)
names, all := firstViews()
if of := strings.TrimSpace(arg(a, "of")); of != "" {
if all[of] == nil {
return nil, fmt.Errorf("the vault keeps no first view of %q here; it keeps %v", of, names)
}
names = []string{of}
}
if len(names) == 0 {
return nil, fmt.Errorf("the vault keeps its first views only when it runs as the module")
}
for _, n := range names {
say("the first view of %s is asked again by %s: %s", n, caller, why)
if err := all[n].Again(context.Background(), why); err != nil {
return nil, err
}
}
return map[string]any{"asked-again": true}, nil
}},
}
}
@@ -1,12 +1,15 @@
package main
import (
"context"
"encoding/json"
"errors"
"os"
"slices"
"sort"
"testing"
"git.novox.be/novox/mesh-sdk/go/views"
)
// memLicenceBindings is the state licence-bindings in memory.
@@ -113,3 +116,52 @@ func TestTheFirstViewOfTheLicenceBindingsIsKept(t *testing.T) {
t.Fatalf("the manifest invokes %v, consumes %v, keeps %s", m.Invokes, m.Consumes, m.State)
}
}
// design 39 bus-and-switch B21, B42: vault_first_views lists the licence bindings' first view beside the assignments';
// vault_first_view_again asks the one its of names, and both when of is absent; an of it keeps no view of is refused.
func TestTheFirstViewToolsCoverTheLicenceBindings(t *testing.T) {
follower := func(owner, state string) *views.Follower {
return &views.Follower{Owner: owner, Tool: owner + ".read", State: state,
Ask: func(context.Context, string) (views.Answer, error) {
return views.Answer{Owner: owner, State: state, Values: map[string]json.RawMessage{}}, nil
}}
}
keptA, keptL := assignmentsView, licenceBindingsView
t.Cleanup(func() { assignmentsView, licenceBindingsView = keptA, keptL })
assignmentsView, licenceBindingsView = follower(assignerModule, "assignments"), follower("claude-licence-manager", "bindings")
run := map[string]toolDef{}
for _, d := range viewTools(nil) {
run[d.Name] = d
}
listed, err := run[FirstViewsTool].run("person.operator", nil)
if err != nil {
t.Fatal(err)
}
var states []string
for _, s := range listed.(map[string]any)["first-views"].([]views.Status) {
states = append(states, s.Owner+"."+s.State)
}
if !slices.Equal(states, []string{"claude-licence-manager.bindings", "mesh-delivery-assigner.assignments"}) {
t.Errorf("vault_first_views lists %v", states)
}
byHand := func() (int, int) { return assignmentsView.Status().ByHand, licenceBindingsView.Status().ByHand }
again := run[FirstViewAgainTool].run
for _, c := range []struct {
of string
assignments, bindings int
refused bool
}{
{of: "claude-licence-manager.bindings", bindings: 1},
{of: "", assignments: 1, bindings: 2},
{of: "mesh-delivery-assigner.assignments", assignments: 2, bindings: 2},
{of: "claude-licence-manager.licences", assignments: 2, bindings: 2, refused: true},
} {
_, err := again("person.operator", map[string]any{"why": "a test", "of": c.of})
if (err != nil) != c.refused {
t.Errorf("of %q: %v", c.of, err)
}
if a, b := byHand(); a != c.assignments || b != c.bindings {
t.Errorf("of %q: asked by hand %d and %d times, want %d and %d", c.of, a, b, c.assignments, c.bindings)
}
}
}