Vault: keep a rotation's hand acts in mesh-sdk's NodeLogin.Acts, and test a tunnel without its key or range

mesh-sdk now carries the hand acts that warranted a node's rotations as NodeLogin.Acts (ADR 0583 §2), so the vault's
own `acts` field beside the codec is gone: a used act is refused from the key's Acts, and a key written without them
keeps the ones recorded. The seat's tunnel takes all five fields; the wire table refuses an issue missing the machine's
tunnel key or the range as well. Built against mesh-sdk 4224665; mesh-vault stays at 134 unit tests.

(issue 774, 774/S43, 774/S46, review round 1b)
Claude-Session: https://claude.ai/code/session_01NcbgfJ9Gum47zkFKWRaovL
This commit is contained in:
2026-11-02 01:42:19 +01:00
parent 09824b4839
commit f41a5a8df1
2 changed files with 21 additions and 86 deletions
@@ -25,26 +25,6 @@ import (
type loginsState struct {
mu sync.Mutex
keys map[string]NodeLogin
acts map[string][]string
}
func (l *loginsState) Acts(node string) ([]string, error) {
l.mu.Lock()
defer l.mu.Unlock()
return slices.Clone(l.acts[node]), nil
}
func (l *loginsState) RecordAct(n NodeLogin, act string) error {
if err := l.Put(n); err != nil {
return err
}
l.mu.Lock()
defer l.mu.Unlock()
if l.acts == nil {
l.acts = map[string][]string{}
}
l.acts[n.Node] = append(l.acts[n.Node], act)
return nil
}
func (l *loginsState) Get(node string) (*NodeLogin, error) {
@@ -78,7 +58,6 @@ func (l *loginsState) Delete(node string) error {
l.mu.Lock()
defer l.mu.Unlock()
delete(l.keys, node)
delete(l.acts, node)
return nil
}
@@ -231,6 +210,8 @@ func TestTheVaultsEnrolmentAnswersAreMeshSDKsWireShapes(t *testing.T) {
{"a tunnel without the hub's key", "enrolment-token", holder, map[string]any{"node": "new1", "tunnel": without("hub_key")}, nil, nil, "hub's tunnel key"},
{"a tunnel without the hub's endpoint", "enrolment-token", holder, map[string]any{"node": "new1", "tunnel": without("hub_endpoint")}, nil, nil, "where the hub's tunnel is dialled"},
{"a tunnel without the address", "enrolment-token", holder, map[string]any{"node": "new1", "tunnel": without("address")}, nil, nil, "this machine's address"},
{"a tunnel without the machine's tunnel key", "enrolment-token", holder, map[string]any{"node": "new1", "tunnel": without("key")}, nil, nil, "the tunnel key it was issued for"},
{"a tunnel without the range", "enrolment-token", holder, map[string]any{"node": "new1", "tunnel": without("range")}, nil, nil, "the private network's range"},
{"a tunnel with a field it has not", "enrolment-token", holder, map[string]any{"node": "new1", "tunnel": with(tunnel, map[string]any{"port": 1})}, nil, nil, "not a first tunnel"},
// Issue answers what the machine must trust; sealed and drop as the holder sends them, the switch as a word.
{"issue", "enrolment-token", holder, map[string]any{"node": "new1", "expires": "2026-11-02T12:00:00Z", "tunnel": tunnel}, &issued, []string{"node", "broker", "fingerprint", "signer"}, ""},
@@ -452,7 +433,7 @@ func TestANodesLoginRotatesOneAtATimeAndEndsWithTheListWithoutTheOldOne(t *testi
t.Error("a finished rotation was abandoned")
}
// Blocker 5 (ADR 0583 §2): the act is recorded in g14's key and warrants no second rotation.
if acts, _ := e.logins.Acts("g14"); !slices.Equal(acts, []string{"a1"}) {
if acts := e.logins.keys["g14"].Acts; !slices.Equal(acts, []string{"a1"}) {
t.Errorf("g14's key records the acts %v", acts)
}
if _, err := ask("g14", "a1"); err == nil || !strings.Contains(err.Error(), "already") {
@@ -52,14 +52,10 @@ const (
// and while a rotation runs the other login with its stage. Never a value or a hash.
type NodeLogin = delivery.NodeLogin
// NodeLoginStore is the vault's state `node-logins`, by node. Beside mesh-sdk's NodeLogin each key keeps `acts`, the ids
// of the hand acts that warranted the node's rotations (ADR 0583 §2: every rotation is recorded), which Put keeps.
// NodeLoginStore is the vault's state `node-logins`, by node.
type NodeLoginStore interface {
Get(node string) (*NodeLogin, error)
Put(n NodeLogin) error
// Acts are the hand acts recorded in node's key; RecordAct writes n with act added to them.
Acts(node string) ([]string, error)
RecordAct(n NodeLogin, act string) error
Delete(node string) error
Keys() ([]string, error)
}
@@ -79,32 +75,8 @@ func (busNodeLogins) Get(node string) (*NodeLogin, error) {
return &n, nil
}
func (b busNodeLogins) Put(n NodeLogin) error {
acts, err := b.Acts(n.Node)
if err != nil {
return err
}
return b.write(n, acts)
}
func (busNodeLogins) Acts(node string) ([]string, error) {
e, err := stdio.State(NodeLoginsState).Get(node)
if err != nil || e == nil {
return nil, err
}
return actsOf(e.Value)
}
func (b busNodeLogins) RecordAct(n NodeLogin, act string) error {
acts, err := b.Acts(n.Node)
if err != nil {
return err
}
return b.write(n, append(acts, act))
}
func (busNodeLogins) write(n NodeLogin, acts []string) error {
raw, err := encodeLoginKey(n, acts)
func (busNodeLogins) Put(n NodeLogin) error {
raw, err := delivery.EncodeNodeLogin(n)
if err != nil {
return err
}
@@ -112,31 +84,6 @@ func (busNodeLogins) write(n NodeLogin, acts []string) error {
return err
}
// encodeLoginKey is n through mesh-sdk's codec with the hand acts that warranted its rotations beside it.
func encodeLoginKey(n NodeLogin, acts []string) ([]byte, error) {
raw, err := delivery.EncodeNodeLogin(n)
if err != nil || len(acts) == 0 {
return raw, err
}
var m map[string]any
if err := json.Unmarshal(raw, &m); err != nil {
return nil, err
}
m["acts"] = acts
return json.Marshal(m)
}
// actsOf are the hand acts a key of node-logins records.
func actsOf(raw []byte) ([]string, error) {
var k struct {
Acts []string `json:"acts"`
}
if err := json.Unmarshal(raw, &k); err != nil {
return nil, fmt.Errorf("a key of %s is not readable: %w", NodeLoginsState, err)
}
return k.Acts, nil
}
func (busNodeLogins) Delete(node string) error { return stdio.State(NodeLoginsState).Delete(node) }
func (busNodeLogins) Keys() ([]string, error) { return stdio.State(NodeLoginsState).Keys() }
@@ -180,10 +127,20 @@ func (v *Vault) getLogin(node string) (*NodeLogin, error) {
return v.Logins.Get(node)
}
// putLogin writes node's key; a key written without its hand acts keeps the ones recorded (ADR 0583 §2).
func (v *Vault) putLogin(n NodeLogin) error {
if v.Logins == nil {
return errors.New("the vault has no state " + NodeLoginsState + " to write")
}
if n.Acts == nil {
was, err := v.Logins.Get(n.Node)
if err != nil {
return err
}
if was != nil {
n.Acts = was.Acts
}
}
return v.Logins.Put(n)
}
@@ -372,17 +329,13 @@ func (v *Vault) recordAct(node, act string) error {
if v.Logins == nil {
return errors.New("the vault has no state " + NodeLoginsState + " to record the rotation in")
}
used, err := v.Logins.Acts(node)
if err != nil {
return err
}
if slices.Contains(used, act) {
return fmt.Errorf("the hand act %s warranted a rotation of %s already: record a new one", act, node)
}
nl, err := v.getLogin(node)
if err != nil {
return err
}
if nl != nil && slices.Contains(nl.Acts, act) {
return fmt.Errorf("the hand act %s warranted a rotation of %s already: record a new one", act, node)
}
if nl == nil {
cur, err := v.currentLogin(node)
if err != nil {
@@ -390,7 +343,8 @@ func (v *Vault) recordAct(node, act string) error {
}
nl = &NodeLogin{Node: node, Login: cur, Since: v.Now().UTC()}
}
if err := v.Logins.RecordAct(*nl, act); err != nil {
nl.Acts = append(slices.Clone(nl.Acts), act)
if err := v.Logins.Put(*nl); err != nil {
return fmt.Errorf("the hand act %s could not be recorded in %s %s: %w", act, NodeLoginsState, node, err)
}
return nil