Read the bus's user-list file's place from what the node runs, not from the controller's compositions
The bus module on the bus node runs from what its agent holds (its own composition, or the declaration kept before the switch: every novox have read 2026-11-02 names its own composition or none, and nats names a build alone), while composed-<node> was written by the controller, which ended (issue 1126). The follower now looks at the agent's record of what runs whenever it changed, writes again only when the file it names moved, and trusts no composition signer. Claude-Session: https://claude.ai/code/session_01NcbgfJ9Gum47zkFKWRaovL
This commit is contained in:
+11
-41
@@ -3,7 +3,6 @@ package main
|
||||
import (
|
||||
"context"
|
||||
"crypto/ed25519"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
@@ -375,42 +374,6 @@ func resourcesOf(cs []delivery.Composed) []declaration.Resource {
|
||||
return out
|
||||
}
|
||||
|
||||
// operatorKeysFile is the file beside the node's state that holds the operator's public keys, one ed25519 key in base64 a
|
||||
// line (a line that starts with # is a note). The switch's last declaration writes it as a file resource, beside the
|
||||
// controller's key the node was enrolled with (novox/hq ADR 0539 §3, §11); a node adopted later has it at its adoption.
|
||||
const operatorKeysFile = "operator-keys"
|
||||
|
||||
// trustedKeys are the keys a composition in the node's composition bucket may be signed by: the one given (none for the
|
||||
// agent; the enrolment's signer for the user-list follower, which reads only where the bus's user-list file is from
|
||||
// such a composition, never its list) and the operator's, read from operatorKeysFile each time it is asked, so a key
|
||||
// that arrives while the agent runs is trusted at the next composition. A file that cannot be read trusts the one given
|
||||
// alone, and says so.
|
||||
func trustedKeys(opts options, controllerNow func() ed25519.PublicKey) func() []ed25519.PublicKey {
|
||||
return func() []ed25519.PublicKey {
|
||||
keys := []ed25519.PublicKey{}
|
||||
if controller := controllerNow(); len(controller) == ed25519.PublicKeySize {
|
||||
keys = append(keys, controller)
|
||||
}
|
||||
raw, err := os.ReadFile(filepath.Join(filepath.Dir(opts.state), operatorKeysFile))
|
||||
if err != nil {
|
||||
return keys
|
||||
}
|
||||
for _, line := range strings.Split(string(raw), "\n") {
|
||||
line = strings.TrimSpace(line)
|
||||
if line == "" || strings.HasPrefix(line, "#") {
|
||||
continue
|
||||
}
|
||||
key, err := base64.StdEncoding.DecodeString(line)
|
||||
if err != nil || len(key) != ed25519.PublicKeySize {
|
||||
fmt.Fprintf(os.Stderr, "mesh-host: a line of %s is not an ed25519 public key in base64, and is not trusted\n", operatorKeysFile)
|
||||
continue
|
||||
}
|
||||
keys = append(keys, ed25519.PublicKey(key))
|
||||
}
|
||||
return keys
|
||||
}
|
||||
}
|
||||
|
||||
// keptOnce reads the kept declaration once and splits it by module: its resources (the node's own are those that say
|
||||
// none, ADR 0539 §9) and its filled paths. Once the read is retired (keptRetired) nothing is read.
|
||||
func keptOnce(opts options, signer ed25519.PublicKey) func() (map[string][]json.RawMessage, map[string][]string) {
|
||||
@@ -511,10 +474,15 @@ func nodeAgent(ctx context.Context, opts options, signer ed25519.PublicKey, trus
|
||||
}
|
||||
}
|
||||
// The bus's user list, the grants part's alone (ADR 0547 §8, clause 11; issue 1126), written into the file the
|
||||
// bus module's composition in the node's bucket declares: on every node, and a node whose compositions carry no
|
||||
// list writes nothing. Those compositions were signed with the enrolment's signer and nobody writes them now; they
|
||||
// are read for the file's place alone, never for a list, until the place is read elsewhere (milestone B17).
|
||||
users := userListFollower(opts, session, composedBucket, trustedKeys(opts, func() ed25519.PublicKey { return signer }), say, func() {
|
||||
// bus module this node runs declares, as its agent holds what runs (its own composition, or the declaration kept
|
||||
// before the switch): never read from the node's composition bucket, which the controller wrote. On every node;
|
||||
// a node that runs no list writes nothing. Looked at again whenever what runs changed (RunsChanged, Settled).
|
||||
users := userListFollower(opts, session, func() []delivery.Composed {
|
||||
if now := current.get(); now != nil {
|
||||
return now.Everything()
|
||||
}
|
||||
return nil
|
||||
}, say, func() {
|
||||
// This engine wrote the list, so the bus reloads its accounts and answers no JetStream request for a few
|
||||
// seconds: what failed in them is tried again once that is over, not at its next wait.
|
||||
time.AfterFunc(reloadSettles, func() {
|
||||
@@ -558,12 +526,14 @@ func nodeAgent(ctx context.Context, opts options, signer ed25519.PublicKey, trus
|
||||
Settled: func(module string) {
|
||||
knockFilling(module+" settled", say)
|
||||
judgeWhatRunsNow(opts.state, signer)
|
||||
users.RunsChanged()
|
||||
},
|
||||
// And once the haves are read at the start, and whenever a module is taken as running without an apply, so
|
||||
// a refusal from the engine's start does not wait for some module to settle (issue 924).
|
||||
RunsChanged: func(why string) {
|
||||
knockFilling(why, say)
|
||||
judgeWhatRunsNow(opts.state, signer)
|
||||
users.RunsChanged()
|
||||
},
|
||||
Say: say,
|
||||
// A failure of the bus not answering for a moment is tried again: 2 s, 5 s, 15 s, 60 s, then every 5 min.
|
||||
|
||||
Reference in New Issue
Block a user