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:
@@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user