Node-engine: reads the seat list through the seat mesh-catalog, holds what is new until it was read, writes its finding after five minutes; verbs seat-list and seat-list-again; whole-module case against a stand-in of the seat (H-7, ADR 0636 §1, §7, §8; issue 1966)

This commit is contained in:
2026-11-05 14:26:15 +01:00
parent a222db8c69
commit 855f4471e2
9 changed files with 436 additions and 8 deletions
+63
View File
@@ -0,0 +1,63 @@
package link
import (
"errors"
"fmt"
"github.com/nats-io/nats.go"
)
// FindingsBucket is the node-engine's state `findings`: the findings this engine writes in its own bucket, one key each,
// which the condition keeper raises as conditions and clears when the key goes (novox/hq ADR 0564 §2, ADR 0636 §1).
const FindingsBucket = "mesh-host_findings"
func (l *natsLink) findingsKV() (nats.KeyValue, error) {
kv, err := l.js.KeyValue(FindingsBucket)
if err != nil {
return nil, fmt.Errorf("the node-engine's state %s cannot be bound (it is created from the node-engine's manifest; "+
"written again once it is): %w", FindingsBucket, err)
}
return kv, nil
}
func (q *Session) findings() (nats.KeyValue, error) {
q.init()
q.mu.Lock()
bus := q.bus
q.mu.Unlock()
l, ok := bus.(interface{ findingsKV() (nats.KeyValue, error) })
if bus == nil || !ok {
return nil, ErrNoLink
}
return l.findingsKV()
}
// PutFinding writes one finding of this engine over the link open now.
func (q *Session) PutFinding(key string, value []byte) error {
kv, err := q.findings()
if err != nil {
return err
}
_, err = kv.Put(key, value)
return err
}
// DeleteFinding clears one finding of this engine over the link open now; a finding already gone is cleared.
func (q *Session) DeleteFinding(key string) error {
kv, err := q.findings()
if err != nil {
return err
}
if err = kv.Delete(key); err != nil && !errors.Is(err, nats.ErrKeyNotFound) {
return err
}
return nil
}
func (o OverNATS) findingsKV() (nats.KeyValue, error) {
kv, err := o.JS.KeyValue(FindingsBucket)
if err != nil {
return nil, fmt.Errorf("the node-engine's state %s cannot be bound: %w", FindingsBucket, err)
}
return kv, nil
}
+18
View File
@@ -51,6 +51,11 @@ const (
ActComposeCandidate = nodeengine.VerbComposeCandidate
// ActManifestReads reads the reader capabilities this engine states (novox/hq ADR 0618 §3).
ActManifestReads = nodeengine.VerbManifestReads
// ActSeatList reads the seat list this engine holds and what it waits for; ActSeatListAgain makes it read the list now
// (novox/hq ADR 0636 §7). The node-engine seat's definition carries both verbs, which answer only the operator and the
// healers.
ActSeatList = "seat-list"
ActSeatListAgain = "seat-list-again"
)
// The states of an act: asked, done and refused are also the seat's events.
@@ -149,6 +154,9 @@ type HandActs struct {
// Packages answers the node's key of the engine's state packages beside the controller's mirror's key and their
// difference, and with write writes the key again from what the engine composed (the verb packages, ADR 0565 §6, §15).
Packages func(write bool) (any, error)
// SeatList answers the seat list the engine holds; SeatListAgain makes it read the list now and answers what it holds.
SeatList func() (any, error)
SeatListAgain func() (any, error)
// ComposeCandidate composes a check's candidate (the indexer's candidate key) on this node in shadow and answers per
// module, applying and writing nothing; with input, also the composition input it composed from (the verb
// compose-candidate, ADR 0593 §7).
@@ -219,6 +227,16 @@ func (h *HandActs) Call(c EngineCall) (any, error) {
return nil, errors.New("this engine composes nothing on a call yet: its node agent has not started")
}
return h.Compose(arg("module"))
case ActSeatList:
if h.SeatList == nil {
return nil, errors.New("this engine reads no seat list yet: its node agent has not started")
}
return h.SeatList()
case ActSeatListAgain:
if h.SeatListAgain == nil {
return nil, errors.New("this engine reads no seat list yet: its node agent has not started")
}
return h.SeatListAgain()
case ActPackages:
if h.Packages == nil {
return nil, errors.New("this engine keeps no packages yet: its node agent has not started")
+21 -5
View File
@@ -181,7 +181,12 @@ func CalledVaultSubject(verb, node string) string {
// CalledVaultSubjectAs is CalledVaultSubject for the login the engine is dialled as (node.<n> or node.<n>--b, ADR 0583 §1):
// the bus lets only that user publish a subject naming it.
func CalledVaultSubjectAs(verb, user string) string {
return "mesh.seat." + VaultSeat + ".call." + verb + "." + strings.ReplaceAll(user, ".", "~")
return CalledSeatSubjectAs(VaultSeat, verb, user)
}
// CalledSeatSubjectAs is where a verb of a seat held once for the mesh is asked naming the login the engine is dialled as.
func CalledSeatSubjectAs(seat, verb, user string) string {
return "mesh.seat." + seat + ".call." + verb + "." + strings.ReplaceAll(user, ".", "~")
}
// VaultWithin is how long the engine gives the vault to answer one step.
@@ -190,6 +195,17 @@ const VaultWithin = 15 * time.Second
// AskVault asks one of the vault's verbs in this node's engine's own name over the link open now, and answers its
// result, or its refusal as an error.
func (q *Session) AskVault(ctx context.Context, verb string, args any) (json.RawMessage, error) {
return q.askCalled(ctx, VaultSeat, verb, args, VaultWithin, "the vault's "+verb)
}
// AskMeshSeat asks a verb of a seat held once for the mesh in this node's engine's own name over the link open now, and
// answers its result, or its refusal as an error: the way the engine reads the seat list from the seat mesh-catalog
// (novox/hq ADR 0636 §4).
func (q *Session) AskMeshSeat(ctx context.Context, seat, verb string, args any) (json.RawMessage, error) {
return q.askCalled(ctx, seat, verb, args, VaultWithin, seat+"."+verb)
}
func (q *Session) askCalled(ctx context.Context, seat, verb string, args any, within time.Duration, what string) (json.RawMessage, error) {
q.init()
q.mu.Lock()
bus := q.bus
@@ -205,18 +221,18 @@ func (q *Session) AskVault(ctx context.Context, verb string, args any) (json.Raw
if err != nil {
return nil, err
}
asking, cancel := context.WithTimeout(ctx, VaultWithin)
asking, cancel := context.WithTimeout(ctx, within)
defer cancel()
reply, err := asker.Ask(asking, CalledVaultSubjectAs(verb, q.Membership.User()), body)
reply, err := asker.Ask(asking, CalledSeatSubjectAs(seat, verb, q.Membership.User()), body)
if err != nil {
return nil, fmt.Errorf("the vault's %s could not be asked: %w", verb, err)
return nil, fmt.Errorf("%s could not be asked: %w", what, err)
}
var env struct {
Result json.RawMessage `json:"result"`
Error string `json:"error"`
}
if err := json.Unmarshal(reply, &env); err != nil {
return nil, fmt.Errorf("the vault answered %s with something that is not a tool's answer", verb)
return nil, fmt.Errorf("%s answered with something that is not a tool's answer", what)
}
if env.Error != "" {
return nil, errors.New(env.Error)
+6 -1
View File
@@ -67,6 +67,7 @@
"$JS.API.STREAM.INFO.KV_mesh-delivery-indexer_verdicts",
"$JS.API.STREAM.INFO.KV_mesh-host_acts",
"$JS.API.STREAM.INFO.KV_mesh-host_data",
"$JS.API.STREAM.INFO.KV_mesh-host_findings",
"$JS.API.STREAM.INFO.KV_mesh-host_health",
"$JS.API.STREAM.INFO.KV_mesh-host_packages",
"$JS.API.STREAM.INFO.KV_mesh-provisioner_provisions",
@@ -91,6 +92,7 @@
"$KV.composed-NODE.>",
"$KV.mesh-host_acts.NODE.>",
"$KV.mesh-host_data.NODE.>",
"$KV.mesh-host_findings.>",
"$KV.mesh-host_health.NODE",
"$KV.mesh-host_packages.NODE",
"$KV.node-engine_have.NODE.>",
@@ -100,6 +102,7 @@
"$SRV.PING.>",
"$SRV.PING.node-tools.NODE",
"mesh.control.NODE.>",
"mesh.seat.mesh-catalog.call.seat-definitions.node~NODE",
"mesh.seat.node-backup.call.backed-up.NODE.node~NODE",
"mesh.seat.node-backup.call.now.NODE.node~NODE",
"mesh.seat.node-backup.tool.backed-up.NODE",
@@ -130,8 +133,10 @@
"mesh.node.NODE.ask.report",
"mesh.node.NODE.ask.setuid-search",
"mesh.node.NODE.declare",
"mesh.seat.mesh-catalog.event.seat-definition-changed",
"mesh.seat.mesh-catalog.event.seat-definition-removed",
"mesh.seat.node-engine.call.*.NODE.*",
"mesh.seat.node-engine.tool.*.NODE"
],
"AllowResponses": true
}
}