The engine dials as the login the vault's node-logins names

A membership may name its Logins (ADR 0583 §1): the login in use and the password of each login held, kept beside the
identity, so a restart dials as the login in use. The engine follows its own key of node-logins as a watch on every link
and dials as the login it names when it holds its value, with the node's one inbox; with no key, or a key naming the first
login, it dials node.<n> with the membership's password as before. A change of the login in use drops the link so it is
opened again as the new one, and the vault is asked in the name of the login dialled (the subject names the caller).
Tests: one added, paid by merging the pinned-broker test with the malformed-pin test (two into one).

(issue 774, 774/S61)

Claude-Session: https://claude.ai/code/session_01NcbgfJ9Gum47zkFKWRaovL
This commit is contained in:
2026-11-02 01:49:01 +01:00
parent 598b0d8193
commit c7468656af
8 changed files with 354 additions and 15 deletions
+86
View File
@@ -0,0 +1,86 @@
package main
import (
"context"
"encoding/json"
"os"
"path/filepath"
"time"
"git.novox.be/novox/mesh-sdk/go/delivery"
"github.com/novox/mesh-host/internal/identity"
"github.com/novox/mesh-host/internal/link"
)
// loginsFile is where this engine keeps the logins it holds between runs (novox/hq ADR 0583 §1): beside its identity, readable by
// its owner alone, as the identity's membership password is.
func loginsFile(state string) string {
return filepath.Join(filepath.Dir(identity.Path(state)), "logins.json")
}
// loadLogins is the engine's logins: what it kept, with the membership's password as the first login's.
func loadLogins(state, node, first string) *link.Logins {
path := loginsFile(state)
var saved link.LoginsState
if raw, err := os.ReadFile(path); err == nil {
_ = json.Unmarshal(raw, &saved)
}
return link.NewLogins(node, first, saved, func(s link.LoginsState) error { return writeLogins(path, s) })
}
func writeLogins(path string, s link.LoginsState) error {
raw, err := json.Marshal(s)
if err != nil {
return err
}
tmp := path + ".new"
if err := os.WriteFile(tmp, raw, 0o600); err != nil {
return err
}
return os.Rename(tmp, path)
}
// loginsFollowAgain bounds the wait before a watch of node-logins that could not be made is tried again on the same link:
// the vault may not serve rotation yet, and until it does this node is not granted the read.
var loginsFollowAgain = 2 * time.Minute
// followLogins follows this node's key of the vault's node-logins on every link, as a watch (never a poll), for as long as ctx
// lasts, and hands each value to hear. A watch that cannot be made is said once and tried again.
func followLogins(ctx context.Context, session *link.Session, node string, hear func(delivery.NodeLogin), say link.Announce) {
said := ""
for ctx.Err() == nil {
linked, cancel, err := session.WhenLinked(ctx)
if err != nil {
return
}
watching, stop := context.WithCancel(linked)
updates, werr := session.KVWatch(watching, delivery.NodeLoginsBucket, node)
if werr != nil {
stop()
if line := "this node's login is not followed yet (" + werr.Error() + "): it dials as it last did, and looks again in " + loginsFollowAgain.String(); line != said {
said = line
say(line)
}
} else {
said = ""
for u := range updates {
if u.Caught || u.Deleted || len(u.Value) == 0 {
continue
}
nl, err := delivery.DecodeNodeLogin(u.Value)
if err != nil {
say("the key " + node + " of " + delivery.NodeLoginsRead + " is not read: " + err.Error())
continue
}
hear(nl)
}
stop()
}
select {
case <-linked.Done():
case <-time.After(loginsFollowAgain):
}
cancel()
}
}
+9
View File
@@ -30,6 +30,7 @@ import (
"time"
"git.novox.be/novox/mesh-sdk/go/declaration"
"git.novox.be/novox/mesh-sdk/go/delivery"
"github.com/novox/mesh-host/internal/accounts"
"github.com/novox/mesh-host/internal/agent"
"github.com/novox/mesh-host/internal/apply"
@@ -1074,6 +1075,8 @@ func runLink(ctx context.Context, opts options) error {
Password: mine.Membership.Password,
Transport: mine.Membership.Transport,
Signer: mine.Membership.Signer,
// Which of its two logins it dials as (ADR 0583 §1): the one the vault's node-logins names.
Logins: loadLogins(opts.state, mine.Node, mine.Membership.Password),
}
// What follows an apply of a module (the node agent's, novox/hq ADR 0539): a membership it delivered for another bus
@@ -1190,6 +1193,12 @@ func runLink(ctx context.Context, opts options) error {
// And what it trusts, followed on the bus, said in its record whenever it changes (ADR 0550 §2).
trust.onChange(func() { factsOfNow(aside, current.get()) })
go followTrust(aside, queue, trust, say)
// And the login it dials as, which the vault's node-logins names for this node (novox/hq ADR 0583 §1).
go followLogins(aside, queue, mine.Node, func(nl delivery.NodeLogin) {
if line := membership.Logins.Heard(nl); line != "" {
say(line)
}
}, say)
go nodeAgent(aside, opts, mine.Membership.Signer, trust, queue, sched, say, afterApply, current, factsOfNow)
go busyPasses(aside, func() {
if ag := current.get(); ag != nil {