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`). 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 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` ## 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 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 != "" 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 { func viewTools(env *Env) []toolDef {
return []toolDef{ return []toolDef{
{Name: FirstViewsTool, {Name: FirstViewsTool,
Description: "The vault's first view of the assigner's assignments: the tool it asks, the revision its picture holds, when it " + Description: "The vault's first views, of the assigner's assignments and of the licence manager's bindings: for each, the tool " +
"asked and was answered, how many asks (and by hand), and whether it is waiting.", "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) { run: func(string, map[string]any) (any, error) {
if assignmentsView == nil { names, all := firstViews()
return nil, fmt.Errorf("the vault reads the assigner's assignments only when it runs as the module") 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, {Name: FirstViewAgainTool,
Description: "Ask the first view of the assigner's assignments once more by hand, with why, recorded in the vault's log.", 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 " +
Input: map[string]any{"why": str("why the first view is asked again")}, "(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) { 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")) why := strings.TrimSpace(arg(a, "why"))
if why == "" { if why == "" {
return nil, fmt.Errorf("say why: a first view asked by hand keeps its 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 { if err := fromAGrantedCaller(caller, FirstViewAgainTool); err != nil {
return nil, err return nil, err
} }
say("the first view of the assigner's assignments is asked again by %s: %s", caller, why) names, all := firstViews()
return map[string]any{"asked-again": true}, assignmentsView.Again(context.Background(), why) 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 package main
import ( import (
"context"
"encoding/json" "encoding/json"
"errors" "errors"
"os" "os"
"slices" "slices"
"sort" "sort"
"testing" "testing"
"git.novox.be/novox/mesh-sdk/go/views"
) )
// memLicenceBindings is the state licence-bindings in memory. // 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) 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)
}
}
}