diff --git a/cmd/mesh-host/nodeagent.go b/cmd/mesh-host/nodeagent.go index 6a9ff5b..e7d8642 100644 --- a/cmd/mesh-host/nodeagent.go +++ b/cmd/mesh-host/nodeagent.go @@ -593,10 +593,12 @@ func nodeAgent(ctx context.Context, opts options, signer ed25519.PublicKey, trus // composer hands nothing new meanwhile, and the node says it has waited five minutes (novox/hq ADR 0636 §1). seats, err := startSeatList(run, session, node, say) if err != nil { - say(err.Error()) + // No reader is no gate that lets everything through: the node acts on nothing new, as one that waits does. + say(err.Error() + "; the node composes nothing new until it is started again and reads the list (novox/hq ADR 0636 §1)") } hooks := composerHooks{ - seatList: seats, + seatList: seats, + seatListFailed: err != nil, running: func() []compositions.Composed { if now := own.get(); now != nil { return runsAsWritten(now.Everything(), opts.state) @@ -639,6 +641,7 @@ func nodeAgent(ctx context.Context, opts options, signer ed25519.PublicKey, trus // its own key, `.node`, under candidates. shadowHooks := hooks shadowHooks.refused, shadowHooks.kept, shadowHooks.certify, shadowHooks.seatList = nil, nil, nil, nil + shadowHooks.seatListFailed = false shadowEnv := composerEnv(session, node, say, shadowHooks) shadow := selfcompose.NewShadow(shadowEnv, func(ctx context.Context, answers map[string]nodeengine.CandidateAnswer) { @@ -994,6 +997,8 @@ type composerHooks struct { certify func(ctx context.Context) error // seatList is this run's reader of the seat list: the composer hands nothing new until it was read (ADR 0636 §1). seatList *seatListState + // seatListFailed says the reader could not start: the composer then holds like a reader that waits (selfcompose.Unread). + seatListFailed bool } // selfComposer is the node's own composer: it composes what the seat's placement places on the node, and nothing while @@ -1047,6 +1052,8 @@ func composerEnv(session *link.Session, node string, say func(string), hooks com VaultAuthority: hooks.vaultAuthority, Certify: hooks.certify} if hooks.seatList != nil { env.SeatList, env.SeatListChanged = hooks.seatList.reader, hooks.seatList.changed + } else if hooks.seatListFailed { + env.SeatList = selfcompose.Unread{} } return env } diff --git a/cmd/mesh-host/seatlist_unread_test.go b/cmd/mesh-host/seatlist_unread_test.go new file mode 100644 index 0000000..ef6c120 --- /dev/null +++ b/cmd/mesh-host/seatlist_unread_test.go @@ -0,0 +1,22 @@ +package main + +import ( + "testing" + + "github.com/novox/mesh-host/internal/selfcompose" +) + +// ADR 0636 §1: when the node's reader of the seat list could not start, the composer is gated by a reader that never +// allows, where a missing reader would let the node act on everything; the shadow, which applies nothing, stays ungated. +func TestAReaderThatCouldNotStartHoldsTheComposer(t *testing.T) { + failed := composerEnv(nil, "n1", func(string) {}, composerHooks{seatListFailed: true}) + if failed.SeatList == nil || failed.SeatList.MayAct() || failed.SeatList.MayAct("node-engine") { + t.Fatalf("the composer of a node whose reader did not start is gated by %v, want a gate that allows nothing", failed.SeatList) + } + if _, ok := failed.SeatList.(selfcompose.Unread); !ok { + t.Errorf("gate is %T", failed.SeatList) + } + if got := composerEnv(nil, "n1", func(string) {}, composerHooks{}).SeatList; got != nil { + t.Errorf("a composer with no reader asked for gates by %v, want none (the shadow)", got) + } +}