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
+15
View File
@@ -136,6 +136,21 @@ the declaration sealed by the vault to their machine, which the controller carri
machine's sealing key as its own node-engine stated it in its own name, in `move-in`'s first step (`sealing_key`).
A machine whose engine stated none, any other key, any other module and any other name are refused and recorded.
**A licence's two secrets, for the licence manager and the machines its bindings name** (design 39 bus-and-switch
§15 to §17, B2, B3, B8, B21, B25, B31, B32, B42; `licences.go`, `licencebindings.go`). The licence manager hands
each licence's grant and access token to the vault with `mesh-vault.licence-put`, in two steps: the licence's key, the
part (`grant` or `access`) and the generation, answered with a call and a key made for it; then the call and the value
sealed to that key. The caller's generation is the record's: the newest is kept, an older one refused, and
`secret-changed` says the name `licence.<key>.<part>` and its generation, never the value. `get` hands a grant, sealed to
the key it is asked with, with no machine guard (B3); it hands an access token only to a machine on which a consumer's binding
names that licence, by the licence's key as the manager says it. Which licence each consumer is bound to the vault
keeps in its own state `licence-bindings` (one key per consumer: licence, key, generation; deleted on a release),
learned from the manager's `binding.changed`, which it consumes, and at its start from its first view,
`anthropic-licence-manager.bindings`, asked until it answers. A first view never replaces a binding of a newer
generation, and an access token asked before the first view answered is refused, saying why. The vault never reads
the manager's state. `vault_first_views` shows this first view beside the assignments', and `vault_first_view_again`
asks one again (`of`: `mesh-delivery-assigner.assignments` or `claude-licence-manager.bindings`; absent, both).
## Taking values in: `secret-ask`, `issued`, `rotate`
Design 57's V2 (issue 675; ADR 0349 clauses 8 to 10, ADR 0277 §22–§27 and ADR 0305 with the vault in the
@@ -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)
}
}
}