Name the expired enrolment's issued event by its id, and carry a node's login fall back in its health report
Blocker 6: enrolment-expired carries the enrolment-issued event's message id (IssuedID), since a bundle's publish answers no stream sequence and ADR 0587 §2 asks only that the event be named; it gains a codec refusing no event named. Blocker 7: NodeHealth gains Fallback (node, the login fallen back from, why, at), from which the watchman raises node-account-fallback (ADR 0583 §6); a fall back naming another node is refused. A token missing its signer now says the vault's signing key, not the retired word. Tested as rows of TestNodeRecord and the control records test; no new test function (1438 unit tests). (issue 774, review round 1) Claude-Session: https://claude.ai/code/session_01NcbgfJ9Gum47zkFKWRaovL
This commit is contained in:
@@ -40,6 +40,10 @@ func TestTheControlRecordsReadTheirWritersValues(t *testing.T) {
|
||||
{"a node's health report, a part pointing at the hub", `{"v":1,"node":"anchor","network":{"state":"unhealthy",` +
|
||||
`"since":"2026-10-29T20:00:00Z","parts":[{"part":"tunnel","state":"unhealthy","reason":"the tunnel to the hub has not handshaken for over five minutes",` +
|
||||
`"toward":["hub"],"targets":["novox"],"since":"2026-10-29T20:00:00Z"}]},"at":"2026-10-29T20:00:00Z"}`, &NodeHealth{}},
|
||||
// A node fallen back to its old login (ADR 0583 §6), which the watchman raises as node-account-fallback.
|
||||
{"a node's health report, fallen back to its old login", `{"v":1,"node":"anchor","fallback":{"node":"anchor",` +
|
||||
`"from":"node.anchor--b","why":"the new login's test-dial was refused","at":"2026-11-02T01:00:00Z"},` +
|
||||
`"at":"2026-11-02T01:00:00Z"}`, &NodeHealth{}},
|
||||
{"the predefined list", `{"v":1,"commit":"abc","at":"2026-10-29T12:00:00Z","list":{"domain":{"type":"string"}}}`, &PredefinedKept{}},
|
||||
} {
|
||||
if err := json.Unmarshal([]byte(c.raw), c.into); err != nil {
|
||||
@@ -60,6 +64,10 @@ func TestTheControlRecordsReadTheirWritersValues(t *testing.T) {
|
||||
if _, err := EncodeNodeHealth(NodeHealth{}); err == nil {
|
||||
t.Error("a report naming no node was encoded")
|
||||
}
|
||||
if _, err := EncodeNodeHealth(NodeHealth{Node: "anchor", Fallback: &LoginFallback{Node: "ace", From: "node.anchor--b",
|
||||
Why: "refused", At: time.Now()}}); err == nil {
|
||||
t.Error("a fall back naming another node was encoded")
|
||||
}
|
||||
// ADR 0571 clause 4: who holds which seat, from placement; a node seat by its node.
|
||||
held := SeatHoldersOf(map[string]Placement{
|
||||
"ace": {Node: "ace", Holds: []Holding{{Claim: "mesh-dns-resolver", Scope: "mesh", Module: "dnsmasq"}, {Claim: "node-files", Scope: "node", Module: "mesh-files"}}},
|
||||
|
||||
@@ -176,11 +176,44 @@ func TokenLife(expires string) (time.Duration, error) {
|
||||
}
|
||||
|
||||
// EnrolmentExpiry is what enrolment-expired carries (§2): the node, and the enrolment-issued event that carried the
|
||||
// key's content, by its sequence on the events' stream, since the bus deleted the key before the holder heard of it.
|
||||
// key's content, named by its message id (the bundle's publish answers no stream sequence, and §2 asks only that the
|
||||
// event be named), since the bus deleted the key before the holder heard of it.
|
||||
type EnrolmentExpiry struct {
|
||||
V int `json:"v"`
|
||||
Node string `json:"node"`
|
||||
Issued uint64 `json:"issued-event"`
|
||||
V int `json:"v"`
|
||||
Node string `json:"node"`
|
||||
IssuedID string `json:"issued-event"`
|
||||
}
|
||||
|
||||
func (x EnrolmentExpiry) check() error {
|
||||
if err := checkName("node", x.Node); err != nil {
|
||||
return err
|
||||
}
|
||||
if strings.TrimSpace(x.IssuedID) == "" {
|
||||
return fmt.Errorf("delivery: the expiry of %s's enrolment names no enrolment-issued event", x.Node)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// EncodeEnrolmentExpiry is x as JSON at Version; it refuses what DecodeEnrolmentExpiry refuses.
|
||||
func EncodeEnrolmentExpiry(x EnrolmentExpiry) ([]byte, error) {
|
||||
if err := x.check(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
x.V = Version
|
||||
return json.Marshal(x)
|
||||
}
|
||||
|
||||
// DecodeEnrolmentExpiry reads enrolment-expired's body; it refuses an unknown version, a node that is not a name and
|
||||
// no issued event named.
|
||||
func DecodeEnrolmentExpiry(data []byte) (EnrolmentExpiry, error) {
|
||||
var x EnrolmentExpiry
|
||||
if err := decode(data, &x); err != nil {
|
||||
return EnrolmentExpiry{}, err
|
||||
}
|
||||
if err := x.check(); err != nil {
|
||||
return EnrolmentExpiry{}, err
|
||||
}
|
||||
return x, nil
|
||||
}
|
||||
|
||||
// **Redeem** (§6): what a joining node-engine sends, and what the holder answers. Shared by the holder and the engine,
|
||||
|
||||
@@ -13,8 +13,8 @@ import (
|
||||
// what it says changed. The watchman (seat mesh-watchdogs) reads it for six alerts the controller raised from the
|
||||
// passing health message: machine-network (Network), network-rewritten (the resolver file's part, Writer and Owner),
|
||||
// network-unreachable (Peers, read across every node's report), machine-units (Units), relogin-needed (Relogin) and
|
||||
// directory-used-as-found (Found). Moved here from mesh-host internal/link (65fbc1e6) unchanged; the engine builds a
|
||||
// report from its own health statement there (NodeHealthOf).
|
||||
// directory-used-as-found (Found); and for node-account-fallback (Fallback, ADR 0583 §6). Moved here from mesh-host
|
||||
// internal/link (65fbc1e6); the engine builds a report from its own health statement there (NodeHealthOf).
|
||||
|
||||
// NodeHealthState and NodeHealthBucket are the node-engine's state health, as a reader names it and as the bus does.
|
||||
const (
|
||||
@@ -40,6 +40,10 @@ type NodeHealth struct {
|
||||
Relogin []NodeAccount `json:"relogin,omitempty"`
|
||||
// Found are the directories used as found.
|
||||
Found []NodeDirectory `json:"found,omitempty"`
|
||||
// Fallback is set when the engine fell back to its node's old login after the new one failed its test-dial or its
|
||||
// confirm (novox/hq ADR 0583 §6): the watchman raises node-account-fallback from it. Nil while the node logs in as
|
||||
// its login in force.
|
||||
Fallback *LoginFallback `json:"fallback,omitempty"`
|
||||
// Root are the engine's verdicts on each account a module declares never to become root without a person (novox/hq
|
||||
// ADR 0266), sorted: the agent account's among them where a module declares it. What the self-check's DA and D-root
|
||||
// judge an agent's confinement by, on the machine's own word.
|
||||
@@ -51,6 +55,15 @@ type NodeHealth struct {
|
||||
At time.Time `json:"at"`
|
||||
}
|
||||
|
||||
// LoginFallback is a node's fall back to its old login (ADR 0583 §6): the node, the login it fell back from (the one the
|
||||
// rotation moved to), why, in words with no credential, and when.
|
||||
type LoginFallback struct {
|
||||
Node string `json:"node"`
|
||||
From string `json:"from"`
|
||||
Why string `json:"why"`
|
||||
At time.Time `json:"at"`
|
||||
}
|
||||
|
||||
// NetworkHealth is the machine's networking (ADR 0241): the worst of its parts, since when, and each part.
|
||||
type NetworkHealth struct {
|
||||
State string `json:"state"`
|
||||
@@ -150,6 +163,9 @@ func EncodeNodeHealth(h NodeHealth) ([]byte, error) {
|
||||
if h.Node == "" {
|
||||
return nil, errors.New("a node's health report names its node")
|
||||
}
|
||||
if f := h.Fallback; f != nil && (f.Node != h.Node || f.From == "" || f.Why == "" || f.At.IsZero()) {
|
||||
return nil, fmt.Errorf("a node's fall back to its old login names its node %s, the login fallen back from, why and when", h.Node)
|
||||
}
|
||||
return json.Marshal(h)
|
||||
}
|
||||
|
||||
|
||||
@@ -160,6 +160,22 @@ func TestNodeRecord(t *testing.T) {
|
||||
walk(reflect.TypeOf(Enrolment{}), "Enrolment")
|
||||
walk(reflect.TypeOf(EnrolmentAddress{}), "EnrolmentAddress")
|
||||
walk(reflect.TypeOf(EnrolmentExpiry{}), "EnrolmentExpiry")
|
||||
// enrolment-expired names the issued event by its message id (§2), never a stream sequence.
|
||||
if raw, err := EncodeEnrolmentExpiry(EnrolmentExpiry{Node: "lab", IssuedID: "enrolment-issued.lab.1"}); err != nil {
|
||||
t.Errorf("an expiry: %v", err)
|
||||
} else if x, err := DecodeEnrolmentExpiry(raw); err != nil || x.IssuedID != "enrolment-issued.lab.1" ||
|
||||
!strings.Contains(string(raw), `"issued-event":"enrolment-issued.lab.1"`) {
|
||||
t.Errorf("an expiry reads back as %+v, %v (%s)", x, err, raw)
|
||||
}
|
||||
for name, raw := range map[string]string{
|
||||
"no issued event": `{"v":1,"node":"lab"}`,
|
||||
"a sequence for it": `{"v":1,"node":"lab","issued-event":7}`,
|
||||
"a node not a name": `{"v":1,"node":"Lab Pi","issued-event":"e"}`,
|
||||
} {
|
||||
if _, err := DecodeEnrolmentExpiry([]byte(raw)); err == nil {
|
||||
t.Errorf("an expiry with %s: taken", name)
|
||||
}
|
||||
}
|
||||
// And the placement file, which holds what a person decides of a node (K2): accounts by name, never a credential.
|
||||
walk(reflect.TypeOf(Placement{}), "Placement")
|
||||
}
|
||||
|
||||
@@ -81,7 +81,7 @@ func (t Token) Missing() []string {
|
||||
missing = append(missing, "the broker certificate's fingerprint")
|
||||
}
|
||||
if len(t.Signer) != ed25519.PublicKeySize {
|
||||
missing = append(missing, "the control plane's signing key")
|
||||
missing = append(missing, "the vault's signing key")
|
||||
}
|
||||
if strings.TrimSpace(t.Secret) == "" {
|
||||
missing = append(missing, "the one-time secret")
|
||||
|
||||
Reference in New Issue
Block a user