The node-engine speaks operator-channel, mesh-vault and node-engine through their contracts and clients; it serves no pull-waits

ADR 0627 §1, §4: mesh-sdk keeps no asks, asker, vault answers or compose-candidate checks, so the node-engine takes them
from mesh-contracts: operator-channel's contract and client (asks, warrants, the asker's client), mesh-vault's sign verb
and answer, and node-engine's own contract and client for compose-candidate's ask, answer, verdicts and bound. The
lead's ruling on pull-waits (no record gives the seat that verb): the verb and its handler go; the row that read the
waits now holds the seat refusing the verb, and failed before the change. The go.mod requires of mesh-contracts' modules
come with the group's pin.

Claude-Session: https://claude.ai/code/session_01JG73KJNrH5EgFXMKKYSrig
This commit is contained in:
2026-11-04 14:02:44 +01:00
parent d8207358b0
commit 2b14f166de
14 changed files with 129 additions and 147 deletions
+3 -2
View File
@@ -13,6 +13,7 @@ import (
"testing"
"time"
"git.novox.be/novox/mesh-contracts/go/seats/meshvault"
"git.novox.be/novox/mesh-sdk/go/delivery"
"git.novox.be/novox/mesh-sdk/go/overlay"
@@ -108,13 +109,13 @@ func TestTheAppliedAuthorityIsKeptForTheRecord(t *testing.T) {
answer := signedBy(vaultCA, vault)
ask := func(_ context.Context, verb string, args any) (json.RawMessage, error) {
asked = append(asked, verb, args)
return json.Marshal(delivery.SignAnswer{Kind: delivery.KindNodeCertificate, Node: "n1", Certificate: answer})
return json.Marshal(meshvault.SignAnswer{Kind: delivery.KindNodeCertificate, Node: "n1", Certificate: answer})
}
written := 0
if err := certifyThisNode(context.Background(), state, "n1", trust, ask, func() { written++ }); err != nil {
t.Fatal(err)
}
if got := recordKeys(state, nil)(); got.Certificate != answer || written != 1 || len(asked) != 2 || asked[0] != delivery.SignVerb {
if got := recordKeys(state, nil)(); got.Certificate != answer || written != 1 || len(asked) != 2 || asked[0] != meshvault.VerbSign {
t.Errorf("the vault's certificate: kept %v, written %d, asked %v", got.Certificate == answer, written, asked)
}
answer = signedBy(testAuthority(t, controllerPub, controller), controller)
+13 -24
View File
@@ -13,10 +13,10 @@ import (
"syscall"
"time"
nec "git.novox.be/novox/mesh-contracts/go/clients/nodeengineclient"
occ "git.novox.be/novox/mesh-contracts/go/clients/operatorchannelclient"
"git.novox.be/novox/mesh-contracts/go/seats/nodeengine"
stdio "git.novox.be/novox/mesh-sdk/go"
meshasker "git.novox.be/novox/mesh-sdk/go/asker"
"git.novox.be/novox/mesh-sdk/go/asks"
"git.novox.be/novox/mesh-sdk/go/delivery"
"github.com/novox/mesh-host/internal/accounts"
"github.com/novox/mesh-host/internal/apply"
"github.com/novox/mesh-host/internal/link"
@@ -256,8 +256,8 @@ func engineActs(opts options, node string, queue *link.Session, search *accounts
return judgeRootFree(node, lastAccounts.Load(), time.Now(), served, why)
},
ManifestReads: queue.StatedReads, Say: queue.SayEngine}
asker := asks.EngineAsker(node)
h.Asker = &meshasker.Client{Module: asker, State: state, Publish: queue.PublishAsk,
asker := occ.EngineAsker(node)
h.Asker = &occ.Client{Module: asker, State: state, Publish: queue.PublishAsk,
Record: func(id string) (*stdio.StateEntry, error) { return queue.RouterRecord(asker + "." + id) }}
return h
}
@@ -284,17 +284,6 @@ const compositionReadFor = 20 * time.Second
// compositionFrom answers the verb composition from the node agent that runs now (issue 774); refused in words while
// none does.
// pullWaitsFrom answers the verb pull-waits from the agent running now (ENG-9).
func pullWaitsFrom(current *agentNow) func() (any, error) {
return func() (any, error) {
ag := current.get()
if ag == nil {
return nil, errors.New("this engine's node agent has not started, so it waits on no store yet")
}
return map[string]any{"waits": ag.PullWaits()}, nil
}
}
func compositionFrom(current *agentNow) func(module, digest string, diff bool) (any, error) {
return func(module, digest string, diff bool) (any, error) {
ag := current.get()
@@ -375,26 +364,26 @@ func packagesFrom(node string, session *link.Session, composer *atomic.Pointer[s
// composeCandidateFrom answers the verb compose-candidate (novox/hq ADR 0593 §7): the check's candidate composed on this
// node in shadow by the run's shadow, applying and writing nothing, within the shadow's own bound (shorter than
// delivery.ComposeAnswerBound, selfcompose candidateWithin). The answer is
// the seat's shape (delivery.ComposeCandidateAnswer), checked as it is encoded; a node whose shadow does not run yet
// nec.AnswerBound, selfcompose candidateWithin). The answer is
// the seat's shape (nodeengine.ComposeCandidateAnswer), checked as it is encoded; a node whose shadow does not run yet
// answers unready, which the checker takes as no answer.
func composeCandidateFrom(node string, shadow *atomic.Pointer[selfcompose.Shadow]) func(candidate string, input bool) (any, error) {
return func(candidate string, input bool) (any, error) {
ask := delivery.ComposeCandidateAsk{Candidate: candidate, Input: input}
if _, err := delivery.EncodeComposeCandidateAsk(ask); err != nil {
ask := nodeengine.ComposeCandidateInput{Candidate: candidate, Input: input}
if _, err := nec.EncodeAsk(ask); err != nil {
return nil, err
}
var a delivery.ComposeCandidateAnswer
var a nodeengine.ComposeCandidateAnswer
if s := shadow.Load(); s == nil {
// Unready, never an error the checker reads words of: its agent is starting (ADR 0593 §9).
a = delivery.ComposeCandidateAnswer{Node: node, Candidate: candidate, Verdict: delivery.ComposeCandidateUnready,
a = nodeengine.ComposeCandidateAnswer{Node: node, Candidate: candidate, Verdict: nec.Unready,
Refused: "this node's shadow does not run now (its node agent is starting), so it composes no candidate"}
} else {
ctx, cancel := context.WithTimeout(context.Background(), delivery.ComposeAnswerBound)
ctx, cancel := context.WithTimeout(context.Background(), nec.AnswerBound)
defer cancel()
a = s.ComposeCandidate(ctx, ask)
}
raw, err := delivery.EncodeComposeCandidateAnswer(a)
raw, err := nec.EncodeAnswer(a)
if err != nil {
return nil, fmt.Errorf("this node's answer to %s does not fit the seat's shape: %w", candidate, err)
}
+6 -6
View File
@@ -8,8 +8,8 @@ import (
"testing"
"time"
meshasker "git.novox.be/novox/mesh-sdk/go/asker"
"git.novox.be/novox/mesh-sdk/go/asks"
occ "git.novox.be/novox/mesh-contracts/go/clients/operatorchannelclient"
oc "git.novox.be/novox/mesh-contracts/go/seats/operatorchannel"
"git.novox.be/novox/mesh-sdk/go/declaration"
"github.com/novox/mesh-host/internal/apply"
"github.com/novox/mesh-host/internal/link"
@@ -58,16 +58,16 @@ func TestALibrarySaysItsWaitWithoutWordsToParse(t *testing.T) {
// 774 (ADR 0563 §4): the library's home engine asks its take of the operator itself, once: no second ask while one is
// open, nor after it ended; and a library waiting for its disk is not asked.
t.Run("the home engine asks the take once", func(t *testing.T) {
var asked []asks.Ask
var asked []oc.Ask
state := &link.MemState{}
h := &link.HandActs{Node: "n1", State: state,
JudgeAccess: func(act, access string) (link.Judged, error) {
return link.Judged{Subject: access, Path: root, Sets: "the group mesh-media", Disk: "0:0 755"}, nil
},
AccessAct: func(link.AccessAct) (string, error) { return "taken", nil }}
h.Asker = &meshasker.Client{Module: asks.EngineAsker("n1"), State: state,
h.Asker = &occ.Client{Module: occ.EngineAsker("n1"), State: state,
Publish: func(subject string, body any, id string) error {
if a, ok := body.(asks.Ask); ok {
if a, ok := body.(oc.Ask); ok {
asked = append(asked, a)
}
return nil
@@ -78,7 +78,7 @@ func TestALibrarySaysItsWaitWithoutWordsToParse(t *testing.T) {
if len(waiting) != 1 || waiting[0] != "library:media" || len(asked) != 1 {
t.Fatalf("waiting %v asked %d times", waiting, len(asked))
}
h.Heard(asks.Warrant{Ask: asked[0].ID, Asker: asks.EngineAsker("n1"), Outcome: asks.OutcomeExpired, At: time.Now()})
h.Heard(oc.Warrant{Ask: asked[0].ID, Asker: occ.EngineAsker("n1"), Outcome: occ.OutcomeExpired, At: occ.FormatTime(time.Now())})
askTakes(h, waiting, nil)
if len(asked) != 1 {
t.Fatalf("the take was asked again after its ask ended: %d", len(asked))
-1
View File
@@ -1015,7 +1015,6 @@ func runLink(ctx context.Context, opts options) error {
go reconcileActs(ctx, queue.Acts)
current := &agentNow{}
queue.Acts.Composition = compositionFrom(current)
queue.Acts.PullWaits = pullWaitsFrom(current)
queue.Acts.Compose = composeFrom(current, &composerNow)
queue.Acts.Packages = packagesFrom(mine.Node, queue, &composerNow)
queue.Acts.ComposeCandidate = composeCandidateFrom(mine.Node, &shadowNow)
+4 -3
View File
@@ -14,6 +14,7 @@ import (
"testing"
"time"
nec "git.novox.be/novox/mesh-contracts/go/clients/nodeengineclient"
"git.novox.be/novox/mesh-sdk/go/buscompose"
"git.novox.be/novox/mesh-sdk/go/declaration"
"git.novox.be/novox/mesh-sdk/go/delivery"
@@ -598,12 +599,12 @@ func h8candidate(t *testing.T, b *itBed, fp, running string) {
t.Fatal(err)
}
raw, _ := got.(json.RawMessage)
a, err := delivery.DecodeComposeCandidateAnswer(raw)
a, err := nec.DecodeAnswer(raw)
if err != nil {
t.Fatalf("the answer %s: %v", raw, err)
}
if a.Verdict != delivery.ComposeCandidateComposed || len(a.Modules) != 1 || a.Modules[0].Module != "web" ||
a.Modules[0].Fp != fpIt("c") || !a.Modules[0].Changed || a.Took > delivery.ComposeAnswerBound.Milliseconds() {
if a.Verdict != nec.Composed || len(a.Modules) != 1 || a.Modules[0].Module != "web" ||
a.Modules[0].Fp != fpIt("c") || !a.Modules[0].Changed || a.Took > nec.AnswerBound.Milliseconds() {
t.Errorf("the node's answer to a candidate changing web: %+v", a)
}
after, _ := b.composed.Status()
+3 -2
View File
@@ -13,6 +13,7 @@ import (
"sync"
"time"
"git.novox.be/novox/mesh-contracts/go/seats/meshvault"
"git.novox.be/novox/mesh-sdk/go/delivery"
"git.novox.be/novox/mesh-sdk/go/overlay"
@@ -259,11 +260,11 @@ func certifyThisNode(ctx context.Context, state, node string, trust *meshTrust,
if err != nil || len(key) != ed25519.PublicKeySize {
return fmt.Errorf("this node's serving key is not an ed25519 key")
}
raw, err := ask(ctx, delivery.SignVerb, map[string]any{"kind": delivery.KindNodeCertificate, "node": node})
raw, err := ask(ctx, meshvault.VerbSign, map[string]any{"kind": delivery.KindNodeCertificate, "node": node})
if err != nil {
return err
}
var a delivery.SignAnswer
var a meshvault.SignAnswer
if err := json.Unmarshal(raw, &a); err != nil {
return fmt.Errorf("the vault's answer is not a signing: %w", err)
}