1 Commits
Author SHA1 Message Date
jochen 81f3c2d01b Test that plan --diff says a left-out module before the diff, through printed()
mesh/merge-gate pass: builds build-agent, mesh-controller, route-proxy → ace, g14, novox, shanks; no bus step; every machine composes with the change as it…
mesh/repo-check pass: its merge-check.sh passed
mesh/delivery ready: it delivers once merged
2026-10-09 14:46:42 +02:00
147 changed files with 461 additions and 12238 deletions
-3
View File
@@ -306,9 +306,6 @@ func answer(ctx context.Context, publisher builder.Publisher, on, workspace stri
for _, r := range built.Read { for _, r := range built.Read {
result.Read = append(result.Read, link.ReadRepository{Repository: r.Repository, Ref: r.Ref}) result.Read = append(result.Read, link.ReadRepository{Repository: r.Repository, Ref: r.Ref})
} }
for _, s := range built.Sources {
result.Sources = append(result.Sources, link.BuildSource{Repository: s.Repository, Ref: s.Ref, Paths: s.Paths})
}
say("built", built.Manifest.Module+" from "+short(built.Commit)) say("built", built.Manifest.Module+" from "+short(built.Commit))
} }
} }
+1 -16
View File
@@ -5,7 +5,6 @@ import (
"errors" "errors"
"fmt" "fmt"
"os" "os"
"strings"
"sync" "sync"
"time" "time"
@@ -180,21 +179,7 @@ func (a *actor) release() {
// holderOf is this process as the lease's holder. // holderOf is this process as the lease's holder.
func holderOf(instance string) lease.Holder { func holderOf(instance string) lease.Holder {
host, _ := os.Hostname() host, _ := os.Hostname()
build := runningBuild() return lease.Holder{Instance: instance, Host: host, Build: version}
if build == "" {
build = version
}
return lease.Holder{Instance: instance, Host: host, Build: build}
}
// RunningBuildVar is where the declaration tells this process which build it is (module.json, the
// controller process's env): the version its bundle is delivered as, `${version}` composed by the
// catalogue from the bundle's digest. Empty for a process placed by hand.
const RunningBuildVar = "MESH_CONTROLLER_VERSION"
// runningBuild is the version of the build this process is, or empty when the declaration did not say.
func runningBuild() string {
return strings.TrimSpace(os.Getenv(RunningBuildVar))
} }
// serveUnderTheLease takes the lease for the serving controller, waiting while another holds it, and // serveUnderTheLease takes the lease for the serving controller, waiting while another holds it, and
+8 -11
View File
@@ -48,8 +48,7 @@ const agentAccountProbe = "DA"
// //
// The one judgement: `node show`, the self-check, and ADR 0259's router honouring a verified sender read // The one judgement: `node show`, the self-check, and ADR 0259's router honouring a verified sender read
// it here. // it here.
// now is the judging clock, threaded so a caller judging several things at one instant judges them all at it. func agentConfined(ctx context.Context, inv *inventory.Inventory, node string) (named, confined bool, why string, err error) {
func agentConfined(ctx context.Context, inv *inventory.Inventory, node string, now time.Time) (named, confined bool, why string, err error) {
n, err := inv.NodeByName(ctx, node) n, err := inv.NodeByName(ctx, node)
if err != nil { if err != nil {
return false, false, "", err return false, false, "", err
@@ -62,7 +61,7 @@ func agentConfined(ctx context.Context, inv *inventory.Inventory, node string, n
if err != nil { if err != nil {
return true, false, "", err return true, false, "", err
} }
confined, why = judgedConfined(n.AgentAccount, h, had, now) confined, why = judgedConfined(n.AgentAccount, h, had, time.Now())
return true, confined, why, nil return true, confined, why, nil
} }
@@ -119,11 +118,9 @@ func judgedConfined(agent string, h inventory.NodeHealth, had bool, now time.Tim
} }
// searchQuietFor is how long the controller lets an agent account's verdict wait for the node-engine's setuid // searchQuietFor is how long the controller lets an agent account's verdict wait for the node-engine's setuid
// search before that is itself the urgent condition: the longest a search is expected to take (link.RootSearchQuiet, // search before that is itself the urgent condition: the engine's bound on one search (link.RootSearchBound, the
// the engine's own value; novox/hq issue 361 — the search runs to completion, with no bound of its own), counted // engine's own value), counted from when this controller first saw it waiting, never from the engine's start.
// from when this controller first saw it waiting, never from the engine's start. Quiet raises nothing; it never const searchQuietFor = link.RootSearchBound
// makes the agent account confined, which only a healthy verdict from a complete, fresh search does.
const searchQuietFor = link.RootSearchQuiet
// The kinds of an agent account's verdict, for the quiet a search earns. // The kinds of an agent account's verdict, for the quiet a search earns.
const ( const (
@@ -209,10 +206,10 @@ func probeAgentAccounts(ctx context.Context, d *doctor) ([]conditions.Observatio
if confined { if confined {
continue continue
} }
// Not judged yet only because the node-engine's search runs and no complete, fresh one judges: not the // Not judged yet only because the first search since the node-engine started is still running: not the
// urgent condition after every restart. The agent is still not confined — ADR 0259's router reads // urgent condition after every restart. The agent is still not confined — ADR 0259's router reads
// agentConfined, not this — and `node show` still says not judged. Loud again once the search fails, // agentConfined, not this — and `node show` still says not judged. Loud again once the search fails,
// waits past searchQuietFor, or the statement goes stale. // runs out its bound, or the statement goes stale.
if quiet { if quiet {
continue continue
} }
@@ -231,7 +228,7 @@ func agentAccountLines(ctx context.Context, inv *inventory.Inventory, n inventor
return []string{fmt.Sprintf(" agents run as the operator account (%s); no agent account is named", return []string{fmt.Sprintf(" agents run as the operator account (%s); no agent account is named",
orNoneKnown(n.Account))} orNoneKnown(n.Account))}
} }
_, confined, why, err := agentConfined(ctx, inv, n.Name, time.Now()) _, confined, why, err := agentConfined(ctx, inv, n.Name)
if err != nil { if err != nil {
return []string{fmt.Sprintf(" agents run as %s (home %s); whether it can become root could NOT be read: %v", return []string{fmt.Sprintf(" agents run as %s (home %s); whether it can become root could NOT be read: %v",
n.AgentAccount, n.AgentHome(), err)} n.AgentAccount, n.AgentHome(), err)}
+10 -16
View File
@@ -106,10 +106,10 @@ func TestTheSelfCheckSaysAnAgentAccountThatCanBecomeRoot(t *testing.T) {
if found, err = probeAgentAccounts(ctx, d); err != nil || len(onlyMachine(found, "anchor")) != 0 { if found, err = probeAgentAccounts(ctx, d); err != nil || len(onlyMachine(found, "anchor")) != 0 {
t.Fatalf("a judged agent account still fails: %+v %v", found, err) t.Fatalf("a judged agent account still fails: %+v %v", found, err)
} }
if named, confined, why, err := agentConfined(ctx, inv, "anchor", time.Now()); err != nil || !named || !confined { if named, confined, why, err := agentConfined(ctx, inv, "anchor"); err != nil || !named || !confined {
t.Fatalf("agentConfined on anchor: %v %v %q %v", named, confined, why, err) t.Fatalf("agentConfined on anchor: %v %v %q %v", named, confined, why, err)
} }
if named, _, why, err := agentConfined(ctx, inv, "laptop", time.Now()); err != nil || named || if named, _, why, err := agentConfined(ctx, inv, "laptop"); err != nil || named ||
!strings.Contains(why, "operator account") { !strings.Contains(why, "operator account") {
t.Fatalf("agentConfined on a machine naming none: %v %q %v", named, why, err) t.Fatalf("agentConfined on a machine naming none: %v %q %v", named, why, err)
} }
@@ -199,20 +199,14 @@ func TestTheNodeVerbOnlyShows(t *testing.T) {
} }
} }
// Until a complete search for setuid programs judges — none since the node-engine's state was kept, or the last // After every node-engine restart its search for setuid programs runs for up to its bound, and the agent account
// older than the engine lets one judge — the agent account is not judged, and the search runs to its end with no // is not judged until it ends. DA does not raise that as urgent while the search is within its bound, counted from
// bound (novox/hq issue 361). DA does not raise that as urgent within searchQuietFor, counted from when this // when this controller first saw it waiting — never from the engine's own "since", which a restart resets, so an
// controller first saw it waiting — never from the engine's own "since", which a restart resets, so an engine // engine restarted in a loop does not keep it quiet. The account is still not confined, and `node show` still
// restarted in a loop does not keep it quiet. The account is still not confined, and `node show` still says not // says not judged; a search that failed, or a way to root found, is urgent at once.
// judged; a search that failed, or a way to root found, is urgent at once.
func TestASearchStillRunningAfterARestartIsNotUrgent(t *testing.T) { func TestASearchStillRunningAfterARestartIsNotUrgent(t *testing.T) {
if searchQuietFor != rootsearch.Quiet { if searchQuietFor != rootsearch.Bound {
t.Fatalf("the quiet is %s and the node-engine's %s: they are one value", searchQuietFor, rootsearch.Quiet) t.Fatalf("the quiet is %s and the node-engine's bound %s: they are one value", searchQuietFor, rootsearch.Bound)
}
// A daily search that finishes within the quiet never leaves the account unjudged between two of them.
if rootsearch.FreshFor < rootsearch.Every+rootsearch.Quiet {
t.Fatalf("a complete search judges for %s, less than a day's search (%s) and the quiet (%s)",
rootsearch.FreshFor, rootsearch.Every, rootsearch.Quiet)
} }
open := aMesh(t) open := aMesh(t)
ctx := t.Context() ctx := t.Context()
@@ -252,7 +246,7 @@ func TestASearchStillRunningAfterARestartIsNotUrgent(t *testing.T) {
if found := say(link.StateUnknown, running); len(found) != 0 { if found := say(link.StateUnknown, running); len(found) != 0 {
t.Fatalf("a search first seen now was raised: %+v", found) t.Fatalf("a search first seen now was raised: %+v", found)
} }
if _, confined, why, _ := agentConfined(ctx, inv, "anchor", time.Now()); confined || !strings.Contains(why, "not judged") { if _, confined, why, _ := agentConfined(ctx, inv, "anchor"); confined || !strings.Contains(why, "not judged") {
t.Fatalf("an account whose search runs was read as confined: %q", why) t.Fatalf("an account whose search runs was read as confined: %q", why)
} }
// The engine restarted again and again, each statement's own since fresh: the controller's clock runs on. // The engine restarted again and again, each statement's own since fresh: the controller's clock runs on.
-831
View File
@@ -1,831 +0,0 @@
package main
// The controller asks, and acts on the operator's warrant (novox/hq ADR 0259 §6). It holds no channel, no
// identity and no factor: it asks the router like any other module, and performs the answer chosen with its
// own grant.
//
// - **For every open, unsilenced condition that needs the operator and names its answers**, one ask is
// published on the `operator-channel` seat under the controller's own name: the condition's words, its
// actions as options at their levels (Silence acknowledges; Release, Stop, Start and Restart approve),
// answered by the operator, expiring after a day (a week when every option only acknowledges). A
// condition that clears, is silenced, or changes its answers has its ask cancelled; an ask that expired
// unanswered is asked again while the condition lasts. Each ask is kept in the controller's bucket
// `asked`, so a restart neither asks twice nor forgets.
// - **On a warrant**, heard on the seat's event under the controller's own name (which only the router may
// say), the controller acts once per ask: only for an ask it holds, only for the option it offered at
// that option's level, and only while the condition is still open. It performs the action as itself —
// a silence through its own conditions, any other through the verb the action names — with the warrant's
// words as its why, and records it in the hand-act log as the operator's decision, naming the channel,
// the ask and the proofs. An ask that ended without a choice is recorded and nothing is done.
// - **A warrant it missed** while away is read from the router's record of its asks, under its own name.
import (
"context"
"crypto/rand"
"encoding/hex"
"encoding/json"
"errors"
"fmt"
"sort"
"strings"
"sync"
"time"
"git.novox.be/novox/mesh-sdk/go/asks"
"github.com/novox/mesh-controller/internal/broker"
"github.com/novox/mesh-controller/internal/conditions"
"github.com/novox/mesh-controller/internal/link"
)
// The asker's name on the seat: the controller's module.
const askerName = broker.ControllerSeat
// How long an ask lasts: a day when an answer approves, a week when every answer only acknowledges.
const (
// askApproveFor is a day less a margin, so an ask is never refused at the router for lasting a day and
// a moment (the SDK's bound is a day).
askApproveFor = 24*time.Hour - 10*time.Minute
askAcknowledgeFor = 7 * 24 * time.Hour
// askEvery is how often what is open is asked about again, beside every change.
askEvery = time.Minute
// askCatchUpAfter is how old an open ask is before the router's record of it is read: a warrant heard
// on the event needs no reading.
askCatchUpAfter = 2 * time.Minute
// askAgainAfterAnswer is how long a condition the operator answered is not asked about again with the
// same answers: what was chosen takes a while to clear it, and asking again at once would ask twice.
askAgainAfterAnswer = time.Hour
// askMostOpen is how many asks the controller holds open at once (the router refuses a fourth): the
// most urgent conditions first, then the oldest.
askMostOpen = asks.MostOpen
)
// What became of an ask, as the controller keeps it.
const (
askOpen = "open"
askCancelled = "cancelled"
)
// asked is one ask the controller made, as it keeps it.
type asked struct {
ID string `json:"id"`
Condition string `json:"condition"`
// Channels is what the channels were when it was asked (asker.channels): an ask the router refused is not
// asked again until the condition's answers or the channels change.
Channels string `json:"channels,omitempty"`
Ask asks.Ask `json:"ask"`
Actions []conditions.Action `json:"actions"`
// Options are the actions by option id.
Options map[string]int `json:"options"`
State string `json:"state"`
Opened time.Time `json:"opened"`
Ended time.Time `json:"ended,omitempty"`
Warrant *asks.Warrant `json:"warrant,omitempty"`
// Acted is what the controller did on the warrant: empty before it did anything, "acting" while it acts,
// then "done", "failed: …" or "nothing: …". Anything but empty is never acted on again.
Acted string `json:"acted,omitempty"`
// Part is which ask of its condition this is (askPart): empty for the one that carries the condition's
// answers, or the authorising ones where it has both; "acknowledge" for its acknowledging answers asked
// apart (the review of 2026-10-09, M1).
Part string `json:"part,omitempty"`
// Rehearsal is an ask started at the controller's terminal (rehearse.go): about no condition, its answers
// perform nothing, and the reconciling of conditions leaves it alone.
Rehearsal bool `json:"rehearsal,omitempty"`
}
// partKey is an ask's place among what is asked: its condition and its part.
func partKey(condition, part string) string { return condition + "#" + part }
// partAcknowledge is the part of a condition asked apart for its acknowledging answers.
const partAcknowledge = "acknowledge"
// askPart is one ask a condition is asked with: its part, what it is about, and its answers.
type askPart struct {
name string
about string
actions []conditions.Action
}
// levelOf is an action's level as an option offers it: one that says none is never taken for less than
// approve.
func levelOf(act conditions.Action) asks.Level {
if act.Level == "" {
return asks.Approve
}
return asks.Level(act.Level)
}
// partsOf is the asks a condition is asked with (the review of 2026-10-09, M1): one, when its answers are all
// of one kind; else its authorising answers (Release, Stop, Restart) in one ask, about the condition, and its
// acknowledging ones (Silence) in another. **An acknowledgement never shares an ask with an approval**: a
// channel that only acknowledges would otherwise answer the ask, and end the approval with it.
func partsOf(c conditions.Condition) []askPart {
var ack, auth []conditions.Action
for _, act := range c.Actions {
if levelOf(act) == asks.Acknowledge {
ack = append(ack, act)
} else {
auth = append(auth, act)
}
}
if len(ack) == 0 || len(auth) == 0 {
return []askPart{{about: c.Key, actions: c.Actions}}
}
return []askPart{{about: c.Key, actions: auth},
{name: partAcknowledge, about: c.Key + "." + partAcknowledge, actions: ack}}
}
// askedStore keeps the asks (broker.AskedBucket). **Every write after the first is a compare-and-set** (the
// review of 2026-10-09, L2): an ask is created once, and changed only over the revision it was read at, the
// change decided again on what is read — so two controllers, or two deliveries of one warrant, never write
// over each other, and of two that would act only the one whose write stands does.
type askedStore interface {
Get(ctx context.Context, id string) (*asked, error)
// Create keeps a new ask, and refuses one already kept under its id.
Create(ctx context.Context, a asked) error
// Change applies change to the ask kept under id, by compare-and-set, and says whether its write stood.
// change says whether to write at all; on a write that came between, it is asked again on what is read.
Change(ctx context.Context, id string, change func(*asked) bool) (bool, error)
All(ctx context.Context) ([]asked, error)
}
// askChangeTries is how often a change is read and tried again when another write came between.
const askChangeTries = 5
// asker is the controller asking the operator and acting on the answer.
type asker struct {
open func(ctx context.Context) ([]conditions.Condition, error)
silence func(ctx context.Context, key string, d time.Duration, by, why string) error
store askedStore
// publish puts a message on a subject's stream, de-duplicated by id.
publish func(ctx context.Context, subject string, body []byte, id string) error
// call performs an action's verb with its arguments, as the controller.
call func(ctx context.Context, a conditions.Action, args map[string]string) error
// record writes the hand-act log.
record func(ctx context.Context, act link.HandAct) error
// routerRecord reads the router's record of an ask for a warrant missed; nil reads nothing.
routerRecord func(ctx context.Context, id string) (*asks.Warrant, error)
// routerHere says whether a router holds the seat and takes asks under the asker's name; nil is yes.
routerHere func(ctx context.Context) (bool, error)
// grantHeld says whether the bus holds the controller's grant to ask: the user list the bus's machine was
// last sent is the one the mesh composes now (novox/hq issue 353). The grant is composed from the router's
// assignment and reaches the bus only when that machine is next pushed, so between `assign` and `push`
// the record says a router is here and the bus refuses every ask. why says what to do; nil is yes.
grantHeld func(ctx context.Context) (held bool, why string, err error)
// channels is what the channels are now, as a fingerprint: who holds which kind, promising what.
channels func(ctx context.Context) string
// raise keeps the asker's own condition (sourceAsker): which conditions needing the operator could not be
// asked, and why. Nil raises nothing (a test that does not look).
raise func(ctx context.Context, obs []conditions.Observation) error
now func() time.Time
logf func(string, ...any)
saidNoRouter bool
saidNoGrant bool
mu sync.Mutex
nudged chan struct{}
}
func (a *asker) nudge() {
if a == nil {
return
}
a.mu.Lock()
if a.nudged == nil {
a.nudged = make(chan struct{}, 1)
}
ch := a.nudged
a.mu.Unlock()
select {
case ch <- struct{}{}:
default:
}
}
// keep asks until ctx ends: now, on every change of a condition, and every askEvery.
func (a *asker) keep(ctx context.Context) {
a.nudge()
tick := time.NewTicker(askEvery)
defer tick.Stop()
a.mu.Lock()
nudged := a.nudged
a.mu.Unlock()
for {
select {
case <-ctx.Done():
return
case <-tick.C:
case <-nudged:
}
if err := a.reconcile(ctx); err != nil {
a.logf("what the operator is asked could not be brought up to date: %v", err)
}
}
}
// wants says whether a condition is one to ask about now.
func wants(c conditions.Condition, now time.Time) bool {
return len(c.Actions) > 0 && c.Needs != "" && !c.SilencedAt(now)
}
func sameAsked(a []conditions.Action, b []conditions.Action) bool {
x, _ := json.Marshal(a)
y, _ := json.Marshal(b)
return string(x) == string(y)
}
// reconcile brings what is asked in line with what is open.
func (a *asker) reconcile(ctx context.Context) error {
now := a.now()
if a.routerHere != nil {
here, err := a.routerHere(ctx)
if err != nil {
return err
}
if !here {
if !a.saidNoRouter {
a.logf("no router takes asks under the controller's name (a module declaring %s with its ask "+
"named by its caller, assigned): the operator is asked nothing until one is", broker.AsksSeat)
a.saidNoRouter = true
}
open, err := a.open(ctx)
if err != nil {
return err
}
var unasked []conditions.Condition
for _, c := range open {
if wants(c, now) {
unasked = append(unasked, c)
}
}
return a.sayUnasked(ctx, unasked, "no router takes the controller's asks: no module holding "+
broker.AsksSeat+" that takes an ask under its asker's name is assigned")
}
a.saidNoRouter = false
}
if a.grantHeld != nil {
held, why, err := a.grantHeld(ctx)
if err != nil {
return err
}
if !held {
if !a.saidNoGrant {
a.logf("the bus does not hold the controller's grant to ask yet: %s; the operator is asked nothing until it does", why)
a.saidNoGrant = true
}
open, err := a.open(ctx)
if err != nil {
return err
}
var unasked []conditions.Condition
for _, c := range open {
if wants(c, now) {
unasked = append(unasked, c)
}
}
return a.sayUnasked(ctx, unasked, "the bus does not hold the controller's grant to ask yet: "+why)
}
a.saidNoGrant = false
}
channels := ""
if a.channels != nil {
channels = a.channels(ctx)
}
open, err := a.open(ctx)
if err != nil {
return err
}
all, err := a.store.All(ctx)
if err != nil {
return err
}
byCondition := map[string]asked{} // by partKey
for _, r := range all {
if r.State == askOpen && !r.Rehearsal {
k := partKey(r.Condition, r.Part)
if prior, held := byCondition[k]; !held || r.Opened.After(prior.Opened) {
byCondition[k] = r
}
}
}
// A warrant missed while away, read from the router's record.
if a.routerRecord != nil {
for _, r := range byCondition {
if now.Sub(r.Opened) < askCatchUpAfter {
continue
}
if w, err := a.routerRecord(ctx, r.ID); err == nil && w != nil {
body, _ := json.Marshal(w)
if err := a.Decided(ctx, body); err != nil {
return err
}
}
}
if all, err = a.store.All(ctx); err != nil {
return err
}
byCondition = map[string]asked{}
for _, r := range all {
if r.State == askOpen && !r.Rehearsal {
byCondition[partKey(r.Condition, r.Part)] = r
}
}
}
// What the operator answered lately, by condition: not asked again at once; and what the router refused,
// newest first: not asked again until the answers or the channels change.
answered, refused := map[string]asked{}, map[string]asked{}
for _, r := range all {
k := partKey(r.Condition, r.Part)
if r.State == string(asks.OutcomeChosen) && now.Sub(r.Ended) < askAgainAfterAnswer {
answered[k] = r
}
if r.State == string(asks.OutcomeRefused) {
if prior, has := refused[k]; !has || r.Opened.After(prior.Opened) {
refused[k] = r
}
}
}
wanted := map[string]bool{}
var unasked []conditions.Condition // refused by the router, and nothing it was refused for changed
var refusedWords []string
// The most urgent first, then the oldest: those are asked when no more than askMostOpen may be.
sort.SliceStable(open, func(i, j int) bool {
ui, uj := open[i].Severity == conditions.Urgent, open[j].Severity == conditions.Urgent
if ui != uj {
return ui
}
if !open[i].Raised.Equal(open[j].Raised) {
return open[i].Raised.Before(open[j].Raised)
}
return open[i].Key < open[j].Key
})
openNow := 0
for _, c := range open {
if !wants(c, now) {
continue
}
for _, p := range partsOf(c) {
if r, held := byCondition[partKey(c.Key, p.name)]; held && sameAsked(r.Actions, p.actions) && now.Before(r.Ask.Expires) {
openNow++
}
}
}
for _, c := range open {
if !wants(c, now) {
continue
}
saidUnasked := false
for _, p := range partsOf(c) {
key := partKey(c.Key, p.name)
wanted[key] = true
if r, was := refused[key]; was && sameAsked(r.Actions, p.actions) && r.Channels == channels {
if _, held := byCondition[key]; !held {
if !saidUnasked {
unasked, saidUnasked = append(unasked, c), true
}
if r.Warrant != nil && r.Warrant.Words != "" {
refusedWords = append(refusedWords, r.Warrant.Words)
}
continue // refused, and nothing it was refused for has changed
}
}
if r, done := answered[key]; done && sameAsked(r.Actions, p.actions) {
if _, held := byCondition[key]; !held {
continue
}
}
if r, held := byCondition[key]; held {
switch {
case !sameAsked(r.Actions, p.actions):
if err := a.cancel(ctx, r, "its answers changed"); err != nil {
return err
}
case !now.Before(r.Ask.Expires):
// Expired unanswered: the router says so too; asked again below while it lasts.
if _, err := a.store.Change(ctx, r.ID, func(x *asked) bool {
if x.State != askOpen {
return false
}
x.State, x.Ended = string(asks.OutcomeExpired), now
return true
}); err != nil {
return err
}
openNow--
default:
continue
}
}
if openNow >= askMostOpen {
continue // asked when one of the open ones ends, most urgent first
}
if err := a.ask(ctx, c, p, channels); err != nil {
a.logf("the operator could not be asked about %s: %v", c.Key, err)
continue
}
openNow++
}
}
stillOpen := map[string]conditions.Condition{}
for _, c := range open {
stillOpen[c.Key] = c
}
for key, r := range byCondition {
if wanted[key] {
continue
}
// **A silence never takes an approval back** (the confirmation review of 2026-10-09, M1). Silence is an
// acknowledgement — anyone at the desk may give it — so a condition silenced while its approval is asked
// keeps that ask open, unchanged, until it is answered on a channel that proves who answered, or expires.
// It is not asked again once it ends, while the silence lasts.
if c, open := stillOpen[r.Condition]; open && c.SilencedAt(now) && r.Ask.Highest() != asks.Acknowledge &&
now.Before(r.Ask.Expires) && keepsItsAnswers(c, r) {
continue
}
if err := a.cancel(ctx, r, "the condition ended, was silenced or needs nothing now"); err != nil {
return err
}
}
why := "the router refused the ask"
if len(refusedWords) > 0 {
why += ": " + refusedWords[0]
}
return a.sayUnasked(ctx, unasked, why)
}
// keepsItsAnswers says a condition still offers the answers an ask kept was asked with.
func keepsItsAnswers(c conditions.Condition, r asked) bool {
for _, p := range partsOf(c) {
if partKey(c.Key, p.name) == partKey(r.Condition, r.Part) {
return sameAsked(r.Actions, p.actions)
}
}
return false
}
// sourceAsker raises the asker's own condition.
const sourceAsker = "asker"
// sayUnasked keeps the asker's one condition: while a condition that needs the operator could not be asked
// on any channel, said loudly (failure must be loud), cleared when every one could be.
func (a *asker) sayUnasked(ctx context.Context, unasked []conditions.Condition, why string) error {
if a.raise == nil {
return nil
}
var obs []conditions.Observation
if len(unasked) > 0 {
keys := make([]string, 0, len(unasked))
severity := conditions.Warning
for _, c := range unasked {
keys = append(keys, c.Key)
if c.Severity == conditions.Urgent {
severity = conditions.Urgent
}
}
sort.Strings(keys)
obs = append(obs, conditions.Observation{Scope: conditions.ScopeSeat, ID: broker.AsksSeat, Token: "unasked",
Kind: "asks-undelivered", Severity: severity, Source: sourceAsker,
Summary: fmt.Sprintf("%d condition(s) that need the operator could not be asked on any channel: %s; %s",
len(keys), strings.Join(keys, ", "), why),
Headline: "Questions for you not delivered",
Explanation: "Needs you: answer them from the mesh MCP server. The mesh could not send you its questions on any channel.",
Needs: "answer them from the mesh MCP server, and check why no channel carries them.",
Resolved: "The mesh can ask you again"})
}
if err := a.raise(ctx, obs); err != nil {
a.logf("whether the operator could be asked could not be kept as a condition: %v", err)
}
return nil
}
// optionID is an action's label as an option's id: "Silence for a week" is silence-for-a-week.
func optionID(label string) string {
var b strings.Builder
dash := false
for _, r := range strings.ToLower(label) {
switch {
case r >= 'a' && r <= 'z', r >= '0' && r <= '9':
b.WriteRune(r)
dash = false
case !dash && b.Len() > 0:
b.WriteByte('-')
dash = true
}
}
return strings.TrimSuffix(b.String(), "-")
}
// doesWords is what an action does, in the words an option says it with.
func doesWords(act conditions.Action) string {
switch {
case act.Arguments["silence"] != "":
return "nothing more is said of it for a week"
case act.Verb == "mesh-delivery.release":
return "the delivery goes on"
case act.Verb == "mesh-delivery.stop":
return "the delivery ends"
case act.Verb == broker.ControllerSeat+".plans" && act.Arguments["go"] != "":
return "the delivery starts"
case act.Verb == broker.ControllerSeat+".plans" && act.Arguments["stop"] != "":
return "the delivery is stopped"
case strings.HasSuffix(act.Verb, ".restart"):
return "its service is restarted on " + act.Machine
}
return strings.ToLower(act.Label)
}
// askText is a condition's words as an ask says them: without where an answer is given when no channel can
// give it (FromMeshMCPServer), since the ask is answered on a channel and the router says where else.
func askText(s string) string {
for _, with := range []string{", " + FromMeshMCPServer, " " + FromMeshMCPServer} {
s = strings.ReplaceAll(s, with, ".")
}
return strings.ReplaceAll(s, "..", ".")
}
// askOf is the ask one part of a condition is asked with.
func askOf(id string, c conditions.Condition, p askPart, now time.Time) (asks.Ask, map[string]int) {
q := asks.Ask{ID: id, Headline: c.Headline, Explanation: askText(c.Explanation), Who: asks.Operator,
OnExpiry: "nothing is done, and you are asked again while it lasts", About: p.about,
Urgent: c.Severity == conditions.Urgent}
options := map[string]int{}
approves := false
for i, act := range p.actions {
level := levelOf(act) // an action that says nothing of its level is never taken for less than approve
approves = approves || level != asks.Acknowledge
oid := optionID(act.Label)
options[oid] = i
// Every option binds the exact act it stands for (novox/hq ADR 0259 §6): the verb, the machine and
// every argument. The warrant then authorises that act and no other.
binds, _ := asks.ActDigest(boundAct(act))
q.Options = append(q.Options, asks.Option{ID: oid, Label: act.Label, Does: doesWords(act), Level: level,
Binds: binds})
}
q.Expires = now.Add(askAcknowledgeFor)
if approves {
q.Expires = now.Add(askApproveFor)
}
return q, options
}
// boundAct is what an option's Binds digests: the act exactly as the controller will perform it — its verb,
// machine, level, and each argument as "arg.<name>" — and never its label or words.
func boundAct(act conditions.Action) asks.Act {
out := asks.Act{"verb": act.Verb, "machine": act.Machine, "level": act.Level}
for k, v := range act.Arguments {
out["arg."+k] = v
}
return out
}
func newAskID() string {
var b [8]byte
_, _ = rand.Read(b[:])
return "c" + hex.EncodeToString(b[:])
}
// askUnsent is an ask kept and never published: asked again at the next look.
const askUnsent = "unsent"
// ask publishes one ask about a part of a condition, kept before it is published (the review of 2026-10-09,
// L3): a warrant for it then always finds it, and one whose publishing failed is marked so and asked again.
func (a *asker) ask(ctx context.Context, c conditions.Condition, p askPart, channels string) error {
now := a.now()
id := newAskID()
q, options := askOf(id, c, p, now)
if err := q.Check(now); err != nil {
return err
}
body, err := json.Marshal(q)
if err != nil {
return err
}
if err := a.store.Create(ctx, asked{ID: id, Condition: c.Key, Part: p.name, Ask: q, Actions: p.actions,
Options: options, State: askOpen, Opened: now, Channels: channels}); err != nil {
return fmt.Errorf("the ask could not be kept, so it was not asked: %w", err)
}
if err := a.publish(ctx, asks.AskSubject(askerName), body, "ask."+id); err != nil {
if _, cerr := a.store.Change(ctx, id, func(x *asked) bool {
if x.State != askOpen || x.Acted != "" {
return false
}
x.State, x.Ended, x.Acted = askUnsent, a.now(), "nothing: it could not be published: "+err.Error()
return true
}); cerr != nil {
a.logf("the ask %s could not be published, and could not be marked so: %v", id, cerr)
}
return err
}
a.logf("asked the operator about %s (%s): %d answer(s)", c.Key, id, len(q.Options))
return nil
}
// cancel takes an ask back: kept cancelled first, so a warrant that comes after is refused, then said to the
// router; a cancel the router did not hear leaves the ask to expire there, and nothing is done on it here.
func (a *asker) cancel(ctx context.Context, r asked, why string) error {
stood, err := a.store.Change(ctx, r.ID, func(x *asked) bool {
if x.State != askOpen {
return false
}
x.State, x.Ended = askCancelled, a.now()
return true
})
if err != nil || !stood {
return err
}
body, _ := json.Marshal(map[string]string{"id": r.ID})
if err := a.publish(ctx, asks.CancelSubject(askerName), body, "cancel."+r.ID); err != nil {
a.logf("the ask %s about %s is taken back here, and the router could not be told (%v): it expires there, "+
"and no answer to it is acted on", r.ID, r.Condition, err)
return nil
}
a.logf("took back the ask %s about %s: %s", r.ID, r.Condition, why)
return nil
}
// Decided takes the router's word on one of the controller's asks (link.Decider). An error is returned only
// when what was decided could not be kept, so the word is held and heard again.
func (a *asker) Decided(ctx context.Context, body []byte) error {
var w asks.Warrant
if err := json.Unmarshal(body, &w); err != nil {
a.logf("the router's word on an ask could not be read; ignored: %v", err)
return nil
}
if w.Asker != askerName {
a.logf("REFUSED a warrant for %s's ask %s: the controller acts only on its own", w.Asker, w.Ask)
return nil
}
r, err := a.store.Get(ctx, w.Ask)
if err != nil {
return err
}
if r == nil {
a.logf("REFUSED a warrant for the ask %s, which the controller does not hold", w.Ask)
return nil
}
if r.Acted != "" {
return nil // heard again: acted on once
}
now := a.now()
if w.Outcome != asks.OutcomeChosen {
acted := "nothing: the ask " + string(w.Outcome)
if w.Words != "" {
acted += ": " + w.Words
}
if _, err := a.store.Change(ctx, r.ID, func(x *asked) bool {
if x.Acted != "" {
return false
}
x.State, x.Ended, x.Warrant, x.Acted = string(w.Outcome), now, &w, acted
return true
}); err != nil {
return err
}
a.logf("the ask %s about %s ended %s; nothing is done", r.ID, r.Condition, w.Outcome)
return nil
}
if r.State != askOpen {
// Cancelled, replaced or expired in the controller's own record: no answer to it is acted on.
a.logf("REFUSED a warrant for the ask %s, which is %s in the controller's own record", r.ID, r.State)
return nil
}
option, err := w.For(askerName, r.Ask)
if err != nil {
a.logf("REFUSED a warrant for the ask %s: %v", r.ID, err)
return nil
}
index, offered := r.Options[option.ID]
if !offered || index >= len(r.Actions) {
a.logf("REFUSED a warrant for the ask %s: it chose %s, which no action stands for", r.ID, option.ID)
return nil
}
act := r.Actions[index]
// The act about to be performed is the one the option bound when the controller asked: a record changed
// since is refused, never performed.
if err := option.Performs(boundAct(act)); err != nil {
a.logf("REFUSED a warrant for the ask %s: %v", r.ID, err)
return nil
}
open, err := a.open(ctx)
if err != nil {
return err
}
stillOpen := r.Rehearsal // a rehearsal is about no condition
for _, c := range open {
stillOpen = stillOpen || c.Key == r.Condition
}
if !stillOpen {
// The asker checks the state is still what it asked about before it acts (to-be 46 §10, step 7).
if _, err := a.store.Change(ctx, r.ID, func(x *asked) bool {
if x.State != askOpen || x.Acted != "" {
return false
}
x.State, x.Warrant, x.Ended, x.Acted = string(asks.OutcomeChosen), &w, now,
"nothing: the condition ended before the answer"
return true
}); err != nil {
return err
}
a.logf("%s, for %s, which ended meanwhile: nothing is done", w.Says(), r.Condition)
return nil
}
// Claimed before acting, by compare-and-set: only the delivery whose write stands acts (security review
// of 2026-10-08, finding 9). Not by the warrant's message id, which another publisher could take first:
// the controller's own record decides.
claimed, err := a.store.Change(ctx, r.ID, func(x *asked) bool {
if x.State != askOpen || x.Acted != "" {
return false
}
x.State, x.Warrant, x.Acted = string(asks.OutcomeChosen), &w, "acting"
return true
})
if err != nil {
return err
}
if !claimed {
a.logf("the warrant for the ask %s was already taken by another delivery; nothing more is done", r.ID)
return nil
}
r.Acted = "acting"
why := fmt.Sprintf("%s (ask %s)", w.Says(), r.ID)
args := map[string]string{}
for k, v := range act.Arguments {
args[k] = v
}
if v, takes := args["why"]; takes && v == "" {
args["why"] = why
}
var acted error
switch {
case r.Rehearsal && act.Verb == rehearsalVerb:
// A rehearsal's answer performs nothing: it is recorded below as the operator's decision.
case act.Arguments["silence"] != "":
acted = a.silence(ctx, act.Arguments["silence"], conditions.MaxSilence, byWords(w), why)
default:
acted = a.call(ctx, act, args)
}
ended, outcome := a.now(), "done"
if acted != nil {
outcome = "failed: " + acted.Error()
}
r.Ended, r.Acted = ended, outcome
if _, err := a.store.Change(ctx, r.ID, func(x *asked) bool {
if x.Acted != "acting" {
return false
}
x.Ended, x.Acted = ended, outcome
return true
}); err != nil {
a.logf("%s was acted on (%s), and how it ended could NOT be kept: %v", r.ID, outcome, err)
}
verbArgs := []string{act.Verb}
if act.Machine != "" {
verbArgs = append(verbArgs, "on "+act.Machine)
}
keys := make([]string, 0, len(args))
for k := range args {
keys = append(keys, k)
}
sort.Strings(keys)
for _, k := range keys {
if k != "why" {
verbArgs = append(verbArgs, k+"="+args[k])
}
}
if err := a.record(ctx, link.HandAct{Verb: handActWarrant, Args: verbArgs, Why: why, By: byWords(w),
Cause: conditions.CauseOperatorAnswer, Condition: r.Condition, Via: viaWords(w), Ask: r.ID,
Proofs: w.Proofs, RequestedBy: r.Condition, Outcome: r.Acted}); err != nil {
a.logf("%s was done, and could NOT be recorded in the hand-act log: %v", why, err)
}
a.logf("%s: %s", why, r.Acted)
return nil
}
// handActWarrant is the verb an act the operator chose on a warrant is recorded under: a person's decision,
// never a repair (handActVerbs).
const handActWarrant = "warrant"
// byWords is who chose, as the hand-act log says it: "the operator, as telegram identity 42".
func byWords(w asks.Warrant) string {
if w.By == nil {
return "the operator"
}
return fmt.Sprintf("the %s, as %s identity %s", w.By.Who, w.By.Kind, w.By.Identity)
}
// viaWords is the channel an answer came through: its module and kind, and how the sender was known.
func viaWords(w asks.Warrant) string {
if w.By == nil {
return w.Channel
}
via := w.Channel + " (" + w.By.Kind + ")"
if w.By.Verified != "" {
via += ", " + w.By.Verified
}
return via
}
// errNotGranted is an action whose verb the controller's grant does not name.
var errNotGranted = errors.New("the controller's grant does not name this verb")
-151
View File
@@ -1,151 +0,0 @@
package main
import (
"context"
"encoding/json"
"sync"
"testing"
"time"
"github.com/nats-io/nats.go"
"github.com/nats-io/nats.go/jetstream"
"git.novox.be/novox/mesh-sdk/go/asks"
"github.com/novox/mesh-controller/internal/broker"
"github.com/novox/mesh-controller/internal/conditions"
"github.com/novox/mesh-controller/internal/link"
"github.com/novox/mesh-controller/internal/testbus"
)
// busAsker is an asker on a real bus's `asked` bucket, counting what it performs: two of them are two
// controllers sharing one record.
type busAskerRig struct {
mu sync.Mutex
called int
acts int
open []conditions.Condition
sent [][]byte
}
func (rig *busAskerRig) asker(t *testing.T, conn *nats.Conn, now time.Time) *asker {
return &asker{
open: func(context.Context) ([]conditions.Condition, error) {
rig.mu.Lock()
defer rig.mu.Unlock()
return rig.open, nil
},
silence: func(context.Context, string, time.Duration, string, string) error { return nil },
store: busAsked{conn: conn},
publish: func(_ context.Context, subject string, body []byte, _ string) error {
rig.mu.Lock()
defer rig.mu.Unlock()
if subject == asks.AskSubject(askerName) {
rig.sent = append(rig.sent, body)
}
return nil
},
call: func(context.Context, conditions.Action, map[string]string) error {
time.Sleep(20 * time.Millisecond) // long enough for the other delivery to arrive meanwhile
rig.mu.Lock()
defer rig.mu.Unlock()
rig.called++
return nil
},
record: func(context.Context, link.HandAct) error {
rig.mu.Lock()
defer rig.mu.Unlock()
rig.acts++
return nil
},
now: func() time.Time { return now },
logf: t.Logf,
}
}
func askedBus(t *testing.T) *nats.Conn {
t.Helper()
conn, err := nats.Connect(testbus.URL(t))
if err != nil {
t.Fatal(err)
}
t.Cleanup(conn.Close)
js, err := jetstream.New(conn)
if err != nil {
t.Fatal(err)
}
if _, err := js.CreateKeyValue(context.Background(), jetstream.KeyValueConfig{Bucket: broker.AskedBucket}); err != nil {
t.Fatal(err)
}
return conn
}
// The review of 2026-10-09 (L7): two deliveries of one warrant, to two controllers at once, perform its act
// exactly once and record it once — the record's compare-and-set decides, never the warrant's message id.
func TestTwoAnswersAtOnceActOnce(t *testing.T) {
conn := askedBus(t)
now := time.Date(2026, 10, 9, 14, 0, 0, 0, time.UTC)
rig := &busAskerRig{open: []conditions.Condition{heldCondition()}}
first, second := rig.asker(t, conn, now), rig.asker(t, conn, now)
if err := first.reconcile(context.Background()); err != nil {
t.Fatal(err)
}
if len(rig.sent) != 1 {
t.Fatalf("asked %d times", len(rig.sent))
}
var q asks.Ask
_ = json.Unmarshal(rig.sent[0], &q)
release, _ := q.Option("release")
w := asks.Warrant{Ask: q.ID, Asker: askerName, About: q.About, Outcome: asks.OutcomeChosen, Option: release.ID,
Label: release.Label, Level: release.Level, Channel: "telegram", Proofs: []string{"P1"}, At: now,
AskDigest: q.Digest(), By: &asks.Person{Who: asks.Operator, Kind: "telegram", Identity: "42", Verified: "user id verified"}}
body, _ := json.Marshal(w)
var wg sync.WaitGroup
for _, a := range []*asker{first, second, first, second} {
wg.Add(1)
go func(a *asker) {
defer wg.Done()
if err := a.Decided(context.Background(), body); err != nil {
t.Error(err)
}
}(a)
}
wg.Wait()
if rig.called != 1 || rig.acts != 1 {
t.Fatalf("performed %d time(s), recorded %d time(s)", rig.called, rig.acts)
}
got, err := busAsked{conn: conn}.Get(context.Background(), q.ID)
if err != nil || got == nil || got.Acted != "done" {
t.Fatalf("kept as %+v (%v)", got, err)
}
}
// The review of 2026-10-09 (L2): a write decided on a record read earlier never lands over one made since. A
// cancel read before the answer was acted on leaves the act's record as it is.
func TestAStaleCancelDoesNotWriteOverAnAct(t *testing.T) {
conn := askedBus(t)
now := time.Date(2026, 10, 9, 14, 0, 0, 0, time.UTC)
rig := &busAskerRig{open: []conditions.Condition{heldCondition()}}
a := rig.asker(t, conn, now)
if err := a.reconcile(context.Background()); err != nil {
t.Fatal(err)
}
var q asks.Ask
_ = json.Unmarshal(rig.sent[0], &q)
stale, _ := busAsked{conn: conn}.Get(context.Background(), q.ID)
release, _ := q.Option("release")
w := asks.Warrant{Ask: q.ID, Asker: askerName, About: q.About, Outcome: asks.OutcomeChosen, Option: release.ID,
Label: release.Label, Level: release.Level, Channel: "telegram", At: now, AskDigest: q.Digest(),
By: &asks.Person{Who: asks.Operator, Kind: "telegram", Identity: "42", Verified: "user id verified"}}
body, _ := json.Marshal(w)
if err := a.Decided(context.Background(), body); err != nil {
t.Fatal(err)
}
if err := a.cancel(context.Background(), *stale, "the condition ended"); err != nil {
t.Fatal(err)
}
got, _ := busAsked{conn: conn}.Get(context.Background(), q.ID)
if got.State != string(asks.OutcomeChosen) || got.Acted != "done" {
t.Errorf("a stale cancel wrote over the act: %+v", got)
}
}
-705
View File
@@ -1,705 +0,0 @@
package main
import (
"context"
"encoding/json"
"errors"
"fmt"
"strings"
"sync"
"testing"
"time"
"git.novox.be/novox/mesh-sdk/go/asks"
"github.com/novox/mesh-controller/internal/conditions"
"github.com/novox/mesh-controller/internal/link"
)
// novox/hq ADR 0259 §6: the controller asks the operator for the answers its conditions name, and performs
// the one chosen on the router's warrant — once, for its own ask, the option offered, at its level.
type memAskedStore map[string]asked
// memAskedMu guards every memAskedStore: Change is a compare-and-set as the bus's is.
var memAskedMu sync.Mutex
func (m memAskedStore) Get(_ context.Context, id string) (*asked, error) {
memAskedMu.Lock()
defer memAskedMu.Unlock()
r, ok := m[id]
if !ok {
return nil, nil
}
return &r, nil
}
func (m memAskedStore) Create(_ context.Context, r asked) error {
memAskedMu.Lock()
defer memAskedMu.Unlock()
if _, kept := m[r.ID]; kept {
return errors.New("an ask is kept under that id")
}
m[r.ID] = r
return nil
}
func (m memAskedStore) Change(_ context.Context, id string, change func(*asked) bool) (bool, error) {
memAskedMu.Lock()
defer memAskedMu.Unlock()
r, ok := m[id]
if !ok || !change(&r) {
return false, nil
}
m[id] = r
return true, nil
}
func (m memAskedStore) All(context.Context) ([]asked, error) {
memAskedMu.Lock()
defer memAskedMu.Unlock()
var out []asked
for _, r := range m {
out = append(out, r)
}
return out, nil
}
type published struct {
subject, id string
body []byte
}
type askerRig struct {
a *asker
open []conditions.Condition
store memAskedStore
sent []published
called []string
silenced []string
acts []link.HandAct
now time.Time
}
func newAskerRig(t *testing.T) *askerRig {
r := &askerRig{store: memAskedStore{}, now: time.Date(2026, 10, 8, 14, 0, 0, 0, time.UTC)}
r.a = &asker{
open: func(context.Context) ([]conditions.Condition, error) { return r.open, nil },
silence: func(_ context.Context, key string, d time.Duration, by, why string) error {
r.silenced = append(r.silenced, key+" for "+d.String()+" by "+by+" because "+why)
// As the controller's conditions do (the confirmation review of 2026-10-09, M1): the condition is
// silenced from now on, so what is asked next sees it silenced.
for i := range r.open {
if r.open[i].Key == key {
r.open[i].Silenced = &conditions.Silence{Until: r.now.Add(d), By: by, Why: why, Since: r.now}
}
}
return nil
},
store: r.store,
publish: func(_ context.Context, subject string, body []byte, id string) error {
r.sent = append(r.sent, published{subject, id, body})
return nil
},
call: func(_ context.Context, a conditions.Action, args map[string]string) error {
raw, _ := json.Marshal(args)
r.called = append(r.called, a.Verb+"@"+a.Machine+" "+string(raw))
return nil
},
record: func(_ context.Context, act link.HandAct) error { r.acts = append(r.acts, act); return nil },
now: func() time.Time { return r.now },
logf: t.Logf,
}
return r
}
func heldCondition() conditions.Condition {
o := stalledObservations([]stalledLine{{ID: "novox/hq@055550802096", State: "held", For: "36h2m6s",
Bound: "24h0m0s", H2: "none: the state is the operator's"}})[0]
return conditions.Condition{Key: o.Key(), Kind: o.Kind, Severity: conditions.Warning, Headline: o.Headline,
Explanation: conditions.Verdict(o.Needs, o.Explanation), Needs: o.Needs, Actions: o.Actions}
}
func unitsCondition() conditions.Condition {
key := "machine.shanks.units"
return conditions.Condition{Key: key, Kind: "machine-units", Severity: conditions.Warning,
Headline: "3 failed services on shanks", Explanation: "Needs you: mend or remove them on shanks, or silence this.",
Needs: "mend or remove them on shanks, or silence this.", Actions: []conditions.Action{conditions.SilenceAction(key)}}
}
func (r *askerRig) asksSent(t *testing.T) []asks.Ask {
t.Helper()
var out []asks.Ask
for _, p := range r.sent {
if p.subject != asks.AskSubject("mesh-controller") {
continue
}
var q asks.Ask
if err := json.Unmarshal(p.body, &q); err != nil {
t.Fatal(err)
}
out = append(out, q)
}
return out
}
func TestAnAskIsMadeForEachConditionThatNamesItsAnswers(t *testing.T) {
r := newAskerRig(t)
quiet := conditions.Condition{Key: "machine.ace.silent", Headline: "ace silent", Explanation: "Nothing for you to do. x"}
r.open = []conditions.Condition{heldCondition(), unitsCondition(), quiet}
if err := r.a.reconcile(context.Background()); err != nil {
t.Fatal(err)
}
sent := r.asksSent(t)
if len(sent) != 2 {
t.Fatalf("asked %d times: %+v", len(sent), sent)
}
byAbout := map[string]asks.Ask{}
for _, q := range sent {
byAbout[q.About] = q
if err := q.Check(r.now); err != nil {
t.Errorf("%s: %v", q.About, err)
}
}
held := byAbout[heldCondition().Key]
if len(held.Options) != 2 || held.Options[0].Label != "Release" || held.Options[0].Level != asks.Approve ||
held.Options[1].ID != "stop" || held.Expires != r.now.Add(askApproveFor) || held.Who != asks.Operator ||
held.OnExpiry == "" {
t.Errorf("the held delivery is asked %+v", held)
}
units := byAbout["machine.shanks.units"]
if len(units.Options) != 1 || units.Options[0].Level != asks.Acknowledge || units.Expires != r.now.Add(askAcknowledgeFor) {
t.Errorf("the failed units are asked %+v", units)
}
// No second ask while one is open.
r.now = r.now.Add(time.Minute)
_ = r.a.reconcile(context.Background())
if n := len(r.asksSent(t)); n != 2 {
t.Errorf("asked again while open: %d", n)
}
}
func TestAnAskIsTakenBackWhenItsConditionEndsAndAskedAgainAfterItExpires(t *testing.T) {
r := newAskerRig(t)
r.open = []conditions.Condition{heldCondition(), unitsCondition()}
_ = r.a.reconcile(context.Background())
// The units are silenced, the held delivery lasts past its ask's day.
units := unitsCondition()
units.Silenced = &conditions.Silence{Until: r.now.Add(48 * time.Hour)}
r.open = []conditions.Condition{heldCondition(), units}
r.now = r.now.Add(askApproveFor)
if err := r.a.reconcile(context.Background()); err != nil {
t.Fatal(err)
}
var cancels int
for _, p := range r.sent {
if p.subject == asks.CancelSubject("mesh-controller") {
cancels++
}
}
if cancels != 1 {
t.Errorf("cancels %d, want the silenced one's", cancels)
}
if sent := r.asksSent(t); len(sent) != 3 || sent[2].About != heldCondition().Key {
t.Errorf("the expired ask was not asked again: %+v", sent)
}
}
// warrantFor is the router's warrant for the open ask about a condition, choosing an option by label.
func (r *askerRig) warrantFor(t *testing.T, condition, label string) asks.Warrant {
t.Helper()
for _, a := range r.store {
if a.Condition != condition || a.State != askOpen {
continue
}
for _, o := range a.Ask.Options {
if o.Label == label {
return asks.Warrant{Ask: a.ID, Asker: "mesh-controller", About: condition, Outcome: asks.OutcomeChosen,
Option: o.ID, Label: o.Label, Level: o.Level, Channel: "telegram", Proofs: []string{"P1"}, At: r.now,
AskDigest: a.Ask.Digest(),
By: &asks.Person{Who: asks.Operator, Kind: "telegram", Identity: "42", Verified: "user id verified"}}
}
}
}
t.Fatalf("no open ask about %s offers %s", condition, label)
return asks.Warrant{}
}
func answerWith(t *testing.T, r *askerRig, w asks.Warrant) {
t.Helper()
body, _ := json.Marshal(w)
if err := r.a.Decided(context.Background(), body); err != nil {
t.Fatal(err)
}
}
func TestAWarrantIsActedOnOnce(t *testing.T) {
r := newAskerRig(t)
r.open = []conditions.Condition{heldCondition()}
_ = r.a.reconcile(context.Background())
w := r.warrantFor(t, heldCondition().Key, "Release")
answerWith(t, r, w)
answerWith(t, r, w) // heard again
if len(r.called) != 1 {
t.Fatalf("called %v", r.called)
}
want := `mesh-delivery.release@ {"id":"novox/hq@055550802096","why":"the operator, via telegram (user id verified), chose Release (ask ` + w.Ask + `)"}`
if r.called[0] != want {
t.Errorf("called\n %s\nwant\n %s", r.called[0], want)
}
if len(r.acts) != 1 {
t.Fatalf("hand-acts %+v", r.acts)
}
act := r.acts[0]
if act.Verb != handActWarrant || act.By != "the operator, as telegram identity 42" ||
act.Via != "telegram (telegram), user id verified" || act.Ask != w.Ask || strings.Join(act.Proofs, ",") != "P1" ||
act.Cause != conditions.CauseOperatorAnswer || act.Condition != heldCondition().Key || act.Outcome != "done" {
t.Errorf("the hand-act %+v", act)
}
if !personsDecision(act) {
t.Error("an act on a warrant counts as a repair")
}
if got := r.store[w.Ask]; got.State != string(asks.OutcomeChosen) || got.Acted != "done" {
t.Errorf("kept %+v", got)
}
}
func TestAWarrantThatIsNotForItsOwnAskIsRefused(t *testing.T) {
for name, change := range map[string]func(*asks.Warrant){
"another asker": func(w *asks.Warrant) { w.Asker = "mesh-delivery" },
"an ask not held": func(w *asks.Warrant) { w.Ask = "c0000000000000000" },
"an option not offered": func(w *asks.Warrant) { w.Option = "delete" },
"another level": func(w *asks.Warrant) { w.Level = asks.Acknowledge },
"no person": func(w *asks.Warrant) { w.By = nil },
"another ask's digest": func(w *asks.Warrant) { w.AskDigest = "sha256:0000" },
"no ask's digest": func(w *asks.Warrant) { w.AskDigest = "" },
} {
t.Run(name, func(t *testing.T) {
r := newAskerRig(t)
r.open = []conditions.Condition{heldCondition()}
_ = r.a.reconcile(context.Background())
w := r.warrantFor(t, heldCondition().Key, "Stop")
change(&w)
answerWith(t, r, w)
if len(r.called)+len(r.acts)+len(r.silenced) != 0 {
t.Errorf("acted on it: %v %v %v", r.called, r.acts, r.silenced)
}
})
}
}
func TestEachAnswerCallsExactlyItsVerb(t *testing.T) {
plan := "plan-1791454185265004861"
waiting := conditions.Condition{Key: "plan." + plan + ".waiting", Severity: conditions.Urgent,
Headline: "openrazer delivery waiting to start", Needs: "start it, or stop it.",
Explanation: "Needs you: start it, or stop it.", Actions: waitingActions(plan, conditions.Urgent)}
module := conditions.Condition{Key: "module.openrazer.g14.unhealthy", Severity: conditions.Warning,
Headline: "openrazer not working on g14", Needs: "restart its service openrazer-daemon on g14.",
Explanation: "Needs you: restart it.", Actions: []conditions.Action{{Label: "Restart",
Verb: "node-service-manager.restart", Machine: "g14", Level: conditions.LevelApprove,
Arguments: map[string]string{"unit": "openrazer-daemon.service", "scope": "user"}}}}
for _, tc := range []struct {
c conditions.Condition
label string
want string
}{
{waiting, "Start", `mesh-controller.plans@ {"cause":"operator-answer","go":"` + plan + `","why":"`},
{waiting, "Stop", `mesh-controller.plans@ {"cause":"operator-answer","stop":"` + plan + `","why":"`},
{module, "Restart", `node-service-manager.restart@g14 {"scope":"user","unit":"openrazer-daemon.service"}`},
} {
r := newAskerRig(t)
r.open = []conditions.Condition{tc.c}
_ = r.a.reconcile(context.Background())
answerWith(t, r, r.warrantFor(t, tc.c.Key, tc.label))
if len(r.called) != 1 || !strings.HasPrefix(r.called[0], tc.want) {
t.Errorf("%s: called %v, want %s…", tc.label, r.called, tc.want)
}
}
}
func TestASilenceChosenIsTheControllersOwnAndAnAnswerToAnAsk(t *testing.T) {
r := newAskerRig(t)
r.open = []conditions.Condition{unitsCondition()}
_ = r.a.reconcile(context.Background())
w := r.warrantFor(t, "machine.shanks.units", "Silence for a week")
w.Level, w.Proofs = asks.Acknowledge, nil
w.By = &asks.Person{Who: asks.Operator, Kind: "desktop", Identity: "g14",
Verified: "a desk click: whoever was at the operator's session on g14"}
w.Channel = "desk-channel"
answerWith(t, r, w)
if len(r.called) != 0 || len(r.silenced) != 1 || !strings.HasPrefix(r.silenced[0], "machine.shanks.units for 168h0m0s by the operator, as desktop identity g14") {
t.Fatalf("silenced %v, called %v", r.silenced, r.called)
}
if len(r.acts) != 1 || r.acts[0].Cause != conditions.CauseOperatorAnswer || len(r.acts[0].Proofs) != 0 {
t.Errorf("%+v", r.acts)
}
}
func TestAnAskThatEndedWithoutAChoiceDoesNothing(t *testing.T) {
r := newAskerRig(t)
r.open = []conditions.Condition{heldCondition()}
_ = r.a.reconcile(context.Background())
w := r.warrantFor(t, heldCondition().Key, "Release")
w.Outcome, w.Option, w.Label, w.Level, w.By, w.Words = asks.OutcomeExpired, "", "", "", nil, "nobody answered in time"
answerWith(t, r, w)
if len(r.called)+len(r.acts) != 0 || r.store[w.Ask].State != string(asks.OutcomeExpired) ||
!strings.HasPrefix(r.store[w.Ask].Acted, "nothing") {
t.Errorf("called %v acts %v kept %+v", r.called, r.acts, r.store[w.Ask])
}
// And a choice for a condition that ended meanwhile does nothing either.
r2 := newAskerRig(t)
r2.open = []conditions.Condition{heldCondition()}
_ = r2.a.reconcile(context.Background())
w2 := r2.warrantFor(t, heldCondition().Key, "Release")
r2.open = nil
answerWith(t, r2, w2)
if len(r2.called) != 0 || r2.store[w2.Ask].Acted != "nothing: the condition ended before the answer" {
t.Errorf("%v %+v", r2.called, r2.store[w2.Ask])
}
}
func TestAWarrantMissedWhileAwayIsReadFromTheRoutersRecord(t *testing.T) {
r := newAskerRig(t)
r.open = []conditions.Condition{heldCondition()}
_ = r.a.reconcile(context.Background())
w := r.warrantFor(t, heldCondition().Key, "Stop")
r.a.routerRecord = func(_ context.Context, id string) (*asks.Warrant, error) {
if id != w.Ask {
return nil, errors.New("another ask")
}
return &w, nil
}
r.now = r.now.Add(askCatchUpAfter)
if err := r.a.reconcile(context.Background()); err != nil {
t.Fatal(err)
}
if len(r.called) != 1 || !strings.HasPrefix(r.called[0], "mesh-delivery.stop@") {
t.Errorf("called %v", r.called)
}
if n := len(r.asksSent(t)); n != 1 {
t.Errorf("asked again after the answer: %d", n)
}
}
// After review (2026-10-08): a refused ask is not asked again until its answers or the channels change.
func TestAnAskTheRouterRefusedWaitsUntilSomethingChanges(t *testing.T) {
r := newAskerRig(t)
channels := "channel/telegram=telegram@anchor[choice]own:true"
r.a.channels = func(context.Context) string { return channels }
r.open = []conditions.Condition{heldCondition()}
_ = r.a.reconcile(context.Background())
first := r.asksSent(t)[0]
refusal, _ := json.Marshal(asks.Warrant{Ask: first.ID, Asker: "mesh-controller", Outcome: asks.OutcomeRefused,
Words: "no channel can carry any of its answers now", At: r.now})
if err := r.a.Decided(context.Background(), refusal); err != nil {
t.Fatal(err)
}
if got := r.store[first.ID]; got.State != string(asks.OutcomeRefused) || !strings.Contains(got.Acted, "nothing") {
t.Fatalf("the refusal was kept as %+v", got)
}
for i := 0; i < 3; i++ {
r.now = r.now.Add(askEvery)
_ = r.a.reconcile(context.Background())
}
if n := len(r.asksSent(t)); n != 1 {
t.Fatalf("asked again %d time(s) though nothing changed", n-1)
}
channels = "channel/telegram=telegram@anchor[choice,verified-sender]own:true"
_ = r.a.reconcile(context.Background())
if n := len(r.asksSent(t)); n != 2 {
t.Errorf("not asked again once the channels changed: %d", n)
}
}
// After review: at most three asks open at once, the most urgent first, then the oldest.
func TestAtMostThreeAsksAreOpenTheMostUrgentFirst(t *testing.T) {
r := newAskerRig(t)
var open []conditions.Condition
for i := 0; i < 4; i++ {
c := unitsCondition()
c.Key = "machine.m" + string(rune('a'+i)) + ".units"
c.Actions = []conditions.Action{conditions.SilenceAction(c.Key)}
c.Raised = r.now.Add(-time.Duration(10-i) * time.Hour)
open = append(open, c)
}
urgent := heldCondition()
urgent.Severity, urgent.Raised = conditions.Urgent, r.now.Add(-time.Minute)
r.open = append(open, urgent)
_ = r.a.reconcile(context.Background())
sent := r.asksSent(t)
if len(sent) != askMostOpen || sent[0].About != urgent.Key || sent[1].About != "machine.ma.units" || sent[2].About != "machine.mb.units" {
var about []string
for _, q := range sent {
about = append(about, q.About)
}
t.Fatalf("asked %v", about)
}
}
// After review: nothing is asked while no router takes asks under the controller's name, and that is said once.
func TestNothingIsAskedWithoutARouter(t *testing.T) {
r := newAskerRig(t)
var said []string
r.a.logf = func(f string, a ...any) { said = append(said, f) }
r.a.routerHere = func(context.Context) (bool, error) { return false, nil }
r.open = []conditions.Condition{heldCondition()}
_ = r.a.reconcile(context.Background())
_ = r.a.reconcile(context.Background())
if len(r.asksSent(t)) != 0 {
t.Error("asked with no router")
}
n := 0
for _, s := range said {
if strings.Contains(s, "no router takes asks") {
n++
}
}
if n != 1 {
t.Errorf("said %d times", n)
}
}
// After review: the condition's words keep where an answer is given without a channel; the ask's text does not.
func TestTheAskDropsWhereItIsAnsweredAndTheConditionKeepsIt(t *testing.T) {
c := heldCondition()
if !strings.Contains(c.Explanation, FromMeshMCPServer) {
t.Fatalf("the condition lost where it is answered: %q", c.Explanation)
}
q, _ := askOf("x", c, partsOf(c)[0], time.Now())
if strings.Contains(q.Explanation, "mesh MCP server") || !strings.HasPrefix(q.Explanation, "Needs you: release it, or stop it.") {
t.Errorf("the ask says %q", q.Explanation)
}
if askApproveFor >= 24*time.Hour {
t.Errorf("an approving ask lasts %s, which the SDK may refuse at its bound", askApproveFor)
}
}
// After review (security finding 9): a warrant is acted on only for an ask open in the controller's own record,
// once the claim stands, and never when given after the ask expired.
func TestAWarrantIsActedOnlyForAnOpenAskItClaimsBeforeItExpired(t *testing.T) {
r := newAskerRig(t)
r.open = []conditions.Condition{heldCondition()}
_ = r.a.reconcile(context.Background())
w := r.warrantFor(t, heldCondition().Key, "Release")
late := w
late.At = r.store[w.Ask].Ask.Expires.Add(time.Minute)
body, _ := json.Marshal(late)
_ = r.a.Decided(context.Background(), body)
if len(r.called) != 0 {
t.Fatalf("acted on a warrant given after the ask expired: %v", r.called)
}
// Claimed already by another delivery: nothing done here.
kept := r.store[w.Ask]
kept.Acted = "acting"
r.store[w.Ask] = kept
body, _ = json.Marshal(w)
_ = r.a.Decided(context.Background(), body)
if len(r.called) != 0 {
t.Fatalf("acted though the claim was another's: %v", r.called)
}
// Cancelled in its own record: refused.
kept.Acted, kept.State = "", askCancelled
r.store[w.Ask] = kept
_ = r.a.Decided(context.Background(), body)
if len(r.called) != 0 {
t.Errorf("acted on a cancelled ask: %v", r.called)
}
}
// novox/hq ADR 0259 §6: a warrant authorises the act its option bound when the controller asked, and no
// other. A record of the act changed after the ask — another delivery, another machine, another argument —
// is refused and nothing is performed.
func TestAWarrantPerformsOnlyTheActItsOptionBound(t *testing.T) {
for name, change := range map[string]func(*conditions.Action){
"another argument": func(a *conditions.Action) {
a.Arguments = map[string]string{"id": "novox/mesh-controller@000000000000"}
},
"another verb": func(a *conditions.Action) { a.Verb = "mesh-delivery.stop" },
"another machine": func(a *conditions.Action) { a.Machine = "anchor" },
} {
t.Run(name, func(t *testing.T) {
r := newAskerRig(t)
r.open = []conditions.Condition{heldCondition()}
_ = r.a.reconcile(context.Background())
w := r.warrantFor(t, heldCondition().Key, "Release")
kept := r.store[w.Ask]
acts := append([]conditions.Action(nil), kept.Actions...)
i := kept.Options[w.Option]
change(&acts[i])
kept.Actions = acts
r.store[w.Ask] = kept
answerWith(t, r, w)
if len(r.called)+len(r.acts) != 0 {
t.Errorf("performed an act the option did not bind: %v %v", r.called, r.acts)
}
})
}
// Every option of an ask binds its act.
q, _ := askOf("x", heldCondition(), partsOf(heldCondition())[0], time.Now())
for _, o := range q.Options {
if o.Binds == "" {
t.Errorf("the option %s binds nothing", o.ID)
}
}
}
// Failure is loud (novox/hq ADR 0259, the self-review of 2026-10-09): a condition that needs the operator and
// could not be asked on any channel — no router, or the router refused the ask — is a condition of its own,
// cleared once it can be asked again.
func TestAnAskThatCannotBeDeliveredIsSaid(t *testing.T) {
r := newAskerRig(t)
var raised [][]conditions.Observation
r.a.raise = func(_ context.Context, obs []conditions.Observation) error {
raised = append(raised, obs)
return nil
}
last := func() []conditions.Observation { return raised[len(raised)-1] }
routerHere := false
r.a.routerHere = func(context.Context) (bool, error) { return routerHere, nil }
channels := "channel/telegram=telegram@anchor[choice]own:true"
r.a.channels = func(context.Context) string { return channels }
r.open = []conditions.Condition{heldCondition()}
_ = r.a.reconcile(context.Background())
if got := last(); len(got) != 1 || got[0].Kind != "asks-undelivered" ||
!strings.Contains(got[0].Summary, heldCondition().Key) || !strings.Contains(got[0].Summary, "no router") {
t.Fatalf("no router, said as %+v", got)
}
routerHere = true
_ = r.a.reconcile(context.Background())
if got := last(); len(got) != 0 {
t.Fatalf("asked, and still said undelivered: %+v", got)
}
first := r.asksSent(t)[0]
refusal, _ := json.Marshal(asks.Warrant{Ask: first.ID, Asker: "mesh-controller", Outcome: asks.OutcomeRefused,
Words: "no channel can carry any of its answers now", At: r.now})
if err := r.a.Decided(context.Background(), refusal); err != nil {
t.Fatal(err)
}
_ = r.a.reconcile(context.Background())
if got := last(); len(got) != 1 || !strings.Contains(got[0].Summary, "no channel can carry") {
t.Fatalf("the router's refusal, said as %+v", got)
}
if why, ok := conditions.PlainWords(conditions.Words{Headline: last()[0].Headline, Explanation: last()[0].Explanation,
Needs: last()[0].Needs, Resolved: last()[0].Resolved}, ""); !ok {
t.Errorf("not plain: %s", why)
}
r.open = nil
_ = r.a.reconcile(context.Background())
if got := last(); len(got) != 0 {
t.Errorf("nothing needs asking, and still said: %+v", got)
}
}
// The review of 2026-10-09 (M1): an acknowledging answer never shares an ask with an authorising one. A
// condition offering Restart and Silence is asked twice — Restart alone, about the condition, and Silence
// alone, apart — so Silence chosen on a channel that only acknowledges leaves the Restart ask open.
func TestAnAcknowledgementNeverSharesAnAskWithAnApproval(t *testing.T) {
r := newAskerRig(t)
key := "module.shanks.plex.down"
c := conditions.Condition{Key: key, Kind: "module-down", Severity: conditions.Urgent, Headline: "Plex down on shanks",
Explanation: "Needs you: restart it, or silence this.", Needs: "restart it, or silence this.",
Actions: []conditions.Action{
{Label: "Restart", Verb: "node-service-manager.restart", Machine: "shanks", Level: conditions.LevelApprove,
Arguments: map[string]string{"unit": "plex"}},
conditions.SilenceAction(key)}}
r.open = []conditions.Condition{c}
if err := r.a.reconcile(context.Background()); err != nil {
t.Fatal(err)
}
sent := r.asksSent(t)
if len(sent) != 2 {
t.Fatalf("asked %d time(s): %+v", len(sent), sent)
}
for _, q := range sent {
if err := q.Check(r.now); err != nil {
t.Errorf("%s: %v", q.About, err)
}
levels := map[asks.Level]bool{}
for _, o := range q.Options {
levels[o.Level] = true
}
if len(levels) != 1 {
t.Errorf("the ask about %s mixes levels: %+v", q.About, q.Options)
}
}
byAbout := map[string]asks.Ask{}
for _, q := range sent {
byAbout[q.About] = q
}
if q := byAbout[key]; len(q.Options) != 1 || q.Options[0].Label != "Restart" {
t.Errorf("the condition's own ask: %+v", q)
}
if q := byAbout[key+".acknowledge"]; len(q.Options) != 1 || q.Options[0].Level != asks.Acknowledge {
t.Errorf("the acknowledging ask: %+v", q)
}
// Silence chosen: performed, and the Restart ask stays open, never asked twice.
answerWith(t, r, r.warrantFor(t, key, "Silence for a week"))
if len(r.silenced) != 1 || len(r.called) != 0 {
t.Fatalf("silenced %v called %v", r.silenced, r.called)
}
_ = r.a.reconcile(context.Background())
open := 0
for _, a := range r.store {
if a.State == askOpen && a.Condition == key {
open++
if a.Part != "" || a.Ask.Options[0].Label != "Restart" {
t.Errorf("the open ask is %+v", a)
}
}
}
if open != 1 || len(r.asksSent(t)) != 2 {
t.Errorf("after the silence: %d open, %d asked", open, len(r.asksSent(t)))
}
// And the approval still answers: Restart chosen on a channel that proves who answered is performed.
answerWith(t, r, r.warrantFor(t, key, "Restart"))
if len(r.called) != 1 || !strings.HasPrefix(r.called[0], "node-service-manager.restart@shanks") {
t.Errorf("the approval kept through a silence was not performed: %v", r.called)
}
}
// novox/hq issue 353: the controller's grant to ask is composed from the router's assignment and reaches the
// bus only when its machine is pushed. Between the two, the bus refuses every ask (measured 2026-10-09, 17:54 to
// 17:56 local: seven refusals of mesh.seat.operator-channel.accept.ask.mesh-controller). So nothing is asked
// while the bus's user list is behind, it is said once, the conditions that need the operator are raised as
// undelivered with what to do, and the asks go out once the bus holds the grant.
func TestNothingIsAskedWhileTheBusLacksTheControllersGrant(t *testing.T) {
r := newAskerRig(t)
var said []string
r.a.logf = func(f string, a ...any) { said = append(said, fmt.Sprintf(f, a...)) }
var raised [][]conditions.Observation
r.a.raise = func(_ context.Context, obs []conditions.Observation) error {
raised = append(raised, obs)
return nil
}
held := false
r.a.grantHeld = func(context.Context) (bool, string, error) {
return held, "the bus's user list on anchor is behind what the mesh composes; `push anchor` carries it", nil
}
r.open = []conditions.Condition{heldCondition()}
_ = r.a.reconcile(context.Background())
_ = r.a.reconcile(context.Background())
if len(r.asksSent(t)) != 0 {
t.Error("asked while the bus lacks the grant")
}
n := 0
for _, s := range said {
if strings.Contains(s, "does not hold the controller's grant") {
n++
}
}
if n != 1 {
t.Errorf("said %d times: %q", n, said)
}
if len(raised) == 0 || len(raised[len(raised)-1]) != 1 ||
!strings.Contains(raised[len(raised)-1][0].Summary, "`push anchor` carries it") ||
raised[len(raised)-1][0].Kind != "asks-undelivered" {
t.Fatalf("not said as a condition with what to do: %+v", raised)
}
held = true
_ = r.a.reconcile(context.Background())
if sent := r.asksSent(t); len(sent) != 1 || sent[0].About != heldCondition().Key {
t.Errorf("not asked once the bus holds the grant: %+v", sent)
}
if last := raised[len(raised)-1]; len(last) != 0 {
t.Errorf("the undelivered condition was not cleared: %+v", last)
}
}
-337
View File
@@ -1,337 +0,0 @@
package main
// The asker on the bus: its asks in the controller's bucket `asked`, its asks and cancels published on the
// seat under the controller's name, the verbs a warrant chooses called with the controller's grant, and the
// router's record of its asks read under its name (novox/hq ADR 0259).
import (
"context"
"encoding/json"
"errors"
"fmt"
"sort"
"strings"
"sync"
"time"
"github.com/nats-io/nats.go"
"github.com/nats-io/nats.go/jetstream"
"git.novox.be/novox/mesh-sdk/go/asks"
"github.com/novox/mesh-controller/internal/broker"
"github.com/novox/mesh-controller/internal/catalogue"
"github.com/novox/mesh-controller/internal/conditions"
"github.com/novox/mesh-controller/internal/inventory"
"github.com/novox/mesh-controller/internal/link"
)
// askerFrom is the serving controller's asker; nil in any other process.
var askerFrom *asker
// askWithin is how long a verb a warrant chose is given to answer.
const askWithin = time.Minute
type busAsked struct{ conn *nats.Conn }
func (b busAsked) kv(ctx context.Context) (jetstream.KeyValue, error) {
js, err := jetstream.New(b.conn)
if err != nil {
return nil, err
}
return js.KeyValue(ctx, broker.AskedBucket)
}
func (b busAsked) Get(ctx context.Context, id string) (*asked, error) {
kv, err := b.kv(ctx)
if err != nil {
return nil, err
}
e, err := kv.Get(ctx, id)
if errors.Is(err, jetstream.ErrKeyNotFound) {
return nil, nil
}
if err != nil {
return nil, err
}
var r asked
return &r, json.Unmarshal(e.Value(), &r)
}
// Create keeps a new ask under its id, and only where none is kept: never over another.
func (b busAsked) Create(ctx context.Context, r asked) error {
kv, err := b.kv(ctx)
if err != nil {
return err
}
body, err := json.Marshal(r)
if err != nil {
return err
}
_, err = kv.Create(ctx, r.ID, body)
return err
}
// Change applies change to the ask kept under id by compare-and-set on its key's revision (the review of
// 2026-10-09, L2): read, changed, and written only over the revision read; when another write came between,
// read again and asked again, at most askChangeTries times. change says whether to write at all.
func (b busAsked) Change(ctx context.Context, id string, change func(*asked) bool) (bool, error) {
kv, err := b.kv(ctx)
if err != nil {
return false, err
}
for try := 0; try < askChangeTries; try++ {
e, err := kv.Get(ctx, id)
if errors.Is(err, jetstream.ErrKeyNotFound) {
return false, nil
}
if err != nil {
return false, err
}
var r asked
if err := json.Unmarshal(e.Value(), &r); err != nil {
return false, err
}
if !change(&r) {
return false, nil
}
body, err := json.Marshal(r)
if err != nil {
return false, err
}
if _, err := kv.Update(ctx, id, body, e.Revision()); err != nil {
var api *jetstream.APIError
if errors.Is(err, jetstream.ErrKeyExists) || (errors.As(err, &api) && api.ErrorCode == jetstream.JSErrCodeStreamWrongLastSequence) {
continue
}
return false, err
}
return true, nil
}
return false, fmt.Errorf("the ask %s changed under every one of %d tries", id, askChangeTries)
}
func (b busAsked) All(ctx context.Context) ([]asked, error) {
kv, err := b.kv(ctx)
if err != nil {
return nil, err
}
lister, err := kv.ListKeys(ctx)
if err != nil {
return nil, err
}
defer func() { _ = lister.Stop() }()
var out []asked
for k := range lister.Keys() {
e, err := kv.Get(ctx, k)
if err != nil {
continue
}
var r asked
if json.Unmarshal(e.Value(), &r) == nil {
out = append(out, r)
}
}
return out, nil
}
// callAction performs an action's verb as the controller, through the grant that names it.
func callAction(conn *nats.Conn) func(ctx context.Context, a conditions.Action, args map[string]string) error {
return func(ctx context.Context, a conditions.Action, args map[string]string) error {
seat, verb, ok := strings.Cut(a.Verb, ".")
if !ok {
return fmt.Errorf("%q names no seat and verb", a.Verb)
}
body := map[string]any{}
for k, v := range args {
body[k] = v
}
if seat == catalogue.DeliverySeat {
_, err := askDeliveryOwner(ctx, conn, verb, body)
return err
}
granted := false
for _, v := range broker.VerbsTheControllerActsOnAWarrant {
granted = granted || (v.Seat == seat && v.Verb == verb)
}
if !granted {
return fmt.Errorf("%s: %w", a.Verb, errNotGranted)
}
var answer link.Answer
var err error
if a.Machine != "" {
answer, err = link.AskSeatTool(ctx, conn, seat, verb, a.Machine, body, askWithin)
} else {
answer, err = link.AskMeshSeatTool(ctx, conn, seat, verb, body, askWithin)
}
if err != nil {
return err
}
if answer.Error != "" {
return fmt.Errorf("%s refused: %s", a.Verb, answer.Error)
}
return nil
}
}
// routerRecordOf reads the router's record of one of the controller's asks, under its name, and answers
// how it ended when it did: the bucket is the one the asks seat's declarer names as its records.
func routerRecordOf(conn *nats.Conn, inv *inventory.Inventory) func(ctx context.Context, id string) (*asks.Warrant, error) {
return func(ctx context.Context, id string) (*asks.Warrant, error) {
bucket, err := asksRecords(ctx, inv)
if err != nil || bucket == "" {
return nil, err
}
reply, err := conn.RequestWithContext(ctx, "$JS.API.DIRECT.GET.KV_"+bucket+".$KV."+bucket+"."+askerName+"."+id, nil)
if err != nil {
return nil, err
}
if reply.Header.Get("Status") != "" {
return nil, nil // none, or not readable: the event says it
}
var rec struct {
State string `json:"state"`
Warrant *asks.Warrant `json:"warrant"`
}
if json.Unmarshal(reply.Data, &rec) != nil || rec.State == "open" || rec.Warrant == nil {
return nil, nil
}
return rec.Warrant, nil
}
}
// asksRecords is the bucket the asks seat's declarer keeps its record of asks in.
func asksRecords(ctx context.Context, inv *inventory.Inventory) (string, error) {
declared, err := inv.Catalogue(ctx)
if err != nil {
return "", err
}
for _, m := range declared {
for _, s := range m.DefinesSeats {
if s.Name == broker.AsksSeat && len(s.Records) > 0 {
return broker.BucketName(m.Module, s.Records[0]), nil
}
}
}
return "", nil
}
// routerHereIn says whether a module declaring the asks seat, with its ask named by its caller, is assigned:
// without it nothing takes an ask, and asking would only fill a queue nobody reads.
func routerHereIn(inv *inventory.Inventory) func(ctx context.Context) (bool, error) {
return func(ctx context.Context) (bool, error) {
entries, err := inv.Catalogued(ctx)
if err != nil {
return false, err
}
for _, e := range entries {
for _, s := range e.Manifest.DefinesSeats {
if s.Name == broker.AsksSeat && s.NamedByCaller("ask") && len(e.On) > 0 {
return true, nil
}
}
}
return false, nil
}
}
// grantHeldIn says whether the bus holds the controller's grant to ask (novox/hq issue 353): the user list the
// machine holding the bus was last sent is the one the mesh composes now (brokerBehind, the same judgement a
// push makes to send that machine first). While it is behind, the controller's ask is refused by the bus,
// whatever the record says of the router, so nothing is asked and the operator is told to push that machine.
// Judged at most every grantLookEvery: composing the list resolves the bus's machine whole.
func grantHeldIn(open *stores) func(ctx context.Context) (bool, string, error) {
var mu sync.Mutex
var at time.Time
var held bool
var why string
return func(ctx context.Context) (bool, string, error) {
mu.Lock()
defer mu.Unlock()
if !at.IsZero() && time.Since(at) < grantLookEvery {
return held, why, nil
}
machine, behind, err := brokerBehind(ctx, open, nil)
if err != nil {
return false, "", err
}
at, held, why = time.Now(), !behind, ""
if behind {
why = fmt.Sprintf("the bus's user list on %s is behind what the mesh composes, so the bus has not been "+
"given the controller's grant to ask; `push %s` carries it", machine, machine)
}
return held, why, nil
}
}
// grantLookEvery is how often the bus's user list is judged against the one its machine was last sent.
const grantLookEvery = 30 * time.Second
// channelsIn is what the channels are now, as a fingerprint: each module claiming a kind of the channel
// bench, where, promising what, and whether of its own account. An ask the router refused is asked again
// once this changes.
func channelsIn(inv *inventory.Inventory) func(ctx context.Context) string {
return func(ctx context.Context) string {
entries, err := inv.Catalogued(ctx)
if err != nil {
return ""
}
var parts []string
for _, e := range entries {
for _, c := range e.Manifest.Claims {
if c.Kind == "" || !catalogue.KindedBenches[c.Name] {
continue
}
on := append([]string(nil), e.On...)
sort.Strings(on)
caps := append([]string(nil), c.Capabilities...)
sort.Strings(caps)
parts = append(parts, fmt.Sprintf("%s/%s=%s@%s[%s]own:%t", c.Name, c.Kind, e.Manifest.Module,
strings.Join(on, ","), strings.Join(caps, ","), e.Manifest.RunsAs != ""))
}
}
sort.Strings(parts)
return strings.Join(parts, ";")
}
}
// startAsking makes the serving controller's asker and hands it the router's words.
func startAsking(ctx context.Context, open *stores, server *link.Server, conn *nats.Conn, keeper *conditions.Keeper) {
js, err := jetstream.New(conn)
if err != nil {
fmt.Printf("the operator cannot be asked: %v\n", err)
return
}
a := &asker{
open: keeper.Open,
silence: func(ctx context.Context, key string, d time.Duration, by, why string) error {
_, err := keeper.Silence(ctx, key, d, by, why)
return err
},
store: busAsked{conn: conn},
publish: func(ctx context.Context, subject string, body []byte, id string) error {
_, err := js.Publish(ctx, subject, body, jetstream.WithMsgID(id))
return err
},
call: callAction(conn),
record: func(ctx context.Context, act link.HandAct) error {
_, err := link.RecordHandAct(ctx, conn, act)
return err
},
routerRecord: routerRecordOf(conn, open.inventory),
routerHere: routerHereIn(open.inventory),
grantHeld: grantHeldIn(open),
channels: channelsIn(open.inventory),
raise: func(ctx context.Context, obs []conditions.Observation) error {
return keeper.Reconcile(ctx, sourceAsker, obs)
},
now: time.Now,
logf: func(format string, args ...any) { fmt.Printf(format+"\n", args...) },
}
if err := server.Decides(a); err != nil {
fmt.Printf("the operator's answers cannot be heard, so nothing is asked: %v\n", err)
return
}
askerFrom = a
go a.keep(ctx)
}
-4
View File
@@ -166,10 +166,6 @@ func buildFrom(result link.BuildResult) inventory.Build {
for _, r := range result.Read { for _, r := range result.Read {
kept.Read = append(kept.Read, inventory.ReadRepository{Repository: r.Repository, Ref: r.Ref}) kept.Read = append(kept.Read, inventory.ReadRepository{Repository: r.Repository, Ref: r.Ref})
} }
// What it was made from, as files (novox/hq ADR 0267): the planner maps the next merge onto it.
for _, s := range result.Sources {
kept.Sources = append(kept.Sources, inventory.BuildSource{Repository: s.Repository, Ref: s.Ref, Paths: s.Paths})
}
var announced []inventory.Artifact var announced []inventory.Artifact
for _, made := range result.Made { for _, made := range result.Made {
announced = append(announced, inventory.Artifact{ announced = append(announced, inventory.Artifact{
+1 -5
View File
@@ -4,7 +4,6 @@ import (
"context" "context"
"encoding/json" "encoding/json"
"errors" "errors"
"fmt"
"hash/fnv" "hash/fnv"
"os" "os"
"os/exec" "os/exec"
@@ -12,7 +11,6 @@ import (
"strings" "strings"
"sync" "sync"
"testing" "testing"
"time"
"github.com/novox/mesh-controller/internal/inventory" "github.com/novox/mesh-controller/internal/inventory"
"github.com/novox/mesh-controller/internal/link" "github.com/novox/mesh-controller/internal/link"
@@ -210,9 +208,7 @@ func TestARollbackNeverPutsBackABuildFromAnotherRepository(t *testing.T) {
if _, _, err := takeIn(ctx, inv, fork); !errors.Is(err, errNotItsSource) { if _, _, err := takeIn(ctx, inv, fork); !errors.Is(err, errNotItsSource) {
t.Fatalf("the fork's build was taken in: %v", err) t.Fatalf("the fork's build was taken in: %v", err)
} }
// Asked after the registered build, whenever the test runs: an id naming a fixed moment read as older failed := onTrunk("build-1791600000000000000", "novox/mesh-catalog", "git", "modules/sudo",
// than the registered build once the clock passed it (2026-10-10 02:40 UTC), and the test failed on main.
failed := onTrunk(fmt.Sprintf("build-%d", time.Now().Add(time.Hour).UnixNano()), "novox/mesh-catalog", "git", "modules/sudo",
map[string]any{"module": "sudo", "version": "2"}) map[string]any{"module": "sudo", "version": "2"})
failed.Commit = "badbadbad0123456" failed.Commit = "badbadbad0123456"
if _, _, err := takeIn(ctx, inv, failed); err != nil { if _, _, err := takeIn(ctx, inv, failed); err != nil {
-48
View File
@@ -53,26 +53,9 @@ func assertBusObjects(ctx context.Context, inv *inventory.Inventory, r broker.Ra
if err != nil { if err != nil {
return nil, err return nil, err
} }
// And the work queues of seats that name their caller or their kind, with each holder's worker
// (novox/hq ADR 0259 §3): an ask queues until the router takes it, a channel's work until that kind
// takes it.
trafficStreams, trafficWorkers, err := seatTrafficObjects(ctx, inv)
if err != nil {
return nil, err
}
// Every one tried, and every failure named: one module's consumer the bus refuses is no reason // Every one tried, and every failure named: one module's consumer the bus refuses is no reason
// the modules after it in the list hear nothing (novox/hq issue 208, where this runs on each send). // the modules after it in the list hear nothing (novox/hq issue 208, where this runs on each send).
var failed []error var failed []error
for _, s := range trafficStreams {
if err := r.EnsureStream(s); err != nil {
failed = append(failed, fmt.Errorf("the work queue %s: %w", s.Name, err))
}
}
for _, c := range trafficWorkers {
if err := r.EnsureConsumer(c); err != nil {
failed = append(failed, fmt.Errorf("the worker %s on %s: %w", c.Name, c.Stream, err))
}
}
for _, c := range consumers { for _, c := range consumers {
if err := r.EnsureConsumer(c.Consumer); err != nil { if err := r.EnsureConsumer(c.Consumer); err != nil {
failed = append(failed, fmt.Errorf("how %s on %s hears what it consumes: %w", c.Module, c.Node, err)) failed = append(failed, fmt.Errorf("how %s on %s hears what it consumes: %w", c.Module, c.Node, err))
@@ -147,37 +130,6 @@ func moduleConsumers(ctx context.Context, inv *inventory.Inventory) ([]broker.Mo
return broker.ConsumersOf(users), nil return broker.ConsumersOf(users), nil
} }
// seatTrafficObjects is the work queues and workers of seats that name their caller or their kind, from
// the records the user list is composed from.
func seatTrafficObjects(ctx context.Context, inv *inventory.Inventory) ([]broker.Stream, []broker.Consumer, error) {
records, err := inv.BusRecords(ctx)
if err != nil {
return nil, nil, err
}
users, err := broker.Users(records)
if err != nil {
return nil, nil, err
}
streams, workers := broker.SeatTrafficObjects(users)
// And the queue of every such seat the catalogue declares, held or not: work queues from registration,
// so what is submitted before a holder is assigned waits for it (the correctness review of 2026-10-08).
declared, err := inv.DeclaredTrafficSeats(ctx)
if err != nil {
return nil, nil, err
}
have := map[string]bool{}
for _, s := range streams {
have[s.Name] = true
}
for _, s := range broker.TrafficQueues(declared) {
if !have[s.Name] {
streams = append(streams, s)
have[s.Name] = true
}
}
return streams, workers, nil
}
// moduleConsumerCount is how many modules hear what they consume, for the raise's one line. // moduleConsumerCount is how many modules hear what they consume, for the raise's one line.
func moduleConsumerCount(ctx context.Context, inv *inventory.Inventory) (int, error) { func moduleConsumerCount(ctx context.Context, inv *inventory.Inventory) (int, error) {
consumers, err := moduleConsumers(ctx, inv) consumers, err := moduleConsumers(ctx, inv)
+1 -16
View File
@@ -32,9 +32,6 @@ import (
// provision it wants that a manifest given here offers. An overflow is refused in the pull request // provision it wants that a manifest given here offers. An overflow is refused in the pull request
// that introduces it — a new requirement, a lowered bound, a longer slug — instead of on the // that introduces it — a new requirement, a lowered bound, a longer slug — instead of on the
// provider's machine when a real machine's name first meets the module's. // provider's machine when a real machine's name first meets the module's.
// SomeManifestsVar, set by the merge gate, says the manifests given are only some of their repository's.
const SomeManifestsVar = "MESH_MODULE_CHECK_SOME"
func moduleCheck(paths []string, out io.Writer) error { func moduleCheck(paths []string, out io.Writer) error {
return moduleCheckFor(paths, catalogue.DefaultLongestMachine, out) return moduleCheckFor(paths, catalogue.DefaultLongestMachine, out)
} }
@@ -88,24 +85,12 @@ func moduleCheckFor(paths []string, longestMachine int, out io.Writer) error {
// Between the manifests: a seat declared twice, a use of a seat nothing declares, a claim on // Between the manifests: a seat declared twice, a use of a seat nothing declares, a claim on
// a seat that does not exist. Run only over what parsed, because a problem inside one manifest // a seat that does not exist. Run only over what parsed, because a problem inside one manifest
// has already been said and would be said again here in a worse form. // has already been said and would be said again here in a worse form.
//
// **Over some of a repository's manifests, a seat none of them declares is a note** (novox/hq issue 364), as
// this command's own word says above: the merge gate passes only the manifests a change touches, and says so
// with SomeManifestsVar, so a module that uses or claims a seat another module declares (the operator
// channel's, a channel bench) was refused there for a manifest it was not given. Given every manifest — the
// catalogue's own check, and registration — it stays a refusal.
some := os.Getenv(SomeManifestsVar) != ""
problems := catalogue.CatalogueProblems(shelf) problems := catalogue.CatalogueProblems(shelf)
sort.Strings(problems) sort.Strings(problems)
for _, p := range problems { for _, p := range problems {
if some && catalogue.IsUndeclaredSeat(p) {
fmt.Fprintf(out, "note: %s among the manifests given; registration judges it against the whole catalogue, "+
"and passing the declaring module's manifest too judges it here\n", p)
continue
}
fmt.Fprintln(out, p) fmt.Fprintln(out, p)
failed++
} }
failed += len(problems)
// Between the manifests too: an identity against the bounds of the provisions it wants, which // Between the manifests too: an identity against the bounds of the provisions it wants, which
// only the provider's manifest states. // only the provider's manifest states.
+1 -3
View File
@@ -82,9 +82,7 @@ func checkHereCommand(ctx context.Context, args []string) error {
if _, err := git("fetch", "--quiet", "origin", *base); err != nil { if _, err := git("fetch", "--quiet", "origin", *base); err != nil {
return fmt.Errorf("cannot fetch %s to say what the change touches: %w", *base, err) return fmt.Errorf("cannot fetch %s to say what the change touches: %w", *base, err)
} }
// Without rename detection, so a file moved out of a build source is said under its old name as well: changedText, err := git("diff", "--name-only", "origin/"+*base+"...HEAD")
// its going is a change to the build that held it (novox/hq ADR 0267).
changedText, err := git("diff", "--name-only", "--no-renames", "origin/"+*base+"...HEAD")
if err != nil { if err != nil {
return err return err
} }
-20
View File
@@ -103,23 +103,3 @@ func TestTheControllersManifestServesEveryVerbOfItsSeat(t *testing.T) {
t.Fatalf("the controller's own module.json fails module check: %v\n%s", err, out.String()) t.Fatalf("the controller's own module.json fails module check: %v\n%s", err, out.String())
} }
} }
// A module that uses a seat another module declares (novox/hq issue 364): refused over the whole catalogue when the
// declarer is missing, a note when the gate says it gives only the manifests a change touches.
func TestASeatAnotherModuleDeclaresIsANoteOverSomeManifests(t *testing.T) {
dir := t.TempDir()
user := filepath.Join(dir, "user.json")
os.WriteFile(user, []byte(`{"module":"asker","version":"1","uses":["operator-channel"]}`), 0o600)
var out bytes.Buffer
if err := moduleCheck([]string{user}, &out); err == nil {
t.Fatalf("a use of a seat nothing given declares passed the whole-catalogue check:\n%s", out.String())
}
t.Setenv(SomeManifestsVar, "1")
out.Reset()
if err := moduleCheck([]string{user}, &out); err != nil {
t.Fatalf("over some manifests the use was refused:\n%s", out.String())
}
if !strings.Contains(out.String(), "note: asker uses the seat \"operator-channel\"") {
t.Fatalf("the note was not said:\n%s", out.String())
}
}
+1 -1
View File
@@ -143,7 +143,7 @@ func (f following) PullUpdated(ctx context.Context, p link.PullUpdated) error {
if err != nil { if err != nil {
return err return err
} }
read, err := readForPlanning(ctx, inv) read, err := inv.ReadRepositories(ctx)
if err != nil { if err != nil {
return err return err
} }
+1 -1
View File
@@ -47,7 +47,7 @@ func keeperOn(ctx context.Context, conn *nats.Conn) (*conditions.Keeper, error)
Say: func(format string, args ...any) { fmt.Fprintf(os.Stderr, format+"\n", args...) }, Say: func(format string, args ...any) { fmt.Fprintf(os.Stderr, format+"\n", args...) },
// What status leads with changed: composed again soon (a nudge outside the serving controller // What status leads with changed: composed again soon (a nudge outside the serving controller
// does nothing). // does nothing).
Changed: func() { statusFrom.nudge(); askerFrom.nudge() }, Changed: statusFrom.nudge,
// Written under the lease, carrying its epoch (novox/hq to-be 45 §6). // Written under the lease, carrying its epoch (novox/hq to-be 45 §6).
Epoch: func() (uint64, error) { return theLease.epoch(context.WithoutCancel(ctx)) }}), nil Epoch: func() (uint64, error) { return theLease.epoch(context.WithoutCancel(ctx)) }}), nil
} }
+1 -1
View File
@@ -604,7 +604,7 @@ func theGraph(ctx context.Context, inv *inventory.Inventory) ([]inventory.Entry,
if err != nil { if err != nil {
return nil, nil, nil, err return nil, nil, nil, err
} }
read, err := readForPlanning(ctx, inv) read, err := inv.ReadRepositories(ctx)
if err != nil { if err != nil {
return nil, nil, nil, err return nil, nil, nil, err
} }
@@ -136,13 +136,12 @@ func TestTheDeliveryOwnerIsAskedOverTheBus(t *testing.T) {
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
// And release and stop, which the operator's warrant chooses (novox/hq ADR 0259). for _, verb := range []string{"stalled", "close"} {
for _, verb := range []string{"stalled", "close", "release", "stop"} {
if !slices.Contains(granted.Publish, link.SeatToolSubject(catalogue.DeliverySeat, verb)) { if !slices.Contains(granted.Publish, link.SeatToolSubject(catalogue.DeliverySeat, verb)) {
t.Errorf("the controller may not ask %s.%s", catalogue.DeliverySeat, verb) t.Errorf("the controller may not ask %s.%s", catalogue.DeliverySeat, verb)
} }
} }
if _, err := askDeliveryOwner(t.Context(), nil, "retire-history", nil); err == nil || !strings.Contains(err.Error(), "grant") { if _, err := askDeliveryOwner(t.Context(), nil, "stop", nil); err == nil || !strings.Contains(err.Error(), "grant") {
t.Fatalf("a verb the grant does not name was asked: %v", err) t.Fatalf("a verb the grant does not name was asked: %v", err)
} }
conn, err := nats.Connect(testbus.URL(t)) conn, err := nats.Connect(testbus.URL(t))
-265
View File
@@ -1,265 +0,0 @@
package main
import (
"context"
"encoding/json"
"errors"
"fmt"
"strings"
"time"
"github.com/nats-io/nats.go"
"github.com/novox/mesh-controller/internal/conditions"
"github.com/novox/mesh-controller/internal/link"
"github.com/novox/mesh-controller/internal/secrets"
)
// A module's own secret given at the operator's desk (novox/hq ADR 0259 §10).
//
// **The value never passes through whoever asked for it.** An agent, or the operator at the mesh MCP
// server, calls `give` with the machine, the module, the secret's name and the desk — never a value. The
// controller makes a sealing keypair for this one call, asks the desk's `node-launcher.secret` to prompt the
// operator without showing what is typed, and is answered with what was typed **sealed to that key**: no
// plaintext on the bus, in a runtime's log or in any call's record. It opens it here, seals it to the
// module's machine exactly as `secret accept` does, and forgets it. What it answers says only that the
// value was taken, or why not.
//
// **What remains** (ADR 0234's accepted residual risk): on an X11 desk any program of the operator's
// account can read the keys as they are typed. And a program that calls the desk's prompt itself, with a
// key of its own, is answered with what the operator typed into a prompt they did not ask for — as it could
// draw a window of its own. The prompt says who asks and for what, so the operator types only into a
// prompt they started.
// deskPromptWithin is how long the prompt waits for the operator: below the runtime's thirty seconds for
// one call, as the launcher's menu is.
const deskPromptWithin = 25
// deskGive is the desk path, its four reaches given so a test needs no store and no bus.
type deskGive struct {
// declares refuses a module or a secret the mesh would refuse, before anybody is asked to type.
declares func(module, name string) error
// known refuses a machine the mesh does not know, before anybody is asked to type; nil knows every one
// (a test that does not look).
known func(machine string) error
// trusted says a module runs as an account of its own: its secret is never taken at a desk (below). Nil is
// never (a test that does not look).
trusted func(module string) (bool, error)
// ask asks one machine's node-launcher.secret and answers its result, or the holder's refusal.
ask func(machine string, args map[string]any) (json.RawMessage, error)
// accept seals the value as `secret accept` does, and says whether it lives until the module's start.
accept func(value string) (untilStart bool, err error)
// record writes the act in the hand-act log.
record func(link.HandAct) error
// announce raises the condition that says a module's own secret was given (secretGivenObservation), on
// every channel; nil announces nothing (a test that does not look).
announce func(node, module, name, how string) error
}
// errNothingGiven is a prompt dismissed, or not answered in time: nothing changes.
var errNothingGiven = errors.New("nothing was given")
// give asks the desk for the value and seals it; it answers the words said to the caller.
func (d deskGive) give(node, module, name, desk string) (string, error) {
for what, v := range map[string]string{"the machine": node, "the module": module, "the secret": name, "the desk": desk} {
if strings.TrimSpace(v) == "" {
return "", fmt.Errorf("%s is not named", what)
}
}
if d.known != nil {
for what, machine := range map[string]string{"the machine the secret is for": node, "the desk": desk} {
if err := d.known(machine); err != nil {
return "", fmt.Errorf("nobody was asked to type anything: %s, %s, is not a machine this mesh knows: %w",
what, machine, err)
}
}
}
if err := d.declares(module, name); err != nil {
return "", fmt.Errorf("nobody was asked to type anything: %w", err)
}
// **A trusted party's secret is not taken at a desk** (the confirmation review of 2026-10-09, N1-give). The
// prompt's answer comes back over the desk machine's bus, on a subject that machine's runtime answers — and
// on a desk machine agents run as the operator, who holds that runtime's credential. An agent could answer
// first, with a bot token of its own sealed to this call's key, and the channel the operator's answers are
// proven on would be the agent's. So the value of a module running as its own account is typed at the
// controller's terminal, where no bus carries it.
if d.trusted != nil {
trusted, err := d.trusted(module)
if err != nil {
return "", fmt.Errorf("nobody was asked to type anything: whether %s is a trusted party could not be read: %w", module, err)
}
if trusted {
return "", fmt.Errorf("nobody was asked to type anything: %s runs as an account of its own and proves the "+
"operator's answers, so its %s is given at the controller's terminal alone — there, run `mesh-controller "+
"secret accept %s %s %s` and type it at its prompt. A desk's prompt is answered over the desk machine's "+
"bus, where an agent may answer first (novox/hq ADR 0259 §10)", module, name, node, module, name)
}
}
public, private, err := secrets.Keypair()
if err != nil {
return "", fmt.Errorf("no key could be made to take the value: %w", err)
}
// By name, never by words: the holder writes the prompt from these, and says the controller asks, which
// the bus alone makes true (broker.ControllerOnly).
raw, err := d.ask(desk, map[string]any{
"module": module,
"secret": name,
"node": node,
"seal_to": public,
"timeout_seconds": deskPromptWithin,
})
if err != nil {
return "", fmt.Errorf("the desk on %s could not be asked: %w", desk, err)
}
var answer struct {
Sealed string `json:"sealed"`
Cancelled bool `json:"cancelled"`
TimedOut bool `json:"timed_out"`
}
if err := json.Unmarshal(raw, &answer); err != nil {
return "", fmt.Errorf("the desk on %s answered something that is not the prompt's answer", desk)
}
switch {
case answer.TimedOut:
return "", fmt.Errorf("%w: the prompt on %s was not answered within %d seconds", errNothingGiven, desk, deskPromptWithin)
case answer.Cancelled:
return "", fmt.Errorf("%w: the prompt on %s was dismissed", errNothingGiven, desk)
case answer.Sealed == "":
return "", fmt.Errorf("the desk on %s answered no sealed value", desk)
}
opened, err := secrets.Open(private, answer.Sealed)
if err != nil {
// Never the value, never what failed to open: only that it was not sealed to this call.
return "", fmt.Errorf("the desk on %s answered a value not sealed to this call; nothing was taken", desk)
}
value := asSupplied(string(opened))
for i := range opened {
opened[i] = 0
}
if strings.TrimSpace(value) == "" {
return "", fmt.Errorf("%w: the prompt on %s was answered empty", errNothingGiven, desk)
}
untilStart, err := d.accept(value)
value = ""
if err != nil {
return "", err
}
act := link.HandAct{Verb: "secret accept", Args: []string{node, module, name, "--at-desk", desk},
Why: fmt.Sprintf("the operator gave %s for %s on %s at the desk on %s", name, module, node, desk),
Cause: "given-at-the-desk"}
recorded := ""
if err := d.record(act); err != nil {
recorded = fmt.Sprintf("\n this act could NOT be recorded in the hand-act log, and is done anyway: %v", err)
}
if d.announce != nil {
if err := d.announce(node, module, name, "at the desk on "+desk); err != nil {
recorded += fmt.Sprintf("\n this change could NOT be announced on the operator's channels: %v", err)
}
}
words := fmt.Sprintf("%s on %s now holds %q, given at the desk on %s and sealed to %s; the mesh cannot read it "+
"back.\n run `push %s` to send it", module, node, name, desk, node, node)
if untilStart {
words += fmt.Sprintf("\n it lives until %s next starts well under the mesh, and is then replaced with a value "+
"the mesh makes (ADR 0228)", module)
}
return words + recorded, nil
}
// giveAtDesk is `secret accept <node> <module> <name> --at-desk <machine>`: the desk path, on this
// controller's stores and bus.
func giveAtDesk(ctx context.Context, node, module, name, desk string) error {
open, err := openStores(ctx)
if err != nil {
return err
}
defer open.Close()
d := deskGive{
declares: func(module, name string) error { return open.inventory.DeclaresOwnSecret(ctx, module, name) },
known: func(machine string) error {
_, err := open.inventory.NodeByName(ctx, machine)
return err
},
trusted: func(module string) (bool, error) { return open.inventory.RunsAsItsOwnAccount(ctx, module) },
ask: func(machine string, args map[string]any) (json.RawMessage, error) {
var result json.RawMessage
err := onTheBus(func(conn *nats.Conn) error {
answer, err := link.AskSeatTool(ctx, conn, "node-launcher", "secret", machine, args,
time.Duration(deskPromptWithin+5)*time.Second)
if err != nil {
return err
}
if answer.Error != "" {
return errors.New(answer.Error)
}
result = answer.Result
return nil
})
return result, err
},
accept: func(value string) (bool, error) {
return open.inventory.AcceptGivenSecret(ctx, node, module, name, value)
},
record: func(act link.HandAct) error {
return onTheBus(func(conn *nats.Conn) error {
_, err := link.RecordHandAct(ctx, conn, act)
return err
})
},
announce: func(node, module, name, how string) error { return announceSecretGiven(ctx, node, module, name, how) },
}
words, err := d.give(node, module, name, desk)
if err != nil {
return err
}
fmt.Println(words)
return nil
}
// kindSecretGiven is the condition every value given for a module's own secret raises (the review of 2026-10-09,
// M4): on every channel, so a bot token changed by somebody else — a channel that now answers for them — is
// heard of. It stays until the operator silences or clears it.
const kindSecretGiven = "secret-given"
// secretGivenObservation is that condition: which secret, of which module on which machine, how and when.
// The summary, for whoever looks closer, names the secret and the time. The words the operator reads are
// held to the plain rule (conditions.PlainWords): no clock time — the channel says when, in the operator's
// time — and the secret's name said as words. Words that broke the rule were replaced by the keeper with
// "needs a look … a problem it calls secret given" (hq issue 359), which told the operator nothing.
func secretGivenObservation(node, module, name, how string, at time.Time) conditions.Observation {
key := node + "." + module + "." + name
where := module + " on " + node
// Within the bounds whatever the names' length: the module and machine, else the module, else the machine.
headline := "New secret given for " + where
for _, h := range []string{"New secret given for " + module, "New secret given on " + node} {
if len(headline) > conditions.HeadlineMax {
headline = h
}
}
resolved := "You saw that " + module + " was given a new secret"
if len(resolved) > conditions.HeadlineMax+20 {
resolved = "You saw that a new secret was given on " + node
}
return conditions.Observation{Scope: conditions.ScopeMachine, ID: key, Token: kindSecretGiven, Kind: kindSecretGiven,
Machine: node, Severity: conditions.Urgent, Source: kindSecretGiven,
Summary: fmt.Sprintf("%s of %s on %s was given %s at %s", name, module, node, how,
at.Local().Format("2006-01-02 15:04")),
Headline: headline,
Explanation: fmt.Sprintf("The secret %s of %s was given %s. If you gave it, nothing else is needed. If you "+
"did not, somebody else now holds what %s acts with.", secretNameWords(name), where, how, module),
Needs: "silence this if you just gave it; if you did not, give it again yourself so that only you hold it.",
Resolved: resolved,
Actions: []conditions.Action{conditions.SilenceAction(conditions.Key(conditions.ScopeMachine, key, kindSecretGiven))}}
}
// secretNameWords is a secret's name as the operator reads it: "telegram-token" is "telegram token".
func secretNameWords(name string) string {
return strings.Join(strings.FieldsFunc(name, func(r rune) bool { return r == '-' || r == '_' || r == '.' }), " ")
}
// announceSecretGiven raises it on this controller's keeper.
func announceSecretGiven(ctx context.Context, node, module, name, how string) error {
return withKeeper(ctx, func(k *conditions.Keeper) error {
_, err := k.Observe(ctx, secretGivenObservation(node, module, name, how, time.Now()))
return err
})
}
-400
View File
@@ -1,400 +0,0 @@
package main
import (
"context"
"encoding/json"
"errors"
"strings"
"testing"
"time"
"github.com/novox/mesh-controller/internal/broker"
"github.com/novox/mesh-controller/internal/conditions"
"github.com/novox/mesh-controller/internal/link"
"github.com/novox/mesh-controller/internal/secrets"
)
const typed = "123456789:AAEhBP0av28P4XFQnIuR-o-7Xnz1kkUzW3g"
// aDesk is the desk path with a prompt the test answers as the operator would, and what it was asked kept.
func aDesk(t *testing.T, answer func(args map[string]any) (json.RawMessage, error)) (deskGive, *[]string, *[]link.HandAct, *[]map[string]any) {
t.Helper()
var accepted []string
var acts []link.HandAct
var asked []map[string]any
return deskGive{
declares: func(module, name string) error {
if module != "telegram" || name != "telegram-token" {
return errors.New(module + " does not declare " + name + " as an own secret")
}
return nil
},
ask: func(machine string, args map[string]any) (json.RawMessage, error) {
asked = append(asked, args)
return answer(args)
},
accept: func(value string) (bool, error) { accepted = append(accepted, value); return false, nil },
record: func(a link.HandAct) error { acts = append(acts, a); return nil },
}, &accepted, &acts, &asked
}
func sealedTo(t *testing.T, value string) func(args map[string]any) (json.RawMessage, error) {
return func(args map[string]any) (json.RawMessage, error) {
sealed, err := secrets.Seal(args["seal_to"].(string), []byte(value+"\n"))
if err != nil {
t.Fatal(err)
}
raw, _ := json.Marshal(map[string]any{"sealed": sealed})
return raw, nil
}
}
// novox/hq ADR 0259 §10: the value typed at the desk is sealed as `secret accept` seals it, and is in no
// answer, no prompt argument and no act recorded.
func TestASecretGivenAtTheDeskIsSealedAndSaidNowhere(t *testing.T) {
d, accepted, acts, asked := aDesk(t, sealedTo(t, typed))
words, err := d.give("anchor", "telegram", "telegram-token", "laptop")
if err != nil {
t.Fatal(err)
}
if len(*accepted) != 1 || (*accepted)[0] != typed {
t.Fatalf("the value sealed is not what was typed, its line ending taken off")
}
if len(*acts) != 1 || (*acts)[0].Verb != "secret accept" || (*acts)[0].Cause != "given-at-the-desk" ||
!strings.Contains((*acts)[0].Why, "at the desk on laptop") {
t.Errorf("the act: %+v", *acts)
}
raw, _ := json.Marshal(struct {
Words string
Acts []link.HandAct
Asked []map[string]any
}{words, *acts, *asked})
if strings.Contains(string(raw), typed) || strings.Contains(string(raw), "AAEhBP0") {
t.Fatal("the value appears in what was said, asked or recorded")
}
if !strings.Contains(words, "push anchor") || !strings.Contains(words, "given at the desk on laptop") {
t.Errorf("%q", words)
}
if p := (*asked)[0]; p["seal_to"] == "" || p["timeout_seconds"] != deskPromptWithin {
t.Errorf("the prompt was asked %v", p)
}
}
func TestNothingIsAskedForASecretTheMeshWouldRefuse(t *testing.T) {
for _, c := range [][2]string{{"telegram", "chat-id"}, {"nobody", "telegram-token"}} {
d, accepted, _, asked := aDesk(t, sealedTo(t, typed))
if _, err := d.give("anchor", c[0], c[1], "laptop"); err == nil || !strings.Contains(err.Error(), "nobody was asked") {
t.Errorf("%v: %v", c, err)
}
if len(*asked) != 0 || len(*accepted) != 0 {
t.Errorf("%v: the operator was asked anyway", c)
}
}
d, _, _, _ := aDesk(t, sealedTo(t, typed))
if _, err := d.give("anchor", "telegram", "telegram-token", ""); err == nil {
t.Error("no desk was refused nowhere")
}
}
func TestADismissedEmptyLateOrForeignAnswerTakesNothing(t *testing.T) {
for want, answer := range map[string]func(map[string]any) (json.RawMessage, error){
"not answered within 25 seconds": func(map[string]any) (json.RawMessage, error) {
return json.RawMessage(`{"cancelled":true,"timed_out":true}`), nil
},
"was dismissed": func(map[string]any) (json.RawMessage, error) { return json.RawMessage(`{"cancelled":true}`), nil },
"answered empty": sealedTo(t, " "),
"not sealed to this call": func(map[string]any) (json.RawMessage, error) {
other, _, _ := secrets.Keypair()
sealed, _ := secrets.Seal(other, []byte(typed))
raw, _ := json.Marshal(map[string]any{"sealed": sealed})
return raw, nil
},
"could not be asked": func(map[string]any) (json.RawMessage, error) { return nil, errors.New("no session answers") },
} {
d, accepted, acts, _ := aDesk(t, answer)
_, err := d.give("anchor", "telegram", "telegram-token", "laptop")
if err == nil || !strings.Contains(err.Error(), want) || strings.Contains(err.Error(), typed) {
t.Errorf("want %q, got %v", want, err)
}
if len(*accepted) != 0 || len(*acts) != 0 {
t.Errorf("%s: something was taken or recorded", want)
}
}
}
func TestTheGiveVerbRunsTheDeskPathAndTheControllerMayAskTheDesk(t *testing.T) {
argv, err := argvFor("give", map[string]any{"node": "anchor", "module": "telegram", "secret": "telegram-token", "at": "laptop"})
if err != nil || strings.Join(argv, " ") != "secret accept anchor telegram telegram-token --at-desk laptop" {
t.Fatalf("%v %v", argv, err)
}
if _, err := argvFor("give", map[string]any{"node": "anchor", "module": "telegram", "secret": "telegram-token"}); err == nil {
t.Error("give without a desk was taken")
}
perms, err := broker.PermissionsFor(broker.Principal{Kind: broker.KindController})
if err != nil {
t.Fatal(err)
}
found := false
for _, p := range perms.Publish {
found = found || p == "mesh.seat.node-launcher.tool.secret.*"
}
if !found {
t.Error("the controller may not ask the desk's prompt")
}
}
// The review of 2026-10-09 (M4): the desk's prompt says who asks in words the caller does not choose — the
// controller, which the bus alone lets ask it — and what for, from names the controller checked; the prompt
// carries no free text of the caller's.
func TestThePromptIsAskedByNameNeverByWordsTheCallerChose(t *testing.T) {
d, _, _, asked := aDesk(t, sealedTo(t, typed))
if _, err := d.give("anchor", "telegram", "telegram-token", "laptop"); err != nil {
t.Fatal(err)
}
p := (*asked)[0]
if p["module"] != "telegram" || p["secret"] != "telegram-token" || p["node"] != "anchor" {
t.Errorf("the prompt was not asked by name: %v", p)
}
for _, free := range []string{"prompt", "message"} {
if _, there := p[free]; there {
t.Errorf("the prompt carries the caller's %s: %v", free, p)
}
}
}
// Every value given for a module's own secret is announced as a condition, on every channel (the review of
// 2026-10-09, M4): a bot token changed by somebody else is a channel that now answers for them.
func TestAValueGivenAtTheDeskIsAnnounced(t *testing.T) {
d, _, _, _ := aDesk(t, sealedTo(t, typed))
var said []string
d.announce = func(node, module, name, how string) error {
said = append(said, node+" "+module+" "+name+" "+how)
return nil
}
if _, err := d.give("anchor", "telegram", "telegram-token", "laptop"); err != nil {
t.Fatal(err)
}
if len(said) != 1 || !strings.Contains(said[0], "anchor telegram telegram-token") || !strings.Contains(said[0], "laptop") {
t.Fatalf("announced %v", said)
}
o := secretGivenObservation("anchor", "telegram", "telegram-token", "at the desk on laptop", time.Date(2026, 10, 9, 12, 3, 0, 0, time.UTC))
if o.Severity != conditions.Urgent || !strings.Contains(o.Explanation, "telegram token") ||
len(o.Actions) == 0 || o.Key() == "" {
t.Errorf("the announcement %+v", o)
}
if strings.Contains(o.Summary+o.Explanation+o.Said, typed) {
t.Error("the announcement carries the value")
}
}
// **The secret-given condition says itself in its own plain words** (hq issue 359): the words it carries pass
// the plain rule, from the controller's terminal and from a desk, so the keeper keeps them — they once held a
// clock time, and the operator read "Novox needs a look … a problem it calls secret given" instead. Its
// severity and its answer stay: it is heard on every channel, and silenced by the operator.
func TestTheSecretGivenConditionSaysItselfInPlainWords(t *testing.T) {
at := time.Date(2026, 10, 9, 23, 32, 0, 0, time.Local)
for _, how := range []string{"at the controller's terminal", "at the desk on laptop"} {
o := secretGivenObservation("anchor", "telegram", "telegram-token", how, at)
w := conditions.Words{Headline: o.Headline, Explanation: o.Explanation, Resolved: o.Resolved, Needs: o.Needs,
Actions: o.Actions}
if why, ok := conditions.PlainWords(w, o.Machine); !ok {
t.Fatalf("given %s, the words are not plain: %s", how, why)
}
k, _ := withConditionsInMemory(t)
c, err := k.Observe(t.Context(), o)
if err != nil {
t.Fatal(err)
}
if c.Headline != "New secret given for telegram on anchor" || c.Severity != conditions.Urgent ||
!strings.HasPrefix(c.Explanation, conditions.NeedsYou+" silence this") ||
!strings.Contains(c.Explanation, "telegram token of telegram on anchor was given "+how) ||
len(c.Actions) != 1 || c.Actions[0].Label != "Silence for a week" {
t.Errorf("given %s, the keeper said %q / %q (%s, %v)", how, c.Headline, c.Explanation, c.Severity, c.Actions)
}
if strings.Contains(c.Headline+c.Explanation+c.Resolved+c.Needs, typed) {
t.Error("the words carry the value")
}
}
// A long module name keeps the headline and the resolved line within their bounds.
for _, module := range []string{"a-module-with-a-rather-long-name-indeed",
"a-module-with-a-name-so-long-that-no-headline-could-ever-hold-it"} {
o := secretGivenObservation("anchor", module, "api-key", "at the controller's terminal", at)
if why, ok := conditions.PlainWords(conditions.Words{Headline: o.Headline, Explanation: o.Explanation,
Resolved: o.Resolved, Needs: o.Needs, Actions: o.Actions}, o.Machine); !ok {
t.Errorf("the module %s: %s", module, why)
}
}
}
// The bus lets the controller alone ask the desk's prompt (the review of 2026-10-09, M4): the runtime, which
// carries every agent's calls, and a person granted every tool are denied it, however wide their grant.
func TestOnlyTheControllerMayAskTheDesksPrompt(t *testing.T) {
for _, p := range []broker.Principal{
{Kind: broker.KindNodeTools, Node: "laptop"},
{Kind: broker.KindPerson, Module: "operator", Invokes: []string{"*"}},
{Kind: broker.KindModule, Node: "laptop", Module: "lab", Invokes: []string{"seat:node-launcher.secret"}},
} {
perms, err := broker.PermissionsFor(p)
if err != nil {
t.Fatal(err)
}
for _, subject := range []string{"mesh.seat.node-launcher.tool.secret.laptop", "mesh.seat.node-launcher.tool.secret",
"mesh.mod.rofi.tool.node-launcher.secret", "mesh.mod.rofi.tool.node-launcher.secret.laptop"} {
if broker.MayPublish(perms, subject) {
t.Errorf("%s may publish %s", p.Username(), subject)
}
}
}
perms, _ := broker.PermissionsFor(broker.Principal{Kind: broker.KindController})
if !broker.MayPublish(perms, "mesh.seat.node-launcher.tool.secret.laptop") {
t.Error("the controller may not ask the desk's prompt")
}
}
// A value for a secret comes from the terminal or the desk, never through a verb (the review of 2026-10-09,
// M4): `secret accept` with a value, run for a verb, is refused before anything is read.
func TestASecretValueIsNeverAcceptedThroughAVerb(t *testing.T) {
t.Setenv(verbVar, "mesh-controller.command")
for _, args := range [][]string{
{"accept", "anchor", "telegram", "telegram-token", "--from", "/dev/null"},
{"accept", "anchor", "app", "db", "--from", "/dev/null", "--provider", "store"},
} {
err := secretCommand(context.Background(), args)
if err == nil || !strings.Contains(err.Error(), "never through a verb") {
t.Errorf("%v: %v", args, err)
}
}
}
// The `give` verb's own line passes the terminal-only rule of ADR 0266, and no other `secret accept` does: a
// value, a file, a provider or an extra word is still the terminal's alone.
func TestOnlyTheGiveLinePassesTheTerminalRuleForSecrets(t *testing.T) {
if err := terminalOnly([]string{"secret", "accept", "anchor", "telegram", "telegram-token", "--at-desk", "laptop"}); err != nil {
t.Errorf("give's line refused: %v", err)
}
for _, argv := range [][]string{
{"secret", "accept", "anchor", "telegram", "telegram-token"},
{"secret", "accept", "anchor", "telegram", "telegram-token", "--from", "/tmp/x"},
{"secret", "accept", "anchor", "telegram", "telegram-token", "--at-desk", "laptop", "--local"},
{"secret", "accept", "anchor", "telegram", "--provider", "--at-desk", "laptop"},
{"secret", "export", "anchor", "telegram", "telegram-token", "--at-desk", "laptop"},
} {
if err := terminalOnly(argv); err == nil {
t.Errorf("%v passed the terminal rule", argv)
}
}
}
// The confirmation review of 2026-10-09, N1-give: a desk's prompt is answered over the desk machine's bus, and
// on a desk machine agents run as the operator, who holds its runtime's credential — so a trusted party's
// secret (a module running as an account of its own: the Telegram bot's token) is never taken at a desk.
// Refused before anybody is asked to type, whoever called, naming the terminal's line.
func TestATrustedPartysSecretIsNeverTakenAtADesk(t *testing.T) {
d, accepted, acts, asked := aDesk(t, sealedTo(t, typed))
d.trusted = func(module string) (bool, error) { return module == "telegram", nil }
_, err := d.give("anchor", "telegram", "telegram-token", "laptop")
if err == nil || !strings.Contains(err.Error(), "controller's terminal alone") ||
!strings.Contains(err.Error(), "secret accept anchor telegram telegram-token") {
t.Fatalf("a trusted party's secret was taken at the desk, or refused without the line: %v", err)
}
if len(*asked)+len(*accepted)+len(*acts) != 0 {
t.Errorf("asked %v, accepted %d, recorded %v", *asked, len(*accepted), *acts)
}
d.trusted = func(string) (bool, error) { return false, errors.New("the store did not answer") }
if _, err := d.give("anchor", "telegram", "telegram-token", "laptop"); err == nil || len(*asked) != 0 {
t.Errorf("a module not known to be untrusted was asked at the desk: %v", err)
}
}
// And who may answer the desk's prompt at all: only the runtime of the machine it is asked on, carrying the
// launcher that holds the seat there — never the controller, another machine's runtime, or a module's own
// account (the confirmation review of 2026-10-09, N1-give).
func TestOnlyTheDeskMachinesLauncherMayAnswerItsPrompt(t *testing.T) {
launcher := broker.Declared{Module: "rofi", Holds: []broker.Seat{{Name: "node-launcher", Scope: "node",
Serves: []string{"run", "secret"}}}}
subject := "mesh.seat.node-launcher.tool.secret.laptop"
for _, c := range []struct {
p broker.Principal
answers bool
}{
{broker.Principal{Kind: broker.KindNodeTools, Node: "laptop", Module: broker.RuntimeModule, Carries: []broker.Declared{launcher}}, true},
{broker.Principal{Kind: broker.KindNodeTools, Node: "anchor", Module: broker.RuntimeModule, Carries: []broker.Declared{launcher}}, false},
{broker.Principal{Kind: broker.KindController}, false},
{broker.Principal{Kind: broker.KindModule, Node: "laptop", Module: "lab"}, false},
{broker.Principal{Kind: broker.KindNode, Node: "laptop"}, false},
} {
perms, err := broker.PermissionsFor(c.p)
if err != nil {
t.Fatal(err)
}
if got := broker.MaySubscribe(perms, subject); got != c.answers {
t.Errorf("%s may answer %s: %v, want %v", c.p.Username(), subject, got, c.answers)
}
}
}
// N1-give at the controller's terminal (the confirmation review of 2026-10-09): a trusted party's secret is
// announced before it is kept, and not kept when the announcement fails; another module's is kept first and
// a failed announcement is said, not undone.
func TestATrustedPartysSecretGivenAtTheTerminalIsAnnouncedBeforeItIsKept(t *testing.T) {
var order []string
announce := func(fail bool) func() error {
return func() error {
order = append(order, "announce")
if fail {
return errors.New("no channel")
}
return nil
}
}
keep := func() (bool, error) { order = append(order, "keep"); return false, nil }
order = nil
if _, unannounced, err := keepGiven(true, announce(false), keep); err != nil || unannounced != nil ||
strings.Join(order, ",") != "announce,keep" {
t.Errorf("trusted: %v %v, order %v; want announced, then kept", unannounced, err, order)
}
order = nil
if _, _, err := keepGiven(true, announce(true), keep); err == nil || strings.Join(order, ",") != "announce" {
t.Errorf("trusted, announcement failed: %v, order %v; want refused and nothing kept", err, order)
}
order = nil
if _, unannounced, err := keepGiven(false, announce(true), keep); err != nil || unannounced == nil ||
strings.Join(order, ",") != "keep,announce" {
t.Errorf("not trusted: %v %v, order %v; want kept, then the failed announcement said", unannounced, err, order)
}
order = nil
failing := func() (bool, error) { order = append(order, "keep"); return false, errors.New("store away") }
if _, _, err := keepGiven(false, announce(false), failing); err == nil || strings.Join(order, ",") != "keep" {
t.Errorf("not trusted, keep failed: %v, order %v; want refused and nothing announced", err, order)
}
}
// A machine the mesh does not know, as the secret's or as the desk, is refused before anybody is asked to type.
func TestAGiveNamingAMachineTheMeshDoesNotKnowAsksNobody(t *testing.T) {
for _, unknown := range []string{"elsewhere", "nodesk"} {
d, accepted, acts, asked := aDesk(t, func(map[string]any) (json.RawMessage, error) {
t.Fatal("the desk was asked")
return nil, nil
})
d.known = func(machine string) error {
if machine == unknown {
return errors.New("no node " + machine)
}
return nil
}
node, desk := "anchor", "laptop"
if unknown == "elsewhere" {
node = unknown
} else {
desk = unknown
}
_, err := d.give(node, "telegram", "telegram-token", desk)
if err == nil || !strings.Contains(err.Error(), "nobody was asked") || !strings.Contains(err.Error(), unknown) {
t.Errorf("%s: %v", unknown, err)
}
if len(*accepted)+len(*acts)+len(*asked) != 0 {
t.Errorf("%s: something happened: %v %v %v", unknown, *accepted, *acts, *asked)
}
}
}
-5
View File
@@ -126,11 +126,6 @@ var probeRegistry = []probe{
{ID: agentAccountProbe, Asserts: "every machine that names an agent account has it judged, on its node-engine's " + {ID: agentAccountProbe, Asserts: "every machine that names an agent account has it judged, on its node-engine's " +
"newest statement, unable to become root without a person", From: "ADR 0266, ADR 0259 §8", "newest statement, unable to become root without a person", From: "ADR 0266, ADR 0259 §8",
Kind: kindAgentCanBecomeRoot, Phase: 1, run: probeAgentAccounts}, Kind: kindAgentCanBecomeRoot, Phase: 1, run: probeAgentAccounts},
// Root where the trusted parties run (novox/hq ADR 0259 §8): while an agent can become root there without a
// person, an answer proven there proves nothing.
{ID: "D-root", Asserts: "no agent can become root without a person on a machine where the router or a channel " +
"proving its sender runs: not by its own account, and not through a tool that runs its command as an account " +
"that can", From: "ADR 0259 §8", Kind: kindRootNotFree, Phase: 2, run: probeAgentRoot},
{ID: "DW", Asserts: "the watchdogs of the signals table ran within three of their intervals", {ID: "DW", Asserts: "the watchdogs of the signals table ran within three of their intervals",
From: "ADR 0227 rule 6: the watchers are watched", Kind: "watchdogs-silent", Phase: 1, run: probeWatchdogs}, From: "ADR 0227 rule 6: the watchers are watched", Kind: "watchdogs-silent", Phase: 1, run: probeWatchdogs},
// The core's health definitions (novox/hq to-be 45 §8, ADR 0236): what a core component's new build is // The core's health definitions (novox/hq to-be 45 §8, ADR 0236): what a core component's new build is
+1 -12
View File
@@ -205,7 +205,7 @@ func gatherFacts(ctx context.Context, open *stores, busVersion string) (snapshot
if err != nil { if err != nil {
return snapshot.Facts{}, err return snapshot.Facts{}, err
} }
read, err := readForPlanning(ctx, inv) read, err := inv.ReadRepositories(ctx)
if err != nil { if err != nil {
return snapshot.Facts{}, err return snapshot.Facts{}, err
} }
@@ -411,18 +411,7 @@ func gatherFacts(ctx context.Context, open *stores, busVersion string) (snapshot
Commit: e.Source.BuiltFrom, Provided: e.Provided, RollOut: current[e.Manifest.Module].RollOut, Commit: e.Source.BuiltFrom, Provided: e.Provided, RollOut: current[e.Manifest.Module].RollOut,
Manifest: raw} Manifest: raw}
for _, r := range read[e.Manifest.Module] { for _, r := range read[e.Manifest.Module] {
// The module's own build source is said apart: a gate that predates it would read an own
// entry among Reads as a context of its own repository.
if r.Own {
if len(r.Paths) > 0 {
mod.Sources = append(mod.Sources, snapshot.BuildSource{Own: true, Paths: r.Paths})
}
continue
}
mod.Reads = append(mod.Reads, snapshot.RepositoryName(r.Repository)) mod.Reads = append(mod.Reads, snapshot.RepositoryName(r.Repository))
if len(r.Paths) > 0 {
mod.Sources = append(mod.Sources, snapshot.BuildSource{Repository: snapshot.RepositoryName(r.Repository), Paths: r.Paths})
}
} }
f.Modules = append(f.Modules, mod) f.Modules = append(f.Modules, mod)
if e.Provided || e.Source.Repository == "" { if e.Provided || e.Source.Repository == "" {
+1 -1
View File
@@ -18,7 +18,7 @@ func foundDirectory(module string, since time.Time) inventory.ResourceHealth {
return inventory.ResourceHealth{Module: module, Resource: module + ".data", Kind: link.KindDirectory, return inventory.ResourceHealth{Module: module, Resource: module + ".data", Kind: link.KindDirectory,
Target: "/srv/" + module, State: link.StateUnhealthy, Since: since, Target: "/srv/" + module, State: link.StateUnhealthy, Since: since,
Reason: link.ReasonUsedAsFound + " owned by 1000:1000, mode 700, as found; root, mode 755 was declared and " + Reason: link.ReasonUsedAsFound + " owned by 1000:1000, mode 700, as found; root, mode 755 was declared and " +
"not given it — `nox node hand-over laptop <directory>` on the control-node, with its path, hands it to the mesh"} "not given it — `mesh-host hand-over` at the machine hands it to the mesh"}
} }
func TestADirectoryFoundBeforeTheSendIsAWaitForAPerson(t *testing.T) { func TestADirectoryFoundBeforeTheSendIsAWaitForAPerson(t *testing.T) {
+4 -158
View File
@@ -87,9 +87,6 @@ const (
healthWaiting healthWaiting
healthNotYet healthNotYet
healthBroken healthBroken
// healthSuperseded is a judging that cannot go on: the machine was sent another build of the module
// after the gate's send (novox/hq issue 352). No verdict on the build judged, and nothing put back.
healthSuperseded
) )
// served is what one machine's node tools answered the bus's discovery with. // served is what one machine's node tools answered the bus's discovery with.
@@ -125,39 +122,6 @@ type gateFacts struct {
// groupsAdded is, per module, whether the move judged puts an account in a group its previous build did // groupsAdded is, per module, whether the move judged puts an account in a group its previous build did
// not (issue 318 review): the only move whose wait for a new login is excused. // not (issue 318 review): the only move whose wait for a new login is excused.
groupsAdded map[string]bool groupsAdded map[string]bool
// sent is, per machine, the declaration the gate's own send carried there (novox/hq issue 352): a
// report is held against it, never against the send made last. sentBuilds is what each machine was
// last sent of every module, and judged the commit of each module this gate judges: a machine last
// sent another build of the module is not running the build judged.
sent map[string]inventory.SentDeclaration
sentBuilds map[string]map[string]string
commits map[string]string
}
// reportedOn says a machine's last report is on what the gate sent it (novox/hq issue 352): on that
// declaration, or one it was sent after it — or, for a gate kept before sends were kept on it, on the
// declaration last sent. On 2026-10-09 a release's gate read the control node's report against a newer
// send another plan had just made there, and failed three builds the machine had reported healthy as
// "has not reported on what it was sent".
func (f gateFacts) reportedOn(machine string, r inventory.Reported) bool {
if sent, kept := f.sent[machine]; kept {
return sent.ReportsOn(r)
}
return r.Current
}
// supersededOn says the machine was last sent another build of the module than the one this gate judges
// (novox/hq issue 352): the judging cannot go on, whatever the machine reports. On 2026-10-09 a controller
// put back by one gate judged another gate's newer controller build passed on the same machine, reading
// the put-back build's health as the newer one's.
func (f gateFacts) supersededOn(module, machine string) (string, bool) {
judged, known := f.commits[module]
sent, has := f.sentBuilds[machine][module]
if !known || !has || judged == "" || sent == "" || sameCommit(sent, judged) {
return "", false
}
return fmt.Sprintf("%s was sent %s %s after this gate's %s: the build judged no longer runs there, and "+
"this judging is superseded by that send's", machine, module, short(sent), short(judged)), true
} }
// gatherGateFacts reads what a judging needs, from the store, the bus and this controller's memory. A // gatherGateFacts reads what a judging needs, from the store, the bus and this controller's memory. A
@@ -235,12 +199,9 @@ func judgeHealth(module, component string, m catalogue.Manifest, machine string,
return healthBroken, fmt.Sprintf("the witness on %s judged the %s %s and %s: %s", machine, r.Component, return healthBroken, fmt.Sprintf("the witness on %s judged the %s %s and %s: %s", machine, r.Component,
short(r.From), r.Outcome, r.Why) short(r.From), r.Outcome, r.Why)
} }
if why, superseded := f.supersededOn(module, machine); superseded {
return healthSuperseded, why
}
r, said := f.reports[machine] r, said := f.reports[machine]
switch { switch {
case !said || r.At == nil || !f.reportedOn(machine, r): case !said || r.At == nil || !r.Current:
return healthNotYet, fmt.Sprintf("%s has not reported on what it was sent", machine) return healthNotYet, fmt.Sprintf("%s has not reported on what it was sent", machine)
case r.Outcome == inventory.OutcomeFailed || r.Outcome == inventory.OutcomeRefused: case r.Outcome == inventory.OutcomeFailed || r.Outcome == inventory.OutcomeRefused:
return healthBroken, fmt.Sprintf("%s %s what it was sent", machine, r.Outcome) return healthBroken, fmt.Sprintf("%s %s what it was sent", machine, r.Outcome)
@@ -248,9 +209,7 @@ func judgeHealth(module, component string, m catalogue.Manifest, machine string,
return healthNotYet, fmt.Sprintf("%s reported %q", machine, r.Outcome) return healthNotYet, fmt.Sprintf("%s reported %q", machine, r.Outcome)
} }
// **No new condition about it**: about the machine itself, or naming the module on that machine, // **No new condition about it**: about the machine itself, or naming the module on that machine,
// raised since the judging began. The gate's own are not evidence about the build. A fault that was // raised since the judging began. The gate's own are not evidence about the build.
// there at the send and reopened since is not new; one that had cleared before the send and came back
// after it is (OpenAt, novox/hq issue 348).
if f.judged { if f.judged {
if f.openErr != nil { if f.openErr != nil {
return healthNotYet, "what is wrong cannot be read, so whether the build made anything wrong is not known: " + return healthNotYet, "what is wrong cannot be read, so whether the build made anything wrong is not known: " +
@@ -260,7 +219,7 @@ func judgeHealth(module, component string, m catalogue.Manifest, machine string,
// A wait for a person's new login, or for a directory used as found to be handed over, is the module's // A wait for a person's new login, or for a directory used as found to be handed over, is the module's
// reading, not a fault raised since the send: the gate reads it from the statement below (ADR 0254, // reading, not a fault raised since the send: the gate reads it from the statement below (ADR 0254,
// novox/hq issue 339). // novox/hq issue 339).
if c.Source == gateProbe || c.OpenAt(since) || c.Kind == kindReloginNeeded || c.Kind == kindUsedAsFound { if c.Source == gateProbe || c.Raised.Before(since) || c.Kind == kindReloginNeeded || c.Kind == kindUsedAsFound {
continue continue
} }
onIt := c.Subject.Machine == machine || slices.Contains(c.Subject.Also, machine) || onIt := c.Subject.Machine == machine || slices.Contains(c.Subject.Also, machine) ||
@@ -372,7 +331,7 @@ func aboutTheMachine(machine string, moved []string, since time.Time, f gateFact
aboutIt := c.Subject.Scope == conditions.ScopeMachine && (c.Subject.ID == machine || c.Subject.Machine == machine || aboutIt := c.Subject.Scope == conditions.ScopeMachine && (c.Subject.ID == machine || c.Subject.Machine == machine ||
slices.Contains(c.Subject.Also, machine)) slices.Contains(c.Subject.Also, machine))
// A directory used as found waits for a person, whatever the send did (novox/hq issue 339). // A directory used as found waits for a person, whatever the send did (novox/hq issue 339).
if !aboutIt || c.Source == gateProbe || c.OpenAt(since) || c.Kind == kindUsedAsFound { if !aboutIt || c.Source == gateProbe || c.Raised.Before(since) || c.Kind == kindUsedAsFound {
kept = append(kept, c) kept = append(kept, c)
continue continue
} }
@@ -477,21 +436,6 @@ func judgeMoves(ctx context.Context, open *stores, g *inventory.PlanGate, pairs
return "", err return "", err
} }
facts.groupsAdded = movesAddingGroups(ctx, open.inventory, g, pairs, shelf) facts.groupsAdded = movesAddingGroups(ctx, open.inventory, g, pairs, shelf)
facts.sent = g.Sent
facts.commits, facts.sentBuilds = judgedCommits(g, pairs), map[string]map[string]string{}
// A module this gate put back at once (putBackBroken) was sent its earlier build by the gate itself:
// not another send, and not a judging superseded.
for _, m := range g.Returned {
delete(facts.commits, m)
}
for _, j := range pairs {
if _, read := facts.sentBuilds[j.node]; read {
continue
}
if builds, known, err := open.inventory.SentBuilds(ctx, j.node); err == nil && known {
facts.sentBuilds[j.node] = builds
}
}
// **What is wrong with a machine itself is the machine's** (novox/hq issue 281): read once for each // **What is wrong with a machine itself is the machine's** (novox/hq issue 281): read once for each
// machine judged, apart from what is wrong with a module there, and never pinned on the module the // machine judged, apart from what is wrong with a module there, and never pinned on the module the
// gate happens to be kept on. // gate happens to be kept on.
@@ -528,13 +472,6 @@ func judgeMoves(ctx context.Context, open *stores, g *inventory.PlanGate, pairs
if _, seen := reading[j.module]; !seen { if _, seen := reading[j.module]; !seen {
modules = append(modules, j.module) modules = append(modules, j.module)
} }
if h == healthSuperseded {
// Decided at once (novox/hq issue 352): nothing of this gate's can be judged on a machine that
// was sent another build of it, and nothing is put back — the later send is what runs there.
g.Failing, g.Last = nil, ""
decide(g, inventory.GateSuperseded, said, now)
return g.Verdict, nil
}
if h == healthBroken && !slices.Contains(g.Broken, j.module) { if h == healthBroken && !slices.Contains(g.Broken, j.module) {
g.Broken = append(g.Broken, j.module) g.Broken = append(g.Broken, j.module)
if g.BrokenWhy == "" { if g.BrokenWhy == "" {
@@ -638,40 +575,6 @@ func judgeMoves(ctx context.Context, open *stores, g *inventory.PlanGate, pairs
return g.Verdict, nil return g.Verdict, nil
} }
// judgedCommits is the commit of each module a gate judges: the gate's own To for its module, and each
// carried move's. Pure.
func judgedCommits(g *inventory.PlanGate, pairs []judged) map[string]string {
out := map[string]string{}
for _, c := range g.Carried {
if c.To != "" {
out[c.Module] = c.To
}
}
if g.To != "" {
for _, j := range pairs {
if _, has := out[j.module]; !has && !slices.ContainsFunc(g.Carried, func(c inventory.CarriedMove) bool { return c.Module == j.module }) {
out[j.module] = g.To
}
}
}
return out
}
// sentNow is what each machine was just sent, read after a send for the gate to keep (novox/hq issue
// 352): a machine whose send is not on record is left out, and its report is read as before.
func sentNow(ctx context.Context, inv *inventory.Inventory, machines []string) map[string]inventory.SentDeclaration {
out := map[string]inventory.SentDeclaration{}
for _, n := range machines {
if s, found, err := inv.SentTo(ctx, n); err == nil && found {
out[n] = s
}
}
if len(out) == 0 {
return nil
}
return out
}
// whyFor is a passing gate's why as one module's verdict says it: the send's, and that module's own wait // whyFor is a passing gate's why as one module's verdict says it: the send's, and that module's own wait
// for a person, never another's (issue 318 review). // for a person, never another's (issue 318 review).
func whyFor(g *inventory.PlanGate, module string) string { func whyFor(g *inventory.PlanGate, module string) string {
@@ -897,16 +800,6 @@ func gateFailed(ctx context.Context, open *stores, p *inventory.Plan, module str
"back", module, short(state.Previous), inventory.KeptBuilds)) "back", module, short(state.Previous), inventory.KeptBuilds))
return return
} }
if g.Component == lease.ComponentController {
// **The controller is never put back to a build older than the store's schema** (novox/hq issue
// 352): the build before it carries fewer migrations than the failed one applied, starts behind
// its own records, and judges the next gate with what it can read. The current build is kept and
// the condition says so; a person decides.
if why, ok := controllerSchemaAllows(ctx, inv, previous); !ok {
notBack(why)
return
}
}
if err := inv.RestoreModule(ctx, previous); err != nil { if err := inv.RestoreModule(ctx, previous); err != nil {
notBack(err.Error()) notBack(err.Error())
return return
@@ -940,53 +833,6 @@ func gateFailed(ctx context.Context, open *stores, p *inventory.Plan, module str
sayRollback(ctx, open, module, g, "") sayRollback(ctx, open, module, g, "")
} }
// controllerSchemaAllows says the store's schema lets this build of the controller be put back: the
// build recorded, when it served, a reach at or past the highest migration the store has applied. One
// that never recorded a reach is not proved safe, and is refused as such (novox/hq issue 352). Why
// says what is kept and why when it is not.
func controllerSchemaAllows(ctx context.Context, inv *inventory.Inventory, previous inventory.Build) (string, bool) {
applied, err := inv.SchemaApplied(ctx)
if err != nil {
return "what the store's schema reaches cannot be read, so whether the build before it can read it is not " +
"known; the current build is kept: " + err.Error(), false
}
build := versionOfBuild(previous)
if build == "" {
return fmt.Sprintf("the build before it (%s) names no bundle to know it by, so whether it can read the store's "+
"schema (migration %04d) is not known; the current build is kept, and a person decides", short(previous.Commit), applied), false
}
reach, known, err := inv.SchemaReachOf(ctx, build)
if err != nil {
return "what the build before it knows of the store's schema cannot be read; the current build is kept: " + err.Error(), false
}
if !known {
return fmt.Sprintf("the build before it (%s, %s) never recorded how far it reads the store's schema — a "+
"controller records that when it serves — so it is not proved to read migration %04d, which the store "+
"has applied; a controller older than its store starts behind its own records and judges with what it "+
"can read, so the current build is kept, and a person decides", short(previous.Commit), build, applied), false
}
if reach < applied {
return fmt.Sprintf("the build before it (%s, %s) reads the store's schema up to migration %04d, and the store "+
"is at %04d: a controller older than its store starts behind its own records and judges with what it "+
"can read, so the current build is kept, and a person decides", short(previous.Commit), build, reach, applied), false
}
return "", true
}
// versionOfBuild is the version a build's bundle is delivered as — its archive's digest, short, as the
// catalogue names it (`${version}`) — read from the build's artifacts; empty when none is a bundle.
func versionOfBuild(b inventory.Build) string {
for _, a := range b.Made {
if a.Kind != catalogue.ArtifactBundle && a.Kind != catalogue.ArtifactArchive {
continue
}
if _, hex, found := strings.Cut(a.Reference, "sha256:"); found && len(hex) >= 12 {
return hex[:12]
}
}
return ""
}
// rollbacks is what a failed send puts back, sent together (novox/hq issue 281): a gate that judged one // rollbacks is what a failed send puts back, sent together (novox/hq issue 281): a gate that judged one
// send judges what it moved as one, and what it found wanting goes back in one send per machine — not // send judges what it moved as one, and what it found wanting goes back in one send per machine — not
// in a send for each module, which is the churn that failed the gate in the first place. // in a send for each module, which is the churn that failed the gate in the first place.
+1 -2
View File
@@ -113,10 +113,9 @@ func aGateMesh(t *testing.T) *gateMesh {
} }
for node, h := range g.health { for node, h := range g.health {
if h == healthNotYet { if h == healthNotYet {
// Not reported on the send: neither the send made last, nor the gate's own (issue 352).
f.rolledBack[node] = nil f.rolledBack[node] = nil
r := f.reports[node] r := f.reports[node]
r.Current, r.Declared, r.ReportedSequence = false, "", 0 r.Current = false
f.reports[node] = r f.reports[node] = r
} }
} }
+3 -16
View File
@@ -60,18 +60,11 @@ var handActVerbs = []handActVerb{
// a person's word (ADR 0242), which the push itself reads from what it carried (recorded_push.go). // a person's word (ADR 0242), which the push itself reads from what it carried (recorded_push.go).
{Verb: "push", Decision: "a recorded build moves only by a person's push: that push is the word its " + {Verb: "push", Decision: "a recorded build moves only by a person's push: that push is the word its " +
"upgrade policy asks for (ADR 0242)", DecidedWhen: pushedRecorded}, "upgrade policy asks for (ADR 0242)", DecidedWhen: pushedRecorded},
// Stopping or starting a walk the operator chose on a warrant (novox/hq ADR 0259) is their decision. {Verb: "plans stop"},
{Verb: "plans stop", Decision: "the operator's answer to an ask is their decision, not a repair (ADR 0259)",
DecidedFor: []string{conditions.CauseOperatorAnswer}},
{Verb: "plans close"}, {Verb: "plans close"},
// A walk started by a person instead of its delivery's owner (novox/hq ADR 0239): the owner down, or // A walk started by a person instead of its delivery's owner (novox/hq ADR 0239): the owner down, or
// not trusted with it — either is a repair the owner should have made. Unless the operator chose it on // not trusted with it — either is a repair the owner should have made.
// a warrant (ADR 0259). {Verb: "plans go"},
{Verb: "plans go", Decision: "the operator's answer to an ask is their decision, not a repair (ADR 0259)",
DecidedFor: []string{conditions.CauseOperatorAnswer}},
// An act the operator chose on a warrant (novox/hq ADR 0259): asked by the controller, answered on a
// channel that proved who answered, performed by the controller as itself.
{Verb: handActWarrant, Decision: "the operator chose it, answering what the controller asked (ADR 0259)"},
{Verb: "broker consumer-reset"}, {Verb: "broker consumer-reset"},
// Silencing the same condition twice says the condition, or what it watches, wants mending — unless // Silencing the same condition twice says the condition, or what it watches, wants mending — unless
// it is the operator's answer on a notification: a decision to live with it (novox/hq ADR 0258). // it is the operator's answer on a notification: a decision to live with it (novox/hq ADR 0258).
@@ -110,9 +103,6 @@ var handActVerbs = []handActVerb{
// to that judgement. Several values rotate for one leak, and a leak that recurs is a defect of the // to that judgement. Several values rotate for one leak, and a leak that recurs is a defect of the
// module that prints them, an issue against it, not a healer that rotates. A rotation for any other // module that prints them, an issue against it, not a healer that rotates. A rotation for any other
// cause — a credential that stopped working — counts: a schedule or a healer could take it over. // cause — a credential that stopped working — counts: a schedule or a healer could take it over.
// A value given at the desk (novox/hq ADR 0259 §10): an outside party's key, such as a bot token, which
// only a person can give. Their word, never a repair.
{Verb: "secret accept", Decision: "a value an outside party issued is given by a person, at their desk"},
{Verb: "secret rotate", Decision: "a value a person judged disclosed is replaced on their word", {Verb: "secret rotate", Decision: "a value a person judged disclosed is replaced on their word",
DecidedFor: []string{causeLeakedInLogs}}, DecidedFor: []string{causeLeakedInLogs}},
} }
@@ -251,9 +241,6 @@ func handActCommand(ctx context.Context, args []string) error {
if len(args) > 0 && args[0] == "drill" { if len(args) > 0 && args[0] == "drill" {
return handActDrill(ctx, args[1:]) return handActDrill(ctx, args[1:])
} }
if len(args) > 0 && args[0] == "warrant" {
return handActWarrantCommand(ctx, args[1:])
}
if len(args) > 0 && args[0] != "list" && !strings.HasPrefix(args[0], "-") { if len(args) > 0 && args[0] != "list" && !strings.HasPrefix(args[0], "-") {
return errors.New("hand-act record <what> --why <text> --cause <word> | hand-act drill <what> --why <text> " + return errors.New("hand-act record <what> --why <text> --cause <word> | hand-act drill <what> --why <text> " +
"| hand-acts [--days N] [--json]") "| hand-acts [--days N] [--json]")
-169
View File
@@ -1,169 +0,0 @@
package main
import (
"bytes"
"errors"
"strings"
"testing"
"time"
"github.com/novox/mesh-controller/internal/conditions"
"github.com/novox/mesh-controller/internal/inventory"
"github.com/novox/mesh-controller/internal/link"
)
// A hand-over's line is judged before anything is asked (novox/hq issue 356): a node's name and the directory's
// absolute path exactly as the engine states it — no `..`, no doubled or trailing separator, nothing relative.
func TestAHandOverLineIsJudgedBeforeItIsAsked(t *testing.T) {
for _, args := range [][]string{
{},
{"laptop"},
{"laptop", "/srv/notes", "extra"},
{"", "/srv/notes"},
{"--node", "/srv/notes"},
{"laptop", "srv/notes"},
{"laptop", "/srv/../etc"},
{"laptop", "/srv//notes"},
{"laptop", "/srv/notes/"},
{"laptop", "/srv/notes/."},
} {
if _, _, err := handOverLine(args); err == nil {
t.Errorf("%q was taken", args)
}
}
node, path, err := handOverLine([]string{"laptop", "/srv/notes"})
if err != nil || node != "laptop" || path != "/srv/notes" {
t.Fatalf("read as %q %q %v", node, path, err)
}
}
// Who hands over is the line's caller in the words every verb's caller is recorded in — the operator through
// mesh-cli — or the controller's terminal when nobody is named.
func TestAHandOverNamesWhoAsked(t *testing.T) {
t.Setenv(link.CallerVar, " jo through mesh-cli on anchor ")
if by := handOverBy(); by != "jo through mesh-cli on anchor" {
t.Fatalf("by %q", by)
}
t.Setenv(link.CallerVar, "")
if by := handOverBy(); by != "the controller's terminal" {
t.Fatalf("by %q", by)
}
}
// **A hand-over is the controller's terminal's alone** (novox/hq issue 356, ADR 0266): through any verb, and
// through mesh-cli outside the terminal, `node hand-over` is refused and nothing runs — at the next apply root
// gives the directory to the account the module declares, and whoever may call a verb includes agents.
func TestAHandOverIsRefusedThroughEveryVerb(t *testing.T) {
line := []string{"node", "hand-over", "laptop", "/srv/notes"}
if err := terminalOnly(line); err == nil {
t.Fatal("node hand-over passed as a verb's line")
}
if _, err := argvFor("command", map[string]any{"command": "node hand-over laptop /srv/notes"}); err == nil {
t.Fatal("the command verb composed node hand-over")
}
if _, err := ordinaryLine(line); err == nil {
t.Fatal("node hand-over composed as an ordinary mesh-cli line")
}
if _, err := argvFor("node", map[string]any{"node": "laptop", "hand-over": "/srv/notes"}); err == nil {
t.Fatal("the node verb composed a hand-over")
}
}
// The condition's words are plain and name no machine's binary; the operator's line, with the node and the path
// (novox/hq ADR 0272), is in the summary the module's health gives (moduleHealthWord) and in the evidence.
func TestTheUsedAsFoundConditionNamesTheOperatorsLine(t *testing.T) {
rs := []inventory.ResourceHealth{foundDirectory("notes", time.Now().Add(-24*time.Hour))}
o := usedAsFoundObservation("notes", "laptop", "notes on laptop uses notes.data as found", rs)
if strings.Contains(o.Needs, "mesh-host") || strings.Contains(o.Explanation, "mesh-host") ||
!strings.Contains(o.Needs, "control-node") {
t.Fatalf("the condition's words: %q %q", o.Needs, o.Explanation)
}
if why, ok := conditions.PlainWords(conditions.Words{Headline: o.Headline, Explanation: o.Explanation, Needs: o.Needs,
Resolved: o.Resolved}, "laptop"); !ok {
t.Fatalf("not plain: %s", why)
}
f := gateFacts{now: time.Now(), health: map[string]inventory.NodeHealth{"laptop": {Node: "laptop", HeardAt: time.Now(),
Resources: rs}}}
h, why := moduleHealthWord("notes", "laptop", time.Now().Add(-time.Hour), f)
if h != healthPerson || !strings.Contains(why, "`nox node hand-over laptop <directory>` on the control-node") ||
strings.Contains(why, "mesh-host") || strings.Contains(why, "/srv/") {
t.Fatalf("the module's health reads %v %q; want the operator's line", h, why)
}
}
// **Nothing is asked of a node the mesh does not know, and the engine's refusal is the command's failure**
// (review of issue 356): a refused hand-over never exits as a success.
func TestAHandOverAsksOnlyAKnownNodeAndFailsOnARefusal(t *testing.T) {
t.Setenv(link.CallerVar, "jo through mesh-cli on anchor")
asked := 0
ask := func(answer link.HandOverAnswer) func(node, path, by string) (link.HandOverAnswer, error) {
return func(node, path, by string) (link.HandOverAnswer, error) {
asked++
if node != "laptop" || path != "/srv/notes" || by != "jo through mesh-cli on anchor" {
t.Fatalf("asked %q %q %q", node, path, by)
}
return answer, nil
}
}
unknown := func(string) error { return errors.New("no node called laptop") }
known := func(string) error { return nil }
var out bytes.Buffer
err := handOverAsked([]string{"laptop", "/srv/notes"}, unknown, ask(link.HandOverAnswer{Said: "x"}), &out)
if err == nil || asked != 0 || !strings.Contains(err.Error(), "nothing was asked") {
t.Fatalf("an unknown node: %v, asked %d", err, asked)
}
err = handOverAsked([]string{"laptop", "/srv/../etc"}, known, ask(link.HandOverAnswer{Said: "x"}), &out)
if err == nil || asked != 0 {
t.Fatalf("a refused line was asked: %v, asked %d", err, asked)
}
err = handOverAsked([]string{"laptop", "/srv/notes"}, known,
ask(link.HandOverAnswer{Refused: "/srv/notes is not used as found; nothing was handed over"}), &out)
if err == nil || !strings.Contains(err.Error(), "laptop refused: /srv/notes is not used as found") || out.Len() != 0 {
t.Fatalf("a refusal: %v, printed %q", err, out.String())
}
err = handOverAsked([]string{"laptop", "/srv/notes"}, known, ask(link.HandOverAnswer{Said: "handed over"}), &out)
if err != nil || !strings.HasPrefix(out.String(), "handed over\n") || !strings.Contains(out.String(), "`nox push laptop`") {
t.Fatalf("a record: %v, printed %q", err, out.String())
}
failing := func(string, string, string) (link.HandOverAnswer, error) {
return link.HandOverAnswer{}, errors.New("no engine")
}
if err := handOverAsked([]string{"laptop", "/srv/notes"}, known, failing, &out); err == nil {
t.Fatal("an ask that failed was a success")
}
}
// The setuid search's line asks only a known node, one name and nothing else, and fails on a refusal
// (novox/hq issue 361).
func TestASetuidSearchAsksOnlyAKnownNodeAndFailsOnARefusal(t *testing.T) {
t.Setenv(link.CallerVar, "jo through mesh-cli on anchor")
asked := 0
ask := func(answer link.HandOverAnswer) func(node, by string) (link.HandOverAnswer, error) {
return func(node, by string) (link.HandOverAnswer, error) {
asked++
if node != "novox" || by != "jo through mesh-cli on anchor" {
t.Fatalf("asked %q %q", node, by)
}
return answer, nil
}
}
known := func(string) error { return nil }
var out bytes.Buffer
for _, args := range [][]string{nil, {"novox", "extra"}, {"-x"}} {
if err := setuidSearchAsked(args, known, ask(link.HandOverAnswer{Said: "x"}), &out); err == nil || asked != 0 {
t.Fatalf("%v was asked: %v", args, err)
}
}
if err := setuidSearchAsked([]string{"novox"}, func(string) error { return errors.New("no node called novox") },
ask(link.HandOverAnswer{Said: "x"}), &out); err == nil || asked != 0 {
t.Fatalf("an unknown node: %v", err)
}
if err := setuidSearchAsked([]string{"novox"}, known, ask(link.HandOverAnswer{Refused: "no; no search was started"}),
&out); err == nil || !strings.Contains(err.Error(), "novox refused") || out.Len() != 0 {
t.Fatalf("a refusal: %v, printed %q", err, out.String())
}
if err := setuidSearchAsked([]string{"novox"}, known, ask(link.HandOverAnswer{Said: "a new search starts"}),
&out); err != nil || !strings.HasPrefix(out.String(), "a new search starts\n") {
t.Fatalf("a start: %v, printed %q", err, out.String())
}
}
-26
View File
@@ -1,26 +0,0 @@
package main
import (
"os"
"golang.org/x/sys/unix"
)
// hideTyping turns a terminal's echo off while a secret is typed at it, and gives back what restores it. On
// anything that is not a terminal (a pipe, a file) it does nothing.
func hideTyping(f *os.File) func() {
fd := int(f.Fd())
before, err := unix.IoctlGetTermios(fd, unix.TCGETS)
if err != nil {
return func() {}
}
hidden := *before
hidden.Lflag &^= unix.ECHO
if err := unix.IoctlSetTermios(fd, unix.TCSETS, &hidden); err != nil {
return func() {}
}
return func() {
_ = unix.IoctlSetTermios(fd, unix.TCSETS, before)
_, _ = os.Stderr.WriteString("\n")
}
}
-221
View File
@@ -1,221 +0,0 @@
package main
import (
"strings"
"testing"
"time"
"github.com/novox/mesh-controller/internal/catalogue"
"github.com/novox/mesh-controller/internal/conditions"
"github.com/novox/mesh-controller/internal/inventory"
"github.com/novox/mesh-controller/internal/lease"
"github.com/novox/mesh-controller/internal/link"
)
// novox/hq issue 348: on 2026-10-09 the control node's resolver stopped answering on its private address
// at 10:57:57 UTC; the machine's network condition was raised at 10:58:45. The node-engine and the
// controller were sent at 10:59:34. The new node-engine's first statement judged the names once — unknown,
// "one look failed; a second decides" — and that statement cleared the condition, though its last evidence
// still said "connection refused". The next look raised it again at 11:00:23, after the send, and both
// builds failed their gate at 11:10 with "raised since it was sent" and were put back, for a fault that
// began before they were sent.
// namesRefused is the control node's names part as its node-engine said it in the outage: its own
// resolver, at its own address, refusing.
func namesRefused(state string, streak int) link.NetworkPart {
p := link.NetworkPart{Part: link.PartNames, State: state, Since: h0, Streak: streak}
if state == link.StateUnhealthy {
p.Reason = "1 of its 2 resolvers do not answer as the mesh's do"
p.Said = "10.77.0.1 — anchor.internal (IPv4): read udp 10.77.0.1:35244->10.77.0.1:53: read: connection refused"
p.Toward = []string{"10.77.0.1"}
}
return p
}
func networkSaying(parts ...link.NetworkPart) *link.NetworkHealth {
state := link.StateHealthy
for _, p := range parts {
switch {
case p.State == link.StateUnhealthy:
state = link.StateUnhealthy
case p.State == link.StateUnknown && state == link.StateHealthy:
state = link.StateUnknown
}
}
return &link.NetworkHealth{State: state, Since: h0, Parts: parts}
}
// TestReplay348 replays the statements of the outage: the condition raised before the send is not
// cleared by the restarted engine's first, undecided statement, and the gate does not count it against
// the builds sent after it began.
func TestReplay348(t *testing.T) {
open := aMesh(t)
ctx := t.Context()
inv, k := open.inventory, conditionsFrom
say := func(at time.Time, n *link.NetworkHealth) {
t.Helper()
if err := stateHealth(ctx, inv, k, "anchor", link.Health{Contract: link.ReadinessContract, At: at, Network: n}, at); err != nil {
t.Fatal(err)
}
}
network := func() (conditions.Condition, bool) {
t.Helper()
list, err := k.Open(ctx)
if err != nil {
t.Fatal(err)
}
for _, c := range list {
if c.Key == "machine.anchor.network" {
return c, true
}
}
return conditions.Condition{}, false
}
// 10:58:45 — the second failing look: raised.
say(h0, networkSaying(namesRefused(link.StateUnhealthy, 2)))
raised, ok := network()
if !ok {
t.Fatal("the resolver refusing on the control node raised nothing")
}
time.Sleep(5 * time.Millisecond)
sent := time.Now().UTC()
time.Sleep(5 * time.Millisecond)
// 10:59:42 — the restarted engine's first statement: one look failed, a second decides.
say(h0.Add(time.Minute), networkSaying(namesRefused(link.StateUnknown, 1)))
if _, ok := network(); !ok {
t.Fatal("a statement that judged nothing yet cleared the condition: the restarted engine's first look " +
"said the fault was gone while it still refused")
}
// 11:00:23 — its second look: unhealthy again, the same raising.
say(h0.Add(2*time.Minute), networkSaying(namesRefused(link.StateUnhealthy, 2)))
again, ok := network()
if !ok || !again.Raised.Equal(raised.Raised) || again.Count != 1 {
t.Fatalf("the same raising was not kept: raised %s (first %s), count %d", again.Raised, raised.Raised, again.Count)
}
// The gate on the control node, for a build sent after the fault began.
open2, err := k.Open(ctx)
if err != nil {
t.Fatal(err)
}
f := gateFacts{judged: true, open: open2}
if w := aboutTheMachine("anchor", []string{"mesh-host"}, sent, f); w.whole != "" || len(w.on) != 0 {
t.Fatalf("a fault from before the send held the build: %+v", w)
}
// Decided healthy: cleared.
say(h0.Add(3*time.Minute), networkSaying(link.NetworkPart{Part: link.PartNames, State: link.StateHealthy, Since: h0}))
if c, ok := network(); ok {
t.Fatalf("a statement that decides the names healthy left %s open", c.Key)
}
}
// A fault there at the send, cleared and reopened after it, is not raised since the send; one that cleared
// before the send and came back after it is — a send that breaks a recovered machine fails its gate (review
// of mesh-controller PR 179, A2). Read through OpenAt, by both of the gate's readings.
func TestAFaultThatFlappedAfterTheSendIsNotTheSendsAndOneThatRecoveredBeforeItIs(t *testing.T) {
since := h0
network := func(first time.Time, gaps ...conditions.Gap) conditions.Condition {
raised := since.Add(time.Minute)
if len(gaps) > 0 {
raised = gaps[len(gaps)-1].Reopened
}
return conditions.Condition{Key: "machine.anchor.network", Kind: kindMachineNetwork,
Subject: conditions.Subject{Scope: conditions.ScopeMachine, ID: "anchor", Machine: "anchor"},
Summary: "anchor's network is not healthy", Source: sourceNetwork, First: first, Gaps: gaps, Raised: raised}
}
held := func(c conditions.Condition) bool {
return aboutTheMachine("anchor", []string{"mesh-controller"}, since, gateFacts{judged: true,
open: []conditions.Condition{c}}).whole != ""
}
// The day's case: raised before the send, cleared 8 s after it, reopened 49 s after it.
flapped := network(since.Add(-49*time.Second),
conditions.Gap{Cleared: since.Add(8 * time.Second), Reopened: since.Add(49 * time.Second)})
if held(flapped) {
t.Fatal("a fault there at the send, flapping after it, held the machine")
}
// Recovered before the send, broken again after it: the send's.
recovered := network(since.Add(-time.Hour),
conditions.Gap{Cleared: since.Add(-30 * time.Second), Reopened: since.Add(20 * time.Second)})
if !held(recovered) {
t.Fatal("a machine recovered at the send and broken after it passed the gate")
}
// An older gap, before the send, and the fault there at the send: not the send's.
twice := network(since.Add(-time.Hour),
conditions.Gap{Cleared: since.Add(-50 * time.Minute), Reopened: since.Add(-45 * time.Minute)},
conditions.Gap{Cleared: since.Add(10 * time.Second), Reopened: since.Add(30 * time.Second)})
if held(twice) {
t.Fatal("a fault there at the send, with an older gap, held the machine")
}
// Raised after the send, never cleared: the send's.
if !held(network(time.Time{})) {
t.Fatal("a fault raised after the send held nothing")
}
// And a module's own, through judgeHealth.
at := since.Add(2 * time.Minute)
g := gateFacts{judged: true, now: at, reports: map[string]inventory.Reported{"anchor": {Node: "anchor",
Outcome: inventory.OutcomeApplied, At: &at, Current: true}}, engines: map[string]string{},
served: map[string]served{}, rolledBack: map[string][]lease.Rollback{},
open: []conditions.Condition{{Key: "provider.app.anchor.x.failing", Subject: conditions.Subject{
Scope: conditions.ScopeProvider, ID: "app.anchor.x", Machine: "anchor"}, Summary: "failing",
First: since.Add(-time.Hour), Raised: since.Add(time.Minute),
Gaps: []conditions.Gap{{Cleared: since.Add(5 * time.Second), Reopened: since.Add(time.Minute)}}}}}
if _, why := judgeHealth("app", "", catalogue.Manifest{Module: "app"}, "anchor", since, g); strings.HasPrefix(why, "raised since it was sent") {
t.Fatalf("a module's own fault there at the send: %s", why)
}
g.open[0].Gaps[0].Cleared = since.Add(-5 * time.Second)
if _, why := judgeHealth("app", "", catalogue.Manifest{Module: "app"}, "anchor", since, g); !strings.HasPrefix(why, "raised since it was sent") {
t.Fatalf("a module's own fault, recovered at the send and back after it, was not counted: %s", why)
}
}
// Only an undecided part holds a condition that names it; a condition about another part clears, and a
// statement unknown as a whole holds every part (review of PR 179, A4). Pure.
func TestAnUndecidedPartHoldsOnlyWhatNamesIt(t *testing.T) {
f := netFacts(map[string]*inventory.NetworkHealth{
"anchor": aNetwork(link.StateUnknown, inventory.NetworkPart{Part: link.PartNames, State: link.StateUnknown, Streak: 1},
inventory.NetworkPart{Part: link.PartRoute, State: link.StateHealthy}),
"laptop": aNetwork(link.StateHealthy, inventory.NetworkPart{Part: link.PartNames, State: link.StateHealthy}),
"spare": aNetwork(link.StateStarting, inventory.NetworkPart{Part: link.PartTunnel, State: link.StateHealthy}),
"other": aNetwork(link.StateStarting, inventory.NetworkPart{Part: link.PartTunnel, State: link.StateStarting}),
})
u := undecidedParts(f)
if !u["anchor"][link.PartNames] || u["anchor"][link.PartRoute] || u["laptop"] != nil || !u["spare"]["*"] ||
!u["other"][link.PartTunnel] || u["other"]["*"] {
t.Fatalf("undecided: %v", u)
}
about := func(machine, said string, also ...string) conditions.Condition {
return conditions.Condition{Subject: conditions.Subject{Scope: conditions.ScopeMachine, ID: machine,
Machine: machine, Also: also}, Evidence: []conditions.Evidence{{Said: said}}}
}
for _, c := range []struct {
c conditions.Condition
held bool
}{
{about("anchor", "names since 2026-10-09 10:58:45 UTC: 10.77.0.1 — refused"), true},
{about("anchor", "route since 2026-10-09 10:58:45 UTC: no default route"), false},
{about("laptop", "names since 2026-10-09 10:58:45 UTC: refused"), false},
{about("spare", "route since …: no default route"), true},
{about("hub", "anchor: names: refused", "anchor"), true},
{about("hub", "anchor: tunnel: no handshake", "anchor"), false},
} {
if got := heldUndecided(c.c, u); got != c.held {
t.Errorf("%s %q held %v, want %v", c.c.Subject.Machine, c.c.Evidence[0].Said, got, c.held)
}
}
}
// A release walks its modules without a record per module: D10 counts what its tier names as rolling,
// so the node-engine a release walks is not "behind, and no plan is rolling it out" on its first machine.
func TestAReleaseRollsOutWhatItsTierNames(t *testing.T) {
plans := []inventory.Plan{
{ID: "release-1", State: inventory.PlanRolling, Tiers: [][]string{{"mesh-host"}}, Modules: map[string]*inventory.PlanModule{}},
{ID: "plan-2", State: inventory.PlanRolling, Modules: map[string]*inventory.PlanModule{"letta": {}}},
}
got := rollingModules(plans)
if !got["mesh-host"] || !got["letta"] || len(got) != 2 {
t.Fatalf("rolling: %v", got)
}
}
-194
View File
@@ -1,194 +0,0 @@
package main
import (
"strings"
"testing"
"time"
"github.com/novox/mesh-controller/internal/catalogue"
"github.com/novox/mesh-controller/internal/inventory"
"github.com/novox/mesh-controller/internal/link"
)
// novox/hq issue 349: on 2026-10-09 the plan of mesh-catalog at a082615b (the merge of a security fix to the
// forge's module) was "superseded at tier 0 by" the plan at 8ff8197a, the merge before it, which the
// catch-up acted on late; what the later plan had not built was folded into a plan at the commit before the
// fix. Plans of one branch are ordered by when the forge made their merges, and a merge older than an open
// plan of its branch is planned at that plan's commit.
// TestTwoMergesActedOnInReverseOrderBuildTheNewerCommit replays it: the later merge acted on first, the
// earlier one second (the catch-up). One plan is left open, at the later commit, and it builds both.
func TestTwoMergesActedOnInReverseOrderBuildTheNewerCommit(t *testing.T) {
open := aCatalogueMesh(t)
ctx := t.Context()
asksWithPaths(t)
for _, m := range []string{"gitea", "notes"} {
if err := open.inventory.RegisterModule(ctx, catalogue.Manifest{Module: m, Version: "1"},
inventory.Source{Repository: "novox/mesh-catalog", Seat: "git", Path: "modules/" + m, Ref: "main",
BuiltFrom: "c0", Head: "c0"}); err != nil {
t.Fatal(err)
}
}
at := time.Now().UTC().Add(-20 * time.Minute).Truncate(time.Second)
fix := link.SourceMoved{Owner: "novox", Repo: "mesh-catalog", Base: "main", Commit: "a082615bfix",
MergedAt: at.Add(2 * time.Minute).Format(time.RFC3339Nano),
Paths: []string{"modules/gitea/module.json"}, ModuleDirs: []string{"modules/gitea"}, ModuleDirsSaid: true}
before := link.SourceMoved{Owner: "novox", Repo: "mesh-catalog", Base: "main", Commit: "8ff8197abefore",
MergedAt: at.Format(time.RFC3339Nano),
Paths: []string{"modules/notes/module.json"}, ModuleDirs: []string{"modules/notes"}, ModuleDirsSaid: true}
for _, m := range []link.SourceMoved{fix, before} {
if err := (following{open: open}).SourceMoved(ctx, m); err != nil {
t.Fatal(err)
}
}
plans, err := open.inventory.OpenPlans(ctx)
if err != nil {
t.Fatal(err)
}
if len(plans) != 1 {
var said []string
for _, p := range plans {
said = append(said, p.ID+" "+p.Commit+" "+p.Note)
}
t.Fatalf("open plans: %s", strings.Join(said, "; "))
}
p := plans[0]
if p.Commit != fix.Commit {
t.Fatalf("the open plan builds %s, not the newer commit %s", p.Commit, fix.Commit)
}
for _, m := range []string{"gitea", "notes"} {
if _, has := p.Modules[m]; !has {
t.Fatalf("the open plan at the newer commit does not build %s: %v", m, p.Modules)
}
}
}
// A late older merge after the newer plan is done (review of PR 179, A1): what it moved is built from the
// newer commit, and what the newer merge already looked at is not built again at the older one.
func TestALateMergeAfterTheNewerPlanEndedBuildsTheNewerCommit(t *testing.T) {
open := aCatalogueMesh(t)
ctx := t.Context()
asksWithPaths(t)
for _, m := range []string{"gitea", "notes"} {
if err := open.inventory.RegisterModule(ctx, catalogue.Manifest{Module: m, Version: "1"},
inventory.Source{Repository: "novox/mesh-catalog", Seat: "git", Path: "modules/" + m, Ref: "main",
BuiltFrom: "c0", Head: "c0"}); err != nil {
t.Fatal(err)
}
}
at := time.Now().UTC().Add(-20 * time.Minute).Truncate(time.Second)
fix := link.SourceMoved{Owner: "novox", Repo: "mesh-catalog", Base: "main", Commit: "a082615bfix",
MergedAt: at.Add(2*time.Minute + 500*time.Millisecond).Format(time.RFC3339Nano),
Paths: []string{"modules/gitea/module.json"}, ModuleDirs: []string{"modules/gitea"}, ModuleDirsSaid: true}
if err := (following{open: open}).SourceMoved(ctx, fix); err != nil {
t.Fatal(err)
}
plans, err := open.inventory.OpenPlans(ctx)
if err != nil || len(plans) != 1 {
t.Fatalf("%v %v", plans, err)
}
done := plans[0]
done.State = inventory.PlanDone
if err := open.inventory.SavePlan(ctx, &done); err != nil {
t.Fatal(err)
}
if got, err := open.inventory.PlanByID(ctx, done.ID); err != nil || !got.Merged.Equal(at.Add(2*time.Minute+500*time.Millisecond)) {
t.Fatalf("the merge time kept is %s, not to the nanosecond (%v)", got.Merged, err)
}
before := link.SourceMoved{Owner: "novox", Repo: "mesh-catalog", Base: "main", Commit: "8ff8197abefore",
MergedAt: at.Format(time.RFC3339Nano),
Paths: []string{"modules/notes/module.json", "modules/gitea/module.json"},
ModuleDirs: []string{"modules/notes", "modules/gitea"}, ModuleDirsSaid: true}
if err := (following{open: open}).SourceMoved(ctx, before); err != nil {
t.Fatal(err)
}
plans, err = open.inventory.OpenPlans(ctx)
if err != nil || len(plans) != 1 {
t.Fatalf("open plans after the late merge: %+v %v", plans, err)
}
p := plans[0]
if p.Commit != fix.Commit {
t.Fatalf("the late merge was planned at %s, not the newer commit %s", p.Commit, fix.Commit)
}
if _, has := p.Modules["notes"]; !has {
t.Fatalf("what only the late merge moved is not built: %v", p.Modules)
}
if _, has := p.Modules["gitea"]; has {
t.Fatalf("what the newer merge already built is built again: %v", p.Modules)
}
}
// Pure: the branch's order is the merges', where both plans know it; and a later merge of the branch is
// found in any state, never a release's, another repository's or another branch's.
func TestTheBranchOrderIsTheMerges(t *testing.T) {
t0 := time.Date(2026, 10, 9, 10, 0, 0, 0, time.UTC)
newerMerge := inventory.Plan{ID: "plan-1", Repository: "novox/mesh-catalog", Branch: "main", Commit: "a082615b",
Merged: t0.Add(time.Minute), Created: t0.Add(2 * time.Minute), State: inventory.PlanRolling}
olderMerge := inventory.Plan{ID: "plan-2", Repository: "novox/mesh-catalog", Branch: "main", Commit: "8ff8197a",
Merged: t0, Created: t0.Add(10 * time.Minute), State: inventory.PlanBuilding}
if earlierOnTheBranch(newerMerge, olderMerge) || !earlierOnTheBranch(olderMerge, newerMerge) {
t.Fatal("ordered by when the plans were made, not by when the merges were")
}
if _, closed := supersededBy(olderMerge, []inventory.Plan{newerMerge}, func(string) bool { return true }); len(closed) != 0 {
t.Fatalf("the plan of an older merge superseded a newer one: %s", closed[0].Note)
}
unknown := newerMerge
unknown.Merged = time.Time{}
if !earlierOnTheBranch(unknown, olderMerge) {
t.Fatal("without a merge time the plans' own order does not stand")
}
same := olderMerge
same.Merged = newerMerge.Merged
if !earlierOnTheBranch(newerMerge, same) || earlierOnTheBranch(same, newerMerge) {
t.Fatal("one merge time: the plans' own order does not stand")
}
m := link.SourceMoved{Owner: "novox", Repo: "mesh-catalog", Base: "main", Commit: "8ff8197a",
MergedAt: t0.Add(500 * time.Millisecond).Format(time.RFC3339Nano)}
newerMerge.State = inventory.PlanDone
if !laterOnTheBranch(m, newerMerge) {
t.Fatal("a later merge of the branch, its plan done, was not found")
}
for name, change := range map[string]func(p *inventory.Plan, m *link.SourceMoved){
"another branch": func(_ *inventory.Plan, m *link.SourceMoved) { m.Base = "release" },
"another repository": func(p *inventory.Plan, _ *link.SourceMoved) { p.Repository = "novox/mesh-controller" },
"a release": func(p *inventory.Plan, _ *link.SourceMoved) { p.Release = &inventory.PlanRelease{} },
"no merge time": func(p *inventory.Plan, _ *link.SourceMoved) { p.Merged = time.Time{} },
"the same commit": func(p *inventory.Plan, m *link.SourceMoved) { m.Commit = p.Commit },
"an older merge": func(p *inventory.Plan, _ *link.SourceMoved) { p.Merged = t0 },
"the same moment": func(p *inventory.Plan, _ *link.SourceMoved) { p.Merged = t0.Add(500 * time.Millisecond) },
"an unreadable time": func(_ *inventory.Plan, m *link.SourceMoved) { m.MergedAt = "yesterday" },
} {
p, mm := newerMerge, m
change(&p, &mm)
if laterOnTheBranch(mm, p) {
t.Errorf("%s was taken as a later merge of the branch", name)
}
}
// To the nanosecond: two merges within a second keep their order.
m.MergedAt = t0.Add(time.Minute + 200*time.Millisecond).Format(time.RFC3339Nano)
if !laterOnTheBranch(m, inventory.Plan{Repository: "novox/mesh-catalog", Branch: "main", Commit: "x",
Merged: t0.Add(time.Minute + 700*time.Millisecond)}) {
t.Fatal("merges within one second lost their order")
}
}
// A merge time with a fraction of a second is kept whole in its plan, and two merges within one second keep
// their order through the plans and the lookup (review of PR 179).
func TestAMergeTimeKeepsItsFractionOfASecond(t *testing.T) {
at := "2026-10-09T10:57:52.123456789Z"
p := planOfMerge(link.SourceMoved{Owner: "novox", Repo: "mesh-catalog", Base: "main", Commit: "c1", MergedAt: at}, nil, nil)
want := time.Date(2026, 10, 9, 10, 57, 52, 123456789, time.UTC)
if !p.Merged.Equal(want) {
t.Fatalf("the plan's merge time is %s, not %s", p.Merged, want)
}
earlier := planOfMerge(link.SourceMoved{Owner: "novox", Repo: "mesh-catalog", Base: "main", Commit: "c0",
MergedAt: "2026-10-09T10:57:52.123456788Z"}, nil, nil)
earlier.Created = p.Created.Add(time.Second) // made after, merged before
if !earlierOnTheBranch(earlier, p) || earlierOnTheBranch(p, earlier) {
t.Fatal("two merges a nanosecond apart lost their order")
}
m := link.SourceMoved{Owner: "novox", Repo: "mesh-catalog", Base: "main", Commit: "c0", MergedAt: "2026-10-09T10:57:52.123456788Z"}
if !laterOnTheBranch(m, p) {
t.Fatal("a merge a nanosecond later was not found as the later one")
}
}
-356
View File
@@ -1,356 +0,0 @@
package main
import (
"context"
"encoding/json"
"errors"
"reflect"
"strings"
"testing"
"time"
"github.com/novox/mesh-controller/internal/catalogue"
"github.com/novox/mesh-controller/internal/conditions"
"github.com/novox/mesh-controller/internal/inventory"
"github.com/novox/mesh-controller/internal/lease"
)
// novox/hq issue 352: on 2026-10-09 a release's gate on the control node read the machine's report against a
// newer send another plan had just made there — not against its own send — and failed three builds the
// machine had reported healthy ("has not reported on what it was sent"), put them back on every machine,
// to a controller older than the store's schema, and that controller then judged the newer plan's
// controller passed from the put-back build's health.
// TestReplay352 replays the walk on the backlog fixture: the release sends anchor and anchor reports;
// another send reaches anchor, unreported; the gate still passes. And a send that moves a judged module
// to another build supersedes the judging: no verdict, nothing put back.
func TestReplay352(t *testing.T) {
t.Run("a newer send to the judged machine does not unreport the gate's", testANewerSendDoesNotUnreportTheGatesOwn)
t.Run("a send that moves the module supersedes the judging", testASendThatMovesTheModuleSupersedesTheJudging)
}
func testANewerSendDoesNotUnreportTheGatesOwn(t *testing.T) {
b := aBacklog(t)
ctx := t.Context()
inv := b.open.inventory
advancePlans(ctx, b.open) // anchor is sent, and the fixture reports it applied
// 16:31:37 — another plan sends anchor a newer declaration, which it has not reported on.
carried, _, _ := inv.SentBuilds(ctx, "anchor")
if err := inv.RecordSent(ctx, nodeID(t, b.open, "anchor"), "d-anchor-newer", carried); err != nil {
t.Fatal(err)
}
reports, _ := inv.LastReports(ctx)
for _, r := range reports {
if r.Node == "anchor" && r.Current {
t.Fatal("the fixture's newer send reads as reported")
}
}
gateEvery, gateBound = 0, 0 // past the bound at once: before the fix, "has not reported" fails it here
for i := 0; i < 4; i++ {
advancePlans(ctx, b.open)
}
p := b.release(t)
if p.State == inventory.PlanFailed || strings.Contains(p.Note, "has not reported") {
t.Fatalf("the release failed on the newer send: %s %s", p.State, p.Note)
}
if g := p.Release.Gate; g != nil && (g.Sent == nil || g.Sent["anchor"].Digest == "") {
t.Fatalf("the gate does not keep what it sent: %+v", g)
}
if v, found, err := inv.GateOf(ctx, "build-app-c2"); err != nil || !found || v.Verdict != inventory.GatePassed {
t.Fatalf("app's pass on anchor was not kept: %+v %v %v", v, found, err)
}
if current, _ := inv.CurrentBuilds(ctx); current["app"].Commit != "c2" {
t.Fatalf("app was put back to %s", current["app"].Commit)
}
}
// A plan's own first send waits while a release judges the same module on that machine with another build.
func TestAPlansFirstSendWaitsForAReleaseJudgingTheModuleThere(t *testing.T) {
b := aBacklog(t)
ctx := t.Context()
advancePlans(ctx, b.open) // the release judges app c2 on anchor
_, _, err := gatedSend(ctx, b.open, "anchor", []inventory.CarriedMove{{Module: "app", Node: "anchor", From: "c2", To: "c3", Build: "build-app-c3"}})
if !errors.Is(err, errWalkedElsewhere) || !strings.Contains(err.Error(), "release-") {
t.Fatalf("a newer build of a judged module was sent under the release's gate: %v", err)
}
if len(b.sent) != 1 {
t.Fatalf("sent %v", b.sent)
}
}
// A merge plan's judging is superseded the same way: another send moved its module on the first machine.
func TestAPlansJudgingIsSupersededByASendThatMovesItsModule(t *testing.T) {
g := aGateMesh(t)
ctx := t.Context()
inv := g.open.inventory
advancePlans(ctx, g.open) // anchor is sent app c2 first
if err := inv.RecordSent(ctx, nodeID(t, g.open, "anchor"), "d-anchor-c3", map[string]string{"app": "c3"}); err != nil {
t.Fatal(err)
}
gateEvery = 0
advancePlans(ctx, g.open)
p := g.plan(t)
if p.State != inventory.PlanSuperseded || !strings.Contains(p.Note, "superseded") || !strings.Contains(p.Note, "c3") {
t.Fatalf("the plan is %s: %s", p.State, p.Note)
}
// Nothing put back: the registered build stands, the build is not marked, and the plan's gate made no
// rollback (a release may walk what the other send left waiting on anchor; that is not a put-back).
if r := p.Modules["app"].Gate.Rollback; r != "" {
t.Fatalf("a superseded judging made a rollback: %q", r)
}
if current, _ := inv.CurrentBuilds(ctx); current["app"].Commit != "c2" {
t.Fatalf("app was put back to %s", current["app"].Commit)
}
if failed, _ := inv.GateFailed(ctx, "build-2"); failed {
t.Fatal("a superseded build was marked failed")
}
}
func testASendThatMovesTheModuleSupersedesTheJudging(t *testing.T) {
b := aBacklog(t)
ctx := t.Context()
inv := b.open.inventory
advancePlans(ctx, b.open)
// Another send moves app on anchor to a build this gate does not judge.
carried, _, _ := inv.SentBuilds(ctx, "anchor")
carried["app"] = "c3"
if err := inv.RecordSent(ctx, nodeID(t, b.open, "anchor"), "d-anchor-c3", carried); err != nil {
t.Fatal(err)
}
gateEvery = 0
advancePlans(ctx, b.open)
p := b.release(t)
if p.State != inventory.PlanSuperseded || !strings.Contains(p.Note, "superseded") || !strings.Contains(p.Note, "c3") {
t.Fatalf("the release is %s: %s", p.State, p.Note)
}
if !reflect.DeepEqual(b.sent, [][]string{{"anchor"}}) {
t.Fatalf("sent %v: a superseded judging puts nothing back", b.sent)
}
if _, found, _ := inv.GateOf(ctx, "build-app-c2"); found {
t.Fatal("a superseded judging kept a verdict")
}
if current, _ := inv.CurrentBuilds(ctx); current["app"].Commit != "c2" {
t.Fatalf("app was put back to %s", current["app"].Commit)
}
}
// A report is on the gate's own send: the declaration itself, or one sequenced after it; a gate kept
// without its send reads the report against the send made last, as before. Pure.
func TestAReportIsHeldAgainstTheGatesOwnSend(t *testing.T) {
sent := inventory.SentDeclaration{Digest: "d-490", Sequence: 490}
for _, c := range []struct {
r inventory.Reported
want bool
}{
{inventory.Reported{Declared: "d-490", Current: false}, true},
{inventory.Reported{Declared: "d-491", ReportedSequence: 491, Current: true}, true},
{inventory.Reported{Declared: "d-489", ReportedSequence: 489, Current: false}, false},
{inventory.Reported{Declared: "other", ReportedSequence: 490}, true}, // the same sequence, said by another digest
{inventory.Reported{Declared: "d-495", ReportedSequence: 495, Current: true}, true}, // the last send: this one or a later one
{inventory.Reported{Declared: "", Current: false}, false},
} {
if got := sent.ReportsOn(c.r); got != c.want {
t.Errorf("%+v on %+v: %v", c.r, sent, got)
}
}
byDigest := inventory.SentDeclaration{Digest: "d-1"}
if !byDigest.ReportsOn(inventory.Reported{Declared: "d-1"}) || byDigest.ReportsOn(inventory.Reported{ReportedSequence: 5}) ||
!byDigest.ReportsOn(inventory.Reported{Current: true}) {
t.Fatal("a send kept without a sequence is matched by its digest and by the last send alone")
}
f := gateFacts{sent: map[string]inventory.SentDeclaration{"anchor": sent}}
if !f.reportedOn("anchor", inventory.Reported{Declared: "d-490"}) || f.reportedOn("anchor", inventory.Reported{Declared: "d-1"}) {
t.Fatal("a gate that kept its send read the report against something other than it")
}
if !f.reportedOn("laptop", inventory.Reported{Current: true}) || f.reportedOn("laptop", inventory.Reported{Current: false}) {
t.Fatal("a gate that did not keep its send does not read the report against the send made last")
}
// Through the merge plan's first-machine wait too.
at := time.Now()
state := inventory.PlanModule{First: []string{"anchor"}, FirstAt: &at,
Gate: &inventory.PlanGate{Machines: []string{"anchor"}, Sent: map[string]inventory.SentDeclaration{"anchor": sent}}}
reports := []inventory.Reported{{Node: "anchor", At: &at, Outcome: inventory.OutcomeApplied, Current: false, Declared: "d-490"}}
if step := nextRollout(state, []string{"anchor", "laptop"}, false, reports, at.Add(time.Minute), time.Hour); step.waiting != "" || step.failed != "" {
t.Fatalf("the first machine's report on the plan's own send read as none: %+v", step)
}
reports[0].Declared = "d-480"
if step := nextRollout(state, []string{"anchor", "laptop"}, false, reports, at.Add(time.Minute), time.Hour); step.waiting == "" {
t.Fatalf("a report on an older send read as the plan's: %+v", step)
}
}
// A gate judges only the build the machine was last sent: last sent another build of the module, the
// judging is superseded, whatever the machine reports. Pure.
func TestAGateJudgesOnlyTheBuildTheMachineWasLastSent(t *testing.T) {
at := time.Now()
f := gateFacts{now: at, reports: map[string]inventory.Reported{"anchor": {Node: "anchor", Outcome: inventory.OutcomeApplied,
At: &at, Current: true}}, engines: map[string]string{}, served: map[string]served{}, rolledBack: map[string][]lease.Rollback{},
commits: map[string]string{"mesh-controller": "e6b00e2e"}, sentBuilds: map[string]map[string]string{"anchor": {"mesh-controller": "ef26d4cb"}}}
taken := at.Add(-30 * time.Second)
f.holder = &lease.Holder{Taken: taken, Health: &lease.Health{Ready: true}}
h, why := judgeHealth("mesh-controller", lease.ComponentController, catalogue.Manifest{}, "anchor", at.Add(-time.Minute), f)
if h != healthSuperseded || !strings.Contains(why, "ef26d4cb") || !strings.Contains(why, "e6b00e2e") {
t.Fatalf("a controller build the machine no longer runs: %v %q", h, why)
}
f.sentBuilds["anchor"]["mesh-controller"] = "e6b00e2e"
if h, why := judgeHealth("mesh-controller", lease.ComponentController, catalogue.Manifest{}, "anchor", at.Add(-time.Minute), f); h == healthSuperseded {
t.Fatalf("the build sent read as another: %q", why)
}
delete(f.sentBuilds, "anchor")
if h, why := judgeHealth("mesh-controller", lease.ComponentController, catalogue.Manifest{}, "anchor", at.Add(-time.Minute), f); h == healthSuperseded {
t.Fatalf("a machine whose send is not known read as superseded: %q", why)
}
g := &inventory.PlanGate{To: "c2", Carried: []inventory.CarriedMove{{Module: "late", Node: "anchor", To: "c5"}}}
if got := judgedCommits(g, []judged{{"app", "anchor"}, {"late", "anchor"}}); got["app"] != "c2" || got["late"] != "c5" {
t.Fatalf("judged commits %v", got)
}
}
// A move of another build of a module to a machine where a release or a plan is judging that module
// waits for that judging; the same build to that machine is already there. Pure.
func TestAWalkWaitsForAJudgingOfTheSameModuleOnThatMachine(t *testing.T) {
at := time.Now()
release := inventory.Plan{ID: "release-1", State: inventory.PlanRolling, Release: &inventory.PlanRelease{
Gate: &inventory.PlanGate{Machines: []string{"novox"}, Carried: []inventory.CarriedMove{
{Module: "mesh-controller", Node: "novox", From: "ef26d4cb", To: "2913c54c"}}}}}
merge := inventory.Plan{ID: "plan-1", State: inventory.PlanRolling, Modules: map[string]*inventory.PlanModule{
"app": {First: []string{"anchor"}, FirstAt: &at, Commit: "c2", Gate: &inventory.PlanGate{Machines: []string{"anchor"}}}}}
f := moveFacts{plans: []inventory.Plan{release, merge}}
for _, c := range []struct {
module, node, to, want string
}{
{"mesh-controller", "novox", "e6b00e2e", "release-1"}, // the day's case: a newer controller to the judged machine
{"mesh-controller", "novox", "2913c54c", ""}, // the same build: already there
{"mesh-controller", "ace", "2913c54c", "release-1"}, // another machine while the first is judged
{"mesh-host", "novox", "x", ""}, // a module the release does not carry
{"app", "anchor", "c2", ""},
{"app", "anchor", "c3", "plan-1"},
{"app", "laptop", "c2", "plan-1"},
} {
if got := f.walkedBy(c.module, c.node, c.to); got != c.want {
t.Errorf("%s %s to %s: walked by %q, want %q", c.module, c.to, c.node, got, c.want)
}
}
release.Release.Gate.Verdict = inventory.GatePassed
merge.Modules["app"].Gate.Verdict = inventory.GatePassed
if f.walkedBy("mesh-controller", "novox", "e6b00e2e") != "" || f.walkedBy("app", "laptop", "c3") != "" {
t.Fatal("a passed judging still holds a move")
}
release.Release.Gate.Verdict = ""
f.plans[0].State = inventory.PlanSuperseded
if f.walkedBy("mesh-controller", "novox", "e6b00e2e") != "" {
t.Fatal("a closed release still holds a move")
}
}
// The controller is never put back to a build that reaches less of the store's schema than the store
// has, or to one that never said what it reaches: the current build is kept, and the condition says so.
func TestTheControllerIsNotPutBackToABuildOlderThanTheStoresSchema(t *testing.T) {
open := aMesh(t)
ctx := t.Context()
inv := open.inventory
keeper, _ := withConditionsInMemory(t)
told := &conditions.Told{}
was := doctorFrom
doctorFrom = &doctor{open: open, keeper: keeper, teller: told}
t.Cleanup(func() { doctorFrom = was })
wasSend := sendRollout
var sent [][]string
sendRollout = func(ctx context.Context, open *stores, names []string) ([]string, error) {
sent = append(sent, names)
return names, nil
}
t.Cleanup(func() { sendRollout = wasSend })
build := func(id, commit, digest string, asked time.Time) inventory.Build {
manifest, _ := json.Marshal(catalogue.Manifest{Module: "mesh-controller", Version: commit})
b := inventory.Build{ID: id, Module: "mesh-controller", Commit: commit, Repository: "novox/mesh-controller", Path: ".",
Manifest: manifest, Asked: asked, At: asked, Made: []inventory.Artifact{{Name: "controller", Kind: catalogue.ArtifactBundle,
Reference: "mesh-artifact://mesh-controller/controller/blobs/sha256:" + digest}}}
if err := inv.RecordBuild(ctx, b); err != nil {
t.Fatal(err)
}
if err := inv.RegisterModule(ctx, catalogue.Manifest{Module: "mesh-controller", Version: commit},
inventory.Source{Repository: "novox/mesh-controller", Seat: "git", Path: ".", BuiltFrom: commit, Head: commit, Asked: asked}); err != nil {
t.Fatal(err)
}
return b
}
previous := build("build-old", "ef26d4cb", strings.Repeat("1", 64), time.Now().Add(-2*time.Hour))
failed := build("build-new", "e6b00e2e", strings.Repeat("2", 64), time.Now().Add(-time.Minute))
if versionOfBuild(previous) != strings.Repeat("1", 12) {
t.Fatalf("the build's version is %q", versionOfBuild(previous))
}
applied, err := inv.SchemaApplied(ctx)
if err != nil || applied < 87 {
t.Fatalf("the store's schema reaches %d (%v)", applied, err)
}
// The build before never recorded what it reads: not proved, refused.
if why, ok := controllerSchemaAllows(ctx, inv, previous); ok || !strings.Contains(why, "never recorded") {
t.Fatalf("an unknown reach: %v %q", ok, why)
}
// It reads less than the store has: refused, naming both.
if err := inv.RecordSchemaReach(ctx, versionOfBuild(previous), applied-1); err != nil {
t.Fatal(err)
}
if why, ok := controllerSchemaAllows(ctx, inv, previous); ok || !strings.Contains(why, "is at") {
t.Fatalf("a reach behind the store: %v %q", ok, why)
}
// Through the gate: the failed build is marked, nothing is put back, nothing is sent, the condition is urgent.
at := time.Now().Add(-5 * time.Minute)
state := &inventory.PlanModule{Build: failed.ID, Commit: failed.Commit, Previous: previous.Commit, First: []string{"anchor"}, FirstAt: &at}
p := inventory.Plan{ID: "plan-352", Repository: "novox/mesh-controller", Branch: "main", Commit: failed.Commit, Created: at,
State: inventory.PlanRolling, Tiers: [][]string{{"mesh-controller"}}, Modules: map[string]*inventory.PlanModule{"mesh-controller": state}}
if err := inv.SavePlan(ctx, &p); err != nil {
t.Fatal(err)
}
gateFailed(ctx, open, &p, "mesh-controller", state, []string{"anchor"}, "not healthy within 10m0s of its apply")
if state.Gate.Rollback != inventory.NotRolledBack || !strings.Contains(p.Note, "NOT put back") || !strings.Contains(p.Note, "the current build is kept") {
t.Fatalf("rollback %q: %s", state.Gate.Rollback, p.Note)
}
if len(sent) != 0 {
t.Fatalf("sent %v: nothing is put back", sent)
}
if current, _ := inv.CurrentBuilds(ctx); current["mesh-controller"].Commit != failed.Commit {
t.Fatalf("the module was put back to %s", current["mesh-controller"].Commit)
}
if marked, _ := inv.GateFailed(ctx, failed.ID); !marked {
t.Fatal("the failed build is not marked failed at its gate")
}
open2, _ := keeper.Open(ctx)
var found bool
for _, c := range open2 {
if c.Kind == kindRollbackFailed && c.Severity == conditions.Urgent && strings.Contains(c.Summary, "current build is kept") {
found = true
}
}
if !found {
t.Fatalf("no urgent rollback-failed condition saying the current build is kept: %+v", open2)
}
// Reaching the store: allowed.
if err := inv.RecordSchemaReach(ctx, versionOfBuild(previous), applied); err != nil {
t.Fatal(err)
}
if why, ok := controllerSchemaAllows(ctx, inv, previous); !ok {
t.Fatalf("a build that reads the whole schema was refused: %q", why)
}
// A build with no bundle to know it by: refused.
if why, ok := controllerSchemaAllows(ctx, inv, inventory.Build{Commit: "x"}); ok || !strings.Contains(why, "names no bundle") {
t.Fatalf("a build without a bundle: %v %q", ok, why)
}
}
// The lease's holder names the build the declaration told it it is, and the version stamp only without one.
func TestTheHolderNamesTheBuildTheDeclarationToldIt(t *testing.T) {
t.Setenv(RunningBuildVar, " ad62528c47c7 ")
if h := holderOf("x"); h.Build != "ad62528c47c7" {
t.Fatalf("the holder's build is %q", h.Build)
}
t.Setenv(RunningBuildVar, "")
if h := holderOf("x"); h.Build != version {
t.Fatalf("without a declared version the holder's build is %q", h.Build)
}
if reach, err := schemaReach(); err != nil || reach < 87 {
t.Fatalf("this build's reach is %d (%v)", reach, err)
}
}
+1 -55
View File
@@ -98,9 +98,8 @@ func judgeNetworks(ctx context.Context, inv *inventory.Inventory, k *conditions.
problems = append(problems, err.Error()) problems = append(problems, err.Error())
} }
} }
undecided := undecidedParts(f)
for _, c := range open { for _, c := range open {
if !slices.Contains(networkKinds, c.Kind) || said[c.Key] || heldUndecided(c, undecided) { if !slices.Contains(networkKinds, c.Kind) || said[c.Key] {
continue continue
} }
why := "no machine says it any more" why := "no machine says it any more"
@@ -117,59 +116,6 @@ func judgeNetworks(ctx context.Context, inv *inventory.Inventory, k *conditions.
return nil return nil
} }
// undecidedParts is, per machine, every part of its newest statement not yet judged: starting, or unknown
// — one look failed and a second decides (novox/hq issue 348). Such a part does not say its fault is gone.
// A statement whose parts are all decided but whose whole is unknown or starting holds every part. Pure.
//
// On 2026-10-09 the control node's resolver refused every question from 10:58 to 11:18 UTC. A build of
// the node-engine sent at 10:59:34 restarted it; its first statement judged the names once (unknown, "one
// look failed; a second decides"), and that statement cleared the control node's network condition while
// its last evidence still said "connection refused". The second look raised it again forty seconds later —
// after the send — and the gate failed the build for a fault from before it.
func undecidedParts(f networkFacts) map[string]map[string]bool {
out := map[string]map[string]bool{}
for m, h := range f.healths {
if h.Network == nil {
continue
}
parts := map[string]bool{}
for _, p := range h.Network.Parts {
if p.State == link.StateUnknown || p.State == link.StateStarting {
parts[p.Part] = true
}
}
if len(parts) == 0 && (h.Network.State == link.StateUnknown || h.Network.State == link.StateStarting) {
parts["*"] = true
}
if len(parts) > 0 {
out[m] = parts
}
}
return out
}
// heldUndecided says an open network condition is kept rather than cleared: a part its newest evidence
// names is undecided in the newest statement of a machine it is about. A condition about other parts
// clears as before. Pure.
func heldUndecided(c conditions.Condition, undecided map[string]map[string]bool) bool {
said := ""
if len(c.Evidence) > 0 {
said = c.Evidence[0].Said
}
for _, m := range append([]string{c.Subject.Machine}, c.Subject.Also...) {
parts := undecided[m]
if parts["*"] {
return true
}
for part := range parts {
if strings.Contains(said, part+" since ") || strings.Contains(said, part+": ") {
return true
}
}
}
return false
}
// pointed is one machine's failing part that points at another machine. // pointed is one machine's failing part that points at another machine.
type pointed struct { type pointed struct {
from string from string
-2
View File
@@ -78,8 +78,6 @@ func run() error {
return rotateCommand(ctx, args[1:]) return rotateCommand(ctx, args[1:])
case "ask": case "ask":
return askCommand(ctx, args[1:]) return askCommand(ctx, args[1:])
case "rehearse":
return rehearseCommand(ctx, args[1:])
case "builds": case "builds":
return buildsCommand(ctx, args[1:]) return buildsCommand(ctx, args[1:])
// The build queue, controlled by hand (novox/hq ADR 0219). // The build queue, controlled by hand (novox/hq ADR 0219).
+1 -12
View File
@@ -1204,18 +1204,7 @@ func graphOfFacts(f snapshot.Facts) ([]inventory.Entry, map[string][]inventory.R
entries = append(entries, inventory.Entry{Manifest: manifest, Provided: mod.Provided, entries = append(entries, inventory.Entry{Manifest: manifest, Provided: mod.Provided,
Source: inventory.Source{Repository: mod.Repository, Path: mod.Path, BuiltFrom: mod.Commit}}) Source: inventory.Source{Repository: mod.Repository, Path: mod.Path, BuiltFrom: mod.Commit}})
for _, r := range mod.Reads { for _, r := range mod.Reads {
entry := inventory.ReadRepository{Repository: r} read[mod.Name] = append(read[mod.Name], inventory.ReadRepository{Repository: r})
for _, s := range mod.Sources {
if !s.Own && s.Repository == r && len(s.Paths) > 0 {
entry.Paths = s.Paths
}
}
read[mod.Name] = append(read[mod.Name], entry)
}
for _, s := range mod.Sources {
if s.Own && len(s.Paths) > 0 {
read[mod.Name] = append(read[mod.Name], inventory.ReadRepository{Own: true, Paths: s.Paths})
}
} }
} }
var edges []inventory.Edge var edges []inventory.Edge
+1 -11
View File
@@ -141,12 +141,6 @@ func runForMeshCLI(ctx context.Context, node string, asked link.CLIAsked, v cliV
if v.refused != "" { if v.refused != "" {
return link.CLIRefusal(v.refused) return link.CLIRefusal(v.refused)
} }
// Standard input is the terminal's alone: a secret given at the terminal reaches `secret accept`, and no ordinary
// call is handed what the asker's standard input held (novox/hq ADR 0259 §10, ADR 0272).
if len(asked.Stdin) > 0 && !v.terminal {
return link.CLIAnswer{Exit: 1, Why: v.why, Refused: "standard input is given to a line that runs as the " +
"controller's terminal alone, and this one does not. Nothing ran"}
}
if cliServers[asked.Line[0]] { if cliServers[asked.Line[0]] {
return link.CLIAnswer{Exit: 1, Why: v.why, Refused: fmt.Sprintf("%s serves until stopped, and is not a "+ return link.CLIAnswer{Exit: 1, Why: v.why, Refused: fmt.Sprintf("%s serves until stopped, and is not a "+
"command line mesh-cli runs. Nothing ran", asked.Line[0])} "command line mesh-cli runs. Nothing ran", asked.Line[0])}
@@ -161,12 +155,8 @@ func runForMeshCLI(ctx context.Context, node string, asked link.CLIAsked, v cliV
} }
cmd := selfCommand(ctx, line) cmd := selfCommand(ctx, line)
cmd.Env = commandEnvironment(fmt.Sprintf("%s through mesh-cli on %s", asked.Account, node), verb, v.terminal) cmd.Env = commandEnvironment(fmt.Sprintf("%s through mesh-cli on %s", asked.Account, node), verb, v.terminal)
// No standard input unless mesh-cli carried one for a terminal line: a command that reads one gets nothing, and // No standard input: a command that reads one gets nothing, and fails saying so (ADR 0272 §5).
// fails saying so (ADR 0272 §5).
cmd.Stdin = nil cmd.Stdin = nil
if len(asked.Stdin) > 0 {
cmd.Stdin = bytes.NewReader(asked.Stdin)
}
var stdout, stderr bytes.Buffer var stdout, stderr bytes.Buffer
cmd.Stdout, cmd.Stderr = &stdout, &stderr cmd.Stdout, cmd.Stderr = &stdout, &stderr
err := cmd.Run() err := cmd.Run()
-99
View File
@@ -1,99 +0,0 @@
package main
import (
"context"
"crypto/sha256"
"encoding/base64"
"encoding/hex"
"encoding/json"
"fmt"
"strings"
"sync"
"testing"
)
// secretAcceptWants makes the test binary, run as a command line, read a secret as `secret accept` reads it and
// say whether it is the value whose SHA-256 the variable names (TestMain).
const secretAcceptWants = "MESH_TEST_SECRET_ACCEPT_WANTS"
// readAsSecretAccept is that process: `secret accept <node> <module> <name> [--from -]`, the value read by
// valueFor, compared by digest, and only the verdict printed.
func readAsSecretAccept(want string, argv []string) int {
if len(argv) < 5 || argv[0] != "secret" || argv[1] != "accept" {
fmt.Printf("not a secret accept line: %q\n", argv)
return 2
}
from := ""
if len(argv) == 7 && argv[5] == "--from" {
from = argv[6]
}
value, err := valueFor(argv[2], argv[3], argv[4], from)
if err != nil {
fmt.Printf("secret accept read nothing: %v\n", err)
return 1
}
sum := sha256.Sum256([]byte(asSupplied(value)))
if hex.EncodeToString(sum[:]) != want {
fmt.Printf("secret accept read something else (%d bytes)\n", len(value))
return 1
}
fmt.Println("secret accept read the value it was given")
return 0
}
// novox/hq ADR 0259 §10, ADR 0272: what mesh-cli's standard input held reaches `secret accept` on a line that runs
// as the controller's terminal, and appears nowhere else — not in the answer, not in the journal, not in the calls
// record; an ordinary line carrying it is refused and nothing runs.
func TestStandardInputReachesSecretAcceptAtTheTerminalAndNowhereElse(t *testing.T) {
token := "123456789:AAEhBOweik6ad9r_QxGivenAtTheTerminal"
sum := sha256.Sum256([]byte(token))
t.Setenv(secretAcceptWants, hex.EncodeToString(sum[:]))
var journal []string
var mu sync.Mutex
was := cliJournal
cliJournal = func(line string) { mu.Lock(); journal = append(journal, line); mu.Unlock() }
t.Cleanup(func() { cliJournal = was })
ctx := context.Background()
for _, line := range [][]string{
{"secret", "accept", "anchor", "telegram", "telegram-token", "--from", "-"},
{"secret", "accept", "anchor", "telegram", "telegram-token"}, // the prompt's path, a line on standard input
} {
asked := cliAsked("operator", 1000, line...)
asked.Stdin = []byte(token + "\n")
a := runForMeshCLI(ctx, "control", asked, cliVerdict{terminal: true, why: "the terminal"})
if a.Exit != 0 || !strings.Contains(string(a.Stdout), "read the value it was given") {
t.Fatalf("%q: secret accept did not read what mesh-cli carried: %+v (%s)", line, a, a.Stdout)
}
if body, _ := json.Marshal(a); strings.Contains(string(body), "AAEh") || strings.Contains(string(body), base64.StdEncoding.EncodeToString([]byte(token))) {
t.Fatalf("the answer carries the secret: %s", body)
}
}
// Without standard input, the line reads nothing, as before.
a := runForMeshCLI(ctx, "control", cliAsked("operator", 1000, "secret", "accept", "anchor", "telegram",
"telegram-token", "--from", "-"), cliVerdict{terminal: true, why: "the terminal"})
if a.Exit == 0 {
t.Fatalf("a line with no standard input read a value: %+v", a)
}
// An ordinary call is never handed it: refused, and nothing ran.
asked := cliAsked("operator", 1000, "status")
asked.Stdin = []byte(token)
a = runForMeshCLI(ctx, "laptop", asked, cliVerdict{why: "not the terminal"})
if a.Exit == 0 || !strings.Contains(a.Refused, "terminal alone") || len(a.Stdout) != 0 {
t.Fatalf("an ordinary line was given standard input: %+v", a)
}
mu.Lock()
defer mu.Unlock()
for _, l := range journal {
if strings.Contains(l, "AAEh") {
t.Fatalf("the journal says the secret: %s", l)
}
}
if len(journal) == 0 {
t.Fatal("the lines were not said in the journal at all")
}
}
+19 -19
View File
@@ -24,7 +24,7 @@ var cliNodes = []inventory.Node{
{Name: "unnamed"}, {Name: "unnamed"},
} }
func cliAsked(account string, uid uint32, line ...string) link.CLIAsked { func asked(account string, uid uint32, line ...string) link.CLIAsked {
return link.CLIAsked{Line: line, Account: account, UID: uid, Session: "session-1.scope"} return link.CLIAsked{Line: line, Account: account, UID: uid, Session: "session-1.scope"}
} }
@@ -39,13 +39,13 @@ func TestMeshCLIIsTheTerminalOnlyForTheControlNodesOperator(t *testing.T) {
refused string refused string
why string why string
}{ }{
{"the control-node's operator", "control", cliAsked("operator", 1000, "status"), control, true, "", "the controller's terminal"}, {"the control-node's operator", "control", asked("operator", 1000, "status"), control, true, "", "the controller's terminal"},
{"another node's operator", "laptop", cliAsked("operator", 1000, "status"), control, false, "", "agents on laptop may run as operator"}, {"another node's operator", "laptop", asked("operator", 1000, "status"), control, false, "", "agents on laptop may run as operator"},
{"another account", "control", cliAsked("agent", 1001, "status"), control, false, "operator account (operator) only", ""}, {"another account", "control", asked("agent", 1001, "status"), control, false, "operator account (operator) only", ""},
{"root", "control", cliAsked("root", 0, "status"), control, false, "never root", ""}, {"root", "control", asked("root", 0, "status"), control, false, "never root", ""},
{"a node with no operator account", "unnamed", cliAsked("operator", 1000, "status"), control, false, "does not know unnamed's operator account", ""}, {"a node with no operator account", "unnamed", asked("operator", 1000, "status"), control, false, "does not know unnamed's operator account", ""},
{"a node the mesh does not know", "elsewhere", cliAsked("operator", 1000, "status"), control, false, "not a node this mesh knows", ""}, {"a node the mesh does not know", "elsewhere", asked("operator", 1000, "status"), control, false, "not a node this mesh knows", ""},
{"two control-nodes", "control", cliAsked("operator", 1000, "status"), []string{"control", "laptop"}, false, "", "2 control-nodes"}, {"two control-nodes", "control", asked("operator", 1000, "status"), []string{"control", "laptop"}, false, "", "2 control-nodes"},
} }
for _, c := range cases { for _, c := range cases {
v := judgeCLI(c.node, c.asked, cliNodes, c.control) v := judgeCLI(c.node, c.asked, cliNodes, c.control)
@@ -70,7 +70,7 @@ func TestTheTerminalRunsWithoutAVerbAndAnOrdinaryCallNamesMeshCLI(t *testing.T)
t.Setenv(servedVar, "1") t.Setenv(servedVar, "1")
ctx := context.Background() ctx := context.Background()
a := runForMeshCLI(ctx, "control", cliAsked("operator", 1000, "status"), cliVerdict{terminal: true, why: "the terminal"}) a := runForMeshCLI(ctx, "control", asked("operator", 1000, "status"), cliVerdict{terminal: true, why: "the terminal"})
if a.Exit != 0 || a.Refused != "" || !a.Terminal { if a.Exit != 0 || a.Refused != "" || !a.Terminal {
t.Fatalf("the terminal's line did not run: %+v", a) t.Fatalf("the terminal's line did not run: %+v", a)
} }
@@ -79,7 +79,7 @@ func TestTheTerminalRunsWithoutAVerbAndAnOrdinaryCallNamesMeshCLI(t *testing.T)
t.Fatalf("the terminal's line ran with %s", got) t.Fatalf("the terminal's line ran with %s", got)
} }
a = runForMeshCLI(ctx, "laptop", cliAsked("operator", 1000, "status"), cliVerdict{why: "not the terminal"}) a = runForMeshCLI(ctx, "laptop", asked("operator", 1000, "status"), cliVerdict{why: "not the terminal"})
if a.Exit != 0 || a.Terminal || a.Why != "not the terminal" { if a.Exit != 0 || a.Terminal || a.Why != "not the terminal" {
t.Fatalf("an ordinary line did not run as one: %+v", a) t.Fatalf("an ordinary line did not run as one: %+v", a)
} }
@@ -93,21 +93,21 @@ func TestAnOrdinaryCallMeetsTheCommandVerbsRefusals(t *testing.T) {
t.Setenv(echoEnvironment, "1") t.Setenv(echoEnvironment, "1")
ctx := context.Background() ctx := context.Background()
ordinary := cliVerdict{why: "not the terminal"} ordinary := cliVerdict{why: "not the terminal"}
a := runForMeshCLI(ctx, "laptop", cliAsked("operator", 1000, "cleanup", "delete", "x"), ordinary) a := runForMeshCLI(ctx, "laptop", asked("operator", 1000, "cleanup", "delete", "x"), ordinary)
if a.Refused == "" || len(a.Stdout) != 0 || a.Exit != 1 || a.Why != "not the terminal" { if a.Refused == "" || len(a.Stdout) != 0 || a.Exit != 1 || a.Why != "not the terminal" {
t.Fatalf("a repair without --why ran as an ordinary call: %+v", a) t.Fatalf("a repair without --why ran as an ordinary call: %+v", a)
} }
a = runForMeshCLI(ctx, "laptop", cliAsked("operator", 1000, "settings", "set", "claude-code", "{}"), ordinary) a = runForMeshCLI(ctx, "laptop", asked("operator", 1000, "settings", "set", "claude-code", "{}"), ordinary)
if a.Refused != "" || !strings.Contains(string(a.Stdout), `verb="mesh-cli"`) { if a.Refused != "" || !strings.Contains(string(a.Stdout), `verb="mesh-cli"`) {
t.Fatalf("an ordinary settings set did not run through the settings verb's path with MESH_VERB set: %+v", a) t.Fatalf("an ordinary settings set did not run through the settings verb's path with MESH_VERB set: %+v", a)
} }
for _, server := range []string{"serve", "api", "board"} { for _, server := range []string{"serve", "api", "board"} {
a := runForMeshCLI(ctx, "control", cliAsked("operator", 1000, server), cliVerdict{terminal: true}) a := runForMeshCLI(ctx, "control", asked("operator", 1000, server), cliVerdict{terminal: true})
if a.Refused == "" || len(a.Stdout) != 0 { if a.Refused == "" || len(a.Stdout) != 0 {
t.Fatalf("%s was run for mesh-cli: %+v", server, a) t.Fatalf("%s was run for mesh-cli: %+v", server, a)
} }
} }
a = runForMeshCLI(ctx, "control", cliAsked("agent", 1001, "status"), cliVerdict{refused: "agent is not answered"}) a = runForMeshCLI(ctx, "control", asked("agent", 1001, "status"), cliVerdict{refused: "agent is not answered"})
if a.Refused != "agent is not answered" || len(a.Stdout) != 0 { if a.Refused != "agent is not answered" || len(a.Stdout) != 0 {
t.Fatalf("a refused line ran: %+v", a) t.Fatalf("a refused line ran: %+v", a)
} }
@@ -184,9 +184,9 @@ func TestEveryMeshCLILineIsSaidInTheJournal(t *testing.T) {
cliJournal = func(line string) { said = append(said, line) } cliJournal = func(line string) { said = append(said, line) }
t.Cleanup(func() { cliJournal = was }) t.Cleanup(func() { cliJournal = was })
ctx := link.WithCallID(context.Background(), "call-1") ctx := link.WithCallID(context.Background(), "call-1")
runForMeshCLI(ctx, "control", cliAsked("operator", 1000, "settings", "set", "x", `{"password":"s3cret"}`), runForMeshCLI(ctx, "control", asked("operator", 1000, "settings", "set", "x", `{"password":"s3cret"}`),
cliVerdict{terminal: true, why: "the terminal"}) cliVerdict{terminal: true, why: "the terminal"})
runForMeshCLI(ctx, "control", cliAsked("agent", 1001, "status"), cliVerdict{refused: "agent is not answered"}) runForMeshCLI(ctx, "control", asked("agent", 1001, "status"), cliVerdict{refused: "agent is not answered"})
all := strings.Join(said, "\n") all := strings.Join(said, "\n")
if len(said) != 2 || !strings.Contains(all, "call-1") || !strings.Contains(all, "operator on control") || if len(said) != 2 || !strings.Contains(all, "call-1") || !strings.Contains(all, "operator on control") ||
!strings.Contains(all, "as the controller's terminal") || !strings.Contains(all, "refused") { !strings.Contains(all, "as the controller's terminal") || !strings.Contains(all, "refused") {
@@ -213,7 +213,7 @@ func TestAnOrdinaryLineRunsNothingTheCommandVerbWouldRefuse(t *testing.T) {
if _, err := ordinaryLine(line); err == nil { if _, err := ordinaryLine(line); err == nil {
t.Errorf("%q composed as an ordinary line", line) t.Errorf("%q composed as an ordinary line", line)
} }
a := runForMeshCLI(context.Background(), "laptop", cliAsked("operator", 1000, line...), cliVerdict{why: "not the terminal"}) a := runForMeshCLI(context.Background(), "laptop", asked("operator", 1000, line...), cliVerdict{why: "not the terminal"})
if a.Refused == "" || len(a.Stdout) != 0 { if a.Refused == "" || len(a.Stdout) != 0 {
t.Errorf("%q ran as an ordinary line: %+v", line, a) t.Errorf("%q ran as an ordinary line: %+v", line, a)
} }
@@ -279,11 +279,11 @@ func TestTheTerminalsMarkIsStrippedFromEveryOtherLine(t *testing.T) {
} }
} }
} }
a := runForMeshCLI(context.Background(), "laptop", cliAsked("operator", 1000, "status"), cliVerdict{why: "not the terminal"}) a := runForMeshCLI(context.Background(), "laptop", asked("operator", 1000, "status"), cliVerdict{why: "not the terminal"})
if got := string(a.Stdout); !strings.Contains(got, "terminal=false") || !strings.Contains(got, `verb="mesh-cli"`) { if got := string(a.Stdout); !strings.Contains(got, "terminal=false") || !strings.Contains(got, `verb="mesh-cli"`) {
t.Fatalf("an ordinary line with the mark in the serving environment ran as %s", got) t.Fatalf("an ordinary line with the mark in the serving environment ran as %s", got)
} }
a = runForMeshCLI(context.Background(), "control", cliAsked("operator", 1000, "status"), cliVerdict{terminal: true}) a = runForMeshCLI(context.Background(), "control", asked("operator", 1000, "status"), cliVerdict{terminal: true})
if got := string(a.Stdout); !strings.Contains(got, "terminal=true") { if got := string(a.Stdout); !strings.Contains(got, "terminal=true") {
t.Fatalf("the terminal's line ran as %s", got) t.Fatalf("the terminal's line ran as %s", got)
} }
+1 -6
View File
@@ -48,7 +48,7 @@ func catchingUpOnMerges(ctx context.Context, open *stores, announced merges) {
if err != nil { if err != nil {
return nil, nil, err return nil, nil, err
} }
read, err := readForPlanning(ctx, open.inventory) read, err := open.inventory.ReadRepositories(ctx)
return entries, read, err return entries, read, err
} }
failing := "" failing := ""
@@ -102,11 +102,6 @@ func catchUpOnMerges(ctx context.Context, now time.Time, announced merges,
return err return err
} }
for _, a := range all { for _, a := range all {
// A merge the forge said no time of is dated by its announcement, so a packaging module's look can
// make it history once acted on, and the catch-up does not act on it again every pass (ADR 0267).
if a.SourceMoved.MergedAt == "" && !a.At.IsZero() {
a.SourceMoved.MergedAt = a.At.UTC().Format(time.RFC3339)
}
if now.Sub(a.At) < mergeGrace { if now.Sub(a.At) < mergeGrace {
continue continue
} }
+3 -6
View File
@@ -402,7 +402,6 @@ func moduleUnhealthyObservation(module, node string, rs []inventory.ResourceHeal
Explanation: fmt.Sprintf("%s on %s is not healthy: %s. It clears as soon as it runs again.", module, node, Explanation: fmt.Sprintf("%s on %s is not healthy: %s. It clears as soon as it runs again.", module, node,
namesWords(plain, 3)), namesWords(plain, 3)),
Needs: needs, Needs: needs,
Actions: moduleActions(node, rs),
Resolved: fmt.Sprintf("%s works again on %s", module, node)} Resolved: fmt.Sprintf("%s works again on %s", module, node)}
} }
@@ -556,8 +555,8 @@ func moduleHealthWord(module, machine string, since time.Time, f gateFacts) (hea
said = append(said, wait) said = append(said, wait)
} }
if len(found) > 0 { if len(found) > 0 {
said = append(said, fmt.Sprintf("on %s, %s uses %s as found and waits for the operator to hand it over "+ said = append(said, fmt.Sprintf("on %s, %s uses %s as found and waits for a person to hand it over "+
"(`nox node hand-over %s <directory>` on the control-node)", machine, module, strings.Join(found, ", "), machine)) "(`mesh-host hand-over <directory>` at the machine)", machine, module, strings.Join(found, ", ")))
} }
return healthPerson, strings.Join(said, "; ") return healthPerson, strings.Join(said, "; ")
} }
@@ -678,9 +677,7 @@ func usedAsFoundObservation(module, node, said string, rs []inventory.ResourceHe
o.Explanation = fmt.Sprintf("A directory of %s was already on %s, with another owner or mode than %s declares. "+ o.Explanation = fmt.Sprintf("A directory of %s was already on %s, with another owner or mode than %s declares. "+
"The mesh left it as it was rather than hand it to an account, so %s may not be able to use it.", "The mesh left it as it was rather than hand it to an account, so %s may not be able to use it.",
module, node, module, module) module, node, module, module)
// Plain words (ADR 0253): the line itself — `nox node hand-over <node> <path>` on the control-node (ADR 0272, o.Needs = fmt.Sprintf("on %s, run mesh-host hand-over with the directory's path as root.", node)
// issue 356) — is in the summary and the evidence, which name the directory; a path is never in these.
o.Needs = "hand the directory over from the control-node, as the operator; the details name it and the line to type."
o.Resolved = fmt.Sprintf("%s's directory on %s is the mesh's", module, node) o.Resolved = fmt.Sprintf("%s's directory on %s is the mesh's", module, node)
o.Actions = nil o.Actions = nil
return o return o
+2 -2
View File
@@ -426,10 +426,10 @@ func overlayShow(ctx context.Context, open *stores) error {
tunnel.Interface, tunnel.Range, tunnel.Port) tunnel.Interface, tunnel.Range, tunnel.Port)
case n.Hub && hubName == n.Name && tunnel.Interface != "": case n.Hub && hubName == n.Name && tunnel.Interface != "":
fmt.Printf(" hub — found a tunnel on %s and did NOT take it over: its key is not the tunnel's; "+ fmt.Printf(" hub — found a tunnel on %s and did NOT take it over: its key is not the tunnel's; "+
"`nox-mesh-host overlay take --tunnel %s` on the machine takes it", tunnel.Interface, tunnel.Interface) "`mesh-host overlay take --tunnel %s` on the machine takes it", tunnel.Interface, tunnel.Interface)
case n.Hub: case n.Hub:
fmt.Print(" hub — found no tunnel; if the machine runs the predecessor's, " + fmt.Print(" hub — found no tunnel; if the machine runs the predecessor's, " +
"`nox-mesh-host overlay take --tunnel <iface>` there adopts it (novox/hq ADR 0105)") "`mesh-host overlay take --tunnel <iface>` there adopts it (novox/hq ADR 0105)")
case !n.Reachable(): case !n.Reachable():
fmt.Print(" not dialable") fmt.Print(" not dialable")
} }
+2 -156
View File
@@ -10,7 +10,6 @@ import (
"io" "io"
"net" "net"
"os" "os"
"path/filepath"
"strings" "strings"
"time" "time"
@@ -18,7 +17,6 @@ import (
"github.com/novox/mesh-controller/internal/catalogue" "github.com/novox/mesh-controller/internal/catalogue"
"github.com/novox/mesh-controller/internal/conditions" "github.com/novox/mesh-controller/internal/conditions"
"github.com/novox/mesh-controller/internal/inventory" "github.com/novox/mesh-controller/internal/inventory"
"github.com/novox/mesh-controller/internal/link"
"github.com/novox/mesh-controller/internal/token" "github.com/novox/mesh-controller/internal/token"
) )
@@ -30,7 +28,7 @@ import (
func nodeCommand(ctx context.Context, args []string) error { func nodeCommand(ctx context.Context, args []string) error {
if len(args) == 0 { if len(args) == 0 {
return errors.New("node add <name>, node list, node show <name>, " + publicDomainUsage + ", or " + handOverUsage) return errors.New("node add <name>, node list, node show <name>, or " + publicDomainUsage)
} }
open, err := openStores(ctx) open, err := openStores(ctx)
if err != nil { if err != nil {
@@ -114,163 +112,11 @@ func nodeCommand(ctx context.Context, args []string) error {
// an optional second argument is the home when it is not /home/<account>. // an optional second argument is the home when it is not /home/<account>.
return nodeAccount(ctx, inv, args[1:]) return nodeAccount(ctx, inv, args[1:])
case "hand-over":
// A directory the node-engine uses as found, handed to the mesh (novox/hq issue 356, issue 339). Here, at
// the controller's terminal, and nowhere else: at the next apply root gives the directory to the account
// the module declares, and whoever may call a verb includes agents.
return nodeHandOver(ctx, open, args[1:])
case "setuid-search":
// A fresh search for setuid programs on a node (novox/hq issue 361), after the operator changed by hand
// what the last one found. Here, at the controller's terminal, and nowhere else: a search never makes a
// machine free wrongly, but asked again and again it would keep the machine unjudged and its disks busy,
// and whoever may call a verb includes agents.
return nodeSetuidSearch(ctx, open, args[1:])
default: default:
return fmt.Errorf("node has no %q; it has add, list, show, public-domain, account, agent-account, hand-over "+ return fmt.Errorf("node has no %q; it has add, list, show, public-domain, account and agent-account", args[0])
"and setuid-search", args[0])
} }
} }
const handOverUsage = "node hand-over <node> <directory> — hand a directory the node-engine on <node> uses as found " +
"to the mesh: its next apply gives it the declared owner and mode. The directory's absolute path, as the module's " +
"condition names it"
// handOverLine reads a hand-over's line: the node and the directory's absolute path, exactly as the engine states
// it. Judged before anything is asked, and judged again by the engine, which is the one that acts.
func handOverLine(args []string) (node, path string, err error) {
if len(args) != 2 {
return "", "", errors.New(handOverUsage)
}
node, path = args[0], args[1]
if node == "" || strings.HasPrefix(node, "-") {
return "", "", fmt.Errorf("%q is not a node's name; %s", node, handOverUsage)
}
if !filepath.IsAbs(path) {
return "", "", fmt.Errorf("%q is not an absolute path; %s", path, handOverUsage)
}
if filepath.Clean(path) != path {
return "", "", fmt.Errorf("%q is not the directory's path as the engine states it (no `..`, no doubled or "+
"trailing separator); %s", path, handOverUsage)
}
return node, path, nil
}
// handOverBy is who hands the directory over, in the words a verb's caller is recorded in: the operator through
// mesh-cli on the control-node, or whoever runs this controller's binary at its terminal.
func handOverBy() string {
if by := strings.TrimSpace(os.Getenv(link.CallerVar)); by != "" {
return by
}
return "the controller's terminal"
}
// nodeHandOver asks the node's engine to take a directory it uses as found as the mesh's, and says what came of
// it. The engine records the hand-over or refuses; nothing is recorded here, because the directory is the
// machine's and the engine is the one that reads it. The ask is signed with the mesh's key (issue 356's review).
func nodeHandOver(ctx context.Context, open *stores, args []string) error {
known := func(node string) error {
_, err := open.inventory.NodeByName(ctx, node)
return err
}
ask := func(node, path, by string) (link.HandOverAnswer, error) {
ident, err := open.Identity(ctx)
if err != nil {
return link.HandOverAnswer{}, fmt.Errorf("the mesh's signing key cannot be read, so nothing was asked of %s: %w",
node, err)
}
address, err := broker.BusAddress()
if err != nil {
return link.HandOverAnswer{}, err
}
js, err := broker.Dial(address)
if err != nil {
return link.HandOverAnswer{}, fmt.Errorf("cannot reach the bus, so nothing was asked of %s: %w", node, err)
}
defer js.Close()
return link.AskHandOver(ctx, js.Conn(), ident, node, path, by, link.HandOverWithin)
}
return handOverAsked(args, known, ask, os.Stdout)
}
// handOverAsked is the hand-over's line with its two acts given: whether the mesh knows the node, and the ask.
// Nothing is asked of a line or a node that is refused, and the engine's refusal is this command's failure —
// never a success with the refusal printed.
func handOverAsked(args []string, known func(node string) error,
ask func(node, path, by string) (link.HandOverAnswer, error), out io.Writer) error {
node, path, err := handOverLine(args)
if err != nil {
return err
}
if err := known(node); err != nil {
return fmt.Errorf("nothing was asked: %w", err)
}
answer, err := ask(node, path, handOverBy())
if err != nil {
return err
}
if answer.Refused != "" {
return fmt.Errorf("%s refused: %s", node, answer.Refused)
}
fmt.Fprintln(out, answer.Said)
fmt.Fprintf(out, " the module's condition clears once %s applies; `nox push %s` applies it now\n", node, node)
return nil
}
const setuidSearchUsage = "node setuid-search <node> — throw away the node-engine's last search for setuid " +
"programs on <node> and start a full one: after a setuid-root program it found was removed by hand. Until it " +
"completes, root-free says the node is not judged yet"
// nodeSetuidSearch asks the node's engine for a fresh search, signed with the mesh's key as a hand-over is.
func nodeSetuidSearch(ctx context.Context, open *stores, args []string) error {
known := func(node string) error {
_, err := open.inventory.NodeByName(ctx, node)
return err
}
ask := func(node, by string) (link.HandOverAnswer, error) {
ident, err := open.Identity(ctx)
if err != nil {
return link.HandOverAnswer{}, fmt.Errorf("the mesh's signing key cannot be read, so nothing was asked of %s: %w",
node, err)
}
address, err := broker.BusAddress()
if err != nil {
return link.HandOverAnswer{}, err
}
js, err := broker.Dial(address)
if err != nil {
return link.HandOverAnswer{}, fmt.Errorf("cannot reach the bus, so nothing was asked of %s: %w", node, err)
}
defer js.Close()
return link.AskSetuidSearch(ctx, js.Conn(), ident, node, by, link.HandOverWithin)
}
return setuidSearchAsked(args, known, ask, os.Stdout)
}
// setuidSearchAsked is the line with its two acts given: whether the mesh knows the node, and the ask. The
// engine's refusal is this command's failure.
func setuidSearchAsked(args []string, known func(node string) error,
ask func(node, by string) (link.HandOverAnswer, error), out io.Writer) error {
if len(args) != 1 || args[0] == "" || strings.HasPrefix(args[0], "-") {
return errors.New(setuidSearchUsage)
}
node := args[0]
if err := known(node); err != nil {
return fmt.Errorf("nothing was asked: %w", err)
}
answer, err := ask(node, handOverBy())
if err != nil {
return err
}
if answer.Refused != "" {
return fmt.Errorf("%s refused: %s", node, answer.Refused)
}
fmt.Fprintln(out, answer.Said)
fmt.Fprintf(out, " the controller's root-free verb shows the search's progress until it completes\n")
return nil
}
// addNode creates a node record, adopted when the operator says so (novox/hq ADR 0100). // addNode creates a node record, adopted when the operator says so (novox/hq ADR 0100).
func addNode(ctx context.Context, inv *inventory.Inventory, args []string) error { func addNode(ctx context.Context, inv *inventory.Inventory, args []string) error {
set := flag.NewFlagSet("node add", flag.ContinueOnError) set := flag.NewFlagSet("node add", flag.ContinueOnError)
+2 -2
View File
@@ -157,7 +157,7 @@ func TestAMergeRebuildsTheModulesItChanged(t *testing.T) {
{"a file directly among the modules", merge([]string{"modules/README.md"}, false), ""}, {"a file directly among the modules", merge([]string{"modules/README.md"}, false), ""},
{"a root file beside a module's", merge([]string{"merge-check.sh", "modules/keycloak/x.ts"}, false), "keycloak"}, {"a root file beside a module's", merge([]string{"merge-check.sh", "modules/keycloak/x.ts"}, false), "keycloak"},
} { } {
if got := named(whatTheMergeTouched(candidates, known, c.m, nil)); got != c.want { if got := named(whatTheMergeTouched(candidates, known, c.m)); got != c.want {
t.Errorf("%s: rebuilt %q, wanted %q", c.what, got, c.want) t.Errorf("%s: rebuilt %q, wanted %q", c.what, got, c.want)
} }
} }
@@ -285,7 +285,7 @@ func TestAChangeInsideAModuleIsThatModulesHeldOrNot(t *testing.T) {
{"an old announcer saying nothing", merge(showcase, nil, false), ""}, {"an old announcer saying nothing", merge(showcase, nil, false), ""},
{"a manifest the merge removed, said or not", merge([]string{"modules/gone/module.json", "modules/gone/x.ts"}, nil, true), ""}, {"a manifest the merge removed, said or not", merge([]string{"modules/gone/module.json", "modules/gone/x.ts"}, nil, true), ""},
} { } {
if got := named(whatTheMergeTouched(candidates, known, c.m, nil)); got != c.want { if got := named(whatTheMergeTouched(candidates, known, c.m)); got != c.want {
t.Errorf("%s: rebuilt %q, wanted %q", c.what, got, c.want) t.Errorf("%s: rebuilt %q, wanted %q", c.what, got, c.want)
} }
} }
+4 -48
View File
@@ -680,8 +680,6 @@ func walkWaitingWords(w waitFacts, in time.Duration, severity conditions.Severit
} }
// waitingNeeds is what the operator does about a walk waiting past its urgent bound: nothing before it. // waitingNeeds is what the operator does about a walk waiting past its urgent bound: nothing before it.
// Start and Stop are also asked of the operator (novox/hq ADR 0259); the condition's own words keep saying
// where they are given without a channel, and the ask's text drops that (askText).
func waitingNeeds(severity conditions.Severity) string { func waitingNeeds(severity conditions.Severity) string {
if severity == conditions.Urgent { if severity == conditions.Urgent {
return "start it, or stop it, " + FromMeshMCPServer return "start it, or stop it, " + FromMeshMCPServer
@@ -689,20 +687,6 @@ func waitingNeeds(severity conditions.Severity) string {
return "" return ""
} }
// waitingActions are the answers to a walk waiting past its urgent bound: start it, or stop it — the plan's
// own verbs, approved by the operator (novox/hq ADR 0259). None before the bound.
func waitingActions(plan string, severity conditions.Severity) []conditions.Action {
if severity != conditions.Urgent || plan == "" {
return nil
}
return []conditions.Action{
{Label: "Start", Verb: "mesh-controller.plans", Level: conditions.LevelApprove,
Arguments: map[string]string{"go": plan, "why": "", "cause": conditions.CauseOperatorAnswer}},
{Label: "Stop", Verb: "mesh-controller.plans", Level: conditions.LevelApprove,
Arguments: map[string]string{"stop": plan, "why": "", "cause": conditions.CauseOperatorAnswer}},
}
}
// moduleNeeds is what the operator can do about a module unhealthy on a machine: log in again where its // moduleNeeds is what the operator can do about a module unhealthy on a machine: log in again where its
// account's groups wait for it (ADR 0252), restart a failed service, or nothing where the mesh restarts it. // account's groups wait for it (ADR 0252), restart a failed service, or nothing where the mesh restarts it.
// No answer is offered for a restart: a desk click performs only an acknowledgement (ADR 0258). // No answer is offered for a restart: a desk click performs only an acknowledgement (ADR 0258).
@@ -717,35 +701,11 @@ func moduleNeeds(node string, rs []inventory.ResourceHealth) string {
} }
} }
if unit != "" { if unit != "" {
// Also asked of the operator (moduleActions); the ask's text drops where (askText).
return fmt.Sprintf("restart its service %s on %s %s", unit, node, FromMeshMCPServer) return fmt.Sprintf("restart its service %s on %s %s", unit, node, FromMeshMCPServer)
} }
return "" return ""
} }
// moduleActions are the answers to a module unhealthy on a machine: restart its failed service there,
// approved by the operator (novox/hq ADR 0259) — none when the mesh restarts it, or a new login is what it
// waits for.
func moduleActions(node string, rs []inventory.ResourceHealth) []conditions.Action {
for _, r := range rs {
if strings.Contains(r.Reason, "relogin needed") {
return nil
}
}
for _, r := range rs {
if r.Kind != link.KindUnit || r.Target == "" {
continue
}
scope := "system"
if r.Account != "" {
scope = "user"
}
return []conditions.Action{{Label: "Restart", Verb: "node-service-manager.restart", Machine: node,
Level: conditions.LevelApprove, Arguments: map[string]string{"unit": r.Target, "scope": scope}}}
}
return nil
}
// FromMeshMCPServer ends what the operator needs when no notification can do it (ADR 0258), naming the mesh MCP // FromMeshMCPServer ends what the operator needs when no notification can do it (ADR 0258), naming the mesh MCP
// server (the glossary's word; "console" is retired): the answer is not an // server (the glossary's word; "console" is retired): the answer is not an
// acknowledgement, so it is given where the operator is known to be the one asking, until answers are // acknowledgement, so it is given where the operator is known to be the one asking, until answers are
@@ -816,19 +776,15 @@ func stalledWords(l stalledLine, o conditions.Observation) (headline, explanatio
long = "for " + humanDuration(d) long = "for " + humanDuration(d)
} }
if o.Resolver == conditions.ResolverOperator { if o.Resolver == conditions.ResolverOperator {
// Asked of the operator, approved on a channel that proves who answered (novox/hq ADR 0259); the // Words only: releasing or stopping a delivery is not an acknowledgement, so no desk click
// router says where each can be answered, so the words do not. // performs it (ADR 0258).
release := conditions.Action{Label: "Release", Verb: "mesh-delivery.release", Level: conditions.LevelApprove,
Arguments: map[string]string{"id": l.ID, "why": ""}}
stop := conditions.Action{Label: "Stop", Verb: "mesh-delivery.stop", Level: conditions.LevelApprove,
Arguments: map[string]string{"id": l.ID, "why": ""}}
switch held { switch held {
case "held": case "held":
needs, actions = "release it, or stop it, "+FromMeshMCPServer, []conditions.Action{release, stop} needs = "release it, or stop it, " + FromMeshMCPServer
case "ready", "checked": case "ready", "checked":
needs = "merge its pull request, or close it." needs = "merge its pull request, or close it."
default: default:
needs, actions = "stop it "+FromMeshMCPServer, []conditions.Action{stop} needs = "stop it " + FromMeshMCPServer
} }
} }
return fmt.Sprintf("Delivery of %s %s %s", name, held, long), return fmt.Sprintf("Delivery of %s %s %s", name, held, long),
+7 -24
View File
@@ -65,21 +65,13 @@ func TestADeliveryWaitingNeedsNothingUntilItsBoundThenOffersStartAndStop(t *test
t.Errorf("the summary lost the way on for whoever looks closer: %q", got[0].Summary) t.Errorf("the summary lost the way on for whoever looks closer: %q", got[0].Summary)
} }
// Past four hours it is urgent, and asks the operator to start or stop it (novox/hq ADR 0259): the plan's // Past four hours it is urgent, and offers the controller's own answers.
// own verbs, approved, which the controller performs on the warrant. The router says where to answer.
f.waits[0].since = now.Add(-5 * time.Hour) f.waits[0].since = now.Add(-5 * time.Hour)
got = watchWaits(f) got = watchWaits(f)
plainExample(t, got[0], "openrazer delivery waiting to start", plainExample(t, got[0], "openrazer delivery waiting to start",
"Needs you: start it, or stop it, from the mesh MCP server; this notification cannot do it. The change to openrazer is merged and built, and mesh-delivery (the "+ "Needs you: start it, or stop it, from the mesh MCP server; this notification cannot do it. The change to openrazer is merged and built, and mesh-delivery (the "+
"module that decides when a delivery goes out) has not let it start for 5 hours, so mesh-delivery may "+ "module that decides when a delivery goes out) has not let it start for 5 hours, so mesh-delivery may "+
"be stuck.", "Start", "Stop") "be stuck.")
for i, want := range []string{"go", "stop"} {
a := got[0].Actions[i]
if a.Verb != "mesh-controller.plans" || a.Arguments[want] != "plan-1791454185265004861" ||
a.Level != conditions.LevelApprove || a.Arguments["cause"] != conditions.CauseOperatorAnswer {
t.Errorf("%s: %+v", a.Label, a)
}
}
// Many modules are counted, not listed in the headline. // Many modules are counted, not listed in the headline.
f.waits[0].modules = []string{"a", "b", "c", "d"} f.waits[0].modules = []string{"a", "b", "c", "d"}
@@ -90,20 +82,16 @@ func TestADeliveryWaitingNeedsNothingUntilItsBoundThenOffersStartAndStop(t *test
} }
// **A module unhealthy**: "openrazer on g14 is not healthy: its unit openrazer-daemon.service failed in the // **A module unhealthy**: "openrazer on g14 is not healthy: its unit openrazer-daemon.service failed in the
// account's own service manager (exit-code)". Restarting is not an acknowledgement: it is asked of the // account's own service manager (exit-code)". Restarting is not an acknowledgement, so it is said in words
// operator at the approve level (novox/hq ADR 0259), so a desk click never performs it (ADR 0258). // and offered as no answer (ADR 0258).
func TestAModuleUnhealthyAsksForARestartInWords(t *testing.T) { func TestAModuleUnhealthyAsksForARestartInWords(t *testing.T) {
o := moduleUnhealthyObservation("openrazer", "g14", []inventory.ResourceHealth{{Kind: link.KindUnit, o := moduleUnhealthyObservation("openrazer", "g14", []inventory.ResourceHealth{{Kind: link.KindUnit,
Resource: "openrazer-daemon", Target: "openrazer-daemon.service", Account: "jochen", Resource: "openrazer-daemon", Target: "openrazer-daemon.service",
Reason: "failed in the account's own service manager (exit-code)", Since: time.Now()}}) Reason: "failed in the account's own service manager (exit-code)", Since: time.Now()}})
plainExample(t, o, "openrazer not working on g14", plainExample(t, o, "openrazer not working on g14",
"Needs you: restart its service openrazer-daemon on g14 from the mesh MCP server; this notification cannot do it. "+ "Needs you: restart its service openrazer-daemon on g14 from the mesh MCP server; this notification cannot do it. "+
"openrazer on g14 is not healthy: its service openrazer-daemon stopped with an error. It clears as soon "+ "openrazer on g14 is not healthy: its service openrazer-daemon stopped with an error. It clears as soon "+
"as it runs again.", "Restart") "as it runs again.")
if a := o.Actions[0]; a.Verb != "node-service-manager.restart" || a.Machine != "g14" || a.Level != conditions.LevelApprove ||
a.Arguments["unit"] != "openrazer-daemon.service" || a.Arguments["scope"] != "user" {
t.Errorf("restart: %+v", a)
}
// An account waiting for a new login (ADR 0252) asks for the login, held to the plain rule. // An account waiting for a new login (ADR 0252) asks for the login, held to the plain rule.
o = moduleUnhealthyObservation("openrazer", "g14", []inventory.ResourceHealth{{Kind: "account", o = moduleUnhealthyObservation("openrazer", "g14", []inventory.ResourceHealth{{Kind: "account",
Resource: "operator-in-group", Target: "jochen", Reason: "relogin needed: the account is in the group"}}) Resource: "operator-in-group", Target: "jochen", Reason: "relogin needed: the account is in the group"}})
@@ -166,12 +154,7 @@ func TestADeliveryHeldAsksForReleaseOrStopInWords(t *testing.T) {
Bound: "24h0m0s", H2: "none: the state is the operator's", Says: "it waits for the operator"}}) Bound: "24h0m0s", H2: "none: the state is the operator's", Says: "it waits for the operator"}})
plainExample(t, got[0], "Delivery of hq held for 36 hours", plainExample(t, got[0], "Delivery of hq held for 36 hours",
"Needs you: release it, or stop it, from the mesh MCP server; this notification cannot do it. A delivery of hq has been held for 36 hours, past its limit.", "Needs you: release it, or stop it, from the mesh MCP server; this notification cannot do it. A delivery of hq has been held for 36 hours, past its limit.",
"Release", "Stop") )
for i, verb := range []string{"mesh-delivery.release", "mesh-delivery.stop"} {
if a := got[0].Actions[i]; a.Verb != verb || a.Arguments["id"] != "novox/hq@055550802096" || a.Level != conditions.LevelApprove {
t.Errorf("%+v", a)
}
}
} }
// **Every kind the controller raises has plain words**, and its words are plain for a subject of every // **Every kind the controller raises has plain words**, and its words are plain for a subject of every
+11 -4
View File
@@ -512,6 +512,15 @@ func sortedKeysOf(m map[string]string) []string {
return out return out
} }
// sayPlanDiff is `plan --diff`: what the declaration leaves out, then the diff. A module left out is not in
// the body, so the diff alone would say "nothing would change" for a module just assigned whose settings
// cannot compose — success-shaped silence. Said first, with why, as push and the plain plan say it
// (novox/hq ADR 0163, rule 6).
func sayPlanDiff(node string, declared sendable, diff func() error) error {
reportLeftOut(node, declared)
return diff()
}
// reportLeftOut says which of a machine's modules its declaration leaves out and why (novox/hq ADR // reportLeftOut says which of a machine's modules its declaration leaves out and why (novox/hq ADR
// 0163, rule 6), one line each: the machine is told everything else, and is told it was left out. // 0163, rule 6), one line each: the machine is told everything else, and is told it was left out.
func reportLeftOut(node string, declared sendable) { func reportLeftOut(node string, declared sendable) {
@@ -1239,11 +1248,9 @@ func planCommand(ctx context.Context, args []string) error {
if err != nil { if err != nil {
return err return err
} }
// A module left out is not in the body, so the diff alone would say "nothing would change" for return sayPlanDiff(args[0], declared, func() error {
// a module just assigned whose settings cannot compose — success-shaped silence. Said first, with
// why, as push and the plain plan say it (novox/hq ADR 0163, rule 6).
reportLeftOut(args[0], declared)
return writePlanDiff(ctx, open.inventory, args[0], body) return writePlanDiff(ctx, open.inventory, args[0], body)
})
} }
if *asJSON { if *asJSON {
declared, err := declarationFor(ctx, open, args[0], plan, settings) declared, err := declarationFor(ctx, open, args[0], plan, settings)
@@ -0,0 +1,25 @@
package main
import (
"os"
"strings"
"testing"
)
// 2026-10-09: nfs-server was assigned to the home server, its file asked for a setting nothing set, and
// `plan --diff` said "nothing would change". The diff now says what is left out, and why, before the diff.
func TestPlanDiffSaysAModuleLeftOutBeforeTheDiff(t *testing.T) {
declared := sendable{LeftOut: []string{"nfs-server"},
leftOutWhy: map[string]string{"nfs-server": `nothing sets "shares" for it`}}
said := printed(t, func() error {
return sayPlanDiff("home", declared, func() error {
writeDiff(os.Stdout, "home", sentDiff{}, nil)
return nil
})
})
left := strings.Index(said, "home: nfs-server left out")
nothing := strings.Index(said, "home: nothing would change")
if left < 0 || !strings.Contains(said, `nothing sets "shares" for it`) || nothing < left {
t.Errorf("the left-out module and why, then the diff:\n%s", said)
}
}
+21 -41
View File
@@ -15,16 +15,16 @@ import (
// inventory.Dependencies (dependenciesOf over the records), and the merge planned by reachOfMerge — the // inventory.Dependencies (dependenciesOf over the records), and the merge planned by reachOfMerge — the
// path a real merge takes, short of the bus. // path a real merge takes, short of the bus.
// //
// **A shared repository moves only what a change's files are in the build source of** (novox/hq ADR 0267, // **The repository rows are CURRENT BEHAVIOUR, documented — not the rule the operator states**
// issues 338 and 363): each build records the build source it said, and a merge is mapped onto those of the // (novox/hq issue 338, and the decision pending on it): a build that read a repository gives its module a
// newest builds. A build that said none (P below, as every build before ADR 0267) is read as before: P moves // packages edge to every module built from that repository, and mergeCandidates moves it on any merge to
// on any merge to the repository it packages, though through no edge. The rows tagged 338 held the opposite // that repository, whatever the files. So a change to C alone, or to a README, moves the module that
// until ADR 0267 was built. // packages C's repository. ADR 0238 §3 records exactly that today ("a repository a recipe names"); the
func TestASharedRepositoryIsPlannedFromTheRecordedBuildSources(t *testing.T) { // expectations marked 338 change with that decision.
func TestASharedRepositoryIsPlannedFromTheRecordsAsItIsToday(t *testing.T) {
inv := inventory.ForTest(t) inv := inventory.ForTest(t)
ctx := t.Context() ctx := t.Context()
asked := time.Now().Add(-time.Hour) asked := time.Now().Add(-time.Hour)
sourcesOf := map[string][]inventory.BuildSource{}
register := func(m catalogue.Manifest, repository, path string, against []string, read []inventory.ReadRepository) { register := func(m catalogue.Manifest, repository, path string, against []string, read []inventory.ReadRepository) {
t.Helper() t.Helper()
if err := inv.RegisterModule(ctx, m, inventory.Source{Repository: repository, Seat: "git", Path: path, if err := inv.RegisterModule(ctx, m, inventory.Source{Repository: repository, Seat: "git", Path: path,
@@ -32,8 +32,7 @@ func TestASharedRepositoryIsPlannedFromTheRecordedBuildSources(t *testing.T) {
t.Fatal(err) t.Fatal(err)
} }
if err := inv.RecordBuild(ctx, inventory.Build{ID: "build-" + m.Module, Repository: repository, Ref: "main", if err := inv.RecordBuild(ctx, inventory.Build{ID: "build-" + m.Module, Repository: repository, Ref: "main",
Module: m.Module, Commit: "old", On: "builder", Path: path, Against: against, Read: read, Asked: asked, Module: m.Module, Commit: "old", On: "builder", Path: path, Against: against, Read: read, Asked: asked}); err != nil {
Sources: sourcesOf[m.Module]}); err != nil {
t.Fatal(err) t.Fatal(err)
} }
} }
@@ -41,16 +40,7 @@ func TestASharedRepositoryIsPlannedFromTheRecordedBuildSources(t *testing.T) {
agent := catalogue.Manifest{Module: "build-agent", Version: "1", agent := catalogue.Manifest{Module: "build-agent", Version: "1",
Claims: []catalogue.Claim{{Name: "node-build-agent", Scope: catalogue.ScopeNode}}} Claims: []catalogue.Claim{{Name: "node-build-agent", Scope: catalogue.ScopeNode}}}
// The shape of issue 338, each build saying its build source (ADR 0267). // The shape of issue 338.
gomod := []string{"go.mod", "go.sum"}
sourcesOf["mesh-controller"] = []inventory.BuildSource{{Paths: append([]string{"module.json", "cmd/mesh-controller/",
"internal/conditions/", "internal/broker/"}, gomod...)}}
sourcesOf["build-agent"] = []inventory.BuildSource{
{Paths: []string{"modules/build-agent/Dockerfile", "modules/build-agent/module.json"}},
{Repository: "novox/mesh-controller", Ref: "main", Paths: append([]string{"cmd/mesh-builder/", "internal/broker/"}, gomod...)}}
sourcesOf["route-proxy"] = []inventory.BuildSource{
{Paths: []string{"modules/route-proxy/Dockerfile", "modules/route-proxy/module.json"}},
{Repository: "novox/mesh-controller", Ref: "main", Paths: append([]string{"examples/route-proxy/", "internal/broker/"}, gomod...)}}
register(catalogue.Manifest{Module: "mesh-controller", Version: "1"}, "novox/mesh-controller", "", nil, nil) register(catalogue.Manifest{Module: "mesh-controller", Version: "1"}, "novox/mesh-controller", "", nil, nil)
register(agent, "novox/mesh-catalog", "modules/build-agent", nil, controllerRead) register(agent, "novox/mesh-catalog", "modules/build-agent", nil, controllerRead)
register(catalogue.Manifest{Module: "route-proxy", Version: "1"}, "novox/mesh-catalog", "modules/route-proxy", nil, controllerRead) register(catalogue.Manifest{Module: "route-proxy", Version: "1"}, "novox/mesh-catalog", "modules/route-proxy", nil, controllerRead)
@@ -91,15 +81,13 @@ func TestASharedRepositoryIsPlannedFromTheRecordedBuildSources(t *testing.T) {
if !reflect.DeepEqual(shared, sharedRepositoryEdges) { if !reflect.DeepEqual(shared, sharedRepositoryEdges) {
t.Errorf("derived %v\nthe hand-written rows use %v", shared, sharedRepositoryEdges) t.Errorf("derived %v\nthe hand-written rows use %v", shared, sharedRepositoryEdges)
} }
// Each kind derived from its record: built against (stands-on), build.on (declared); a read draws none. // Each kind derived from its record: built against (stands-on), build.on (declared), read (packages).
for _, e := range edges {
if e.Kind == inventory.EdgePackages {
t.Errorf("a packages edge was drawn (ADR 0267 rule 4): %v", e)
}
}
for _, want := range []inventory.Edge{ for _, want := range []inventory.Edge{
dep("d", inventory.EdgeStandsOn, "a"), dep("d", inventory.EdgeStandsOn, "a"),
dep("e", inventory.EdgeDeclared, "b"), dep("e", inventory.EdgeDeclared, "b"),
dep("p", inventory.EdgePackages, "a"),
dep("p", inventory.EdgePackages, "b"),
dep("p", inventory.EdgePackages, "c"),
dep("d", inventory.EdgeBuiltBy, "build-agent"), dep("d", inventory.EdgeBuiltBy, "build-agent"),
} { } {
found := false found := false
@@ -117,23 +105,15 @@ func TestASharedRepositoryIsPlannedFromTheRecordedBuildSources(t *testing.T) {
want string want string
issue338 bool issue338 bool
}{ }{
{"A and B changed, C untouched: D after A, E after B; P, which said no build source, reads all", "one", {"A and B changed, C untouched: D after A, E after B; P packages their repository", "one",
[]string{"modules/a/x.go", "modules/b/x.go"}, "a,b,p | d,e", false}, []string{"modules/a/x.go", "modules/b/x.go"}, "a,b,p | d,e", false},
{"C alone: C, and P, read whole as before; P after nothing", "one", {"C alone: C, and P, which packages C's repository", "one",
[]string{"modules/c/x.go"}, "c,p", false}, []string{"modules/c/x.go"}, "c,p", true},
{"a README of the repository P packages: P, read whole as before", "one", {"a README of the repository P packages: P moves, nothing built from it does", "one",
[]string{"README.md"}, "p", false}, []string{"README.md"}, "p", true},
{"the dependent's repository: D alone", "two", []string{"d/main.go"}, "d", false}, {"the dependent's repository: D alone", "two", []string{"d/main.go"}, "d", false},
{"a README of the controller's repository: no module", "mesh-controller", {"a README of the controller's repository: all three, three tiers", "mesh-controller",
[]string{"README.md"}, "", true}, []string{"README.md"}, "mesh-controller | build-agent | route-proxy", true},
{"the controller's own command: the controller alone", "mesh-controller",
[]string{"cmd/mesh-controller/main.go"}, "mesh-controller", true},
{"a package only the controller builds from: the controller alone", "mesh-controller",
[]string{"internal/conditions/condition.go"}, "mesh-controller", true},
{"the route proxy's program: the route proxy alone", "mesh-controller",
[]string{"examples/route-proxy/main.go"}, "route-proxy", true},
{"a package all three build from: all three", "mesh-controller",
[]string{"internal/broker/broker.go"}, "mesh-controller | build-agent | route-proxy", false},
{"the route proxy's directory in the catalogue: it alone", "mesh-catalog", {"the route proxy's directory in the catalogue: it alone", "mesh-catalog",
[]string{"modules/route-proxy/module.json"}, "route-proxy", false}, []string{"modules/route-proxy/module.json"}, "route-proxy", false},
{"the build agent's directory: it alone, nothing it builds", "mesh-catalog", {"the build agent's directory: it alone, nothing it builds", "mesh-catalog",
@@ -143,7 +123,7 @@ func TestASharedRepositoryIsPlannedFromTheRecordedBuildSources(t *testing.T) {
if got != c.want { if got != c.want {
tag := "" tag := ""
if c.issue338 { if c.issue338 {
tag = " (issue 338, flipped by ADR 0267)" tag = " (current behaviour, issue 338)"
} }
t.Errorf("%s: planned %q, wanted %q%s", c.what, got, c.want, tag) t.Errorf("%s: planned %q, wanted %q%s", c.what, got, c.want, tag)
} }
+45 -278
View File
@@ -1,15 +1,12 @@
package main package main
import ( import (
"encoding/json"
"fmt" "fmt"
"math/rand/v2" "math/rand/v2"
"sort" "sort"
"strings" "strings"
"testing" "testing"
"time"
snapshot "github.com/novox/mesh-controller/internal/facts"
"github.com/novox/mesh-controller/internal/inventory" "github.com/novox/mesh-controller/internal/inventory"
"github.com/novox/mesh-controller/internal/link" "github.com/novox/mesh-controller/internal/link"
) )
@@ -24,7 +21,7 @@ import (
// kind widens the plan orders the tiers // kind widens the plan orders the tiers
// stands-on yes yes, after its base is built // stands-on yes yes, after its base is built
// declared yes yes, after its base is built // declared yes yes, after its base is built
// packages no no — retired by novox/hq ADR 0267; one recorded before is read and ignored // packages yes no, the same tier (a code dependency)
// built-by no yes, after the build machine — except for what the build machine stands // built-by no yes, after the build machine — except for what the build machine stands
// on, and for the controller whose worker it binds // on, and for the controller whose worker it binds
// worker-of no yes, the build seat's holder after the controller (hq issue 206) // worker-of no yes, the build seat's holder after the controller (hq issue 206)
@@ -126,9 +123,9 @@ func TestAPlanIsWhatTheChangeTouchedAndWhatIsBuiltOnIt(t *testing.T) {
{what: "transitive: F on D on A, A changed", {what: "transitive: F on D on A, A changed",
edges: []inventory.Edge{dep("f", standsOn, "d"), dep("d", standsOn, "a")}, edges: []inventory.Edge{dep("f", standsOn, "d"), dep("d", standsOn, "a")},
repo: "one", paths: []string{"modules/a/x"}, want: "a | d | f"}, repo: "one", paths: []string{"modules/a/x"}, want: "a | d | f"},
{what: "transitive across kinds stops at a packages edge: F declared on D, D packages A (ADR 0267)", {what: "transitive across kinds: F declared on D, D packages A",
edges: []inventory.Edge{dep("f", declared, "d"), dep("d", packages, "a")}, edges: []inventory.Edge{dep("f", declared, "d"), dep("d", packages, "a")},
repo: "one", paths: []string{"modules/a/x"}, want: "a"}, repo: "one", paths: []string{"modules/a/x"}, want: "a,d | f"},
// Each kind alone: X depends on A, A changed (widening), then both changed (ordering). // Each kind alone: X depends on A, A changed (widening), then both changed (ordering).
{what: "stands-on (built against A's artifact) widens", edges: []inventory.Edge{dep("x", standsOn, "a")}, {what: "stands-on (built against A's artifact) widens", edges: []inventory.Edge{dep("x", standsOn, "a")},
@@ -139,8 +136,8 @@ func TestAPlanIsWhatTheChangeTouchedAndWhatIsBuiltOnIt(t *testing.T) {
repo: "one", paths: []string{"modules/a/x"}, want: "a | x"}, repo: "one", paths: []string{"modules/a/x"}, want: "a | x"},
{what: "declared orders", edges: []inventory.Edge{dep("x", declared, "a")}, {what: "declared orders", edges: []inventory.Edge{dep("x", declared, "a")},
repo: "one", paths: []string{"modules/a/x", "modules/x/y"}, want: "a | x"}, repo: "one", paths: []string{"modules/a/x", "modules/x/y"}, want: "a | x"},
{what: "packages, recorded before ADR 0267, widens nothing", edges: []inventory.Edge{dep("x", packages, "a")}, {what: "packages widens, into the same tier", edges: []inventory.Edge{dep("x", packages, "a")},
repo: "one", paths: []string{"modules/a/x"}, want: "a"}, repo: "one", paths: []string{"modules/a/x"}, want: "a,x"},
{what: "packages does not order", edges: []inventory.Edge{dep("x", packages, "a")}, {what: "packages does not order", edges: []inventory.Edge{dep("x", packages, "a")},
repo: "one", paths: []string{"modules/a/x", "modules/x/y"}, want: "a,x"}, repo: "one", paths: []string{"modules/a/x", "modules/x/y"}, want: "a,x"},
{what: "built-by never widens", edges: []inventory.Edge{dep("x", builtBy, "a")}, {what: "built-by never widens", edges: []inventory.Edge{dep("x", builtBy, "a")},
@@ -191,7 +188,7 @@ func TestAPlanIsWhatTheChangeTouchedAndWhatIsBuiltOnIt(t *testing.T) {
repo: "one", paths: []string{"modules/z/x"}, want: "z | a,b | d"}, repo: "one", paths: []string{"modules/z/x"}, want: "z | a,b | d"},
{what: "a diamond of mixed kinds orders on the ordering side only", {what: "a diamond of mixed kinds orders on the ordering side only",
edges: []inventory.Edge{dep("d", standsOn, "a"), dep("d", packages, "b")}, edges: []inventory.Edge{dep("d", standsOn, "a"), dep("d", packages, "b")},
repo: "one", paths: []string{"modules/b/x"}, want: "b"}, repo: "one", paths: []string{"modules/b/x"}, want: "b,d"},
// A cycle the catalogue should never produce: what remains is one last tier, and said. // A cycle the catalogue should never produce: what remains is one last tier, and said.
{what: "a cycle is one last tier, not lost", edges: []inventory.Edge{dep("a", standsOn, "b"), dep("b", standsOn, "a"), {what: "a cycle is one last tier, not lost", edges: []inventory.Edge{dep("a", standsOn, "b"), dep("b", standsOn, "a"),
@@ -228,16 +225,22 @@ func TestAPlanIsWhatTheChangeTouchedAndWhatIsBuiltOnIt(t *testing.T) {
} }
} }
// **A shared repository moves only what a change's files are in the build source of** (novox/hq ADR 0267, // **CURRENT BEHAVIOUR, documented — not the rule the operator states.** novox/hq issue 338 (a module
// issue 338, issue 363). The controller is built from its repository's root as a Go bundle; the route proxy // built from a shared repository moves on every merge to it) and the decision pending on it would change
// and the build seat's holder build images whose context is that repository and which name the package they // every row here. Today:
// compile. Each newest trunk build said its build source — the import closure of its program — and a merge
// is mapped onto those. The rows tagged 338 held the opposite until ADR 0267 was built: every merge to the
// controller's repository planned all three, in three tiers.
// //
// The build sources are this repository's own programs as GoBuildSource reads them (held to that by // - mesh-controller is built from its repository's root, so every file of that repository touches it;
// TestThisRepositorysProgramsHaveBuildSourcesOfTheirOwn in internal/builder), cut to what the rows need. // - route-proxy and build-agent package the whole of that repository (a build context), so the build
func TestASharedRepositoryMovesOnlyWhatItsBuildSourceHolds(t *testing.T) { // record's `read` makes them move on any merge to it, whatever the files, and dependenciesOf gives
// each a packages edge to every module built from it;
// - built-by (route-proxy on build-agent) and worker-of (build-agent on the controller) make it three
// tiers.
//
// These follow ADR 0238 §3 as written ("the whole repository for a module built from its root, and a
// repository a recipe names"), so they are not failures; when the decision on issue 338 lands, these
// expectations change with it. The edges are the ones dependenciesOf derives from this catalogue — held
// to that by TestASharedRepositoryIsPlannedFromTheRecordsAsItIsToday, which derives them from the store.
func TestASharedRepositoryMovesWhatPackagesItAsItDoesToday(t *testing.T) {
const catalogueRepo = "http://forge.internal:20000/novox/mesh-catalog.git" const catalogueRepo = "http://forge.internal:20000/novox/mesh-catalog.git"
const controllerRepo = "http://forge.internal:20000/novox/mesh-controller.git" const controllerRepo = "http://forge.internal:20000/novox/mesh-controller.git"
entries := []inventory.Entry{ entries := []inventory.Entry{
@@ -246,26 +249,7 @@ func TestASharedRepositoryMovesOnlyWhatItsBuildSourceHolds(t *testing.T) {
fromRepo("route-proxy", catalogueRepo, "modules/route-proxy"), fromRepo("route-proxy", catalogueRepo, "modules/route-proxy"),
fromRepo("gitea", catalogueRepo, "modules/gitea"), fromRepo("gitea", catalogueRepo, "modules/gitea"),
} }
gomod := []string{"go.mod", "go.sum", "vendor/modules.txt"}
with := func(paths ...string) []string { return append(append([]string{}, gomod...), paths...) }
read := map[string][]inventory.ReadRepository{ read := map[string][]inventory.ReadRepository{
"mesh-controller": {{Own: true, Paths: with("module.json", "cmd/mesh-controller/", "internal/conditions/",
"internal/broker/", "internal/builder/", "internal/inventory/", "internal/inventory/migrations/**",
"vendor/github.com/nats-io/nats.go/")}},
"build-agent": {
{Repository: "novox/mesh-controller", Ref: "main", Paths: with("cmd/mesh-builder/", "internal/broker/",
"internal/builder/", "internal/inventory/", "internal/inventory/migrations/**", "vendor/github.com/nats-io/nats.go/")},
{Own: true, Paths: []string{"modules/build-agent/Dockerfile", "modules/build-agent/module.json"}},
},
"route-proxy": {
{Repository: "novox/mesh-controller", Ref: "main", Paths: with("examples/route-proxy/", "internal/broker/",
"vendor/github.com/nats-io/nats.go/")},
{Own: true, Paths: []string{"modules/route-proxy/Dockerfile", "modules/route-proxy/module.json"}},
},
}
// With no build source said — before each module's first trunk build under ADR 0267, or while an
// earlier merge's build of it is pending — a module is read as before.
unsaid := map[string][]inventory.ReadRepository{
"build-agent": {{Repository: "novox/mesh-controller", Ref: "main"}}, "build-agent": {{Repository: "novox/mesh-controller", Ref: "main"}},
"route-proxy": {{Repository: "novox/mesh-controller", Ref: "main"}}, "route-proxy": {{Repository: "novox/mesh-controller", Ref: "main"}},
} }
@@ -273,176 +257,51 @@ func TestASharedRepositoryMovesOnlyWhatItsBuildSourceHolds(t *testing.T) {
for _, c := range []struct { for _, c := range []struct {
what, repo string what, repo string
paths []string paths []string
read map[string][]inventory.ReadRepository
want string want string
unread string
}{ }{
// The operator's acceptance: a merge of the controller's own code plans the controller alone. // The live three-tier plan of 2026-10-08 (issue 338), in the worker-of order (issue 206) that
{"the controller's own command: the controller alone (338)", "mesh-controller", // TestAMergeIsPlannedInTiersAlongTheThreeKindsOfDependency's controller case holds too.
[]string{"cmd/mesh-controller/main.go"}, read, "mesh-controller", ""}, {"a README of the controller's repository moves all three, in three tiers", "mesh-controller",
{"a package only the controller builds from: the controller alone (338)", "mesh-controller", []string{"README.md"}, "mesh-controller | build-agent | route-proxy"},
[]string{"internal/conditions/condition.go", "internal/conditions/bus.go"}, read, "mesh-controller", ""}, {"the controller's own code: the same", "mesh-controller",
{"a test beside the controller's command: nothing is built from it", "mesh-controller", []string{"cmd/mesh-controller/main.go"}, "mesh-controller | build-agent | route-proxy"},
[]string{"cmd/mesh-controller/main_test.go"}, read, "", "cmd/mesh-controller/main_test.go"}, {"the route proxy's program alone: the same, the controller with it", "mesh-controller",
{"a README of the controller's repository: no module (338)", "mesh-controller", []string{"examples/route-proxy/main.go"}, "mesh-controller | build-agent | route-proxy"},
[]string{"README.md"}, read, "", "README.md"}, // In the catalogue, where they live, the rule is path-precise.
{"the route proxy's program alone: the route proxy alone (338)", "mesh-controller", {"the route proxy's directory in the catalogue: it alone", "mesh-catalog",
[]string{"examples/route-proxy/main.go"}, read, "route-proxy", ""}, []string{"modules/route-proxy/module.json"}, "route-proxy"},
{"the build seat's program alone: its holder alone", "mesh-controller", {"the build agent's directory: it alone, nothing it builds", "mesh-catalog",
[]string{"cmd/mesh-builder/main.go"}, read, "build-agent", ""}, []string{"modules/build-agent/module.json"}, "build-agent"},
{"a package the build seat's program and the controller build from: both", "mesh-controller",
[]string{"internal/builder/builder.go"}, read, "mesh-controller | build-agent", ""},
{"a migration the controller and the build seat's program embed: both", "mesh-controller",
[]string{"internal/inventory/migrations/0088-a-build-says-its-build-source.sql"}, read,
"mesh-controller | build-agent", ""},
// A package all three build from: all three; the build seat's holder after the controller whose worker
// it binds (worker-of, issue 206), the proxy after the holder that builds it (built-by).
{"a package all three build from: all three", "mesh-controller",
[]string{"internal/broker/broker.go"}, read, "mesh-controller | build-agent | route-proxy", ""},
{"a vendored package all three build from: all three", "mesh-controller",
[]string{"vendor/github.com/nats-io/nats.go/nats.go"}, read, "mesh-controller | build-agent | route-proxy", ""},
{"go.sum: all three", "mesh-controller",
[]string{"go.sum"}, read, "mesh-controller | build-agent | route-proxy", ""},
{"a file added to the proxy's package: the proxy", "mesh-controller",
[]string{"examples/route-proxy/new.go"}, read, "route-proxy", ""},
// Before any build source is said: as before.
{"no build source said: a README moves all three, as before", "mesh-controller",
[]string{"README.md"}, unsaid, "mesh-controller | build-agent | route-proxy", ""},
// In the catalogue, where they live, each by its own build source.
{"the route proxy's recipe: it alone", "mesh-catalog",
[]string{"modules/route-proxy/Dockerfile"}, read, "route-proxy", ""},
{"the route proxy's manifest: it alone", "mesh-catalog",
[]string{"modules/route-proxy/module.json"}, read, "route-proxy", ""},
{"the route proxy's README: nothing", "mesh-catalog",
[]string{"modules/route-proxy/README.md"}, read, "", "modules/route-proxy/README.md"},
{"the build agent's manifest: it alone, nothing it builds", "mesh-catalog",
[]string{"modules/build-agent/module.json"}, read, "build-agent", ""},
{"another module of the catalogue: neither", "mesh-catalog", {"another module of the catalogue: neither", "mesh-catalog",
[]string{"modules/gitea/index.ts"}, read, "gitea", ""}, []string{"modules/gitea/index.ts"}, "gitea"},
} { } {
r, got := planMerge(t, c.repo, c.paths, entries, c.read, edges) r, got := planMerge(t, c.repo, c.paths, entries, read, edges)
if got != c.want { if got != c.want {
t.Errorf("%s: planned %q, wanted %q", c.what, got, c.want) t.Errorf("%s: planned %q, wanted %q (as today; issue 338)", c.what, got, c.want)
} }
if u := strings.Join(r.Unread, ","); u != c.unread { if c.repo == "mesh-controller" && strings.Join(r.Unread, ",") != "" {
t.Errorf("%s: unread %q, wanted %q", c.what, u, c.unread) t.Errorf("%s: a root-built module reads every file, and %v were said unread", c.what, r.Unread)
} }
} }
// Files not all said: everything the repository builds, as before.
m := link.SourceMoved{Owner: "novox", Repo: "mesh-controller", Base: "main", Commit: "head",
Paths: []string{"README.md"}, PathsTruncated: true}
if got := tiered(reachOfMerge(m, entries, read, edges).Plan.Tiers); got != "mesh-controller | build-agent | route-proxy" {
t.Errorf("a merge whose files were not all said: planned %q, wanted all three", got)
}
}
// **A module whose recorded build source a plan has overtaken is read whole** (novox/hq ADR 0267): a merge
// that added an import to the route proxy is planned; before a build of it works — still building, failed,
// or its plan closed before reaching it — a merge changing only the newly imported package must still move
// the proxy, since the build source its last build said does not hold that package.
func TestAModuleAPlanOvertookIsReadWhole(t *testing.T) {
built := time.Date(2026, 10, 10, 1, 0, 0, 0, time.UTC)
read := map[string][]inventory.ReadRepository{
"route-proxy": {
{Repository: "novox/mesh-controller", Ref: "main", Paths: []string{"examples/route-proxy/", "go.mod"}, Built: built},
{Own: true, Paths: []string{"modules/route-proxy/module.json"}, Built: built},
},
"mesh-controller": {{Own: true, Paths: []string{"module.json", "cmd/mesh-controller/"}, Built: built}},
}
m := link.SourceMoved{Owner: "novox", Repo: "mesh-controller", Base: "main", Paths: []string{"internal/newly/imported.go"}}
if readsFrom(read["route-proxy"], m) {
t.Fatal("the said build source holds the new package: the fixture is wrong")
}
proxy := func(state string) map[string]*inventory.PlanModule {
return map[string]*inventory.PlanModule{"route-proxy": {State: state}, "gitea": {State: "built"}}
}
for _, c := range []struct {
what string
plans []inventory.Plan
whole bool
}{
{"no plan", nil, false},
{"a plan still building it", []inventory.Plan{{State: inventory.PlanBuilding, Created: built.Add(-time.Hour),
Modules: proxy("building")}}, true},
{"a plan made after its build that failed before building it", []inventory.Plan{{State: "failed",
Created: built.Add(time.Minute), Modules: proxy("waiting")}}, true},
{"a plan made after its build that built it", []inventory.Plan{{State: inventory.PlanDone,
Created: built.Add(time.Minute), Modules: proxy("built")}}, false},
{"a plan closed before its build", []inventory.Plan{{State: "failed", Created: built.Add(-time.Hour),
Modules: proxy("waiting")}}, false},
} {
view := planningView(read, c.plans)
if readsFrom(view["route-proxy"], m) != c.whole {
t.Errorf("%s: read whole %v, wanted %v", c.what, !c.whole, c.whole)
}
if (ownSource(view["route-proxy"]) == nil) != c.whole {
t.Errorf("%s: its own build source kept %v", c.what, ownSource(view["route-proxy"]) != nil)
}
if ownSource(view["mesh-controller"]) == nil {
t.Errorf("%s: a module no plan holds lost its build source", c.what)
}
}
}
// **A missed merge that moves only a module packaging the repository is acted on** (novox/hq ADR 0267,
// issue 266): the catch-up asks wouldMove, which counts it; once a plan or build of it is made after the
// merge, the merge is history for it, and the catch-up leaves it.
func TestAMissedMergeMovingOnlyAPackagingModuleIsActedOnOnce(t *testing.T) {
merged := time.Date(2026, 10, 10, 1, 0, 0, 0, time.UTC)
entries := []inventory.Entry{
fromRepo("mesh-controller", "http://forge.internal:20000/novox/mesh-controller.git", ""),
fromRepo("route-proxy", "http://forge.internal:20000/novox/mesh-catalog.git", "modules/route-proxy"),
}
read := map[string][]inventory.ReadRepository{
"mesh-controller": {{Own: true, Paths: []string{"module.json", "cmd/mesh-controller/"}, Built: merged.Add(-time.Hour)}},
"route-proxy": {{Repository: "novox/mesh-controller", Ref: "main", Paths: []string{"examples/route-proxy/"},
Built: merged.Add(-time.Hour), Looked: merged.Add(-time.Hour)}},
}
m := link.SourceMoved{Owner: "novox", Repo: "mesh-controller", Base: "main", Commit: "c1",
MergedAt: merged.Format(time.RFC3339), Paths: []string{"examples/route-proxy/main.go"}}
if got := wouldMove(m, entries, planningView(read, nil)); len(got) != 1 || got[0].Manifest.Module != "route-proxy" {
t.Fatalf("a missed merge of the proxy's program would move %v", got)
}
// Acted on at once: the plan answering this very merge is a look, however close the clocks.
atOnce := []inventory.Plan{{State: inventory.PlanBuilding, Commit: "c1", Created: merged.Add(2 * time.Second),
Modules: map[string]*inventory.PlanModule{"route-proxy": {State: "building"}}}}
if got := wouldMove(m, entries, planningView(read, atOnce)); len(got) != 0 {
t.Fatalf("a merge whose own plan holds the proxy would move %v again", got)
}
acted := []inventory.Plan{{State: inventory.PlanBuilding, Created: merged.Add(2 * time.Minute),
Modules: map[string]*inventory.PlanModule{"route-proxy": {State: "building"}}}}
if got := wouldMove(m, entries, planningView(read, acted)); len(got) != 0 {
t.Fatalf("a merge acted on for the proxy would move %v again", got)
}
// A plan that closed without building it looked at nothing: the merge is still news for it.
closed := []inventory.Plan{{State: "failed", Created: merged.Add(2 * time.Minute),
Modules: map[string]*inventory.PlanModule{"route-proxy": {State: "waiting"}}}}
if got := wouldMove(m, entries, planningView(read, closed)); len(got) != 1 {
t.Fatalf("a plan that never built the proxy hid the merge from it: %v", got)
}
// A look just before the merge, on clocks a little apart, is no look after it.
skewed := []inventory.Plan{{State: inventory.PlanBuilding, Created: merged.Add(30 * time.Second),
Modules: map[string]*inventory.PlanModule{"route-proxy": {State: "building"}}}}
if got := wouldMove(m, entries, planningView(read, skewed)); len(got) != 1 {
t.Fatalf("a look within the clocks' margin made the merge history: %v", got)
}
} }
// sharedRepositoryEdges is what dependenciesOf derives for the catalogue of the test above, sorted as it // sharedRepositoryEdges is what dependenciesOf derives for the catalogue of the test above, sorted as it
// sorts them: no packages edge (novox/hq ADR 0267 rule 4). // sorts them.
var sharedRepositoryEdges = []inventory.Edge{ var sharedRepositoryEdges = []inventory.Edge{
dep("build-agent", inventory.EdgePackages, "mesh-controller"),
dep("build-agent", inventory.EdgeWorkerOf, "mesh-controller"), dep("build-agent", inventory.EdgeWorkerOf, "mesh-controller"),
dep("gitea", inventory.EdgeBuiltBy, "build-agent"), dep("gitea", inventory.EdgeBuiltBy, "build-agent"),
dep("mesh-controller", inventory.EdgeBuiltBy, "build-agent"), dep("mesh-controller", inventory.EdgeBuiltBy, "build-agent"),
dep("route-proxy", inventory.EdgeBuiltBy, "build-agent"), dep("route-proxy", inventory.EdgeBuiltBy, "build-agent"),
dep("route-proxy", inventory.EdgePackages, "mesh-controller"),
} }
// **The planner's invariant, over random catalogues.** For any catalogue whose dependencies form no cycle // **The planner's invariant, over random catalogues.** For any catalogue whose dependencies form no cycle
// and any set of changed files in one repository: // and any set of changed files in one repository:
// //
// - the plan is exactly the modules of that repository whose directory holds a changed file (every file, // - the plan is exactly the modules of that repository whose directory holds a changed file (every file,
// for a module built from the root), and everything reachable from them along stands-on and declared — // for a module built from the root), and everything reachable from them along stands-on, declared and
// never along packages (novox/hq ADR 0267), built-by or worker-of; // packages — never along built-by or worker-of;
// - every stands-on, declared, built-by and worker-of edge with both ends in the plan has the module // - every stands-on, declared, built-by and worker-of edge with both ends in the plan has the module
// depended on in an earlier tier; // depended on in an earlier tier;
// - no cycle is said. // - no cycle is said.
@@ -451,7 +310,7 @@ var sharedRepositoryEdges = []inventory.Edge{
func TestAPlanIsTheTouchedModulesAndWhatIsReachableAlongTheWideningEdges(t *testing.T) { func TestAPlanIsTheTouchedModulesAndWhatIsReachableAlongTheWideningEdges(t *testing.T) {
kinds := []string{inventory.EdgeStandsOn, inventory.EdgeDeclared, inventory.EdgePackages, kinds := []string{inventory.EdgeStandsOn, inventory.EdgeDeclared, inventory.EdgePackages,
inventory.EdgeBuiltBy, inventory.EdgeWorkerOf} inventory.EdgeBuiltBy, inventory.EdgeWorkerOf}
widens := map[string]bool{inventory.EdgeStandsOn: true, inventory.EdgeDeclared: true} widens := map[string]bool{inventory.EdgeStandsOn: true, inventory.EdgeDeclared: true, inventory.EdgePackages: true}
orders := map[string]bool{inventory.EdgeStandsOn: true, inventory.EdgeDeclared: true, orders := map[string]bool{inventory.EdgeStandsOn: true, inventory.EdgeDeclared: true,
inventory.EdgeBuiltBy: true, inventory.EdgeWorkerOf: true} inventory.EdgeBuiltBy: true, inventory.EdgeWorkerOf: true}
// Directory names drawn from one pool, so two repositories hold directories of the same name, and one // Directory names drawn from one pool, so two repositories hold directories of the same name, and one
@@ -586,95 +445,3 @@ func describe(entries []inventory.Entry) []string {
} }
return out return out
} }
// **The merge gate reads the build sources the snapshot carries** (novox/hq ADR 0267): the gate's plan of a
// change is the merge handler's, so a snapshot taken by a controller that records build sources narrows the
// gate's plan as it narrows the merge's; one without them reads every module as before.
func TestTheGatePlansFromTheBuildSourcesTheSnapshotCarries(t *testing.T) {
manifest := func(name string) json.RawMessage { return json.RawMessage(`{"module":"` + name + `","version":"1"}`) }
facts := snapshot.Facts{Modules: []snapshot.Module{
{Name: "mesh-controller", Repository: "novox/mesh-controller", Manifest: manifest("mesh-controller"),
Sources: []snapshot.BuildSource{{Own: true, Paths: []string{"module.json", "cmd/mesh-controller/", "internal/broker/"}}}},
{Name: "route-proxy", Repository: "novox/mesh-catalog", Path: "modules/route-proxy", Manifest: manifest("route-proxy"),
Reads: []string{"novox/mesh-controller"},
Sources: []snapshot.BuildSource{{Repository: "novox/mesh-controller", Paths: []string{"examples/route-proxy/", "internal/broker/"}},
{Own: true, Paths: []string{"modules/route-proxy/module.json"}}}},
}}
for paths, want := range map[string]string{
"cmd/mesh-controller/main.go": "mesh-controller",
"examples/route-proxy/main.go": "route-proxy",
"internal/broker/broker.go": "mesh-controller,route-proxy",
"README.md": "",
} {
r, err := reachOfChange(facts, "novox/mesh-controller", []string{paths}, "")
if err != nil {
t.Fatal(err)
}
if got := tiered(r.Plan.Tiers); got != want {
t.Errorf("%s: the gate planned %q, wanted %q", paths, got, want)
}
}
// A snapshot without build sources: as before.
for i := range facts.Modules {
facts.Modules[i].Sources = nil
}
r, err := reachOfChange(facts, "novox/mesh-controller", []string{"README.md"}, "")
if err != nil {
t.Fatal(err)
}
if got := tiered(r.Plan.Tiers); got != "mesh-controller,route-proxy" {
t.Errorf("a snapshot without build sources: the gate planned %q for a README", got)
}
}
// **A plan says why each module is in it** (novox/hq ADR 0267, issue 363): the files of its build source the
// merge changed, or why it is read whole — never that it packages a repository as though that moved it.
func TestAPlanSaysWhyEachModuleIsInIt(t *testing.T) {
const controllerRepo = "http://forge.internal:20000/novox/mesh-controller.git"
controller := fromRepo("mesh-controller", controllerRepo, "")
agent := fromRepo("build-agent", "http://forge.internal:20000/novox/mesh-catalog.git", "modules/build-agent")
gitea := fromRepo("gitea", "http://forge.internal:20000/novox/mesh-catalog.git", "modules/gitea")
built := time.Date(2026, 10, 10, 12, 26, 0, 0, time.UTC)
read := map[string][]inventory.ReadRepository{
"mesh-controller": {{Own: true, Paths: []string{"module.json", "cmd/mesh-controller/", "internal/link/"}, Built: built}},
"build-agent": {
{Repository: "novox/mesh-controller", Ref: "main", Paths: []string{"cmd/mesh-builder/", "internal/link/"}, Built: built},
{Own: true, Paths: []string{"modules/build-agent/module.json"}, Built: built},
},
}
merge := func(repo string, paths ...string) link.SourceMoved {
return link.SourceMoved{Owner: "novox", Repo: repo, Base: "main", Paths: paths}
}
open := []inventory.Plan{{ID: "plan-1", State: inventory.PlanBuilding, Created: built.Add(time.Minute),
Modules: map[string]*inventory.PlanModule{"build-agent": {State: "asked"}}}}
for _, c := range []struct {
what string
e inventory.Entry
read map[string][]inventory.ReadRepository
m link.SourceMoved
says string
}{
{"the controller's own closure", controller, read, merge("mesh-controller", "README.md", "internal/link/handacts.go"),
"its build source changed: 1 changed file(s) in it, e.g. internal/link/handacts.go"},
{"the build seat's closure, through its context", agent, read, merge("mesh-controller", "internal/link/handacts.go"),
"its build source in novox/mesh-controller changed: 1 changed file(s) in it, e.g. internal/link/handacts.go"},
{"an open plan has yet to build it", agent, planningView(read, open), merge("mesh-controller", "cmd/mesh-controller/main.go"),
"read whole: plan plan-1 has not built it yet, so every file of novox/mesh-controller, which its build context is, is its build source"},
{"nothing recorded, built from its root", controller, nil, merge("mesh-controller", "README.md"),
"read whole: no build source recorded, so every file of its repository is its build source"},
{"nothing recorded, in its directory", gitea, nil, merge("mesh-catalog", "modules/gitea/index.ts"),
"read whole: no build source recorded, so its directory is its build source; e.g. modules/gitea/index.ts"},
{"a root manifest is not in a module's directory", gitea, nil, merge("mesh-catalog", "module.json", "modules/gitea/x.ts"),
"read whole: no build source recorded, so its directory is its build source; e.g. modules/gitea/x.ts"},
{"a context on another branch says nothing of this one", agent, map[string][]inventory.ReadRepository{"build-agent": {
{Repository: "novox/mesh-controller", Ref: "release", Paths: []string{"internal/link/"}},
{Repository: "novox/mesh-controller", Ref: "main"}}}, merge("mesh-controller", "internal/link/handacts.go"),
"read whole: no build source recorded, so every file of novox/mesh-controller, which its build context is, is its build source"},
{"files not all said", controller, read, link.SourceMoved{Owner: "novox", Repo: "mesh-controller", PathsTruncated: true,
Paths: []string{"x"}}, "read whole: the merge's changed files were not all said"},
} {
if got := whyMoved(c.e, c.read[c.e.Manifest.Module], c.m); got != c.says {
t.Errorf("%s:\n said %q\n wanted %q", c.what, got, c.says)
}
}
}
-378
View File
@@ -1,378 +0,0 @@
package main
import (
"context"
"fmt"
"slices"
"sort"
"strings"
"time"
"github.com/nats-io/nats.go"
"github.com/novox/mesh-controller/internal/catalogue"
"github.com/novox/mesh-controller/internal/conditions"
"github.com/novox/mesh-controller/internal/inventory"
)
// Who can become root where the trusted parties run (novox/hq ADR 0259 §8, as reviewed on 2026-10-09).
//
// The router and every channel proving its sender run as accounts of their own, so that no agent reads what
// they hold or speaks as them. **Root on their machine undoes all of it**, and so does an agent running as the
// operator's account there. A machine is **root-free** — an answer proven there may authorise — only when all
// of these are measured, now, and hold:
//
// 1. the machine names an account agents run as (novox/hq ADR 0266), so no agent runs as the operator's
// account, which may become root;
// 2. its node-engine — running as root, which no agent controls — judged that account unable to become root
// without a person, in a statement heard within the last 15 minutes (agentConfined, judgedConfined). The
// engine gives that verdict only from a complete search for setuid programs younger than mesh-host's
// rootsearch.FreshFor; before one, it says "not judged yet", which is no pass (novox/hq issue 361);
// 3. the login shell's `execute` is not served there (novox/hq ADR 0268): its holder's setting withholds it
// **and** the bus was asked and heard no `execute` answered there. `execute` runs commands as the machine's
// runtime account, which the mesh's acting tools give passwordless sudo; that account is taken to become
// root, always, so no measure of it is asked.
//
// **Nothing else is a pass.** A machine that names no agent account, an unknown machine, a store or bus that
// could not be read, a verdict stale or absent — each is not root-free, and says why. The sudo module's own
// measure is no longer part of this judgement: it ran in the machine's runtime, as the very account an agent
// could become, so it could not be believed.
//
// The one judgement (judgeRoot) is read two ways: the self-check raises `root-not-free` on every machine where
// the router or a module of its own account runs and the judgement fails; and the `root-free` verb answers it
// live, to the router, which honours a verified sender only on its pass. A machine holding the operator's
// graphical session, where a messaging client's desktop app may run, is not judged here: the operator accepted
// that gap for now (hq issue 344).
// kindRootNotFree is the condition a trusted party's machine that is not root-free raises. Its own key, apart
// from ADR 0266's agent-can-become-root (Token agent-root, from DA): the two judge different things — DA the
// agent account alone, this the whole of root-free — and one key from two probes flapped between them (the
// confirmation review of 2026-10-09).
const kindRootNotFree = "root-not-free"
// routerSeat is the seat the router holds: where it runs counts as a trusted party's machine.
const routerSeat = "operator-channel"
const (
loginShellSeat = "node-login-shell"
// loginShellVerb is the seat's verb that runs a command, and the name of the setting its holder withholds
// it by (novox/hq ADR 0268).
loginShellVerb = "execute"
// executeServes is the one value of that setting that serves the verb; anything else withholds it.
executeServes = "serve"
)
// loginShellServed judges whether the login shell's execute is served on a machine, failing closed (novox/hq
// ADR 0268): served while the holder's setting there is `serve` (or the holder has no such setting and claims
// the verb), or while the bus heard the verb answered there, or while the bus could not be asked. why says
// which, in words.
func loginShellServed(holder string, claims bool, setting *any, heard, asked bool) (bool, string) {
var why []string
switch {
case setting != nil:
if v, _ := (*setting).(string); v == executeServes {
why = append(why, holder+"'s execute setting there is "+executeServes)
}
case claims:
why = append(why, holder+" claims execute and has no setting that withholds it")
}
if heard {
why = append(why, "the bus hears execute answered there")
} else if !asked {
why = append(why, "the bus could not be asked whether execute is answered there")
}
return len(why) > 0, strings.Join(why, "; ")
}
// rootFacts is what the judgement reads of one machine.
type rootFacts struct {
Machine string
// Unread is every read that failed, in words: any one is a fail.
Unread []string
// AgentNamed is whether the machine names an agent account; Confined whether its node-engine judged it
// unable to become root, freshly; ConfinedWhy the judgement's words either way.
AgentNamed bool
Confined bool
ConfinedWhy string
// Execute is whether the login shell's execute is served there; ExecuteWhy why, in words.
Execute bool
ExecuteWhy string
// SearchPending is the agent account unjudged only because the node-engine's setuid search runs, within
// the quiet the controller gives it (searchQuietFor; ADR 0266's quiet window, issue 361).
SearchPending bool
}
// rootVerdict is the judgement on one machine, as the root-free verb answers it.
type rootVerdict struct {
Machine string `json:"machine"`
Free bool `json:"free"`
Why string `json:"why"`
Judged time.Time `json:"judged"`
// Quiet is a machine not free only because its setuid search still runs, within searchQuietFor: the
// self-check raises nothing for it then (ADR 0266's quiet window). It is never free for it.
Quiet bool `json:"quiet,omitempty"`
}
// judgeRoot is the one judgement: free only when nothing failed to read, an agent account is named and judged
// confined, and execute is not served.
func judgeRoot(f rootFacts, now time.Time) rootVerdict {
v := rootVerdict{Machine: f.Machine, Judged: now.UTC()}
var not []string
if len(f.Unread) > 0 {
not = append(not, "not measured: "+strings.Join(f.Unread, "; "))
}
switch {
case f.ConfinedWhy == "":
// Not read (said above), or nothing said of it: never a pass.
if len(f.Unread) == 0 {
not = append(not, "whether agents there can become root was not judged")
}
case !f.AgentNamed:
not = append(not, "agents run as the operator's account there, which may become root ("+f.ConfinedWhy+")")
case !f.Confined:
not = append(not, f.ConfinedWhy)
}
if f.Execute {
not = append(not, "the login shell runs any command an agent gives it as the machine's runtime account, "+
"which can become root ("+orNoneKnown(f.ExecuteWhy)+")")
}
if len(not) > 0 {
v.Why = strings.Join(not, "; ")
// The one failure is the agent account not judged yet, because its search runs.
v.Quiet = len(not) == 1 && f.SearchPending && f.AgentNamed && !f.Confined && len(f.Unread) == 0 && !f.Execute
return v
}
v.Free = true
v.Why = f.ConfinedWhy + "; the login shell's execute is not served there"
return v
}
// rootReader reads the facts of machines live: the catalogue's placements, the node-engine's verdicts and the
// bus's discovery, each once per reader.
type rootReader struct {
entries []inventory.Entry
read error
heard map[string]map[string]map[string]bool
asked error
// confined is agentConfined; settings the login shell holder's settings on a machine. Replaceable in a test.
confined func(ctx context.Context, node string, now time.Time) (named, confined bool, why string, err error)
// quiet says the one thing keeping a machine's agent account unjudged is the node-engine's setuid
// search, within searchQuietFor (ADR 0266, searchStillRunning). Read by the self-check alone, to raise nothing
// then; nil reads no quiet. It never makes a machine root-free.
quiet func(ctx context.Context, node string, now time.Time) bool
settings func(ctx context.Context, node, module string) ([]catalogue.Layer, error)
}
func newRootReader(ctx context.Context, inv *inventory.Inventory, conn *nats.Conn) *rootReader {
r := &rootReader{}
r.entries, r.read = inv.Catalogued(ctx)
if conn == nil {
r.asked = fmt.Errorf("this process holds no connection to the bus")
} else {
r.heard, r.asked = discoverSeatVerbs(ctx, conn)
}
r.confined = func(ctx context.Context, node string, now time.Time) (bool, bool, string, error) {
return agentConfined(ctx, inv, node, now)
}
r.settings = inv.SettingsFor
return r
}
// facts reads one machine, at now.
func (r *rootReader) facts(ctx context.Context, machine string, now time.Time) rootFacts {
f := rootFacts{Machine: machine}
if r.read != nil {
f.Unread = append(f.Unread, "the catalogue's placements could not be read: "+r.read.Error())
}
named, confined, why, err := r.confined(ctx, machine, now)
if err != nil {
f.Unread = append(f.Unread, "the account agents run as could not be read: "+err.Error())
} else {
f.AgentNamed, f.Confined, f.ConfinedWhy = named, confined, why
}
f.Execute, f.ExecuteWhy = r.executeServed(ctx, machine)
if r.quiet != nil && f.AgentNamed && !f.Confined {
f.SearchPending = r.quiet(ctx, machine, now)
}
return f
}
// executeServed is whether the login shell's execute is served on a machine, failing closed: the bus not
// asked, the placements not read, or a holder's setting not read, is served.
func (r *rootReader) executeServed(ctx context.Context, machine string) (bool, string) {
heard := r.heard[loginShellSeat][loginShellVerb][machine]
asked := r.asked == nil
var whys []string
served := false
holders := 0
for _, e := range r.entries {
if !e.Manifest.ClaimsSeat(loginShellSeat) || !slices.Contains(e.On, machine) {
continue
}
holders++
var setting *any
if _, declared := e.Manifest.Settings[loginShellVerb]; declared {
layers, err := r.settings(ctx, machine, e.Manifest.Module)
if err != nil {
served = true
whys = append(whys, e.Manifest.Module+"'s setting there could not be read: "+err.Error())
continue
}
for _, s := range catalogue.Effective(e.Manifest, layers) {
if s.Key == loginShellVerb {
v := s.Value
setting = &v
}
}
}
if s, why := loginShellServed(e.Manifest.Module, claimServes(e.Manifest, loginShellSeat, loginShellVerb),
setting, heard, asked); s {
served = true
whys = append(whys, why)
}
}
if holders == 0 {
// Nobody is assigned to serve it; the bus must still hear nobody answering it.
if s, why := loginShellServed("no holder", false, nil, heard, asked); s {
served = true
whys = append(whys, why)
}
}
if r.read != nil {
served = true
whys = append(whys, "who holds the login shell there could not be read")
}
return served, strings.Join(whys, "; ")
}
// claimServes says whether a manifest's claim of a seat names a verb among those it serves.
func claimServes(m catalogue.Manifest, seat, verb string) bool {
for _, c := range m.Claims {
if c.Name == seat && slices.Contains(c.Serves, verb) {
return true
}
}
return false
}
// judgeRootFree is the root-free verb's answer: each named machine judged now. It never fails: what could not
// be read is a machine not free, saying so.
func judgeRootFree(ctx context.Context, r *rootReader, machines []string, now time.Time) []rootVerdict {
out := make([]rootVerdict, 0, len(machines))
for _, m := range machines {
out = append(out, judgeRoot(r.facts(ctx, m, now), now))
}
return out
}
// trustedMachines are the machines where the router or a module of its own account runs, each with those
// modules.
func trustedMachines(entries []inventory.Entry) map[string][]string {
trusted := map[string][]string{}
for _, e := range entries {
for _, node := range e.On {
if e.Manifest.RunsAs != "" || e.Manifest.ClaimsSeat(routerSeat) {
trusted[node] = append(trusted[node], e.Manifest.Module)
}
}
}
return trusted
}
// agentRootObservation is the condition of a trusted party's machine that is not root-free.
func agentRootObservation(v rootVerdict, trusted []string) conditions.Observation {
trusted = append([]string(nil), trusted...)
sort.Strings(trusted)
return conditions.Observation{Scope: conditions.ScopeMachine, ID: v.Machine, Token: kindRootNotFree,
Machine: v.Machine, Kind: kindRootNotFree, Severity: conditions.Urgent,
Summary: fmt.Sprintf("%s is not root-free, where %s run: until it is, the router approves nothing proven "+
"there (novox/hq ADR 0259 §8): %s", v.Machine, strings.Join(trusted, ", "), v.Why),
Headline: "Phone answers held on " + v.Machine,
Needs: "give the programs working for you on " + v.Machine + " an account that cannot become root.",
Explanation: "The modules that prove your answers from your phone run on " + v.Machine + ", and the mesh " +
"cannot show that a program working for you there is unable to become root or to act as you. Until " +
"it can, answers from your phone can only acknowledge.",
Resolved: "Answers from your phone can approve again on " + v.Machine}
}
// probeAgentRoot is the probe: every trusted party's machine, judged by the one judgement.
func probeAgentRoot(ctx context.Context, d *doctor) ([]conditions.Observation, error) {
var conn *nats.Conn
if d.js != nil {
conn = d.js.Conn()
}
inv := d.open.inventory
r := newRootReader(ctx, inv, conn)
if r.read != nil {
return nil, r.read
}
// The self-check alone reads ADR 0266's quiet window: nothing raised while a machine's setuid search
// runs, within searchQuietFor. The root-free verb never reads it, so the machine still answers not free.
r.quiet = func(ctx context.Context, node string, now time.Time) bool {
n, err := inv.NodeByName(ctx, node)
if err != nil || n.AgentAccount == "" {
return false
}
h, had, err := inv.HealthOf(ctx, node)
if err != nil {
return false
}
quiet, err := searchStillRunning(ctx, inv, node, n.AgentAccount, h, had, now)
return err == nil && quiet
}
return rootObservations(ctx, r, trustedMachines(r.entries), time.Now()), nil
}
// rootObservations judges the machines and says each that fails.
func rootObservations(ctx context.Context, r *rootReader, trusted map[string][]string, now time.Time) []conditions.Observation {
var machines []string
for m := range trusted {
machines = append(machines, m)
}
sort.Strings(machines)
var out []conditions.Observation
for _, v := range judgeRootFree(ctx, r, machines, now) {
if !v.Free && !v.Quiet {
out = append(out, agentRootObservation(v, trusted[v.Machine]))
}
}
return out
}
// rootClock is the clock the root-free verb judges by.
var rootClock = time.Now
// rootFreeAnswer is the root-free verb: the named machines, each judged now by the serving controller. Only it
// answers: a process that is not serving says so, and a caller reads that as no machine free.
func rootFreeAnswer(ctx context.Context, machines string, now time.Time) (any, error) {
d := doctorFrom
if d == nil || d.open == nil || d.open.inventory == nil {
return nil, fmt.Errorf("this controller is not serving, so it judges no machine root-free: ask again, and " +
"the serving controller answers")
}
var names []string
for _, m := range strings.Split(machines, ",") {
if m = strings.TrimSpace(m); m != "" && !slices.Contains(names, m) {
names = append(names, m)
}
}
if len(names) == 0 {
return nil, fmt.Errorf("root-free judges the machines named, and none was")
}
var conn *nats.Conn
if d.js != nil {
conn = d.js.Conn()
}
return map[string]any{"machines": judgeRootFree(ctx, newRootReader(ctx, d.open.inventory, conn), names, now)}, nil
}
// rootFreeNow is the machines judged root-free, for composing a push's memberships: only those that pass.
func rootFreeNow(ctx context.Context, r *rootReader, machines []string, now time.Time) map[string]bool {
free := map[string]bool{}
for _, v := range judgeRootFree(ctx, r, machines, now) {
if v.Free {
free[v.Machine] = true
}
}
return free
}
@@ -1,265 +0,0 @@
package main
import (
"context"
"errors"
"strings"
"testing"
"time"
"github.com/novox/mesh-controller/internal/catalogue"
"github.com/novox/mesh-controller/internal/conditions"
"github.com/novox/mesh-controller/internal/inventory"
"github.com/novox/mesh-controller/internal/link"
)
var rootNow = time.Date(2026, 10, 9, 12, 0, 0, 0, time.UTC)
// A machine is root-free only on a positive measure of each thing (the review of 2026-10-09, H2/H3): every
// read succeeded, an agent account is named and judged confined by the node-engine, execute is not served.
func TestRootFreeIsAPositiveMeasureAndNothingElse(t *testing.T) {
pass := rootFacts{Machine: "anchor", AgentNamed: true, Confined: true,
ConfinedWhy: "the agent account agents cannot become root without a person (judged 2026-10-09 12:00)"}
if v := judgeRoot(pass, rootNow); !v.Free || v.Machine != "anchor" || !v.Judged.Equal(rootNow) {
t.Fatalf("the one pass: %+v", v)
}
fails := map[string]func(*rootFacts){
"a read failed": func(f *rootFacts) { f.Unread = []string{"the store did not answer"} },
"no agent account named": func(f *rootFacts) { f.AgentNamed, f.Confined = false, false },
"not confined": func(f *rootFacts) { f.Confined = false },
"nothing said of it": func(f *rootFacts) { f.ConfinedWhy = "" },
"execute served": func(f *rootFacts) { f.Execute, f.ExecuteWhy = true, "the bus hears execute answered there" },
"confined, but agent read": func(f *rootFacts) { f.Unread, f.ConfinedWhy = []string{"x"}, "" },
}
for name, mutate := range fails {
f := pass
mutate(&f)
if v := judgeRoot(f, rootNow); v.Free || v.Why == "" {
t.Errorf("%s: judged %+v", name, v)
}
}
}
// The reader fails closed on every case the review named: the agent account read only where the coding-agent
// module runs (old :225), no account to measure taken for a pass (old :246), and a measure that did not answer
// taken for "not root" (old :114, :123).
func TestTheRootReaderFailsClosed(t *testing.T) {
shell := catalogue.Manifest{Module: "zsh", Claims: []catalogue.Claim{{Name: loginShellSeat, Serves: []string{"execute"}}},
Settings: map[string]catalogue.SettingDeclaration{"execute": {Default: "withhold"}}}
reader := func() *rootReader {
return &rootReader{
entries: []inventory.Entry{{Manifest: shell, On: []string{"anchor"}}},
heard: map[string]map[string]map[string]bool{},
confined: func(context.Context, string, time.Time) (bool, bool, string, error) {
return true, true, "the agent account agents cannot become root without a person", nil
},
settings: func(context.Context, string, string) ([]catalogue.Layer, error) { return nil, nil },
}
}
ctx := context.Background()
if v := judgeRootFree(ctx, reader(), []string{"anchor"}, rootNow)[0]; !v.Free {
t.Fatalf("the control: agents confined, execute withheld and unheard, everything read: %+v", v)
}
cases := map[string]func(*rootReader){
"no agent account named, no coding-agent module there": func(r *rootReader) {
r.confined = func(context.Context, string, time.Time) (bool, bool, string, error) {
return false, false, "anchor names no agent account: agents run as the operator account (ops)", nil
}
},
"the node-engine's verdict not read": func(r *rootReader) {
r.confined = func(context.Context, string, time.Time) (bool, bool, string, error) {
return false, false, "", errors.New("the store did not answer")
}
},
"a stale verdict": func(r *rootReader) {
r.confined = func(context.Context, string, time.Time) (bool, bool, string, error) {
return true, false, "the agent account agents is not judged: the machine's newest statement was heard at …", nil
}
},
"the bus not asked": func(r *rootReader) { r.asked = errors.New("no bus") },
"the placements not read": func(r *rootReader) { r.read = errors.New("no store") },
"the holder's setting not read": func(r *rootReader) {
r.settings = func(context.Context, string, string) ([]catalogue.Layer, error) { return nil, errors.New("no store") }
},
"execute heard on the bus": func(r *rootReader) {
r.heard = map[string]map[string]map[string]bool{loginShellSeat: {"execute": {"anchor": true}}}
},
"a holder that serves execute": func(r *rootReader) {
r.entries[0].Manifest.Settings = nil
},
}
for name, mutate := range cases {
r := reader()
mutate(r)
if v := judgeRootFree(ctx, r, []string{"anchor"}, rootNow)[0]; v.Free {
t.Errorf("%s: judged free: %+v", name, v)
}
}
// A machine with no login shell holder at all: still the bus must hear none.
r := reader()
r.entries = nil
r.heard = map[string]map[string]map[string]bool{loginShellSeat: {"execute": {"anchor": true}}}
if v := judgeRootFree(ctx, r, []string{"anchor"}, rootNow)[0]; v.Free {
t.Errorf("execute answered by a module nobody assigned: %+v", v)
}
}
// The probe says agent-root, by the same judgement, on each machine where the router or a module of its own
// account runs and that is not root-free; urgent and in plain words; nothing where every one is free.
func TestTheProbeSaysEachMachineThatIsNotRootFree(t *testing.T) {
entries := []inventory.Entry{
{Manifest: catalogue.Manifest{Module: "telegram", RunsAs: "telegram"}, On: []string{"anchor"}},
{Manifest: catalogue.Manifest{Module: "messenger", RunsAs: "messenger",
Claims: []catalogue.Claim{{Name: routerSeat}}}, On: []string{"anchor"}},
{Manifest: catalogue.Manifest{Module: "xorg", Claims: []catalogue.Claim{{Name: catalogue.DisplayServerSeat}}},
On: []string{"laptop"}},
}
trusted := trustedMachines(entries)
if len(trusted["anchor"]) != 2 || len(trusted["laptop"]) != 0 {
t.Fatalf("trusted %v", trusted)
}
r := &rootReader{entries: entries, heard: map[string]map[string]map[string]bool{},
confined: func(_ context.Context, node string, _ time.Time) (bool, bool, string, error) {
return false, false, node + " names no agent account: agents run as the operator account (ops)", nil
},
settings: func(context.Context, string, string) ([]catalogue.Layer, error) { return nil, nil }}
got := rootObservations(context.Background(), r, trusted, rootNow)
if len(got) != 1 || got[0].Machine != "anchor" || got[0].Kind != kindRootNotFree || got[0].Severity != conditions.Urgent ||
!strings.Contains(got[0].Summary, "messenger, telegram") || !strings.Contains(got[0].Summary, "names no agent account") {
t.Fatalf("said %+v", got)
}
o := got[0]
if why, ok := conditions.PlainWords(conditions.Words{Headline: o.Headline, Explanation: o.Explanation,
Needs: o.Needs, Resolved: o.Resolved}, o.Machine); !ok {
t.Errorf("not plain: %s", why)
}
r.confined = func(context.Context, string, time.Time) (bool, bool, string, error) {
return true, true, "confined", nil
}
if got := rootObservations(context.Background(), r, trusted, rootNow); len(got) != 0 {
t.Errorf("said of a free machine: %+v", got)
}
}
// novox/hq ADR 0268: the login shell counts only where its execute is served, and fails closed.
func TestTheLoginShellCountsOnlyWhereExecuteIsServed(t *testing.T) {
val := func(v any) *any { return &v }
cases := []struct {
name string
claims bool
setting *any
heard, asked bool
served bool
saysInTheWhys string
}{
{"withheld by the setting and silent on the bus", true, val("withhold"), false, true, false, ""},
{"withheld by the setting, the machine not yet pushed", true, val("withhold"), true, true, true, "the bus hears"},
{"the setting serves", true, val("serve"), false, true, true, "setting there is serve"},
{"a wrong value withholds, as the holder does", true, val("Serve"), false, true, false, ""},
{"the setting withholds, the bus could not be asked", true, val("withhold"), false, false, true, "could not be asked"},
{"a holder with no such setting that claims execute", true, nil, false, true, true, "claims execute"},
{"a holder with no such setting that does not claim it, heard all the same", false, nil, true, true, true, "the bus hears"},
{"a holder with no such setting that does not claim it, silent", false, nil, false, true, false, ""},
}
for _, c := range cases {
served, why := loginShellServed("zsh", c.claims, c.setting, c.heard, c.asked)
if served != c.served || (c.saysInTheWhys != "" && !strings.Contains(why, c.saysInTheWhys)) {
t.Errorf("%s: served %v (%q), want %v saying %q", c.name, served, why, c.served, c.saysInTheWhys)
}
}
}
// The root-free verb is the serving controller's alone, answered in its process and never as a command; a
// controller not serving answers an error, which the router reads as no machine free.
func TestRootFreeIsAnsweredOnlyByTheServingController(t *testing.T) {
was := doctorFrom
doctorFrom = nil
t.Cleanup(func() { doctorFrom = was })
if _, err := rootFreeAnswer(context.Background(), "anchor", rootNow); err == nil {
t.Error("a controller not serving judged a machine")
}
if !inProcess["root-free"] {
t.Error("root-free is not answered in the serving process")
}
if _, err := argvFor("root-free", map[string]any{"machines": "anchor"}); err == nil {
t.Error("root-free ran as a command")
}
// The router names its machines as a list (its contract with this verb); one text separated by commas is
// the same; anything else in the list is refused.
for _, given := range []any{[]any{"anchor", "relay"}, "anchor, relay"} {
a, err := readArguments("root-free", map[string]any{"machines": given})
if err != nil || a.given["machines"] != "anchor,relay" && a.given["machines"] != "anchor, relay" {
t.Errorf("root-free given %v read %v (%v)", given, a, err)
}
}
if _, err := readArguments("root-free", map[string]any{"machines": []any{"anchor", 7}}); err == nil {
t.Error("root-free took a number for a machine")
}
if _, err := readArguments("status", map[string]any{"machines": []any{"anchor"}}); err == nil {
t.Error("a verb that takes no list took one")
}
}
// The confirmation review of 2026-10-09: ADR 0266's quiet window (#175) keeps the self-check from raising
// agent-can-become-root while the node-engine's setuid search runs and no complete one judges. It must not make root-free answer free:
// root-free needs a complete, fresh verdict. A verdict still waiting for the search is "not judged" to
// agentConfined, so the machine is not root-free, whatever the quiet says — and the same statement, complete
// and healthy, is the control.
func TestAMachineWaitingForItsFirstSetuidSearchIsNotRootFree(t *testing.T) {
now := rootNow
statement := func(state, reason string) inventory.NodeHealth {
return inventory.NodeHealth{Node: "anchor", Contract: link.RootContract, SaidAt: now, HeardAt: now,
Resources: []inventory.ResourceHealth{{Module: "claude-code", Resource: "agent", Kind: link.KindAccount,
Target: "agents", State: state, Reason: reason, Root: link.RootNever}}}
}
judged := func(h inventory.NodeHealth) rootVerdict {
confined, why := judgedConfined("agents", h, true, now)
return judgeRoot(rootFacts{Machine: "anchor", AgentNamed: true, Confined: confined, ConfinedWhy: why}, now)
}
if v := judged(statement(link.StateHealthy, "")); !v.Free {
t.Fatalf("the control: a complete healthy verdict, fresh: %+v", v)
}
pending := statement(link.StateUnknown, link.ReasonRootPending+": the search runs")
if rootVerdictKind("agents", pending, true, now) != verdictPending {
t.Fatal("the statement is not one the quiet window counts as waiting for the search")
}
if v := judged(pending); v.Free {
t.Errorf("a machine whose setuid search is pending was judged root-free: %+v", v)
}
}
// The confirmation review of 2026-10-09, on #154 beside ADR 0266: D-root keeps ADR 0266's quiet window — nothing
// raised while the one thing unjudged is the setuid search, within searchQuietFor — while the root-free verb
// still answers the machine not free; and D-root's condition has a key of its own, apart from DA's.
func TestRootNotFreeIsQuietWhileTheFirstSearchRunsAndKeyedApartFromDA(t *testing.T) {
entries := []inventory.Entry{{Manifest: catalogue.Manifest{Module: "telegram", RunsAs: "telegram"}, On: []string{"anchor"}}}
r := &rootReader{entries: entries, heard: map[string]map[string]map[string]bool{},
confined: func(context.Context, string, time.Time) (bool, bool, string, error) {
return true, false, "the agent account agents is not judged: the search for setuid programs runs", nil
},
settings: func(context.Context, string, string) ([]catalogue.Layer, error) { return nil, nil },
quiet: func(context.Context, string, time.Time) bool { return true }}
trusted := trustedMachines(entries)
if got := rootObservations(context.Background(), r, trusted, rootNow); len(got) != 0 {
t.Errorf("raised while the search runs: %+v", got)
}
if v := judgeRootFree(context.Background(), r, []string{"anchor"}, rootNow)[0]; v.Free || !v.Quiet {
t.Errorf("root-free while the search runs: %+v", v)
}
// Quiet hides nothing else: the login shell served as well is said.
r.heard = map[string]map[string]map[string]bool{loginShellSeat: {"execute": {"anchor": true}}}
if got := rootObservations(context.Background(), r, trusted, rootNow); len(got) != 1 {
t.Errorf("a second failure was kept quiet: %+v", got)
}
// Past searchQuietFor, said.
r.heard, r.quiet = map[string]map[string]map[string]bool{}, func(context.Context, string, time.Time) bool { return false }
got := rootObservations(context.Background(), r, trusted, rootNow)
if len(got) != 1 {
t.Fatalf("a search past searchQuietFor was not said: %+v", got)
}
// One key per judgement: DA's is machine.<m>.agent-root, this one its own.
da := conditions.Observation{Scope: conditions.ScopeMachine, ID: "anchor", Token: "agent-root", Machine: "anchor"}
if got[0].Key() == da.Key() {
t.Errorf("D-root and DA share the key %s", da.Key())
}
}
+31 -88
View File
@@ -252,10 +252,6 @@ func askEveryResolver(ctx context.Context, resolvers map[string]string, places [
v.wrong[0], andMore(len(v.wrong)-1)) v.wrong[0], andMore(len(v.wrong)-1))
} else { } else {
// Nothing but silence: held for the next run, which raises it if the resolver is still silent. // Nothing but silence: held for the next run, which raises it if the resolver is still silent.
// Refused on every try too: a few hundred milliseconds of refusals is a resolver restarting as
// well as one that stopped, and the two looks a finding needs are this run and the next
// (novox/hq issue 348). The machine's own node-engine is the fast detector: its names check
// raised the control node's resolver within a minute on 2026-10-09.
o.Confirm = true o.Confirm = true
o.Summary = fmt.Sprintf("the mesh's resolver on %s does not answer: %d of the %d question(s) about the "+ o.Summary = fmt.Sprintf("the mesh's resolver on %s does not answer: %d of the %d question(s) about the "+
"machines' names went unanswered, each asked %d times — the first, %s", node, len(v.unanswered), v.asked, "machines' names went unanswered, each asked %d times — the first, %s", node, len(v.unanswered), v.asked,
@@ -641,8 +637,31 @@ const (
// discoverHolders asks the bus's discovery who serves what, and answers seat → machine for every // discoverHolders asks the bus's discovery who serves what, and answers seat → machine for every
// endpoint a seat's verb is served on. // endpoint a seat's verb is served on.
func discoverHolders(ctx context.Context, conn *nats.Conn) (map[string]map[string]bool, error) { func discoverHolders(ctx context.Context, conn *nats.Conn) (map[string]map[string]bool, error) {
inbox := conn.NewRespInbox()
sub, err := conn.SubscribeSync(inbox)
if err != nil {
return nil, err
}
defer func() { _ = sub.Unsubscribe() }()
if err := conn.PublishRequest("$SRV.INFO", inbox, nil); err != nil {
return nil, fmt.Errorf("asking the bus who serves what: %w", err)
}
out := map[string]map[string]bool{} out := map[string]map[string]bool{}
err := discoverServices(ctx, conn, func(info micro.Info) { deadline := time.Now().Add(discoveryPatience)
for time.Now().Before(deadline) {
wait, cancel := context.WithTimeout(ctx, discoveryQuiet)
msg, err := sub.NextMsgWithContext(wait)
cancel()
if err != nil {
if ctx.Err() != nil {
return nil, ctx.Err()
}
break
}
var info micro.Info
if json.Unmarshal(msg.Data, &info) != nil {
continue
}
for _, e := range info.Endpoints { for _, e := range info.Endpoints {
seat, node := e.Metadata["seat"], e.Metadata["node"] seat, node := e.Metadata["seat"], e.Metadata["node"]
if seat == "" { if seat == "" {
@@ -661,69 +680,8 @@ func discoverHolders(ctx context.Context, conn *nats.Conn) (map[string]map[strin
out[info.Name] = map[string]bool{} out[info.Name] = map[string]bool{}
} }
out[info.Name][info.ID] = true out[info.Name][info.ID] = true
})
return out, err
} }
return out, nil
// discoverSeatVerbs asks the bus's discovery the same, one level finer: seat → verb → machine, for every
// seat verb answered (an endpoint's `tool` is its verb). A seat held where a verb is withheld (novox/hq ADR
// 0268) shows the seat and not that verb.
func discoverSeatVerbs(ctx context.Context, conn *nats.Conn) (map[string]map[string]map[string]bool, error) {
out := map[string]map[string]map[string]bool{}
err := discoverServices(ctx, conn, func(info micro.Info) {
for _, e := range info.Endpoints {
seat, verb, node := e.Metadata["seat"], e.Metadata["tool"], e.Metadata["node"]
if seat == "" || verb == "" {
continue
}
if node == "" {
node = info.ID
}
if out[seat] == nil {
out[seat] = map[string]map[string]bool{}
}
if out[seat][verb] == nil {
out[seat][verb] = map[string]bool{}
}
out[seat][verb][node] = true
}
})
return out, err
}
// discoverServices asks the bus's discovery once and hands every service's answer to visit, waiting
// discoveryQuiet after the last and discoveryPatience at the most.
func discoverServices(ctx context.Context, conn *nats.Conn, visit func(micro.Info)) error {
if conn == nil {
return errors.New("the controller holds no connection to the bus")
}
inbox := conn.NewRespInbox()
sub, err := conn.SubscribeSync(inbox)
if err != nil {
return err
}
defer func() { _ = sub.Unsubscribe() }()
if err := conn.PublishRequest("$SRV.INFO", inbox, nil); err != nil {
return fmt.Errorf("asking the bus who serves what: %w", err)
}
deadline := time.Now().Add(discoveryPatience)
for time.Now().Before(deadline) {
wait, cancel := context.WithTimeout(ctx, discoveryQuiet)
msg, err := sub.NextMsgWithContext(wait)
cancel()
if err != nil {
if ctx.Err() != nil {
return ctx.Err()
}
break
}
var info micro.Info
if json.Unmarshal(msg.Data, &info) != nil {
continue
}
visit(info)
}
return nil
} }
// probeArchives is D4: every archive the mesh keeps is held by its manifest in the artifact store. // probeArchives is D4: every archive the mesh keeps is held by its manifest in the artifact store.
@@ -1082,7 +1040,12 @@ func probeCoreBuilds(ctx context.Context, d *doctor) ([]conditions.Observation,
return nil, err return nil, err
} }
hostVersions := deliveredVersions(shelf[hostModule]) hostVersions := deliveredVersions(shelf[hostModule])
rolling := rollingModules(plans) rolling := map[string]bool{}
for _, p := range plans {
for m := range p.Modules {
rolling[m] = true
}
}
nodes, err := inv.Nodes(ctx) nodes, err := inv.Nodes(ctx)
if err != nil { if err != nil {
return nil, err return nil, err
@@ -1140,26 +1103,6 @@ func probeCoreBuilds(ctx context.Context, d *doctor) ([]conditions.Observation,
return out, nil return out, nil
} }
// rollingModules is every module an open plan is rolling out: those it keeps a record of, and those its
// tiers name. **A release keeps no record per module** — its walk is per machine, its modules only in its
// tier — so reading the records alone, D10 said "no plan is rolling them out" about the node-engine while
// a release walked it, and the gate on that release's first machine waited on what its own send causes
// (novox/hq issue 348). Pure.
func rollingModules(plans []inventory.Plan) map[string]bool {
rolling := map[string]bool{}
for _, p := range plans {
for m := range p.Modules {
rolling[m] = true
}
for _, tier := range p.Tiers {
for _, m := range tier {
rolling[m] = true
}
}
}
return rolling
}
// deliveredVersions are the versions a module's registered build is delivered as: the last element // deliveredVersions are the versions a module's registered build is delivered as: the last element
// of every resource path under a `versions/` directory, which registration filled from the artifact's // of every resource path under a `versions/` directory, which registration filled from the artifact's
// digest (catalogue `${version}`). The node-engine names itself by that directory. // digest (catalogue `${version}`). The node-engine names itself by that directory.
+1 -34
View File
@@ -76,21 +76,6 @@ func serve(ctx context.Context) (err error) {
} }
defer open.Close() defer open.Close()
inv := open.inventory inv := open.inventory
// **What this build reads of the store's schema, on record** (novox/hq issue 352): the highest migration
// it carries, by its version, so a gate that would put this build back later knows it reads the store
// as it is then. A build that does not know its version records nothing, and is never put back.
if build := runningBuild(); build != "" {
if reach, err := schemaReach(); err != nil {
fmt.Printf("what this build reads of the store's schema is not recorded: %v\n", err)
} else if err := inv.RecordSchemaReach(ctx, build, reach); err != nil {
fmt.Printf("what this build (%s) reads of the store's schema is not recorded: %v\n", build, err)
} else {
fmt.Printf("this build (%s) reads the store's schema up to migration %04d; recorded\n", build, reach)
}
} else {
fmt.Printf("this process was not told which build it is (%s), so what it reads of the store's schema is not "+
"recorded, and a gate will never put it back\n", RunningBuildVar)
}
ident, err := openIdentity(ctx) ident, err := openIdentity(ctx)
if err != nil { if err != nil {
@@ -1265,14 +1250,11 @@ func issueMemberships(ctx context.Context, open *stores, server *link.Server, se
if err != nil { if err != nil {
return err return err
} }
where := broker.PlacementsOf(records, records.Interchangeable)
bus, ok := server.Bus().(link.OverNATS) bus, ok := server.Bus().(link.OverNATS)
if !ok { if !ok {
return nil return nil
} }
// Which machines are root-free now (novox/hq ADR 0259 §8): a channel's verified sender is composed for the
// router only from one, beside a router on one. Judged once per push, by the root-free verb's judgement.
records.RootFree = rootFreeNow(ctx, newRootReader(ctx, open.inventory, bus.Conn), records.Nodes, time.Now())
where := broker.PlacementsOf(records, records.Interchangeable)
// **Every declared state's bucket, before the memberships that name it** (novox/hq ADR 0201). The // **Every declared state's bucket, before the memberships that name it** (novox/hq ADR 0201). The
// raise at start asserts them too, but a module registered and assigned since would otherwise have // raise at start asserts them too, but a module registered and assigned since would otherwise have
// its bucket only after the control plane next restarts — found the first time a module declared // its bucket only after the control plane next restarts — found the first time a module declared
@@ -1613,18 +1595,3 @@ func reportUnheldPushed(w io.Writer, named bool, asked []string, unheld map[stri
fmt.Fprintf(w, "%s: %d unmet seat dependenc(ies) — see `status`\n", node, len(lines)) fmt.Fprintf(w, "%s: %d unmet seat dependenc(ies) — see `status`\n", node, len(lines))
} }
} }
// schemaReach is the highest migration this build carries for the inventory's store (novox/hq issue 352).
func schemaReach() (int, error) {
migrations, err := inventory.Migrations()
if err != nil {
return 0, err
}
reach := 0
for _, m := range migrations {
if m.Number > reach {
reach = m.Number
}
}
return reach, nil
}
-114
View File
@@ -1,114 +0,0 @@
package main
// The rehearsal of the operator's answers — not a drill, which in the glossary is something broken on purpose (novox/hq ADR 0259, the live acceptance after rollout): an ask the
// operator starts at the controller's terminal, answered on the phone, whose approval changes nothing and is
// recorded as a person's decision like any other.
//
// mesh-controller rehearse [--for 15m]
//
// It asks with two answers, Approve and Decline, each bound to the rehearsal's own act and **both at the level
// approve** (the review of 2026-10-09, M1: an acknowledgement never shares an ask with an approval), so only a
// channel that proves who answered carries either — the rehearsal is of exactly that. The serving controller acts on the warrant
// as on any other: it claims the ask once, checks the act is the one bound, performs nothing, and records the
// hand-act `warrant` with who answered, through which channel, and the proofs. `hand-acts` then shows it.
//
// **The terminal's alone** (startedAtTheTerminal): a command a verb runs, an ordinary mesh-cli line and anything the
// serving controller started are refused, so no agent starts a rehearsal — a
// rehearsal is a question the operator expects, and one an agent could start would teach them to approve what they
// did not ask for.
import (
"context"
"encoding/json"
"errors"
"flag"
"fmt"
"time"
"github.com/nats-io/nats.go"
"git.novox.be/novox/mesh-sdk/go/asks"
"github.com/novox/mesh-controller/internal/conditions"
)
// rehearsalVerb is the act a rehearsal's answers bind: nothing is called.
const rehearsalVerb = "rehearsal"
// rehearsalActions are the rehearsal's two answers.
func rehearsalActions() []conditions.Action {
return []conditions.Action{
{Label: "Approve", Verb: rehearsalVerb, Level: conditions.LevelApprove, Arguments: map[string]string{"rehearsal": "approve"}},
{Label: "Decline", Verb: rehearsalVerb, Level: conditions.LevelApprove, Arguments: map[string]string{"rehearsal": "decline"}},
}
}
// rehearsalAsk is the rehearsal's ask, as the router is sent it.
func rehearsalAsk(id string, now time.Time, lasts time.Duration) (asks.Ask, map[string]int) {
q := asks.Ask{ID: id, Headline: "Rehearsal: approve this test question?", Who: asks.Operator,
Explanation: "Needs you: approve or decline. You started this rehearsal at the controller's terminal. Approving " +
"changes nothing on the mesh; it is recorded as your decision, so you can check the record.",
OnExpiry: "nothing is done", Expires: now.Add(lasts), About: "rehearsal." + id}
options := map[string]int{}
for i, act := range rehearsalActions() {
binds, _ := asks.ActDigest(boundAct(act))
oid := optionID(act.Label)
options[oid] = i
q.Options = append(q.Options, asks.Option{ID: oid, Label: act.Label, Does: doesRehearsal(act), Level: asks.Level(act.Level),
Binds: binds})
}
return q, options
}
func doesRehearsal(act conditions.Action) string {
if act.Arguments["rehearsal"] == "approve" {
return "nothing changes; your approval is recorded"
}
return "nothing changes; your answer is recorded"
}
func rehearseCommand(ctx context.Context, args []string) error {
// The terminal as main judges it (startedAtTheTerminal): not a verb, not the serving controller or anything it
// started, and a mesh-cli line only when it is the control-node's operator's (novox/hq ADR 0272 §4).
if !startedAtTheTerminal() {
return errors.New("rehearse is the controller's terminal's alone: a verb, a mesh-cli line from anybody but " +
"the control-node's operator, or a process the serving controller started may not start one, so no agent " +
"asks the operator a question they did not start (novox/hq ADR 0259)")
}
set := flag.NewFlagSet("rehearse", flag.ContinueOnError)
lasts := set.Duration("for", 15*time.Minute, "how long the question waits for an answer")
if err := set.Parse(args); err != nil {
return err
}
if *lasts < time.Minute || *lasts > askApproveFor {
return fmt.Errorf("a rehearsal waits between a minute and %s", askApproveFor)
}
js, err := aBus()
if err != nil {
return err
}
defer js.Close()
now := time.Now()
id := newAskID()
q, options := rehearsalAsk(id, now, *lasts)
if err := q.Check(now); err != nil {
return err
}
store := busAsked{conn: js.Conn()}
// Kept before it is published, as the asker keeps every ask, so a warrant always finds it.
if err := store.Create(ctx, asked{ID: id, Condition: q.About, Ask: q, Actions: rehearsalActions(), Options: options,
State: askOpen, Opened: now, Rehearsal: true}); err != nil {
return fmt.Errorf("the rehearsal could not be kept in the controller's asks: %w", err)
}
body, err := json.Marshal(q)
if err != nil {
return err
}
if _, err := js.Context().Publish(asks.AskSubject(askerName), body, nats.MsgId("ask."+id), nats.Context(ctx)); err != nil {
return fmt.Errorf("the rehearsal could not be asked: %w", err)
}
fmt.Printf("rehearsal %s asked: answer it on your phone before %s. Then `mesh-controller hand-acts` shows the "+
"answer as a warrant, with who answered, through which channel and the proofs; nothing else changes.\n",
id, q.Expires.Local().Format("15:04"))
return nil
}
-76
View File
@@ -1,76 +0,0 @@
package main
import (
"context"
"github.com/novox/mesh-controller/internal/link"
"strings"
"testing"
"time"
"git.novox.be/novox/mesh-sdk/go/asks"
)
// A rehearsal (the live acceptance of novox/hq ADR 0259): its approval is a warrant like any other — claimed once,
// its act checked against what the option bound, recorded as the operator's decision with who, how and the
// proofs — and it performs nothing. The reconciling of conditions leaves it open.
func TestARehearsalsApprovalIsRecordedAndPerformsNothing(t *testing.T) {
r := newAskerRig(t)
q, options := rehearsalAsk("crehearsal", r.now, askerRehearsalFor)
if err := q.Check(r.now); err != nil {
t.Fatalf("the rehearsal's ask is refused: %v", err)
}
r.store["crehearsal"] = asked{ID: "crehearsal", Condition: q.About, Ask: q, Actions: rehearsalActions(), Options: options,
State: askOpen, Opened: r.now, Rehearsal: true}
if err := r.a.reconcile(context.Background()); err != nil {
t.Fatal(err)
}
if got := r.store["crehearsal"]; got.State != askOpen {
t.Fatalf("the reconciling of conditions ended the rehearsal: %+v", got)
}
approve, _ := q.Option("approve")
w := asks.Warrant{Ask: "crehearsal", Asker: "mesh-controller", Outcome: asks.OutcomeChosen, Option: approve.ID,
Label: approve.Label, Level: approve.Level, Channel: "telegram", Proofs: []string{"P1"}, At: r.now,
AskDigest: q.Digest(), By: &asks.Person{Who: asks.Operator, Kind: "telegram", Identity: "42", Verified: "user id verified"}}
answerWith(t, r, w)
answerWith(t, r, w) // heard again
if len(r.called)+len(r.silenced) != 0 {
t.Errorf("a rehearsal performed something: %v %v", r.called, r.silenced)
}
if len(r.acts) != 1 {
t.Fatalf("hand-acts %+v", r.acts)
}
act := r.acts[0]
if act.Verb != handActWarrant || act.By != "the operator, as telegram identity 42" || act.Ask != "crehearsal" ||
strings.Join(act.Args, " ") != "rehearsal rehearsal=approve" || act.Outcome != "done" || strings.Join(act.Proofs, ",") != "P1" {
t.Errorf("the rehearsal's record: %+v", act)
}
if !personsDecision(act) {
t.Error("a rehearsal's answer counts as a repair")
}
}
// Only the terminal starts a rehearsal: a verb's process is refused before anything is asked.
func TestARehearsalIsTheTerminalsAlone(t *testing.T) {
t.Setenv(verbVar, "mesh-controller.command")
if err := rehearseCommand(context.Background(), nil); err == nil || !strings.Contains(err.Error(), "terminal") {
t.Fatalf("a verb started a rehearsal: %v", err)
}
// Nor a mesh-cli line from anybody but the control-node's operator (hq ADR 0272 §4): run without a verb,
// naming its caller, and without the terminal's mark — and nor anything the serving controller started.
for name, env := range map[string]map[string]string{
"an ordinary mesh-cli line": {verbVar: "", link.CallerVar: "laptop/agent"},
"a process the serving controller ran": {verbVar: "", servedVar: "1"},
} {
t.Run(name, func(t *testing.T) {
for k, v := range env {
t.Setenv(k, v)
}
if err := rehearseCommand(context.Background(), nil); err == nil || !strings.Contains(err.Error(), "terminal") {
t.Fatalf("%s started a rehearsal: %v", name, err)
}
})
}
}
// askerRehearsalFor is how long the test's rehearsal waits.
const askerRehearsalFor = 15 * time.Minute
+7 -47
View File
@@ -180,35 +180,12 @@ func (f moveFacts) moves(node string, modules []string, sent map[string]string,
return out return out
} }
// walkedBy is the open plan that has started walking a module's build — sent it to a first machine, not // walkedBy is the open plan that has started walking a module's build — sent it to a first machine,
// yet passed — and whose walk this move would cross; empty when none does. A move of the same build to a // not yet passed — other than to this machine; empty when none does.
// machine that plan already sent it is not a crossing: the build is there. A move of **another** build of func (f moveFacts) walkedBy(module, node string) string {
// the module to that machine is (novox/hq issue 352): on 2026-10-09 a merge's plan sent the control node a
// newer controller while a release's gate was judging the controller there, the release's gate read the
// machine's report against the newer send, failed three builds and put them back under the new plan's
// feet. A release keeps no module records: its open gate's carried moves are its walk.
func (f moveFacts) walkedBy(module, node, to string) string {
for _, p := range f.plans { for _, p := range f.plans {
if !p.Open() {
continue
}
if p.Release != nil {
g := p.Release.Gate
if g == nil || g.Verdict != "" {
continue
}
for _, c := range g.Carried {
if c.Module == module && !(slices.Contains(g.Machines, node) && sameCommit(c.To, to)) {
return p.ID
}
}
continue
}
s, holds := p.Modules[module] s, holds := p.Modules[module]
if !holds || s == nil || s.FirstAt == nil || s.SentAt != nil { if !p.Open() || !holds || s == nil || s.FirstAt == nil || s.SentAt != nil || slices.Contains(s.First, node) {
continue
}
if slices.Contains(s.First, node) && sameCommit(s.Commit, to) {
continue continue
} }
if s.Gate != nil && s.Gate.Verdict == inventory.GatePassed { if s.Gate != nil && s.Gate.Verdict == inventory.GatePassed {
@@ -300,19 +277,11 @@ func gatedSend(ctx context.Context, open *stores, node string, owns []inventory.
if own(mv.Module) { if own(mv.Module) {
continue continue
} }
if id := f.walkedBy(mv.Module, node, mv.To); id != "" { if id := f.walkedBy(mv.Module, node); id != "" {
return nil, nil, fmt.Errorf("%w: %s's build %s waits on %s, which %s is walking", errWalkedElsewhere, return nil, nil, fmt.Errorf("%w: %s's build %s waits on %s, which %s is walking", errWalkedElsewhere,
mv.Module, short(mv.To), node, id) mv.Module, short(mv.To), node, id)
} }
} }
// **And the plan's own modules wait too** (novox/hq issue 352): a walk of the same module by another
// plan, or a release, on this machine is not crossed with a newer build; this send waits for its gate.
for _, o := range owns {
if id := f.walkedBy(o.Module, node, o.To); id != "" {
return nil, nil, fmt.Errorf("%w: %s's build %s waits on %s, which %s is walking", errWalkedElsewhere,
o.Module, short(o.To), node, id)
}
}
for _, o := range owns { for _, o := range owns {
i := slices.IndexFunc(moves, func(mv inventory.CarriedMove) bool { return mv.Module == o.Module }) i := slices.IndexFunc(moves, func(mv inventory.CarriedMove) bool { return mv.Module == o.Module })
switch { switch {
@@ -557,7 +526,7 @@ func waitingMoves(ctx context.Context, open *stores, all bool) (map[string][]inv
return nil, err return nil, err
} }
for _, mv := range moves { for _, mv := range moves {
if f.walkedBy(mv.Module, n.Name, mv.To) == "" { if f.walkedBy(mv.Module, n.Name) == "" {
out[n.Name] = append(out[n.Name], mv) out[n.Name] = append(out[n.Name], mv)
} }
} }
@@ -689,7 +658,7 @@ func advanceRelease(ctx context.Context, open *stores, p *inventory.Plan) (bool,
r.Next++ r.Next++
continue continue
} }
r.Gate = &inventory.PlanGate{Machines: sent, Since: &now, Carried: moves, Sent: sentNow(ctx, open.inventory, sent)} r.Gate = &inventory.PlanGate{Machines: sent, Since: &now, Carried: moves}
p.Note = fmt.Sprintf("sent %s %d build(s) that waited for a gate; judging them there", node, len(moves)) p.Note = fmt.Sprintf("sent %s %d build(s) that waited for a gate; judging them there", node, len(moves))
if said := recreationsSaid(moves); said != "" { if said := recreationsSaid(moves); said != "" {
p.Note += "; " + said p.Note += "; " + said
@@ -724,15 +693,6 @@ func advanceRelease(ctx context.Context, open *stores, p *inventory.Plan) (bool,
r.Next++ r.Next++
r.Gate = nil r.Gate = nil
return true, nil return true, nil
case inventory.GateSuperseded:
// Another send moved a judged module on the judged machine (novox/hq issue 352): no verdict on what
// was carried, nothing put back, and this release ends; the builds still waiting are released again
// by the next pass, judged afresh.
p.State = inventory.PlanSuperseded
p.Note = fmt.Sprintf("superseded on %s: %s — nothing judged, nothing put back; what still waits is released again",
strings.Join(g.Machines, ", "), g.Why)
fmt.Printf("%s: %s\n", p.ID, p.Note)
return true, nil
} }
p.Note = "" p.Note = ""
batched, back := batchingRollbacks(ctx) batched, back := batchingRollbacks(ctx)
+16 -86
View File
@@ -138,10 +138,9 @@ func reachableFrom(moved []string, edges []inventory.Edge) []string {
grew = false grew = false
for _, e := range edges { for _, e := range edges {
// Built-by and worker-of order a plan; neither widens it. A new build machine changes // Built-by and worker-of order a plan; neither widens it. A new build machine changes
// nothing it builds, and a new controller changes nothing about the holder it orders. A // nothing it builds, and a new controller changes nothing about the holder it orders —
// packages edge, read from a record made before novox/hq ADR 0267, widens nothing either: // what packages the controller's source is already a code edge.
// a shared file moves each module whose build source holds it, directly. if e.Kind == inventory.EdgeBuiltBy || e.Kind == inventory.EdgeWorkerOf {
if e.Kind == inventory.EdgeBuiltBy || e.Kind == inventory.EdgeWorkerOf || e.Kind == inventory.EdgePackages {
continue continue
} }
if in[e.To] && !in[e.From] { if in[e.To] && !in[e.From] {
@@ -189,13 +188,11 @@ func planOfMerge(m link.SourceMoved, moved []string, edges []inventory.Edge) inv
for _, name := range set { for _, name := range set {
modules[name] = &inventory.PlanModule{} modules[name] = &inventory.PlanModule{}
} }
merged, _ := time.Parse(time.RFC3339Nano, m.MergedAt)
return inventory.Plan{ return inventory.Plan{
ID: fmt.Sprintf("plan-%d", time.Now().UnixNano()), ID: fmt.Sprintf("plan-%d", time.Now().UnixNano()),
Repository: m.Owner + "/" + m.Repo, Repository: m.Owner + "/" + m.Repo,
Branch: m.Base, Branch: m.Base,
Commit: m.Commit, Commit: m.Commit,
Merged: merged.UTC(),
Created: time.Now().UTC(), Created: time.Now().UTC(),
State: inventory.PlanBuilding, State: inventory.PlanBuilding,
Tiers: tiers, Tiers: tiers,
@@ -223,20 +220,12 @@ func planOfMerge(m link.SourceMoved, moved []string, edges []inventory.Edge) inv
// //
// A plan with no branch recorded is from before branches were kept, and is superseded by the next // A plan with no branch recorded is from before branches were kept, and is superseded by the next
// plan of its repository: what it had not built is folded in, so nothing is lost by it. // plan of its repository: what it had not built is folded in, so nothing is lost by it.
//
// **Newer is the branch's order, not the plans'** (novox/hq issue 349). A merge the bus did not hand
// over is acted on late, by the catch-up, so its plan is made after the plan of a merge that came after
// it — and that later-made plan of the earlier commit superseded the later merge's, and built what it
// folded in from the commit before the later merge: twice on 2026-10-09, once a security fix. So where
// both plans know when their merge was made, that decides; only where one does not do the plans' own
// times. A merge older than the newest planned merge of its branch never reaches here at its own commit:
// it is planned at that merge's commit, which contains it (laterOnTheBranch).
func supersededBy(newer inventory.Plan, open []inventory.Plan, rollsOut func(string) bool) ([]string, []inventory.Plan) { func supersededBy(newer inventory.Plan, open []inventory.Plan, rollsOut func(string) bool) ([]string, []inventory.Plan) {
folded := map[string]bool{} folded := map[string]bool{}
var closed []inventory.Plan var closed []inventory.Plan
for _, old := range open { for _, old := range open {
if old.ID == newer.ID || !old.Open() || !strings.EqualFold(old.Repository, newer.Repository) || if old.ID == newer.ID || !old.Open() || !strings.EqualFold(old.Repository, newer.Repository) ||
(old.Branch != "" && old.Branch != newer.Branch) || !earlierOnTheBranch(old, newer) { (old.Branch != "" && old.Branch != newer.Branch) || !old.Created.Before(newer.Created) {
continue continue
} }
var took []string var took []string
@@ -265,30 +254,6 @@ func supersededBy(newer inventory.Plan, open []inventory.Plan, rollsOut func(str
return out, closed return out, closed
} }
// earlierOnTheBranch says plan a answers a merge made before b's: by when the forge made each merge where
// both are known, else by when each plan was made. A merge's own plan made again at the same commit
// (the same merge time) is ordered by when it was made. Pure.
func earlierOnTheBranch(a, b inventory.Plan) bool {
if !a.Merged.IsZero() && !b.Merged.IsZero() && !a.Merged.Equal(b.Merged) {
return a.Merged.Before(b.Merged)
}
return a.Created.Before(b.Created)
}
// laterOnTheBranch says the plan newest answers a merge into m's branch made after m: then newest's commit
// contains m's change, and m is planned there, so the newest commit of the branch is what is built (novox/hq
// issue 349). newest is the newest merge's plan of the branch in any state: a later plan already done
// built what it moved at its commit, and m planned at its own would build m's dependents back at the older
// one. Pure.
func laterOnTheBranch(m link.SourceMoved, newest inventory.Plan) bool {
merged, err := time.Parse(time.RFC3339Nano, m.MergedAt)
if err != nil || newest.Release != nil || newest.Commit == m.Commit {
return false
}
return strings.EqualFold(newest.Repository, m.Owner+"/"+m.Repo) && newest.Branch == m.Base &&
newest.Merged.After(merged)
}
// gates is what the next tier needs running from this one: a module of the tier that a later // gates is what the next tier needs running from this one: a module of the tier that a later
// tier is built by — the runtime dependency — and whose policy rolls it out, must be applied by // tier is built by — the runtime dependency — and whose policy rolls it out, must be applied by
// the machines running it before the next tier is asked. A base an image stands on need only be // the machines running it before the next tier is asked. A base an image stands on need only be
@@ -794,15 +759,6 @@ func advanceOnce(ctx context.Context, open *stores, p *inventory.Plan,
case inventory.GateFailed: case inventory.GateFailed:
failFirstSend(ctx, open, p, m, state, state.Gate.Machines, state.Gate.Why, step.rest) failFirstSend(ctx, open, p, m, state, state.Gate.Machines, state.Gate.Why, step.rest)
return true, nil return true, nil
case inventory.GateSuperseded:
// Another send moved this module on its first machine (novox/hq issue 352): the build is not
// judged, not marked, not put back; the plan ends here, said, and a newer plan carries on.
state.Why = "superseded: " + state.Gate.Why
p.State = inventory.PlanSuperseded
p.Note = fmt.Sprintf("%s's judging on %s was superseded: %s", m, strings.Join(state.Gate.Machines, ", "),
state.Gate.Why)
fmt.Printf("%s: %s\n", p.ID, p.Note)
return true, nil
case inventory.GatePassed: case inventory.GatePassed:
if !state.Gate.Kept { if !state.Gate.Kept {
gatePassed(ctx, open, p, m, state) gatePassed(ctx, open, p, m, state)
@@ -974,9 +930,6 @@ func firstSend(ctx context.Context, open *stores, p *inventory.Plan, node string
} }
now := time.Now().UTC() now := time.Now().UTC()
lead := modules[0] lead := modules[0]
// What each machine was just sent, kept on the gate (novox/hq issue 352): its report is held against
// this send, whatever it is sent after.
sentWhat := sentNow(ctx, inv, sent)
for _, m := range modules { for _, m := range modules {
s := p.Modules[m] s := p.Modules[m]
// What the first machine ran before: what a failed gate puts back (ADR 0236). // What the first machine ran before: what a failed gate puts back (ADR 0236).
@@ -985,7 +938,7 @@ func firstSend(ctx context.Context, open *stores, p *inventory.Plan, node string
} }
s.First, s.FirstAt = sent, &now s.First, s.FirstAt = sent, &now
s.Gate = &inventory.PlanGate{Component: coreComponent(m), Machines: firstRunning(sent, runningOf[m]), s.Gate = &inventory.PlanGate{Component: coreComponent(m), Machines: firstRunning(sent, runningOf[m]),
From: s.Previous, To: s.Commit, Since: &now, Sent: sentWhat} From: s.Previous, To: s.Commit, Since: &now}
s.GatedBy = "" s.GatedBy = ""
if m == lead { if m == lead {
s.Gate.Carried = carried s.Gate.Carried = carried
@@ -1144,15 +1097,8 @@ func nextRollout(s inventory.PlanModule, running []string, together bool, report
var waiting, failed []string var waiting, failed []string
for _, n := range s.First { for _, n := range s.First {
r, said := byNode[n] r, said := byNode[n]
// Only a report about what this plan sent it — or what it was sent after that — says anything about // Only a report about what it was last sent says anything about this build.
// this build (novox/hq issue 352); a plan from before sends were kept on the gate reads the report if !said || r.At == nil || !r.Current {
// against the send made last, as before.
reported := r.Current
if s.Gate != nil && s.Gate.Sent != nil {
sent, kept := s.Gate.Sent[n]
reported = kept && sent.ReportsOn(r)
}
if !said || r.At == nil || !reported {
waiting = append(waiting, n) waiting = append(waiting, n)
continue continue
} }
@@ -1562,12 +1508,11 @@ func planWhatIf(ctx context.Context, inv *inventory.Inventory, repository string
if err != nil { if err != nil {
return err return err
} }
read, err := readForPlanning(ctx, inv) read, err := inv.ReadRepositories(ctx)
if err != nil { if err != nil {
return err return err
} }
var from, packaging []inventory.Entry var from, packaging []inventory.Entry
deleted := map[string]bool{}
named := map[string]bool{} named := map[string]bool{}
for _, name := range modules { for _, name := range modules {
named[name] = true named[name] = true
@@ -1577,9 +1522,6 @@ func planWhatIf(ctx context.Context, inv *inventory.Inventory, repository string
// request's check ask it (novox/hq ADR 0238). // request's check ask it (novox/hq ADR 0238).
r := reachOfMerge(m, entries, read, nil) r := reachOfMerge(m, entries, read, nil)
from, packaging = append(append([]inventory.Entry{}, r.Touched...), r.Deleted...), r.Packaging from, packaging = append(append([]inventory.Entry{}, r.Touched...), r.Deleted...), r.Packaging
for _, e := range r.Deleted {
deleted[e.Manifest.Module] = true
}
} else { } else {
for _, e := range entries { for _, e := range entries {
switch { switch {
@@ -1606,18 +1548,6 @@ func planWhatIf(ctx context.Context, inv *inventory.Inventory, repository string
} }
p := planOfMerge(m, names, edges) p := planOfMerge(m, names, edges)
fmt.Printf("a merge of %s would build %d module(s) in %d tier(s):\n", repository, len(p.Modules), len(p.Tiers)) fmt.Printf("a merge of %s would build %d module(s) in %d tier(s):\n", repository, len(p.Modules), len(p.Tiers))
why := map[string]string{}
for _, e := range packaging {
why[e.Manifest.Module] = whyMoved(e, read[e.Manifest.Module], m)
}
if len(named) == 0 {
for _, e := range from {
why[e.Manifest.Module] = whyMoved(e, read[e.Manifest.Module], m)
if deleted[e.Manifest.Module] {
why[e.Manifest.Module] = "its manifest is removed: deleted at its source, forgotten where nothing holds it, not built"
}
}
}
rolls := map[string]string{} rolls := map[string]string{}
for i, tier := range p.Tiers { for i, tier := range p.Tiers {
fmt.Printf(" tier %d\n", i) fmt.Printf(" tier %d\n", i)
@@ -1635,19 +1565,19 @@ func planWhatIf(ctx context.Context, inv *inventory.Inventory, repository string
rolls[name] = how rolls[name] = how
} }
fmt.Printf(" %-22s %s\n", name, how) fmt.Printf(" %-22s %s\n", name, how)
reason := why[name]
switch {
case reason == "" && len(named) > 0 && named[name]:
reason = "named"
case reason == "":
reason = "it stands on a module the merge moves"
}
fmt.Printf(" %-22s why: %s\n", "", reason)
} }
} }
if hasCycle(p.Tiers, edges) { if hasCycle(p.Tiers, edges) {
fmt.Println(" the last tier depends on itself and would be built together, in no order") fmt.Println(" the last tier depends on itself and would be built together, in no order")
} }
if len(packaging) > 0 {
var also []string
for _, e := range packaging {
also = append(also, e.Manifest.Module)
}
fmt.Printf(" %s package source from %s, so they are rebuilt without their own source moving\n",
strings.Join(also, ", "), repository)
}
return nil return nil
} }
+6 -7
View File
@@ -59,18 +59,17 @@ func TestAMergeIsPlannedInTiersAlongTheThreeKindsOfDependency(t *testing.T) {
t.Fatalf("no cycle here: %v", tiers) t.Fatalf("no cycle here: %v", tiers)
} }
// The controller alone moved: the controller alone — what packages its repository moves only when the // The controller alone moved: the proxy with it, nothing else.
// change is in its own build source (novox/hq ADR 0267), so a packages edge widens nothing. small := reachableFrom([]string{"mesh-controller"}, edges)
if alone := reachableFrom([]string{"mesh-controller"}, edges); len(alone) != 1 { if len(small) != 3 {
t.Fatalf("a controller merge rebuilds the controller alone: %v", alone) t.Fatalf("a controller merge rebuilds the controller and what packages it: %v", small)
} }
// A change to a package all three build from moves all three. The builder holds // The builder and the proxy package the controller's source, which orders nothing. The builder holds
// the build seat, whose worker the controller defines, so it follows the controller (worker-of, // the build seat, whose worker the controller defines, so it follows the controller (worker-of,
// novox/hq issue 206), and the controller's built-by edge to it yields: the controller is built by the // novox/hq issue 206), and the controller's built-by edge to it yields: the controller is built by the
// build machine that is running. The proxy is built by the new builder: the controller, the builder, // build machine that is running. The proxy is built by the new builder: the controller, the builder,
// the proxy — the live plan of every controller merge. (This read "the builder, then the controller // the proxy — the live plan of every controller merge. (This read "the builder, then the controller
// and the proxy together" before issue 206, and the fixture had no worker-of edge.) // and the proxy together" before issue 206, and the fixture had no worker-of edge.)
small := reachableFrom([]string{"mesh-controller", "builder", "route-proxy"}, edges)
smallTiers := tiersOf(small, edges) smallTiers := tiersOf(small, edges)
if got := tiered(smallTiers); got != "mesh-controller | builder | route-proxy" { if got := tiered(smallTiers); got != "mesh-controller | builder | route-proxy" {
t.Fatalf("the controller, then the builder, then the proxy: %v", smallTiers) t.Fatalf("the controller, then the builder, then the proxy: %v", smallTiers)
@@ -244,7 +243,7 @@ func TestAChangeToTheBuildAgentRebuildsTheBuildAgentAlone(t *testing.T) {
} { } {
m := link.SourceMoved{Owner: "novox", Repo: "mesh-catalog", Base: "main", Commit: "abc", Paths: paths, m := link.SourceMoved{Owner: "novox", Repo: "mesh-catalog", Base: "main", Commit: "abc", Paths: paths,
ModuleDirs: []string{"modules/build-agent"}, ModuleDirsSaid: true} ModuleDirs: []string{"modules/build-agent"}, ModuleDirsSaid: true}
touched := whatTheMergeTouched(entries, entries, m, nil) touched := whatTheMergeTouched(entries, entries, m)
if len(touched) != 1 || touched[0].Manifest.Module != "build-agent" { if len(touched) != 1 || touched[0].Manifest.Module != "build-agent" {
t.Fatalf("%v touched %v", paths, touched) t.Fatalf("%v touched %v", paths, touched)
} }
-5
View File
@@ -28,11 +28,6 @@ import (
func TestMain(m *testing.M) { func TestMain(m *testing.M) {
// The process a mesh-cli test runs as a command line: it says the verb and the caller it was given, and // The process a mesh-cli test runs as a command line: it says the verb and the caller it was given, and
// ends (meshcli_test.go). // ends (meshcli_test.go).
// The process a mesh-cli test runs as `secret accept`: it reads its value exactly as `secret accept` does
// (valueFor) and says whether it is the one the test gave, never the value (meshcli_stdin_test.go).
if want := os.Getenv(secretAcceptWants); want != "" {
os.Exit(readAsSecretAccept(want, os.Args[1:]))
}
if os.Getenv(echoEnvironment) != "" { if os.Getenv(echoEnvironment) != "" {
fmt.Printf("verb=%q caller=%q terminal=%v\n", os.Getenv("MESH_VERB"), os.Getenv("MESH_CALLER"), startedAtTheTerminal()) fmt.Printf("verb=%q caller=%q terminal=%v\n", os.Getenv("MESH_VERB"), os.Getenv("MESH_CALLER"), startedAtTheTerminal())
os.Exit(0) os.Exit(0)
-6
View File
@@ -135,12 +135,6 @@ func handOver(ctx context.Context, seatName, to string, adding bool) error {
if !ok || nodeName == "" || module == "" { if !ok || nodeName == "" || module == "" {
return fmt.Errorf("the new holder is named <node>/<module>, not %q", to) return fmt.Errorf("the new holder is named <node>/<module>, not %q", to)
} }
// A kinded bench is held once per kind, by the claims themselves (novox/hq ADR 0234 §2, ADR 0259): the
// record of who holds a seat has no kind, so a handover would name one holder for every kind.
if catalogue.KindedBenches[seatName] {
return fmt.Errorf("%s is a kinded bench: each kind is held by the module claiming it, and is not handed "+
"over by `seat` — assign the module that claims the kind, or unassign the one that does", seatName)
}
open, err := openStores(ctx) open, err := openStores(ctx)
if err != nil { if err != nil {
return err return err
-12
View File
@@ -1,8 +1,6 @@
package main package main
import ( import (
"context"
"strings"
"testing" "testing"
"github.com/novox/mesh-controller/internal/catalogue" "github.com/novox/mesh-controller/internal/catalogue"
@@ -64,13 +62,3 @@ func TestAClaimOutsideTheSetIsShownNotHidden(t *testing.T) {
t.Fatalf("a claim outside the set was not shown: %+v", outside) t.Fatalf("a claim outside the set was not shown: %+v", outside)
} }
} }
// A kinded bench is not handed over by `seat`: each kind is held by its claim (novox/hq ADR 0259).
func TestAKindedBenchIsNotHandedOver(t *testing.T) {
for _, bench := range []string{"channel", "intake"} {
err := handOver(context.Background(), bench, "anchor/telegram", false)
if err == nil || !strings.Contains(err.Error(), "is a kinded bench") {
t.Errorf("%s: %v", bench, err)
}
}
}
+3 -60
View File
@@ -114,14 +114,6 @@ func declaredArguments(v catalogue.Verb) (names []string, switches map[string]bo
return names, switches return names, switches
} }
// isList says a verb's argument is declared a list of text (catalogue's listed): given as a JSON array, it is
// read as its items joined by commas, as the same argument given as one text would be.
func isList(v catalogue.Verb, name string) bool {
props, _ := v.Input["properties"].(map[string]any)
p, _ := props[name].(map[string]any)
return p != nil && p["type"] == "array"
}
// readArguments refuses what the verb does not take, before anything is composed. // readArguments refuses what the verb does not take, before anything is composed.
func readArguments(verb string, args map[string]any) (*verbArguments, error) { func readArguments(verb string, args map[string]any) (*verbArguments, error) {
v, known := controllerVerb(verb) v, known := controllerVerb(verb)
@@ -158,19 +150,6 @@ func readArguments(verb string, args map[string]any) (*verbArguments, error) {
return nil, fmt.Errorf("%s: %q is text, not true or false", verb, k) return nil, fmt.Errorf("%s: %q is text, not true or false", verb, k)
} }
value = fmt.Sprint(x) value = fmt.Sprint(x)
case []any:
if !isList(v, k) {
return nil, fmt.Errorf("%s: %q is text, and was given a list", verb, k)
}
items := make([]string, 0, len(x))
for _, item := range x {
text, ok := item.(string)
if !ok || strings.TrimSpace(text) == "" || strings.Contains(text, ",") {
return nil, fmt.Errorf("%s: %q is a list of names, and holds %v", verb, k, item)
}
items = append(items, strings.TrimSpace(text))
}
value = strings.Join(items, ",")
default: default:
return nil, fmt.Errorf("%s: %q is text, and was given %T", verb, k, x) return nil, fmt.Errorf("%s: %q is text, and was given %T", verb, k, x)
} }
@@ -303,8 +282,6 @@ func (a *verbArguments) commandLine() ([]string, error) {
return nil, errors.New("tools is answered from the records, not by a command") return nil, errors.New("tools is answered from the records, not by a command")
case "dead-letters": case "dead-letters":
return nil, errors.New("dead-letters is answered by the serving controller, on its own connection, not by a command") return nil, errors.New("dead-letters is answered by the serving controller, on its own connection, not by a command")
case "root-free":
return nil, errors.New("root-free is judged by the serving controller, on its own connection, not by a command")
case "status": case "status":
return []string{"status", "--json"}, nil return []string{"status", "--json"}, nil
case "nodes": case "nodes":
@@ -536,11 +513,6 @@ func (a *verbArguments) commandLine() ([]string, error) {
argv = append(argv, "--condition", c) argv = append(argv, "--condition", c)
} }
return argv, nil return argv, nil
case "warranted":
if err := need("asker", "ask"); err != nil {
return nil, err
}
return []string{"hand-act", "warrant", "--asker", str("asker"), "--ask", str("ask")}, nil
case "hand-acts": case "hand-acts":
argv := []string{"hand-acts", "--json"} argv := []string{"hand-acts", "--json"}
if d := str("days"); d != "" { if d := str("days"); d != "" {
@@ -767,11 +739,6 @@ func (a *verbArguments) commandLine() ([]string, error) {
argv = append(argv, "--probe", p) argv = append(argv, "--probe", p)
} }
return append(argv, "--json"), nil return append(argv, "--json"), nil
case "give":
if err := need("node", "module", "secret", "at"); err != nil {
return nil, err
}
return []string{"secret", "accept", str("node"), str("module"), str("secret"), "--at-desk", str("at")}, nil
case "rotate": case "rotate":
if p := str("provision"); p != "" { if p := str("provision"); p != "" {
argv := []string{"rotate", p} argv := []string{"rotate", p}
@@ -940,8 +907,6 @@ func repairingCommand(argv []string) string {
return "plans " + argv[1] return "plans " + argv[1]
case argv[0] == "broker" && len(argv) > 1 && argv[1] == "consumer-reset": case argv[0] == "broker" && len(argv) > 1 && argv[1] == "consumer-reset":
return "broker consumer-reset" return "broker consumer-reset"
case argv[0] == "hand-act" && len(argv) > 1 && argv[1] == "warrant":
return "" // the router's record of a person's answer, never a repair (novox/hq ADR 0274)
case argv[0] == "hand-act" && len(argv) > 1 && argv[1] == "drill": case argv[0] == "hand-act" && len(argv) > 1 && argv[1] == "drill":
return "hand-act drill" return "hand-act drill"
case argv[0] == "hand-act": case argv[0] == "hand-act":
@@ -1127,9 +1092,6 @@ func seatToolHandlers() (map[string]link.ToolHandler, []string, error) {
if verb == "dead-letters" { if verb == "dead-letters" {
return deadLettersAnswer(ctx, a) return deadLettersAnswer(ctx, a)
} }
if verb == "root-free" {
return rootFreeAnswer(ctx, a.given["machines"], rootClock())
}
if verb == "doctor" { if verb == "doctor" {
// From the serving controller, which runs the self-check and hears the signals // From the serving controller, which runs the self-check and hears the signals
// (novox/hq to-be 45 §4): the last verdict at once, or a run now. // (novox/hq to-be 45 §4): the last verdict at once, or a run now.
@@ -1220,10 +1182,7 @@ func actsOnAPlan(args map[string]any) bool {
var inProcess = map[string]bool{"tools": true, "calls": true, "doctor": true, var inProcess = map[string]bool{"tools": true, "calls": true, "doctor": true,
// What a consumer gave up on, read and changed on the serving controller's own connection (novox/hq // What a consumer gave up on, read and changed on the serving controller's own connection (novox/hq
// issue 330). // issue 330).
"dead-letters": true, "dead-letters": true}
// Whether a machine is root-free, judged live on the serving controller's store and connection (novox/hq ADR
// 0259 §8): the router asks it before an approval.
"root-free": true}
// answersFirst is a command line whose caller is answered before it runs: a push, by its verb or // answersFirst is a command line whose caller is answered before it runs: a push, by its verb or
// through `command`. A push sends the machine holding the bus first when its user list changed, the // through `command`. A push sends the machine holding the bus first when its user list changed, the
@@ -1507,20 +1466,6 @@ var terminalOnlyCommands = map[string]string{
"licence": "the licences' secrets", "licence": "the licences' secrets",
} }
// givenAtTheDesk is exactly the line the `give` verb composes, and nothing beside it: `secret accept <node>
// <module> <secret> --at-desk <machine>`, with no other word — no value, no file, no provider.
func givenAtTheDesk(argv []string) bool {
if len(argv) != 7 || argv[0] != "secret" || argv[1] != "accept" || argv[5] != "--at-desk" {
return false
}
for _, w := range argv[2:5] {
if w == "" || strings.HasPrefix(w, "-") {
return false
}
}
return argv[6] != "" && !strings.HasPrefix(argv[6], "-")
}
// terminalOnly refuses, through any verb, a command that is the operator's at the controller's terminal // terminalOnly refuses, through any verb, a command that is the operator's at the controller's terminal
// alone (novox/hq ADR 0266). **Every `node` subcommand that is not a read**: `node account` and // alone (novox/hq ADR 0266). **Every `node` subcommand that is not a read**: `node account` and
// `node agent-account` above all. Whoever may call a verb includes agents, and an agent that named itself // `node agent-account` above all. Whoever may call a verb includes agents, and an agent that named itself
@@ -1534,10 +1479,8 @@ func terminalOnly(argv []string) error {
return terminalRefusal("%s is run at the controller's terminal only, never through a verb: it holds %s, and "+ return terminalRefusal("%s is run at the controller's terminal only, never through a verb: it holds %s, and "+
"whoever may call a verb includes agents (novox/hq ADR 0266). Nothing was done", argv[0], what) "whoever may call a verb includes agents (novox/hq ADR 0266). Nothing was done", argv[0], what)
} }
// Of a secret's commands only rotation, which seals the new value to the machine that uses it, and the // Of a secret's commands only rotation, which seals the new value to the machine that uses it.
// `give` verb's own line: an own secret typed by the operator into the desk's hidden prompt, sealed to this if argv[0] == "secret" && (len(argv) < 2 || argv[1] != "rotate") {
// call and then to the module's machine, so no value travels in the verb or its answer (hq ADR 0259 §10).
if argv[0] == "secret" && (len(argv) < 2 || argv[1] != "rotate") && !givenAtTheDesk(argv) {
return terminalRefusal("secret %s is run at the controller's terminal only, never through a verb: accepting, "+ return terminalRefusal("secret %s is run at the controller's terminal only, never through a verb: accepting, "+
"recovering or exporting a secret hands it to whoever asks, and that includes agents (novox/hq ADR "+ "recovering or exporting a secret hands it to whoever asks, and that includes agents (novox/hq ADR "+
"0266). Nothing was done", strings.Join(argv[1:], " ")) "0266). Nothing was done", strings.Join(argv[1:], " "))
@@ -275,13 +275,6 @@ var accountedFlags = map[string]map[string]string{
"json": "set by the verb: the answer is data", "json": "set by the verb: the answer is data",
"all": "withheld: every measurement of a fortnight is more than a call should carry; `command` reaches it", "all": "withheld: every measurement of a fortnight is more than a call should carry; `command` reaches it",
}, },
// The desk path of `secret accept` (novox/hq ADR 0259 §10): a value is never an argument of a call.
"secret accept": {
"at-desk": "=at",
"from": "withheld: a file of the control node's is read at a shell, never named by a call",
"provider": "withheld: a pair credential's value is given at a shell; give takes a module's own secret",
"local": "withheld: it goes with --provider",
},
"hand-acts": {"json": "set by the verb: the answer is data"}, "hand-acts": {"json": "set by the verb: the answer is data"},
"conditions": {"json": "set by the verb: the answer is data"}, "conditions": {"json": "set by the verb: the answer is data"},
"retire": {"json": "set by the verb: the answer is data"}, "retire": {"json": "set by the verb: the answer is data"},
+2 -55
View File
@@ -55,9 +55,6 @@ func secretCommand(ctx context.Context, args []string) error {
provider := set.String("provider", "", provider := set.String("provider", "",
"the node providing <name>: the value becomes the PAIR credential between <module> on <node> "+ "the node providing <name>: the value becomes the PAIR credential between <module> on <node> "+
"and that provider, sealed to both — the vault's operator-delivered secret (ADR 0092)") "and that provider, sealed to both — the vault's operator-delivered secret (ADR 0092)")
desk := set.String("at-desk", "",
"ask the operator for the value in a prompt that does not show it, on this machine's desk; the "+
"answer comes back sealed to this call alone (novox/hq ADR 0259 §10)")
local := set.String("local", "", local := set.String("local", "",
"with --provider: the name the credential goes by inside <module>, where its manifest keeps "+ "with --provider: the name the credential goes by inside <module>, where its manifest keeps "+
"several for <name> (ADR 0094)") "several for <name> (ADR 0094)")
@@ -68,18 +65,6 @@ func secretCommand(ctx context.Context, args []string) error {
return errors.New(secretUsage) return errors.New(secretUsage)
} }
node, module, name := rest[0], rest[1], rest[2] node, module, name := rest[0], rest[1], rest[2]
// A value comes from the terminal or the desk, never through a verb (the review of 2026-10-09, M4): a
// verb's caller may be an agent, and a value it chose would become what a module acts with.
if verb, through := throughAVerb(); through && *desk == "" {
return fmt.Errorf("a secret's value is given at the controller's terminal or at the desk (`give`), never "+
"through a verb (this line came through %q): nothing was read or sealed", verb)
}
if *desk != "" {
if *from != "" || *provider != "" {
return errors.New("--at-desk gives a module's own secret, and takes neither --from nor --provider")
}
return giveAtDesk(ctx, node, module, name, *desk)
}
value, err := valueFor(node, module, name, *from) value, err := valueFor(node, module, name, *from)
if err != nil { if err != nil {
@@ -108,26 +93,10 @@ func secretCommand(ctx context.Context, args []string) error {
fmt.Printf(" run `push %s` and `push %s` to send it\n", *provider, node) fmt.Printf(" run `push %s` and `push %s` to send it\n", *provider, node)
return nil return nil
} }
// A trusted party's secret is announced before it is kept (the confirmation review of 2026-10-09, N1-give): untilStart, err := open.inventory.AcceptGivenSecret(ctx, node, module, name, value)
// on every channel, the one it replaces among them, which still runs on its old value until the next push.
// Not announced, it is not kept: a channel whose token changed unheard of answers for somebody else.
trusted, err := open.inventory.RunsAsItsOwnAccount(ctx, module)
if err != nil { if err != nil {
return err return err
} }
untilStart, unannounced, err := keepGiven(trusted,
func() error { return announceSecretGiven(ctx, node, module, name, "at the controller's terminal") },
func() (bool, error) { return open.inventory.AcceptGivenSecret(ctx, node, module, name, value) })
if err != nil {
if trusted && unannounced != nil {
return fmt.Errorf("%s runs as an account of its own, and the change of its %s could not be announced on "+
"your channels first, so nothing was kept: %w", module, name, err)
}
return err
}
if unannounced != nil {
fmt.Printf(" this change could NOT be announced on the operator's channels: %v\n", unannounced)
}
// Not printed back, and there is nowhere it could be printed from: it is sealed to that // Not printed back, and there is nowhere it could be printed from: it is sealed to that
// machine and the mesh cannot read it again. // machine and the mesh cannot read it again.
fmt.Printf("%s on %s now holds %q, sealed to that machine.\n", module, node, name) fmt.Printf("%s on %s now holds %q, sealed to that machine.\n", module, node, name)
@@ -149,7 +118,7 @@ func secretCommand(ctx context.Context, args []string) error {
} }
const secretUsage = "secret rotate <node> <module> <name> [--why <text> [--cause <word>]]\n" + const secretUsage = "secret rotate <node> <module> <name> [--why <text> [--cause <word>]]\n" +
"secret accept <node> <module> <name> [--from <file> | --at-desk <machine>] [--provider <node> [--local <name>]]\n" + "secret accept <node> <module> <name> [--from <file>] [--provider <node> [--local <name>]]\n" +
"secret recover <node> <module> <name> --key <operator-key> [--out <file>] [--from-export <file>] [--provider <node>]\n" + "secret recover <node> <module> <name> --key <operator-key> [--out <file>] [--from-export <file>] [--provider <node>]\n" +
"secret export [--out <file>]" "secret export [--out <file>]"
@@ -400,8 +369,6 @@ func valueFor(node, module, name, from string) (string, error) {
fmt.Fprintf(os.Stderr, fmt.Fprintf(os.Stderr,
"reading %s's %q for %s from standard input; it is not echoed anywhere\n", "reading %s's %q for %s from standard input; it is not echoed anywhere\n",
module, name, node) module, name, node)
// At a terminal, what is typed is not shown either: echo off while it is read.
defer hideTyping(os.Stdin)()
line, err := bufio.NewReader(os.Stdin).ReadString('\n') line, err := bufio.NewReader(os.Stdin).ReadString('\n')
if err != nil && line == "" { if err != nil && line == "" {
return "", fmt.Errorf("nothing was given on standard input: %w", err) return "", fmt.Errorf("nothing was given on standard input: %w", err)
@@ -485,23 +452,3 @@ func whoAsked() string {
} }
return "the mesh" return "the mesh"
} }
// keepGiven keeps a value given at the controller's terminal, and announces it on the operator's channels
// (the confirmation review of 2026-10-09, N1-give). **A trusted party's — a module running as an account of its
// own: the router, a verified channel — is announced before it is kept, and not kept when the announcement
// fails**: a channel whose token changed unheard of answers for somebody else. Any other module's is kept first
// and announced after, and a failed announcement is said (unannounced) without undoing it.
func keepGiven(trusted bool, announce func() error, keep func() (bool, error)) (untilStart bool, unannounced, err error) {
if trusted {
if err := announce(); err != nil {
return false, err, err
}
untilStart, err = keep()
return untilStart, nil, err
}
untilStart, err = keep()
if err != nil {
return false, nil, err
}
return untilStart, announce(), nil
}
+1 -2
View File
@@ -84,7 +84,7 @@ const (
// callBounds are the verbs that may run longer than callDefault, and how long (S7). // callBounds are the verbs that may run longer than callDefault, and how long (S7).
var callBounds = map[string]time.Duration{ var callBounds = map[string]time.Duration{
"push": 30 * time.Minute, "rotate": 30 * time.Minute, "give": 5 * time.Minute, "assign": 15 * time.Minute, "push": 30 * time.Minute, "rotate": 30 * time.Minute, "assign": 15 * time.Minute,
"unassign": 15 * time.Minute, "command": 30 * time.Minute, "doctor": 3 * time.Minute, "unassign": 15 * time.Minute, "command": 30 * time.Minute, "doctor": 3 * time.Minute,
} }
@@ -378,7 +378,6 @@ func watchWaits(f *signalFacts) []conditions.Observation {
Headline: deliveryName(w.modules, w.repository) + " waiting to start", Headline: deliveryName(w.modules, w.repository) + " waiting to start",
Explanation: walkWaitingWords(w, in, severity), Explanation: walkWaitingWords(w, in, severity),
Needs: waitingNeeds(severity), Needs: waitingNeeds(severity),
Actions: waitingActions(w.id, severity),
Resolved: deliveryName(w.modules, w.repository) + " no longer waiting"}) Resolved: deliveryName(w.modules, w.repository) + " no longer waiting"})
} }
return out return out
+46 -296
View File
@@ -12,7 +12,6 @@ import (
"sync" "sync"
"time" "time"
"github.com/novox/mesh-controller/internal/builder"
"github.com/novox/mesh-controller/internal/catalogue" "github.com/novox/mesh-controller/internal/catalogue"
"github.com/novox/mesh-controller/internal/inventory" "github.com/novox/mesh-controller/internal/inventory"
"github.com/novox/mesh-controller/internal/link" "github.com/novox/mesh-controller/internal/link"
@@ -314,26 +313,11 @@ func (f following) SourceMoved(ctx context.Context, m link.SourceMoved) error {
if err != nil { if err != nil {
return notNow(err) return notNow(err)
} }
read, err := readForPlanning(ctx, inv) read, err := inv.ReadRepositories(ctx)
if err != nil { if err != nil {
return notNow(err) return notNow(err)
} }
// **A merge older than the newest planned merge of its branch is planned at that merge's commit**
// (novox/hq issue 349): the catch-up acts on a merge the bus did not hand over after the merges that
// came after it, and planned at its own commit it built what a later merge had fixed — the forge's
// security fix, on 2026-10-09 — from the commit before it, dependents and all. The later commit contains
// this merge's change. Planned there, with that merge's time, a module the later merge already looked at
// reads as history and is not built again; the rest are built from the newest commit.
newest, known, err := inv.NewestMergeOf(ctx, m.Owner+"/"+m.Repo, m.Base)
if err != nil {
return notNow(err)
}
if known && laterOnTheBranch(m, newest) {
fmt.Printf(" %s/%s %.8s was merged before %.8s (%s, %s): what it moved is built from %.8s, which "+
"contains it\n", m.Owner, m.Repo, m.Commit, newest.Commit, newest.ID, newest.State, newest.Commit)
m.Commit, m.MergedAt = newest.Commit, newest.Merged.Format(time.RFC3339Nano)
}
from, packaging, already := mergeCandidates(m, entries, read) from, packaging, already := mergeCandidates(m, entries, read)
if len(from) == 0 && len(packaging) == 0 { if len(from) == 0 && len(packaging) == 0 {
// "Already built from it" and "nothing reads it" are different facts, and reading the first // "Already built from it" and "nothing reads it" are different facts, and reading the first
@@ -347,6 +331,12 @@ func (f following) SourceMoved(ctx context.Context, m link.SourceMoved) error {
m.Owner, m.Repo, m.Base, m.Commit) m.Owner, m.Repo, m.Base, m.Commit)
return nil return nil
} }
// The same judgement for the packaging kind, against the newest look at that repository by
// anything built from it: they keep no record of it themselves, and a replayed old merge should
// not rebuild them either.
if isHistory(m.MergedAt, lastLookAt(entries, m)) {
packaging = nil
}
// Said, never silent (novox/hq 04-ISSUES/215): a module built from this repository that follows // Said, never silent (novox/hq 04-ISSUES/215): a module built from this repository that follows
// another branch is not part of this merge, and whoever is waiting for its change should read why. // another branch is not part of this merge, and whoever is waiting for its change should read why.
for _, e := range entries { for _, e := range entries {
@@ -355,7 +345,7 @@ func (f following) SourceMoved(ctx context.Context, m link.SourceMoved) error {
e.Manifest.Module, m.Owner, m.Repo, e.Source.Ref, m.Base) e.Manifest.Module, m.Owner, m.Repo, e.Source.Ref, m.Base)
} }
} }
touched, added, _ := touchedBy(from, entries, m, read) touched, added, _ := touchedBy(from, entries, m)
// **A module the merge deleted is not built** (novox/hq ADR 0236): its manifest is gone, so the build // **A module the merge deleted is not built** (novox/hq ADR 0236): its manifest is gone, so the build
// seat finds nothing saying what it is, and the plan failed on it (`has no module.json at …`) with // seat finds nothing saying what it is, and the plan failed on it (`has no module.json at …`) with
// every other module of its tier left unsent. It is forgotten where nothing holds it, said otherwise. // every other module of its tier left unsent. It is forgotten where nothing holds it, said otherwise.
@@ -465,13 +455,13 @@ func (f following) SourceMoved(ctx context.Context, m link.SourceMoved) error {
} }
fmt.Printf("%s/%s merged into %s (%.8s); plan %s, %d module(s) in %d tier(s)\n %s\n", fmt.Printf("%s/%s merged into %s (%.8s); plan %s, %d module(s) in %d tier(s)\n %s\n",
m.Owner, m.Repo, m.Base, m.Commit, plan.ID, len(plan.Modules), len(plan.Tiers), strings.Join(tiers, "\n ")) m.Owner, m.Repo, m.Base, m.Commit, plan.ID, len(plan.Modules), len(plan.Tiers), strings.Join(tiers, "\n "))
// Why each is in it (issue 363): the files of its build source the merge changed, or why it is read whole. if len(packaging) > 0 {
for _, e := range moved { var also []string
fmt.Printf(" %s: %s\n", e.Manifest.Module, whyMoved(e, read[e.Manifest.Module], m))
}
for _, e := range packaging { for _, e := range packaging {
fmt.Printf(" %s reads %s/%s through its build context: its own source record is left where it is\n", also = append(also, e.Manifest.Module)
e.Manifest.Module, m.Owner, m.Repo) }
fmt.Printf(" %s package source from it, so they are rebuilt and their own source record "+
"is left where it is\n", strings.Join(also, ", "))
} }
if plan.Waiting() { if plan.Waiting() {
plan.Note = waitingNote(plan) plan.Note = waitingNote(plan)
@@ -565,119 +555,25 @@ func mergeCandidates(m link.SourceMoved, entries []inventory.Entry,
} }
from = append(from, e) from = append(from, e)
case readsFrom(read[e.Manifest.Module], m): case readsFrom(read[e.Manifest.Module], m):
// **A merge older than the module's last look is history for it** (novox/hq ADR 0267): a
// build or plan of it after the merge already read the repository with the merge in it. Per
// module, since a merge that moved only the module built from the repository says nothing
// about the ones packaging it.
// Judged with a margin for the forge's clock running behind the store's: too late a look
// rebuilds once more, too early one would miss the merge.
if lookedAtCommit(read[e.Manifest.Module], m.Commit) {
continue
}
if looked := lookedOf(read[e.Manifest.Module]); !looked.IsZero() &&
isHistory(m.MergedAt, looked.Add(-historyMargin)) {
continue
}
packaging = append(packaging, e) packaging = append(packaging, e)
} }
} }
return from, packaging, already return from, packaging, already
} }
// lookedAtCommit is whether a plan that built a module, or is building it, answered this merge commit. // wouldMove is the modules built from the merged repository that acting on this merge would mark as
func lookedAtCommit(read []inventory.ReadRepository, commit string) bool { // moved and rebuild — SourceMoved's judgement, made without acting (novox/hq issue 266). Empty for a
for _, r := range read { // merge already acted on: acting marks each of them as looked at, so the merge then reads as history.
if slices.Contains(r.LookedAt, commit) {
return true
}
}
return false
}
// historyMargin is how far a packaging module's last look is taken back before a merge is history for it.
const historyMargin = time.Minute
// whyMoved is why a merge moves a module, as a plan says it (novox/hq ADR 0267, issue 363): the changed
// files in its build source, or why it is read whole. For a module built from the merged repository or one
// whose build context is that repository; a dependent is in a plan for what it stands on.
func whyMoved(e inventory.Entry, read []inventory.ReadRepository, m link.SourceMoved) string {
if len(m.Paths) == 0 || m.PathsTruncated {
return "read whole: the merge's changed files were not all said"
}
whole := "no build source recorded"
for _, r := range read {
if r.Own && r.Whole != "" {
whole = r.Whole
}
}
held := func(paths []string) []string {
var in []string
for _, p := range m.Paths {
if builder.SourceHolds(paths, p) {
in = append(in, p)
}
}
return in
}
changed := func(where string, in []string) string {
return fmt.Sprintf("its build source%s changed: %d changed file(s) in it, e.g. %s", where, len(in), in[0])
}
if sameRepository(e.Source.Repository, m) {
if own := ownSource(read); own != nil {
if in := held(own); len(in) > 0 {
return changed("", in)
}
}
dir := strings.Trim(e.Source.Path, "/")
if dir == "" {
return "read whole: " + whole + ", so every file of its repository is its build source"
}
for _, p := range m.Paths {
if inside(p, dir) {
return "read whole: " + whole + ", so its directory is its build source; e.g. " + p
}
}
return "read whole: " + whole
}
for _, r := range read {
if r.Own || !sameRepository(r.Repository, m) || (r.Ref != "" && r.Ref != m.Base) {
continue
}
if len(r.Paths) > 0 {
if in := held(r.Paths); len(in) > 0 {
return changed(" in "+m.Owner+"/"+m.Repo, in)
}
continue
}
return "read whole: " + whole + ", so every file of " + m.Owner + "/" + m.Repo + ", which its build context is, is its build source"
}
return "read whole: " + whole
}
// lookedOf is when a module packaging another repository was last looked at, as readForPlanning says.
func lookedOf(read []inventory.ReadRepository) time.Time {
var at time.Time
for _, r := range read {
if r.Looked.After(at) {
at = r.Looked
}
}
return at
}
// wouldMove is the modules acting on this merge would move and rebuild — SourceMoved's judgement, made
// without acting (novox/hq issue 266). Empty for a merge already acted on: acting marks each module built
// from the repository as looked at, so the merge then reads as history for it.
// //
// **The ones packaging source from it too** (novox/hq ADR 0267): with a module moved only by the files of // **Only the modules built from it, never the ones that merely package source from it.** Acting
// its build source, a merge can move a packaging module and nothing built from the repository, and a missed // records nothing about those, so a merge acted on would go on reading as unacted for them, and be
// one of those was never acted on. A packaging module's look is its newest build or plan (lookedAt), so a // acted on again on every look. A merge that moves both is caught by the first kind, and acting on it
// merge acted on for it reads as history once its plan is made. // rebuilds the second as well.
func wouldMove(m link.SourceMoved, entries []inventory.Entry, func wouldMove(m link.SourceMoved, entries []inventory.Entry,
read map[string][]inventory.ReadRepository) []inventory.Entry { read map[string][]inventory.ReadRepository) []inventory.Entry {
from, packaging, _ := mergeCandidates(m, entries, read) from, _, _ := mergeCandidates(m, entries, read)
touched, _ := splitDeleted(whatTheMergeTouched(from, entries, m, read), m) touched, _ := splitDeleted(whatTheMergeTouched(from, entries, m), m)
return append(touched, packaging...) return touched
} }
// splitDeleted parts the modules a merge touched into those it changed and those whose manifest it // splitDeleted parts the modules a merge touched into those it changed and those whose manifest it
@@ -761,168 +657,34 @@ func sameRepository(repository string, m link.SourceMoved) bool {
(m.CloneURL != "" && repo == strings.ToLower(strings.TrimSuffix(m.CloneURL, ".git"))) (m.CloneURL != "" && repo == strings.ToLower(strings.TrimSuffix(m.CloneURL, ".git")))
} }
// readsFrom is whether a merge changed what a module's build read in another repository: the second // readsFrom is whether a module's build read the repository a merge names: the second repository its
// repository its recipe packages source from. Its ref must be the branch that moved, or unset — the same // recipe packages source from. Its ref must be the branch that moved, or unset — the same rule a
// rule a module's own source follows. // module's own source follows.
//
// **Only a changed file in what the build read there** (novox/hq ADR 0267 rule 2): where the module's
// newest trunk build said its build source in that repository, a merge touching none of it is no change
// to the module (issue 338: every merge to the controller's repository moved the route proxy and the
// build seat's holder). Where it said none, or the merge's files are not all said, the whole repository is
// read, as before.
func readsFrom(read []inventory.ReadRepository, m link.SourceMoved) bool { func readsFrom(read []inventory.ReadRepository, m link.SourceMoved) bool {
for _, r := range read { for _, r := range read {
if r.Own || !sameRepository(r.Repository, m) || (r.Ref != "" && r.Ref != m.Base) { if sameRepository(r.Repository, m) && (r.Ref == "" || r.Ref == m.Base) {
continue
}
if len(r.Paths) == 0 || len(m.Paths) == 0 || m.PathsTruncated {
return true return true
} }
for _, p := range m.Paths {
if builder.SourceHolds(r.Paths, p) {
return true
}
}
} }
return false return false
} }
// ownSource is the build source a module's newest trunk build said it read in its own repository; nil // lastLookAt is the most recent look at this repository by anything built from it.
// when it said none, and the module's own directory — or, built from the root, its whole repository — is func lastLookAt(entries []inventory.Entry, m link.SourceMoved) time.Time {
// its build source, as before (novox/hq ADR 0267). var newest time.Time
func ownSource(read []inventory.ReadRepository) []string { for _, e := range entries {
for _, r := range read { if sameRepository(e.Source.Repository, m) && e.Source.Seen.After(newest) {
if r.Own && len(r.Paths) > 0 { newest = e.Source.Seen
return r.Paths
} }
} }
return nil return newest
}
// readsFile is whether a module built from the merged repository reads one of its changed files: in its
// build source where its newest trunk build said one, else anywhere in its directory, or anywhere at all
// for a module built from the repository's root.
func readsFile(e inventory.Entry, read []inventory.ReadRepository, p string) bool {
if own := ownSource(read); own != nil {
return builder.SourceHolds(own, p)
}
return strings.Trim(e.Source.Path, "/") == "" || inside(p, e.Source.Path)
}
// staleIn is the modules whose recorded build source a plan has overtaken (novox/hq ADR 0267): a plan still
// working that has yet to build one, or a plan made after that build which never built it — failed, stopped
// or superseded. What such a module is built from is changing, or changed without a build to say so: a merge
// that added an import to it, and a later one changing only what that import names, would otherwise move
// nothing. Each is read whole, as before, until a build of it works again.
//
// Each is answered with the plan that overtook it, for saying why it is read whole.
func staleIn(read map[string][]inventory.ReadRepository, plans []inventory.Plan) map[string]string {
stale := map[string]string{}
for name, rs := range read {
var since time.Time
for _, r := range rs {
if r.Built.After(since) {
since = r.Built
}
}
for _, p := range plans {
s, in := p.Modules[name]
if !in || (s != nil && (s.State == "built" || s.State == planDeleted)) {
continue
}
if p.Open() || p.Created.After(since) {
stale[name] = p.ID
}
}
}
return stale
}
// lookedAt is when a merge was last acted on for a module that packages another repository's source: its
// newest build, or the newest plan that built it or is still building it, whichever is later. A build asked
// after a merge clones that repository with the merge in it, so an older merge is history for it; a plan
// that closed without building it looked at nothing.
func lookedAt(name string, read []inventory.ReadRepository, plans []inventory.Plan) time.Time {
var at time.Time
for _, r := range read {
if r.Looked.After(at) {
at = r.Looked
}
}
for _, p := range plans {
s, in := p.Modules[name]
if in && (p.Open() || (s != nil && s.State == "built")) && p.Created.After(at) {
at = p.Created
}
}
return at
}
// readForPlanning is what each module's build read, as the planner maps a change onto it — for a merge
// acting now, the merge gate, a pull request's check, a delivery's order and the what-if alike, so planning
// and gating cannot disagree (novox/hq ADR 0238): the build sources the newest trunk builds said, but for
// the modules a plan has overtaken (staleIn), and with when each was last looked at (lookedAt).
func readForPlanning(ctx context.Context, inv *inventory.Inventory) (map[string][]inventory.ReadRepository, error) {
read, err := inv.ReadRepositories(ctx)
if err != nil {
return nil, err
}
// Every plan since the oldest build whose source is recorded: one made after a module's build can have
// overtaken it, however long ago, so no window of recent plans would do.
var oldest time.Time
for _, rs := range read {
for _, r := range rs {
if !r.Built.IsZero() && (oldest.IsZero() || r.Built.Before(oldest)) {
oldest = r.Built
}
}
}
plans, err := inv.PlansSince(ctx, oldest)
if err != nil {
return nil, err
}
return planningView(read, plans), nil
}
// planningView is readForPlanning over what was read, so a test can hand it records.
func planningView(read map[string][]inventory.ReadRepository, plans []inventory.Plan) map[string][]inventory.ReadRepository {
stale := staleIn(read, plans)
out := make(map[string][]inventory.ReadRepository, len(read))
for name, rs := range read {
looked := lookedAt(name, rs, plans)
var commits []string
for _, p := range plans {
if st, in := p.Modules[name]; in && p.Commit != "" && (p.Open() || (st != nil && st.State == "built")) {
commits = append(commits, p.Commit)
}
}
var kept []inventory.ReadRepository
overtaken, isStale := stale[name]
for _, r := range rs {
if isStale {
if r.Own {
continue
}
r.Paths = nil
}
r.Looked, r.LookedAt = looked, commits
kept = append(kept, r)
}
if isStale {
kept = append(kept, inventory.ReadRepository{Own: true, Whole: "plan " + overtaken + " has not built it yet",
Looked: looked, LookedAt: commits})
}
out[name] = kept
}
return out
} }
// whatTheMergeTouched narrows the modules built from a repository to the ones the merge changed: **a // whatTheMergeTouched narrows the modules built from a repository to the ones the merge changed: **a
// changed file touches exactly the modules whose build reads it** (novox/hq issue 280, ADR 0238). It is // changed file touches exactly the modules whose build reads it** (novox/hq issue 280, ADR 0238). It is
// touchedBy's first answer; touchedBy is the one place the mesh maps a changed file onto its modules. // touchedBy's first answer; touchedBy is the one place the mesh maps a changed file onto its modules.
func whatTheMergeTouched(candidates, known []inventory.Entry, m link.SourceMoved, func whatTheMergeTouched(candidates, known []inventory.Entry, m link.SourceMoved) []inventory.Entry {
read map[string][]inventory.ReadRepository) []inventory.Entry { touched, _, _ := touchedBy(candidates, known, m)
touched, _, _ := touchedBy(candidates, known, m, read)
return touched return touched
} }
@@ -930,11 +692,8 @@ func whatTheMergeTouched(candidates, known []inventory.Entry, m link.SourceMoved
// merge handler, the release planner's what-if, the merge gate and a pull request's check alike (novox/hq // merge handler, the release planner's what-if, the merge gate and a pull request's check alike (novox/hq
// ADR 0238), so planning and gating cannot disagree about what a change touches. // ADR 0238), so planning and gating cannot disagree about what a change touches.
// //
// **A changed file touches exactly the modules whose build reads it.** What a build reads is its build // **A changed file touches exactly the modules whose build reads it.** What a build reads is the module's
// source, where the module's newest trunk build said one (novox/hq ADR 0267): a Go program's import closure, // own directory — the builder clones the repository and builds within that directory alone: the manifest,
// an archive's directory, a recipe, its manifest — so a README at the root of a repository whose module is
// built from its root, or another program's package beside it, touches nothing. Where none was said, it is
// the module's own directory — the builder clones the repository and builds within that directory alone: the manifest,
// the recipes, the bundles' sources, the Docker context — or the whole repository for a module built from // the recipes, the bundles' sources, the Docker context — or the whole repository for a module built from
// its root. A second repository a recipe packages (an artifact's `context`) is read too; that is the build // its root. A second repository a recipe packages (an artifact's `context`) is read too; that is the build
// record's `read`, answered by readsFrom in mergeCandidates. So a changed file inside a module's directory // record's `read`, answered by readsFrom in mergeCandidates. So a changed file inside a module's directory
@@ -954,8 +713,7 @@ func whatTheMergeTouched(candidates, known []inventory.Entry, m link.SourceMoved
// //
// Nothing said about the files, or not all of them said, is still everything: what is not known cannot // Nothing said about the files, or not all of them said, is still everything: what is not known cannot
// be narrowed. // be narrowed.
func touchedBy(candidates, known []inventory.Entry, m link.SourceMoved, func touchedBy(candidates, known []inventory.Entry, m link.SourceMoved) (touched []inventory.Entry, added, unread []string) {
read map[string][]inventory.ReadRepository) (touched []inventory.Entry, added, unread []string) {
knownDirs := map[string]bool{} knownDirs := map[string]bool{}
for _, e := range known { for _, e := range known {
if !e.Provided && sameRepository(e.Source.Repository, m) { if !e.Provided && sameRepository(e.Source.Repository, m) {
@@ -990,27 +748,19 @@ func touchedBy(candidates, known []inventory.Entry, m link.SourceMoved,
return candidates, added, nil return candidates, added, nil
} }
for _, e := range candidates { for _, e := range candidates {
for _, p := range m.Paths { if strings.Trim(e.Source.Path, "/") == "" || anyInside(m.Paths, e.Source.Path) {
if readsFile(e, read[e.Manifest.Module], p) {
touched = append(touched, e) touched = append(touched, e)
break
}
} }
} }
for _, p := range m.Paths { for _, p := range m.Paths {
isRead := newDir["."] read := newDir["."]
for _, e := range candidates { for _, e := range candidates {
isRead = isRead || readsFile(e, read[e.Manifest.Module], p) read = read || strings.Trim(e.Source.Path, "/") == "" || inside(p, e.Source.Path)
} }
for d := range newDir { for d := range newDir {
isRead = isRead || inside(p, d) read = read || inside(p, d)
} }
// A file a module packages from this repository is read too, by that module's build. if !read {
for _, e := range known {
isRead = isRead || readsFrom(read[e.Manifest.Module], link.SourceMoved{Owner: m.Owner, Repo: m.Repo,
Base: m.Base, CloneURL: m.CloneURL, Paths: []string{p}})
}
if !isRead {
unread = append(unread, p) unread = append(unread, p)
} }
} }
@@ -1069,7 +819,7 @@ func (r mergeReach) Dependents() []string {
func reachOfMerge(m link.SourceMoved, entries []inventory.Entry, read map[string][]inventory.ReadRepository, func reachOfMerge(m link.SourceMoved, entries []inventory.Entry, read map[string][]inventory.ReadRepository,
edges []inventory.Edge) mergeReach { edges []inventory.Edge) mergeReach {
from, packaging, already := mergeCandidates(m, entries, read) from, packaging, already := mergeCandidates(m, entries, read)
touched, added, unread := touchedBy(from, entries, m, read) touched, added, unread := touchedBy(from, entries, m)
kept, deleted := splitDeleted(touched, m) kept, deleted := splitDeleted(touched, m)
r := mergeReach{Touched: kept, Deleted: deleted, Packaging: packaging, Already: already, Added: added, Unread: unread} r := mergeReach{Touched: kept, Deleted: deleted, Packaging: packaging, Already: already, Added: added, Unread: unread}
var building []string var building []string
-130
View File
@@ -1,130 +0,0 @@
package main
// A module's act on the operator's warrant, recorded in the hand-act log (novox/hq ADR 0274, ADR 0259 §6).
//
// mesh-controller hand-act warrant --asker <module> --ask <id>
//
// The verb `warranted` runs it. A module that asks the operator (an asker) acts on the warrant with its own grants;
// the controller's log is where a person's decisions are read back, so the module asks the controller to record
// it. **What is recorded is the router's word, never the caller's**: the controller reads the router's own record
// of that asker's ask — the bus lets only the router write it — and records who chose, through which channel, with
// which proofs, and which answer. The caller gives nothing but which ask: a word of its own, recorded first under
// the one id, would stand for every node's (the review of 2026-10-10). Recorded once per
// ask, under an id the ask decides, however many of the module's instances ask; an ask still open, ended without
// a choice, or another asker's is refused and nothing is written.
import (
"context"
"encoding/json"
"errors"
"flag"
"fmt"
"regexp"
"github.com/nats-io/nats.go"
"git.novox.be/novox/mesh-sdk/go/asks"
"github.com/novox/mesh-controller/internal/conditions"
"github.com/novox/mesh-controller/internal/link"
)
var askerModule = regexp.MustCompile(`^[a-z0-9][a-z0-9-]{0,62}$`)
// warrantedID is the one entry an ask's warrant is recorded under.
func warrantedID(asker, ask string) string { return "warrant-" + asker + "-" + ask }
// warrantedAct is the entry for an asker's act on the warrant the router recorded for its ask (state, w), or why
// none is written.
func warrantedAct(asker, ask, caller, state string, w *asks.Warrant) (link.HandAct, error) {
switch {
case !askerModule.MatchString(asker):
return link.HandAct{}, fmt.Errorf("%q is not a module's name", asker)
case asker == askerName:
return link.HandAct{}, errors.New("the controller records its own acts on a warrant as it performs them")
case !asks.UsableID(ask):
return link.HandAct{}, fmt.Errorf("%q is not an ask's id", ask)
case state == "" || w == nil:
return link.HandAct{}, fmt.Errorf("the router holds no closed record of %s's ask %s", asker, ask)
case state == "open":
return link.HandAct{}, fmt.Errorf("%s's ask %s is still open: nobody has answered it", asker, ask)
case w.Asker != asker || w.Ask != ask:
return link.HandAct{}, fmt.Errorf("the router's record is for %s's ask %s", w.Asker, w.Ask)
case w.Outcome != asks.OutcomeChosen || w.By == nil:
return link.HandAct{}, fmt.Errorf("%s's ask %s ended %s: no person chose, so there is no warrant to record", asker, ask, w.Outcome)
case w.AskDigest == "":
return link.HandAct{}, fmt.Errorf("the router's warrant for %s's ask %s names no ask digest", asker, ask)
}
return link.HandAct{ID: warrantedID(asker, ask), Verb: handActWarrant, Args: []string{fmt.Sprintf("the operator chose %s on %s's ask %s", w.Label, asker, ask)},
Why: fmt.Sprintf("%s (ask %s of %s)", w.Says(), ask, asker), By: byWords(*w), Cause: conditions.CauseOperatorAnswer,
Via: viaWords(*w), Ask: ask, Proofs: w.Proofs, RequestedBy: asker + ", recorded at the word of " + caller,
Outcome: "chosen; what " + asker + " did with it is in its own record", At: w.At.UTC()}, nil
}
// readRouterRecord reads the router's record of one asker's ask: its state and warrant, or "" when there is none.
// The controller's grant reaches the JetStream API whole (`$JS.API.>`), so it reads any asker's record.
func readRouterRecord(ctx context.Context, conn *nats.Conn, bucket, asker, ask string) (string, *asks.Warrant, error) {
reply, err := conn.RequestWithContext(ctx, "$JS.API.DIRECT.GET.KV_"+bucket+".$KV."+bucket+"."+asker+"."+ask, nil)
if err != nil {
return "", nil, err
}
if status := reply.Header.Get("Status"); status != "" {
if status == "404" {
return "", nil, nil
}
return "", nil, fmt.Errorf("the router's record could not be read: %s %s", status, reply.Header.Get("Description"))
}
var rec struct {
State string `json:"state"`
Warrant *asks.Warrant `json:"warrant"`
}
if err := json.Unmarshal(reply.Data, &rec); err != nil {
return "", nil, fmt.Errorf("the router's record of %s's ask %s cannot be read: %w", asker, ask, err)
}
return rec.State, rec.Warrant, nil
}
func handActWarrantCommand(ctx context.Context, args []string) error {
set := flag.NewFlagSet("hand-act warrant", flag.ContinueOnError)
asker := set.String("asker", "", "the module that asked")
ask := set.String("ask", "", "its ask's id")
positionals, err := parseAround(set, args)
if err != nil {
return err
}
if *asker == "" || *ask == "" || len(positionals) > 0 {
return errors.New("hand-act warrant --asker <module> --ask <id>: what is recorded is the router's record, and nothing else")
}
open, err := openStores(ctx)
if err != nil {
return err
}
defer open.Close()
bucket, err := asksRecords(ctx, open.inventory)
if err != nil {
return err
}
if bucket == "" {
return errors.New("no module declares the operator channel's records, so no warrant can be read")
}
return onTheBus(func(conn *nats.Conn) error {
state, w, err := readRouterRecord(ctx, conn, bucket, *asker, *ask)
if err != nil {
return err
}
act, err := warrantedAct(*asker, *ask, link.Caller(), state, w)
if err != nil {
return fmt.Errorf("%w. Nothing was recorded", err)
}
written, err := link.RecordHandActOnce(ctx, conn, act)
if err != nil {
return fmt.Errorf("the warrant could not be recorded: %w", err)
}
if !written {
fmt.Printf("already recorded as %s: %s\n", act.ID, act.Why)
return nil
}
fmt.Printf("recorded as %s: %s, through %s\n", act.ID, act.Why, act.Via)
return nil
})
}
-82
View File
@@ -1,82 +0,0 @@
package main
import (
"slices"
"strings"
"testing"
"time"
"git.novox.be/novox/mesh-sdk/go/asks"
"github.com/novox/mesh-controller/internal/conditions"
)
func chosenWarrant() *asks.Warrant {
return &asks.Warrant{Ask: "instr-1", Asker: "claude-code", Outcome: asks.OutcomeChosen, Option: "approve",
Label: "Approve", Level: asks.Approve, Channel: "telegram", Proofs: []string{"P1"},
By: &asks.Person{Who: asks.Operator, Kind: "telegram", Identity: "42", Verified: "user id verified"},
At: time.Date(2026, 10, 10, 4, 0, 0, 0, time.UTC), AskDigest: "sha256:ab"}
}
// What is recorded of a module's act on a warrant is the router's word (novox/hq ADR 0274): who chose, how and
// with which proofs; the caller gives only what it did. Nothing is recorded without a person's choice.
func TestAWarrantIsRecordedFromTheRoutersRecordAlone(t *testing.T) {
act, err := warrantedAct("claude-code", "instr-1", "node-tools.shanks", "chosen", chosenWarrant())
if err != nil {
t.Fatal(err)
}
if act.ID != "warrant-claude-code-instr-1" || act.Verb != handActWarrant || act.Cause != conditions.CauseOperatorAnswer ||
act.By != "the operator, as telegram identity 42" || act.Ask != "instr-1" || !slices.Equal(act.Proofs, []string{"P1"}) ||
!strings.Contains(act.Why, "the operator, via telegram (user id verified), chose Approve") ||
!strings.Contains(act.RequestedBy, "node-tools.shanks") ||
!slices.Equal(act.Args, []string{"the operator chose Approve on claude-code's ask instr-1"}) {
t.Fatalf("recorded as %+v", act)
}
for name, c := range map[string]struct {
asker, ask, state string
w func() *asks.Warrant
}{
"no record": {"claude-code", "instr-1", "", func() *asks.Warrant { return nil }},
"still open": {"claude-code", "instr-1", "open", chosenWarrant},
"another asker's": {"messenger", "instr-1", "chosen", chosenWarrant},
"another ask's": {"claude-code", "instr-2", "chosen", chosenWarrant},
"the controller's": {"mesh-controller", "instr-1", "chosen", chosenWarrant},
"not a module": {"Claude Code", "instr-1", "chosen", chosenWarrant},
"expired": {"claude-code", "instr-1", "expired", func() *asks.Warrant {
w := chosenWarrant()
w.Outcome, w.By = asks.OutcomeExpired, nil
return w
}},
"no digest": {"claude-code", "instr-1", "chosen", func() *asks.Warrant {
w := chosenWarrant()
w.AskDigest = ""
return w
}},
} {
if _, err := warrantedAct(c.asker, c.ask, "x", c.state, c.w()); err == nil {
t.Errorf("%s: recorded", name)
}
}
}
func TestTheWarrantedVerbRunsTheWarrantLineWithoutAWhy(t *testing.T) {
if _, err := argvFor("warranted", map[string]any{"asker": "claude-code", "ask": "instr-1", "what": "a word of the caller's"}); err == nil {
t.Error("the caller's own words were taken into the record")
}
argv, err := argvFor("warranted", map[string]any{"asker": "claude-code", "ask": "instr-1"})
if err != nil {
t.Fatal(err)
}
if !slices.Equal(argv, []string{"hand-act", "warrant", "--asker", "claude-code", "--ask", "instr-1"}) {
t.Fatalf("%v", argv)
}
if repairingCommand(argv) != "" {
t.Error("recording a person's answer is taken for a repair")
}
if terminalOnly(argv) != nil {
t.Error("the verb is kept for the terminal")
}
if _, err := argvFor("warranted", map[string]any{"asker": "claude-code"}); err == nil {
t.Error("a call naming no ask was taken")
}
}
-3
View File
@@ -700,9 +700,6 @@ func watchTheMesh(ctx context.Context, open *stores, server *link.Server, bus li
// under the lease and the brake, every act said. // under the lease and the brake, every act said.
healers := newHealing(open, keeper, bus, server.JetStream()) healers := newHealing(open, keeper, bus, server.JetStream())
go healers.keep(watching) go healers.keep(watching)
// And the asker (novox/hq ADR 0259): what needs the operator and names its answers is asked of them,
// and the answer chosen is performed on its warrant.
startAsking(watching, open, server, bus.Conn, keeper)
go forgettingOldHeals(watching, open.inventory) go forgettingOldHeals(watching, open.inventory)
fmt.Printf("watching the mesh: %d signal(s) every %s, %d probe(s) every %s; what is wrong is kept in %s "+ fmt.Printf("watching the mesh: %d signal(s) every %s, %d probe(s) every %s; what is wrong is kept in %s "+
"and said as %s events\n", len(watchedRows()), watchEvery, len(runnableProbes()), doctorEvery, "and said as %s events\n", len(watchedRows()), watchEvery, len(runnableProbes()), doctorEvery,
+3 -5
View File
@@ -19,12 +19,10 @@ func TestTheBuildSeatsHolderFollowsTheControllerThatDefinesItsWorker(t *testing.
{From: "route-proxy", To: "mesh-controller", Kind: inventory.EdgePackages}, {From: "route-proxy", To: "mesh-controller", Kind: inventory.EdgePackages},
{From: "route-proxy", To: "build-agent", Kind: inventory.EdgeBuiltBy}, {From: "route-proxy", To: "build-agent", Kind: inventory.EdgeBuiltBy},
} }
// A packages edge recorded before novox/hq ADR 0267 widens nothing: the controller moved alone moves set := reachableFrom([]string{"mesh-controller"}, edges)
// alone, and a change to a package all three build from moves all three, each by its own build source. if len(set) != 3 {
if alone := reachableFrom([]string{"mesh-controller"}, edges); len(alone) != 1 { t.Fatalf("the controller, what packages it, and nothing more: %v", set)
t.Fatalf("the controller alone, whatever packages its repository: %v", alone)
} }
set := reachableFrom([]string{"mesh-controller", "build-agent", "route-proxy"}, edges)
tiers := tiersOf(set, edges) tiers := tiersOf(set, edges)
pos := map[string]int{} pos := map[string]int{}
for i, tier := range tiers { for i, tier := range tiers {
+4 -4
View File
@@ -3,14 +3,11 @@ module github.com/novox/mesh-controller
go 1.26.0 go 1.26.0
require ( require (
git.novox.be/novox/mesh-sdk/go v0.1.11-0.20261009143344-f047d0a4a970
github.com/jackc/pgx/v5 v5.10.0 github.com/jackc/pgx/v5 v5.10.0
github.com/nats-io/nats-server/v2 v2.11.17
github.com/nats-io/nats.go v1.54.0 github.com/nats-io/nats.go v1.54.0
github.com/novox/mesh-host v0.0.0 github.com/novox/mesh-host v0.0.0
golang.org/x/crypto v0.57.0 golang.org/x/crypto v0.57.0
golang.org/x/net v0.58.0 golang.org/x/net v0.58.0
golang.org/x/sys v0.48.0
) )
require ( require (
@@ -22,9 +19,12 @@ require (
github.com/klauspost/compress v1.20.0 // indirect github.com/klauspost/compress v1.20.0 // indirect
github.com/minio/highwayhash v1.0.4 // indirect github.com/minio/highwayhash v1.0.4 // indirect
github.com/nats-io/jwt/v2 v2.8.1 // indirect github.com/nats-io/jwt/v2 v2.8.1 // indirect
github.com/nats-io/nats-server/v2 v2.11.17 // indirect
github.com/nats-io/nkeys v0.4.16 // indirect github.com/nats-io/nkeys v0.4.16 // indirect
github.com/nats-io/nuid v1.0.1 // indirect github.com/nats-io/nuid v1.0.1 // indirect
go.uber.org/automaxprocs v1.6.0 // indirect
golang.org/x/sync v0.23.0 // indirect golang.org/x/sync v0.23.0 // indirect
golang.org/x/sys v0.48.0 // indirect
golang.org/x/text v0.42.0 // indirect golang.org/x/text v0.42.0 // indirect
golang.org/x/time v0.15.0 // indirect golang.org/x/time v0.15.0 // indirect
) )
@@ -35,4 +35,4 @@ require (
// committed. Every build (the build agent's `go build`, the Dockerfile) compiles from vendor/ and // committed. Every build (the build agent's `go build`, the Dockerfile) compiles from vendor/ and
// fetches nothing; go refuses to build when vendor/ and this file disagree, so a pin moved without // fetches nothing; go refuses to build when vendor/ and this file disagree, so a pin moved without
// `go mod vendor` fails loudly, at once, everywhere. // `go mod vendor` fails loudly, at once, everywhere.
replace github.com/novox/mesh-host => git.novox.be/novox/mesh-host v0.0.0-20261009231844-b8c854611812 replace github.com/novox/mesh-host => git.novox.be/novox/mesh-host v0.0.0-20261009103656-1c61b72f354d
+14 -4
View File
@@ -1,7 +1,15 @@
git.novox.be/novox/mesh-host v0.0.0-20261009231844-b8c854611812 h1:pzVzwF5VMWaTECxu8+Pd1dNoOHNEm7upC5wPadQTkBw= git.novox.be/novox/mesh-host v0.0.0-20261006095519-3e80b7ae325e h1:g9h4QRaAMg5yaJLwqtb0FoOs23DVGUYpW6qvnQ3oY5A=
git.novox.be/novox/mesh-host v0.0.0-20261009231844-b8c854611812/go.mod h1:K3/xEzVgmrNKLMV2vv4M80MwmPnQNXqvQ4C5Jj0fJT4= git.novox.be/novox/mesh-host v0.0.0-20261006095519-3e80b7ae325e/go.mod h1:VlilMCRZ5yyNXg7SNigNBLr0Gt32jrGw5KSNq5JAVYs=
git.novox.be/novox/mesh-sdk/go v0.1.11-0.20261009143344-f047d0a4a970 h1:9tFDQsgmI+4X7/BpZGXIr+HemPKE7YddYGqWV0lINAI= git.novox.be/novox/mesh-host v0.0.0-20261007120832-bdd44154ccac h1:KvnKtJ2rWeIE/t4GweK+JL0OjKSNxsrVP3/nMdpii8o=
git.novox.be/novox/mesh-sdk/go v0.1.11-0.20261009143344-f047d0a4a970/go.mod h1:GFuZUElBZ9A++mxgIKo97aXXo+kV0uJ/UkbhQPPIbrY= git.novox.be/novox/mesh-host v0.0.0-20261007120832-bdd44154ccac/go.mod h1:VlilMCRZ5yyNXg7SNigNBLr0Gt32jrGw5KSNq5JAVYs=
git.novox.be/novox/mesh-host v0.0.0-20261007162834-56e2ebec4bac h1:yLtFS0pDCCqIE9Zx8hgXEFG9fUWzf8L9WQoKV+Amk1E=
git.novox.be/novox/mesh-host v0.0.0-20261007162834-56e2ebec4bac/go.mod h1:VlilMCRZ5yyNXg7SNigNBLr0Gt32jrGw5KSNq5JAVYs=
git.novox.be/novox/mesh-host v0.0.0-20261009081005-b28d7bbcbff4 h1:f4rBnKSemuN0Z9dTtRJMigIGfEs6ltFPOILJGHGab74=
git.novox.be/novox/mesh-host v0.0.0-20261009081005-b28d7bbcbff4/go.mod h1:72ZATZjxMLaJfWdvlSDJrygIoBzCmKIjCDMhEXxVzTo=
git.novox.be/novox/mesh-host v0.0.0-20261009101157-2673e7a2c95e h1:H7eVqDILL6e9cMbWSLHTbCqu9ZxDOmyeQhUmWl9QBV0=
git.novox.be/novox/mesh-host v0.0.0-20261009101157-2673e7a2c95e/go.mod h1:72ZATZjxMLaJfWdvlSDJrygIoBzCmKIjCDMhEXxVzTo=
git.novox.be/novox/mesh-host v0.0.0-20261009103656-1c61b72f354d h1:IrmJ+lz21n+eSqKrmXREtR/7raUCBJ+fZvs+BNhuXVI=
git.novox.be/novox/mesh-host v0.0.0-20261009103656-1c61b72f354d/go.mod h1:72ZATZjxMLaJfWdvlSDJrygIoBzCmKIjCDMhEXxVzTo=
github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op h1:Z/MZK75wC/NSrkgqeNIa7jexam9uWzhLmFTSCPI/kn0= github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op h1:Z/MZK75wC/NSrkgqeNIa7jexam9uWzhLmFTSCPI/kn0=
github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op/go.mod h1:FQyySiasQQM8735Ddel3MRojmy4dA1IqCeyJ5jmPMbI= github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op/go.mod h1:FQyySiasQQM8735Ddel3MRojmy4dA1IqCeyJ5jmPMbI=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
@@ -38,6 +46,8 @@ github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UV
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
go.uber.org/automaxprocs v1.6.0 h1:O3y2/QNTOdbF+e/dpXNNW7Rx2hZ4sTIPyybbxyNqTUs=
go.uber.org/automaxprocs v1.6.0/go.mod h1:ifeIMSnPZuznNm6jmdzmU3/bfk01Fe2fotchwEFJ8r8=
golang.org/x/crypto v0.57.0 h1:3ZVCjf8Ggz7zneR/EHRVx68Ctf+2pmIMP2UFhh9cC6M= golang.org/x/crypto v0.57.0 h1:3ZVCjf8Ggz7zneR/EHRVx68Ctf+2pmIMP2UFhh9cC6M=
golang.org/x/crypto v0.57.0/go.mod h1:Fdz0i5U6CoizGwLda9DttjSk6qlZo25zYNtR+ycvuZA= golang.org/x/crypto v0.57.0/go.mod h1:Fdz0i5U6CoizGwLda9DttjSk6qlZo25zYNtR+ycvuZA=
golang.org/x/net v0.58.0 h1:ynWG7rqYi4ccpTEuPZ2QGWHktVEM9DMCj9yzDE0Q7To= golang.org/x/net v0.58.0 h1:ynWG7rqYi4ccpTEuPZ2QGWHktVEM9DMCj9yzDE0Q7To=
-53
View File
@@ -1,53 +0,0 @@
package broker
import (
"slices"
"testing"
)
// novox/hq ADR 0259 §6: the controller asks the operator through the router's seat as any user of it, under
// its own name, hears its own warrants, reads its own record, and calls the verbs a warrant chooses.
func TestTheControllerAsksUnderItsOwnNameAndCallsTheVerbsAWarrantChooses(t *testing.T) {
records := Records{Nodes: []string{"anchor"}, Assigned: map[string][]Declared{"anchor": {
{Module: "messenger", Holds: []Seat{operatorChannel()}},
}}}
users, err := Users(records)
if err != nil {
t.Fatal(err)
}
got := perms(t, users[0])
for _, s := range []string{
"mesh.seat.operator-channel.accept.ask.mesh-controller",
"mesh.seat.operator-channel.accept.cancel.mesh-controller",
"$JS.API.DIRECT.GET.KV_messenger_asks.$KV.messenger_asks.mesh-controller.c1",
"mesh.seat.mesh-delivery.tool.release", "mesh.seat.mesh-delivery.tool.stop",
"mesh.seat.node-service-manager.tool.restart.g14", "mesh.seat.mesh-controller.tool.plans",
} {
if !allowed(got.Publish, s) {
t.Errorf("the controller may not publish %s", s)
}
}
for _, s := range []string{
"mesh.seat.operator-channel.accept.ask.mesh-delivery",
"mesh.seat.operator-channel.event.decided.mesh-controller",
// (A direct get of another asker's record is not refused here: the controller holds the whole
// JetStream API, as the only writer of stream definitions.)
"mesh.seat.node-service-manager.tool.stop.g14",
} {
if allowed(got.Publish, s) {
t.Errorf("the controller may publish %s", s)
}
}
if !allowed(got.Subscribe, DecidedSubject) || allowed(got.Subscribe, "mesh.seat.operator-channel.event.decided.mesh-delivery") {
t.Error("the controller does not hear exactly its own warrants")
}
// Its events consumer carries them, so a controller that was away hears what was decided meanwhile.
if !slices.Contains(ControllerFollows, DecidedSubject) {
t.Error("the controller does not follow its warrants")
}
// Without a holder of the seat it is granted no ask at all.
alone, _ := Users(Records{Nodes: []string{"anchor"}, Assigned: map[string][]Declared{}})
if allowed(perms(t, alone[0]).Publish, "mesh.seat.operator-channel.accept.ask.mesh-controller") {
t.Error("asked a seat nobody holds")
}
}
+2 -21
View File
@@ -34,15 +34,8 @@ var (
// LeaseBucket holds the controller's lease (to-be 45 §6): one key, `holder`, which the instance // LeaseBucket holds the controller's lease (to-be 45 §6): one key, `holder`, which the instance
// allowed to act writes by compare-and-set and renews; its revision when taken is the epoch. // allowed to act writes by compare-and-set and renews; its revision when taken is the epoch.
LeaseBucket = BucketName(ControllerSeat, "lease") LeaseBucket = BucketName(ControllerSeat, "lease")
// AskedBucket keeps what the controller asked the operator about its conditions (novox/hq ADR 0259):
// each ask by its id, its options and the actions they stand for, how it ended and whether the
// controller acted on its warrant — so a restart neither asks twice nor acts twice.
AskedBucket = BucketName(ControllerSeat, "asked")
) )
// AskedKeptFor is how long an ask is kept after it was made: a month, as the router keeps its own.
const AskedKeptFor = 30 * 24 * time.Hour
// LeaseTTL is how long the lease's key lives unrenewed (to-be 45 §6): fifteen seconds, renewed // LeaseTTL is how long the lease's key lives unrenewed (to-be 45 §6): fifteen seconds, renewed
// every five. The bucket's age, so the bus forgets a holder that stopped renewing. // every five. The bucket's age, so the bus forgets a holder that stopped renewing.
const LeaseTTL = 15 * time.Second const LeaseTTL = 15 * time.Second
@@ -66,12 +59,12 @@ const (
// IsControllerBucket says a bucket is the controller's own, not a module's state nothing declares. // IsControllerBucket says a bucket is the controller's own, not a module's state nothing declares.
func IsControllerBucket(bucket string) bool { func IsControllerBucket(bucket string) bool {
return bucket == CallsBucket || bucket == HandActsBucket || bucket == ConditionsBucket || return bucket == CallsBucket || bucket == HandActsBucket || bucket == ConditionsBucket ||
bucket == ConditionHistoryBucket || bucket == LeaseBucket || bucket == AskedBucket bucket == ConditionHistoryBucket || bucket == LeaseBucket
} }
// ControllerBuckets are the controller's own buckets, in the order they are asserted. // ControllerBuckets are the controller's own buckets, in the order they are asserted.
func ControllerBuckets() []string { func ControllerBuckets() []string {
return []string{LeaseBucket, CallsBucket, HandActsBucket, ConditionsBucket, ConditionHistoryBucket, AskedBucket} return []string{LeaseBucket, CallsBucket, HandActsBucket, ConditionsBucket, ConditionHistoryBucket}
} }
// ControllerBucketsAsserter is what raising the controller's buckets needs of a connection. // ControllerBucketsAsserter is what raising the controller's buckets needs of a connection.
@@ -156,18 +149,6 @@ func (j *JetStream) EnsureControllerBuckets() error {
}); err != nil { }); err != nil {
return fmt.Errorf("asserting bucket %s: %w", ConditionHistoryBucket, err) return fmt.Errorf("asserting bucket %s: %w", ConditionHistoryBucket, err)
} }
if _, err := js.CreateOrUpdateKeyValue(ctx, jetstream.KeyValueConfig{
Bucket: AskedBucket,
Description: "what the controller asked the operator about its conditions, and what came of each (novox/hq " +
"ADR 0259): written by the controller alone; an ask acted on is acted on once",
History: 1,
TTL: AskedKeptFor,
MaxValueSize: 32 << 10,
MaxBytes: 32 << 20,
Storage: jetstream.FileStorage,
}); err != nil {
return fmt.Errorf("asserting bucket %s: %w", AskedBucket, err)
}
return nil return nil
} }
@@ -1,15 +1,9 @@
package broker package broker
import ( import (
"context"
"slices" "slices"
"strings" "strings"
"testing" "testing"
"time"
"github.com/nats-io/nats.go/jetstream"
"github.com/novox/mesh-controller/internal/testbus"
) )
// **The controller may write every bucket it writes** (novox/hq to-be 45 §1, issue 269). Writing a // **The controller may write every bucket it writes** (novox/hq to-be 45 §1, issue 269). Writing a
@@ -65,34 +59,3 @@ func TestTheWatchedSignalsMayBeSaidAndHeard(t *testing.T) {
t.Error("the controller may not ask who answers, or hears every API call") t.Error("the controller may not ask who answers, or hears every API call")
} }
} }
// The controller's record of what it asked the operator is bounded (correctness review of 2026-10-08): one
// value a key, a month's age, and a size it cannot outgrow.
func TestWhatTheControllerAskedIsBounded(t *testing.T) {
js, err := Dial(testbus.URL(t))
if err != nil {
t.Fatal(err)
}
defer js.Close()
if err := js.EnsureControllerBuckets(); err != nil {
t.Fatal(err)
}
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
kv, err := jetstream.New(js.Conn())
if err != nil {
t.Fatal(err)
}
bucket, err := kv.KeyValue(ctx, AskedBucket)
if err != nil {
t.Fatal(err)
}
status, err := bucket.Status(ctx)
if err != nil {
t.Fatal(err)
}
info := status.(*jetstream.KeyValueBucketStatus).StreamInfo()
if status.History() != 1 || status.TTL() != AskedKeptFor || info.Config.MaxBytes <= 0 || info.Config.MaxBytes > 64<<20 {
t.Errorf("history %d, age %s, bytes %d", status.History(), status.TTL(), info.Config.MaxBytes)
}
}
+1 -3
View File
@@ -126,9 +126,7 @@ func ConsumerFor(p Principal) (Consumer, bool) {
// A module that reacts to anything — a module's events or a role's (novox/hq ADR 0121). Watching // A module that reacts to anything — a module's events or a role's (novox/hq ADR 0121). Watching
// a role was missing here, so the one module that does it got no consumer at all: it started, // a role was missing here, so the one module that does it got no consumer at all: it started,
// connected, and its graph stayed empty with nothing anywhere reporting why. // connected, and its graph stayed empty with nothing anywhere reporting why.
// And one that hears its own answers on a seat it uses (novox/hq ADR 0259 §3): an asker's warrants. if p.Kind != KindModule || (len(p.Consumes) == 0 && len(p.Watches) == 0) {
hearsItsOwn := len(SeatTrafficOf(p.Module, nil, p.Uses, nil).Subscribe) > 0
if p.Kind != KindModule || (len(p.Consumes) == 0 && len(p.Watches) == 0 && !hearsItsOwn) {
return Consumer{}, false return Consumer{}, false
} }
perms, err := PermissionsFor(p) perms, err := PermissionsFor(p)
-88
View File
@@ -2,7 +2,6 @@ package broker
import ( import (
"encoding/json" "encoding/json"
"slices"
"sort" "sort"
"strings" "strings"
) )
@@ -51,12 +50,6 @@ type Membership struct {
// and refuses, with the reason, what is not on it — the bus enforces only the union over every // and refuses, with the reason, what is not on it — the bus enforces only the union over every
// module on the machine. // module on the machine.
State []StateIssued `json:"state,omitempty"` State []StateIssued `json:"state,omitempty"`
// SeatTraffic is what this module's code may submit, say, hear, take, ask, answer and read on seats
// that name their caller or their kind (novox/hq ADR 0259 §3). The runtime carrying the module
// publishes, takes and answers for it only what is listed here: the bus enforces only the union
// over every module on the machine, so one module's code reaching another's name or kind through
// the runtime is the runtime's to refuse.
SeatTraffic *SeatTraffic `json:"seat-traffic,omitempty"`
} }
// Served is one address a tool is answered on. // Served is one address a tool is answered on.
@@ -85,8 +78,6 @@ type Placements struct {
// Interchangeable is each module whose definition says its instances are the same anywhere, // Interchangeable is each module whose definition says its instances are the same anywhere,
// so the module's plain subject is issued to all of them in one queue. // so the module's plain subject is issued to all of them in one queue.
Interchangeable map[string]bool Interchangeable map[string]bool
// Kinds is every kind held of a kinded bench, by whom and with what capabilities (ADR 0259 §5).
Kinds []KindHeld
} }
// AnswersForTheModule says whether an instance of a module on one machine is issued the module's // AnswersForTheModule says whether an instance of a module on one machine is issued the module's
@@ -113,28 +104,11 @@ func MembershipFor(node string, d Declared, where Placements) Membership {
m.Serves = append(m.Serves, Served{Subject: own + ".tool.{tool}", Queue: "serve." + d.Module}) m.Serves = append(m.Serves, Served{Subject: own + ".tool.{tool}", Queue: "serve." + d.Module})
} }
for _, s := range d.Holds { for _, s := range d.Holds {
if servedOnlyByTheController(s) {
continue // answered by the serving controller alone, never through a membership
}
for _, verb := range s.Serves { for _, verb := range s.Serves {
m.Seats = append(m.Seats, SeatServed{Seat: s.Name, Verb: verb, Subject: seatToolSubject(s, verb, node)}) m.Seats = append(m.Seats, SeatServed{Seat: s.Name, Verb: verb, Subject: seatToolSubject(s, verb, node)})
} }
} }
m.State = stateIssuedFor(d, node) m.State = stateIssuedFor(d, node)
t := SeatTrafficOf(d.Module, d.Holds, d.Uses, d.Watches)
for _, s := range append(append([]Seat{}, d.Uses...), d.Watches...) {
if !s.Kinded {
continue
}
for _, k := range where.Kinds {
if k.Seat == s.Name && !kindListed(t.Kinds, k) {
t.Kinds = append(t.Kinds, k)
}
}
}
if len(t.Publish)+len(t.Subscribe)+len(t.Answers)+len(t.Workers)+len(t.Records)+len(t.Kinds) > 0 {
m.SeatTraffic = &t
}
if len(d.Invokes) > 0 { if len(d.Invokes) > 0 {
m.Reaches = map[string][]string{} m.Reaches = map[string][]string{}
for _, t := range d.Invokes { for _, t := range d.Invokes {
@@ -171,67 +145,5 @@ func PlacementsOf(r Records, interchangeable map[string]bool) Placements {
for _, nodes := range p.Nodes { for _, nodes := range p.Nodes {
sort.Strings(nodes) sort.Strings(nodes)
} }
// Where the router runs: a verified sender is believed only while its machine is root-free too. A router
// placed nowhere, or on more than one machine, frees nothing.
var routerNodes []string
for node, declared := range r.Assigned {
for _, d := range declared {
for _, s := range d.Holds {
if s.Name == routerSeat && !slices.Contains(routerNodes, node) {
routerNodes = append(routerNodes, node)
}
}
}
}
routerFree := len(routerNodes) == 1 && r.RootFree[routerNodes[0]]
for node, declared := range r.Assigned {
for _, d := range declared {
for _, s := range d.Holds {
if s.Kinded && s.Kind != "" {
p.Kinds = append(p.Kinds, KindHeld{Seat: s.Name, Kind: s.Kind, Module: d.Module, Node: node,
Capabilities: placedCapabilities(s.Capabilities, d.RunsAs, routerFree && r.RootFree[node])})
}
}
}
}
sort.Slice(p.Kinds, func(i, j int) bool {
a, b := p.Kinds[i], p.Kinds[j]
if a.Seat != b.Seat {
return a.Seat < b.Seat
}
if a.Kind != b.Kind {
return a.Kind < b.Kind
}
return a.Node < b.Node
})
return p return p
} }
// routerSeat is the seat the router of asks holds (novox/hq ADR 0259 §3).
const routerSeat = "operator-channel"
// placedCapabilities is what a kind's claim promises, as far as its placement lets the router believe it
// (novox/hq ADR 0259 §8): `verified-sender` only from a holder that runs as an account of its own, on a bus
// account of its own — never one the machine's runtime carries as the operator's account — and only while
// its machine and the router's were root-free when composed (rootFree; the review of 2026-10-09, H3). The
// membership carrying it is composed at a push, so it can outlive a pass that later fails: the router asks
// the controller's root-free verb again before it honours an approval, and that is the check that holds.
func placedCapabilities(declared []string, runsAs string, rootFree bool) []string {
var out []string
for _, c := range declared {
if c == "verified-sender" && (runsAs == "" || !rootFree) {
continue
}
out = append(out, c)
}
return out
}
func kindListed(list []KindHeld, k KindHeld) bool {
for _, x := range list {
if x.Seat == k.Seat && x.Kind == k.Kind && x.Module == k.Module && x.Node == k.Node {
return true
}
}
return false
}
+7 -184
View File
@@ -63,23 +63,6 @@ type Seat struct {
Emits []string Emits []string
Serves []string Serves []string
Versions []string // protocol versions served beside the current one; empty for v1 only Versions []string // protocol versions served beside the current one; empty for v1 only
// Kinded says the seat is a kinded bench (novox/hq ADR 0234 §2, ADR 0259 §3): each holder claims one
// kind, and its verbs' subjects carry it. Kind is the kind this principal's claim names, for a seat it
// holds.
Kinded bool
Kind string
// ByCaller are the accepts and emits whose last token names the calling module (ADR 0259 §3).
ByCaller []string
// Proofs are the seat's proof verbs: core request and reply, never on a stream (ADR 0259 §3).
Proofs []string
// Records are the holder's buckets, by their full name, each user reads under its own name.
Records []string
// Capabilities are what this principal's claim of a kinded bench promises (ADR 0234 §2).
Capabilities []string
// DeclaredBy is the module that declares the seat. On a kinded bench it alone submits work to a kind
// and answers its proofs (novox/hq ADR 0259 §8): the router, not any user or watcher of the bench.
DeclaredBy string
} }
// A Principal is one user of the bus. Its permissions are derived from what it declares and // A Principal is one user of the bus. Its permissions are derived from what it declares and
@@ -183,63 +166,11 @@ var VerbsTheSelfCheckAsks = []SeatVerb{{Seat: "node-intrusion-prevention", Verb:
// the controller's grant that acts, and only through the step a person starts. // the controller's grant that acts, and only through the step a person starts.
var VerbsTheBusStepAsks = []SeatVerb{{Seat: "node-backup", Verb: "now"}} var VerbsTheBusStepAsks = []SeatVerb{{Seat: "node-backup", Verb: "now"}}
// VerbsTheControllerAsksForASecret are the seat verbs `give` calls (novox/hq ADR 0259 §10): the operator's
// desk opens a prompt that does not show what is typed, and answers it sealed to the controller's call.
var VerbsTheControllerAsksForASecret = []SeatVerb{{Seat: "node-launcher", Verb: "secret"}}
// ControllerOnly are the subjects the controller alone may publish, however wide another's grant (the review
// of 2026-10-09, M4): the desk's hidden prompt, on its seat's subjects and on any holder's own module
// subjects. A grant of every tool — the runtime's, which carries every agent's calls, or a person's `*` — would
// otherwise reach it, and the prompt says the controller asks: only the bus makes that true.
func ControllerOnly() []string {
var out []string
for _, v := range VerbsTheControllerAsksForASecret {
for _, base := range []string{"mesh.seat." + v.Seat + ".tool." + v.Verb, "mesh.mod.*.tool." + v.Seat + "." + v.Verb} {
out = append(out, base, base+".*")
}
}
return out
}
// MayPublish says whether permissions let a principal publish one subject: an allow covers it and no deny does.
func MayPublish(perms Permissions, subject string) bool {
for _, d := range perms.PublishDeny {
if SubjectsOverlap(d, subject) {
return false
}
}
for _, a := range perms.Publish {
if SubjectsOverlap(a, subject) {
return true
}
}
return false
}
// MaySubscribe says whether a principal with these permissions may subscribe to (and so answer) a subject.
func MaySubscribe(perms Permissions, subject string) bool {
for _, a := range perms.Subscribe {
if SubjectsOverlap(a, subject) {
return true
}
}
return false
}
// VerbsTheControllerAsksTheDeliveryOwner are the mesh-delivery seat's verbs the controller calls (novox/hq // VerbsTheControllerAsksTheDeliveryOwner are the mesh-delivery seat's verbs the controller calls (novox/hq
// ADR 0239): its self-check reads `stalled`, and healer H2 takes the one transition the table allows // ADR 0239): its self-check reads `stalled`, and healer H2 takes the one transition the table allows
// through `close`. A mesh seat's verb is flat: no machine in the subject. // through `close`. A mesh seat's verb is flat: no machine in the subject.
//
// And, since novox/hq ADR 0259, `release` and `stop`: the controller asks the operator for them about a
// delivery held past its bound, and calls them on the operator's warrant, with its why.
var VerbsTheControllerAsksTheDeliveryOwner = []SeatVerb{{Seat: "mesh-delivery", Verb: "stalled"}, var VerbsTheControllerAsksTheDeliveryOwner = []SeatVerb{{Seat: "mesh-delivery", Verb: "stalled"},
{Seat: "mesh-delivery", Verb: "close"}, {Seat: "mesh-delivery", Verb: "release"}, {Seat: "mesh-delivery", Verb: "stop"}} {Seat: "mesh-delivery", Verb: "close"}}
// VerbsTheControllerActsOnAWarrant are the other seat verbs the controller calls when the operator's warrant
// chooses them (novox/hq ADR 0259): a machine's service restarted, and a walk started or stopped through the
// controller's own `plans`. Named one by one; a node seat's on any machine, a mesh seat's flat.
var VerbsTheControllerActsOnAWarrant = []SeatVerb{{Seat: "node-service-manager", Verb: "restart"},
{Seat: ControllerSeat, Verb: "plans"}}
// perMachineEvents are a node-scoped seat's events about the holder itself, whose last token is the // perMachineEvents are a node-scoped seat's events about the holder itself, whose last token is the
// holder's machine (novox/hq ADR 0219): `paused.<node>`, the build agent saying whether it takes work. // holder's machine (novox/hq ADR 0219): `paused.<node>`, the build agent saying whether it takes work.
@@ -289,19 +220,6 @@ func (p Principal) Username() string {
// (novox/hq to-be 45 §6): its answer is an ordinary report, on its own report subject. // (novox/hq to-be 45 §6): its answer is an ordinary report, on its own report subject.
func AskReportSubject(node string) string { return "mesh.node." + node + ".ask.report" } func AskReportSubject(node string) string { return "mesh.node." + node + ".ask.report" }
// AskHandOverSubject is where the controller's terminal asks one machine's node-engine to hand a directory it
// uses as found to the mesh (novox/hq issue 356, issue 339): a request on core NATS, answered once on the reply
// it carries. Only the controller is granted a publish here (the writers table holds it), but **that is not who
// the engine hears**: the bus lets any principal allowed to answer reply to a message it received, on the reply
// subject that message named, so a message can arrive here from any responder. The ask is therefore signed with
// the mesh's key (link.SignedHandOver), and the engine verifies it before reading anything out of it.
func AskHandOverSubject(node string) string { return "mesh.node." + node + ".ask.hand-over" }
// AskSetuidSearchSubject is where the controller's terminal asks one machine's node-engine to throw its last
// search for setuid programs away and start a full one (novox/hq issue 361): a request answered once, signed as
// a hand-over is (link.SetuidSearchContext), for the same reason.
func AskSetuidSearchSubject(node string) string { return "mesh.node." + node + ".ask.setuid-search" }
// inbox is a principal's own reply space. No user is ever granted a bare `_INBOX.>` (design 25 // inbox is a principal's own reply space. No user is ever granted a bare `_INBOX.>` (design 25
// §4): with one account, inbox privacy is the permission list or it is nothing, so each user's // §4): with one account, inbox privacy is the permission list or it is nothing, so each user's
// inbox is derived from its own identity and its permissions name that prefix and no other. // inbox is derived from its own identity and its permissions name that prefix and no other.
@@ -310,9 +228,6 @@ func (p Principal) inbox() string { return "_INBOX." + p.Username() + ".>" }
// Permissions is what a principal may publish and subscribe, and whether it may answer. // Permissions is what a principal may publish and subscribe, and whether it may answer.
type Permissions struct { type Permissions struct {
Publish []string Publish []string
// PublishDeny are subjects refused although an allow covers them: the controller's alone (ControllerOnly),
// denied to everybody whose grant is wide enough to reach them. The server's deny outranks its allow.
PublishDeny []string
Subscribe []string Subscribe []string
// AllowResponses lets a principal reply to a request it received, on the reply subject that // AllowResponses lets a principal reply to a request it received, on the reply subject that
// request carried, once. // request carried, once.
@@ -451,28 +366,10 @@ func PermissionsFor(p Principal) (Permissions, error) {
for _, v := range VerbsTheBusStepAsks { for _, v := range VerbsTheBusStepAsks {
pub = append(pub, "mesh.seat."+v.Seat+".tool."+v.Verb+".*") pub = append(pub, "mesh.seat."+v.Seat+".tool."+v.Verb+".*")
} }
// And the operator's desk, for a secret given there (ADR 0259 §10).
for _, v := range VerbsTheControllerAsksForASecret {
pub = append(pub, "mesh.seat."+v.Seat+".tool."+v.Verb+".*")
}
// And the delivery's owner, a mesh seat, asked on its flat subjects (ADR 0239). // And the delivery's owner, a mesh seat, asked on its flat subjects (ADR 0239).
for _, v := range VerbsTheControllerAsksTheDeliveryOwner { for _, v := range VerbsTheControllerAsksTheDeliveryOwner {
pub = append(pub, "mesh.seat."+v.Seat+".tool."+v.Verb) pub = append(pub, "mesh.seat."+v.Seat+".tool."+v.Verb)
} }
// And the verbs a warrant chooses (novox/hq ADR 0259): a node seat's on any machine, its own flat.
for _, v := range VerbsTheControllerActsOnAWarrant {
if v.Seat == ControllerSeat {
pub = append(pub, "mesh.seat."+v.Seat+".tool."+v.Verb)
continue
}
pub = append(pub, "mesh.seat."+v.Seat+".tool."+v.Verb+".*")
}
// And asking the operator (novox/hq ADR 0259): an ask and its cancel under its own name, its warrants
// heard under its own name, the record of its asks read under its own name — as any user of the seat,
// derived the same way, from the seat its holder declares.
tp, ts := SeatTrafficOf(ControllerSeat, nil, p.Uses, nil).grants()
pub = append(pub, tp...)
sub = append(sub, ts...)
// And asks who answers (novox/hq to-be 45 §4, D3): the self-check finds every seat's holder by // And asks who answers (novox/hq to-be 45 §4, D3): the self-check finds every seat's holder by
// the same discovery the console reads. The question only; the answers come to its own inbox. // the same discovery the console reads. The question only; the answers come to its own inbox.
pub = append(pub, "$SRV.INFO") pub = append(pub, "$SRV.INFO")
@@ -575,12 +472,7 @@ func PermissionsFor(p Principal) (Permissions, error) {
// And the mesh asking it to say again what it last applied (novox/hq to-be 45 §6, the // And the mesh asking it to say again what it last applied (novox/hq to-be 45 §6, the
// `report` verb healer H1 asks): its own machine's, on core NATS and off any stream. It // `report` verb healer H1 asks): its own machine's, on core NATS and off any stream. It
// answers through its report, the one thing it already says — no reply to anybody's inbox. // answers through its report, the one thing it already says — no reply to anybody's inbox.
AskReportSubject(p.Node), AskReportSubject(p.Node)}
// And the controller's terminal asking it to hand a directory used as found to the mesh (novox/hq
// issue 356), which it answers on the request's reply: the one request a node is asked.
AskHandOverSubject(p.Node),
// And asking it for a fresh search for setuid programs (novox/hq issue 361), answered the same way.
AskSetuidSearchSubject(p.Node)}
// The node-engine witnesses the core builds it places (novox/hq to-be 45 §8, ADR 0236; the // The node-engine witnesses the core builds it places (novox/hq to-be 45 §8, ADR 0236; the
// contract is lease/witness.go): it asks its own machine's node tools PING, and, where the // contract is lease/witness.go): it asks its own machine's node tools PING, and, where the
// machine runs the controller, reads the lease's one key — read, never written. // machine runs the controller, reads the lease's one key — read, never written.
@@ -638,9 +530,6 @@ func PermissionsFor(p Principal) (Permissions, error) {
// 2b. Events of a role it watches, under the seat's own namespace. Subscribe only: watching a // 2b. Events of a role it watches, under the seat's own namespace. Subscribe only: watching a
// role is hearing what it announced, not taking part in it. // role is hearing what it announced, not taking part in it.
for _, w := range p.Watches { for _, w := range p.Watches {
if w.Kinded {
continue // composed by SeatTrafficOf below
}
for _, e := range w.Emits { for _, e := range w.Emits {
sub = append(sub, seatSubject(w, "event", e)) sub = append(sub, seatSubject(w, "event", e))
} }
@@ -657,19 +546,8 @@ func PermissionsFor(p Principal) (Permissions, error) {
"$JS.API.CONSUMER.INFO."+consumerStream(p)+"."+consumerDurable(p), "$JS.API.CONSUMER.INFO."+consumerStream(p)+"."+consumerDurable(p),
"$JS.API.CONSUMER.MSG.NEXT."+consumerStream(p)+"."+consumerDurable(p)) "$JS.API.CONSUMER.MSG.NEXT."+consumerStream(p)+"."+consumerDurable(p))
// 3. Seats it holds: full participation — but the controller's own seat, whose verbs only the serving // 3. Seats it holds: full participation.
// controller answers, on its own connection (servedOnlyByTheController).
for _, s := range p.Holds { for _, s := range p.Holds {
if servedOnlyByTheController(s) {
continue
}
if s.isNewTraffic() {
// Composed by SeatTrafficOf below, worker and all; only its tools are served here.
for _, t := range s.Serves {
sub = append(sub, seatToolSubject(s, t, p.Node))
}
continue
}
// Taking work from the role's queue: the worker consumer every holder shares (asked // Taking work from the role's queue: the worker consumer every holder shares (asked
// about, pulled from, acknowledged), on the seat's own stream (novox/hq ADR 0190). A // about, pulled from, acknowledged), on the seat's own stream (novox/hq ADR 0190). A
// holder pulls — asks the consumer for its next message, answered on its own inbox — // holder pulls — asks the consumer for its next message, answered on its own inbox —
@@ -712,7 +590,7 @@ func PermissionsFor(p Principal) (Permissions, error) {
// seat's inbound subject and watch other modules' traffic, nor publish its outbound // seat's inbound subject and watch other modules' traffic, nor publish its outbound
// events and lie about outcomes (design 29 §2). // events and lie about outcomes (design 29 §2).
for _, s := range p.Uses { for _, s := range p.Uses {
for _, a := range plainVerbs(s, s.Accepts) { for _, a := range s.Accepts {
pub = append(pub, seatSubject(s, "accept", a)) pub = append(pub, seatSubject(s, "accept", a))
} }
for _, t := range s.Serves { for _, t := range s.Serves {
@@ -725,12 +603,6 @@ func PermissionsFor(p Principal) (Permissions, error) {
pub = append(pub, stateGrants(stateAccess{Module: p.Module, Node: p.Node, Keeps: p.State, pub = append(pub, stateGrants(stateAccess{Module: p.Module, Node: p.Node, Keeps: p.State,
PerMachine: p.PerMachine, Reads: p.Reads, KeyedReads: p.KeyedReads})...) PerMachine: p.PerMachine, Reads: p.Reads, KeyedReads: p.KeyedReads})...)
// 6. Its traffic on seats that name their caller or their kind, ask proofs or keep records
// (novox/hq ADR 0259 §3).
tp, ts := SeatTrafficOf(p.Module, p.Holds, p.Uses, p.Watches).grants()
pub = append(pub, tp...)
sub = append(sub, ts...)
case KindNodeTools: case KindNodeTools:
// **One process serves what every module on the machine would have served for itself** // **One process serves what every module on the machine would have served for itself**
// (novox/hq ADR 0175). Each carried module's whole tool namespace — the same grant that // (novox/hq ADR 0175). Each carried module's whole tool namespace — the same grant that
@@ -756,9 +628,6 @@ func PermissionsFor(p Principal) (Permissions, error) {
pub = append(pub, own+".event."+e) pub = append(pub, own+".event."+e)
} }
for _, s := range d.Holds { for _, s := range d.Holds {
if servedOnlyByTheController(s) {
continue
}
for _, t := range s.Serves { for _, t := range s.Serves {
sub = append(sub, seatToolSubject(s, t, p.Node)) sub = append(sub, seatToolSubject(s, t, p.Node))
} }
@@ -811,25 +680,6 @@ func PermissionsFor(p Principal) (Permissions, error) {
pub = append(pub, stateGrants(stateAccess{Module: d.Module, Node: p.Node, Keeps: stateNames(d.State), pub = append(pub, stateGrants(stateAccess{Module: d.Module, Node: p.Node, Keeps: stateNames(d.State),
PerMachine: perMachineNames(d.State), Reads: d.Reads, KeyedReads: d.KeyedReads})...) PerMachine: perMachineNames(d.State), Reads: d.Reads, KeyedReads: d.KeyedReads})...)
} }
// **Never the traffic of a trusted holder** (novox/hq ADR 0259 §8): the machine's runtime runs as the
// operator's account, which every agent runs as, so a module saying warrants or speaking for a kind
// that proves its sender is never composed into it — refused here, naming it, whatever registration
// let through.
for _, d := range p.Carries {
if why := trustedTraffic(d); why != "" {
return Permissions{}, fmt.Errorf("%s on %s is carried by the machine's runtime, and %s: it runs "+
"as an account of its own, never the runtime's (novox/hq ADR 0259)", d.Module, p.Node, why)
}
}
// **And the seat traffic of the modules it carries** (novox/hq ADR 0259 §3): a bundle reaches the
// bus only through its runtime, so the runtime is granted the union. That one module's code does
// not publish under another's name or kind through it is the runtime's to keep, from the seat
// traffic each module's membership lists.
for _, d := range p.Carries {
tp, ts := SeatTrafficOf(d.Module, d.Holds, d.Uses, d.Watches).grants()
pub = append(pub, tp...)
sub = append(sub, ts...)
}
sub = unique(sub) sub = unique(sub)
pub = unique(pub) pub = unique(pub)
} }
@@ -864,29 +714,13 @@ func PermissionsFor(p Principal) (Permissions, error) {
if err := CheckWriters(p, pub); err != nil { if err := CheckWriters(p, pub); err != nil {
return Permissions{}, err return Permissions{}, err
} }
// What the controller alone may publish is denied to everybody else whose grant reaches it.
var deny []string
if p.Kind != KindController {
for _, only := range ControllerOnly() {
for _, a := range pub {
if SubjectsOverlap(a, only) {
deny = append(deny, only)
break
}
}
}
}
return Permissions{ return Permissions{
Publish: pub, Publish: pub,
PublishDeny: deny,
Subscribe: sub, Subscribe: sub,
// A module answers what it was asked — a tool call reaches it on its own namespace, so the // A module answers what it was asked — a tool call reaches it on its own namespace, so the
// authority is bounded by having been asked — and so does the controller. A node is asked one // authority is bounded by having been asked — and so does the controller. A node and a
// thing, a hand-over on its own subject (novox/hq issue 356), and answers that: the node is its // person are never asked anything, and are granted nothing here.
// machine's engine, root there already, and it is delivered only its own subjects. What it answers is AllowResponses: p.Kind == KindModule || p.Kind == KindController || p.Kind == KindNodeTools,
// never trusted for being an answer — the controller reads the engine's words and records nothing. A
// person is never asked anything, and is granted nothing here.
AllowResponses: p.Kind == KindModule || p.Kind == KindController || p.Kind == KindNodeTools || p.Kind == KindNode,
}, nil }, nil
} }
@@ -909,13 +743,6 @@ func seatSubject(s Seat, kind, verb string) string {
// seat carries the node it is asked of, because a flat subject would reach every machine's holder // seat carries the node it is asked of, because a flat subject would reach every machine's holder
// and the queue group would silently pick a winner (novox/hq ADR 0132, design 33 §4). A holder // and the queue group would silently pick a winner (novox/hq ADR 0132, design 33 §4). A holder
// subscribes its own node's; a user publishes any node's (`*`) and names the machine in the subject. // subscribes its own node's; a user publishes any node's (`*`) and names the machine in the subject.
// servedOnlyByTheController says a seat's verbs are answered by the serving controller alone, on its own
// connection (the KindController grant), never by a module claiming the seat or a runtime carrying it: the
// controller's own seat. Its verbs decide what the mesh is — and `root-free` decides whether the router believes
// a verified sender (novox/hq ADR 0259 §8) — so a machine's runtime, whose credential an agent on that machine
// may hold, answering one would be an agent answering it (the confirmation review of 2026-10-09).
func servedOnlyByTheController(s Seat) bool { return s.Name == ControllerSeat }
func seatToolSubject(s Seat, verb, node string) string { func seatToolSubject(s Seat, verb, node string) string {
base := seatSubject(s, "tool", verb) base := seatSubject(s, "tool", verb)
if s.Scope == "node" && node != "" { if s.Scope == "node" && node != "" {
@@ -1104,11 +931,7 @@ func ComposeAccounts(principals []Principal) (string, error) {
return "", fmt.Errorf("%s has no password hash: a user without one is a user anybody is", p.Username()) return "", fmt.Errorf("%s has no password hash: a user without one is a user anybody is", p.Username())
} }
fmt.Fprintf(&b, " { user: %q, password: %q, permissions: {\n", p.Username(), p.PasswordHash) fmt.Fprintf(&b, " { user: %q, password: %q, permissions: {\n", p.Username(), p.PasswordHash)
if len(perms.PublishDeny) > 0 {
fmt.Fprintf(&b, " publish: { allow: [%s], deny: [%s] }\n", quoted(perms.Publish), quoted(perms.PublishDeny))
} else {
fmt.Fprintf(&b, " publish: { allow: [%s] }\n", quoted(perms.Publish)) fmt.Fprintf(&b, " publish: { allow: [%s] }\n", quoted(perms.Publish))
}
fmt.Fprintf(&b, " subscribe: { allow: [%s] }\n", quoted(perms.Subscribe)) fmt.Fprintf(&b, " subscribe: { allow: [%s] }\n", quoted(perms.Subscribe))
if perms.AllowResponses { if perms.AllowResponses {
fmt.Fprintf(&b, " allow_responses: { max: 1, ttl: \"%dm\" }\n", int(ResponseTTL/time.Minute)) fmt.Fprintf(&b, " allow_responses: { max: 1, ttl: \"%dm\" }\n", int(ResponseTTL/time.Minute))
+3 -10
View File
@@ -103,8 +103,7 @@ func TestAnInboxIsScopedToItsOwner(t *testing.T) {
// A responder answers on the caller's inbox, which it has no permission for. allow_responses is // A responder answers on the caller's inbox, which it has no permission for. allow_responses is
// what makes a scoped inbox workable at all — the authority is bounded by having been asked. A // what makes a scoped inbox workable at all — the authority is bounded by having been asked. A
// module is asked on its own namespace and may answer; a node is asked one thing, a hand-over on its own // module is asked on its own namespace and may answer; a node and a person are never asked.
// subject (novox/hq issue 356), and may answer that; a person is never asked.
func TestOnlyWhatCanBeAskedMayAnswer(t *testing.T) { func TestOnlyWhatCanBeAskedMayAnswer(t *testing.T) {
module, _ := PermissionsFor(Principal{Kind: KindModule, Node: "one", Module: "audit", module, _ := PermissionsFor(Principal{Kind: KindModule, Node: "one", Module: "audit",
Consumes: []string{"shop.order.placed"}, PasswordHash: "x"}) Consumes: []string{"shop.order.placed"}, PasswordHash: "x"})
@@ -112,14 +111,8 @@ func TestOnlyWhatCanBeAskedMayAnswer(t *testing.T) {
t.Fatal("a module cannot answer a tool call on its own namespace") t.Fatal("a module cannot answer a tool call on its own namespace")
} }
node, _ := PermissionsFor(Principal{Kind: KindNode, Node: "one", PasswordHash: "x"}) node, _ := PermissionsFor(Principal{Kind: KindNode, Node: "one", PasswordHash: "x"})
if !node.AllowResponses || !slices.Contains(node.Subscribe, AskHandOverSubject("one")) || if node.AllowResponses {
!slices.Contains(node.Subscribe, AskSetuidSearchSubject("one")) || t.Fatal("a node was granted the right to answer, and nothing asks a node anything")
slices.Contains(node.Subscribe, AskSetuidSearchSubject("two")) {
t.Fatalf("a node cannot answer the hand-over it is asked: %v %v", node.AllowResponses, node.Subscribe)
}
person, _ := PermissionsFor(Principal{Kind: KindPerson, Node: "one", Module: "jo", PasswordHash: "x"})
if person.AllowResponses {
t.Fatal("a person was granted the right to answer, and nothing asks a person anything")
} }
} }
-305
View File
@@ -1,305 +0,0 @@
package broker
import "sort"
// What a module may say and take on the seats it holds, uses and watches, beyond tools (novox/hq ADR
// 0259 §3, to-be 46 §10).
//
// Three rules are added to the ones a seat always had, each a subject whose last token names who may
// publish it, granted to that publisher alone — the way a node seat's event about a machine carries the
// machine (ADR 0219):
//
// - **A verb named by its caller** (`by-caller`). A user of the seat submits that accept, and hears that
// event, under its own module's name and no other: `accept.ask.<module>`, `event.decided.<module>`. The
// holder takes every caller's accept and says the event to any caller. So an ask's asker is a fact the
// server enforces, and a warrant reaches only the asker it is for.
// - **A kinded bench** (ADR 0234 §2). A holder claims one kind and takes its own kind's accepts, says its
// own kind's events and asks its own kind's proofs, and nothing of another kind; a user submits to any
// kind. Each kind has its own worker on the seat's queue, so a holder that is away keeps its work and
// holds up no other kind.
// - **A proof** (`proofs`): core request and reply on `mesh.seat.<seat>.proof.<verb>.<kind>`. No stream's
// subjects cover it, so what travels there — a code typed by the operator — is never persisted. A kinded
// holder asks with its own kind; the modules that watch the seat answer.
//
// And one read: **a holder's records**, a bucket the seat names, read by each user under its own name only
// (`$KV.<bucket>.<module>.>`), so an asker reads the state of its own asks and no other asker's.
// Worker is one durable consumer a holder pulls a seat's work from.
type Worker struct {
Stream string
Consumer string
Filter string
}
// SeatTraffic is one module's seat traffic beyond tools. Publish and Subscribe are subject patterns, in the
// server's wildcards; the runtime that carries the module checks a bundle's request against them, since
// the runtime's own principal holds the union of every module it carries.
type SeatTraffic struct {
// Publish is what it submits (accepts of seats it uses), says (events of seats it holds) and asks
// (proofs of seats it holds a kind of).
Publish []string `json:"publish,omitempty"`
// Subscribe is what it hears: events of seats it uses that are named by caller, events of seats it
// watches, and accepts of seats it holds.
Subscribe []string `json:"subscribe,omitempty"`
// Answers is the proof subjects it answers, as a watcher of a kinded seat.
Answers []string `json:"answers,omitempty"`
// Workers are the work queues it takes from, as a holder.
Workers []Worker `json:"workers,omitempty"`
// Records is the direct-get subjects of the records it reads under its own name.
Records []string `json:"records,omitempty"`
// Kinds are the holders of every kinded bench it uses or watches, with what each promises: the one
// account of which channel is which, and what it can carry, that the router judges an answer by. The
// controller's, from the claims, never a channel's word (novox/hq ADR 0259 §5).
Kinds []KindHeld `json:"kinds,omitempty"`
}
// KindHeld is one kind of a kinded bench and who holds it.
type KindHeld struct {
Seat string `json:"seat"`
Kind string `json:"kind"`
Module string `json:"module"`
Node string `json:"node"`
Capabilities []string `json:"capabilities,omitempty"`
}
// KindedBenches are the seats that may be kinded (ADR 0234 §2): making another is a decision, recorded.
var KindedBenches = map[string]bool{"channel": true, "intake": true}
// WorkerName is the worker a seat's holders pull from: one for the seat, or one per kind on a kinded bench.
func WorkerName(seat, kind string) string {
if kind == "" {
return "SEAT_" + upperSnake(seat) + "_worker"
}
return "SEAT_" + upperSnake(seat) + "_" + upperSnake(kind) + "_worker"
}
func namesVerb(list []string, s string) bool {
for _, x := range list {
if x == s {
return true
}
}
return false
}
// SeatTrafficOf derives one module's seat traffic from the seats it holds, uses and watches. Only seats
// carrying one of the rules above are read: every other seat is composed as it always was.
func SeatTrafficOf(module string, holds, uses, watches []Seat) SeatTraffic {
var t SeatTraffic
for _, s := range holds {
if !s.isNewTraffic() {
continue
}
kind := ""
if s.Kinded {
kind = s.Kind
if kind == "" || !safeSubject.MatchString(kind) {
// A kinded claim without a usable kind is refused at registration; here it is granted
// nothing, which is the same answer at the last place it could be asked.
continue
}
}
if len(s.Accepts) > 0 {
stream := seatStreamName(s.Name)
filter := "mesh.seat." + s.Name + ".accept.>"
if kind != "" {
filter = "mesh.seat." + s.Name + ".accept.*." + kind
}
t.Workers = append(t.Workers, Worker{Stream: stream, Consumer: WorkerName(s.Name, kind), Filter: filter})
}
for _, a := range s.Accepts {
switch {
case kind != "":
t.Subscribe = append(t.Subscribe, seatSubject(s, "accept", a+"."+kind))
case namesVerb(s.ByCaller, a):
t.Subscribe = append(t.Subscribe, seatSubject(s, "accept", a+".*"))
}
}
for _, e := range s.Emits {
switch {
case kind != "":
t.Publish = append(t.Publish, seatSubject(s, "event", e+"."+kind))
case namesVerb(s.ByCaller, e):
t.Publish = append(t.Publish, seatSubject(s, "event", e+".*"))
}
}
if kind != "" {
for _, v := range s.Proofs {
t.Publish = append(t.Publish, seatSubject(s, "proof", v+"."+kind))
}
}
}
for _, s := range uses {
if !s.isNewTraffic() {
continue
}
for _, a := range s.Accepts {
switch {
case namesVerb(s.ByCaller, a):
t.Publish = append(t.Publish, seatSubject(s, "accept", a+"."+module))
case s.Kinded && module == s.DeclaredBy:
// Work for a kind is put on its queue by the bench's own router, and by no other user.
t.Publish = append(t.Publish, seatSubject(s, "accept", a+".*"))
}
}
for _, e := range s.Emits {
if namesVerb(s.ByCaller, e) {
t.Subscribe = append(t.Subscribe, seatSubject(s, "event", e+"."+module))
}
}
for _, b := range s.Records {
if !safeSubject.MatchString(b) {
continue
}
t.Records = append(t.Records, "$JS.API.DIRECT.GET.KV_"+b+".$KV."+b+"."+module+".>")
}
}
for _, w := range watches {
if w.Kinded {
for _, e := range w.Emits {
t.Subscribe = append(t.Subscribe, seatSubject(w, "event", e+".*"))
}
if module == w.DeclaredBy {
// A code is answered by the bench's own router, and by no other watcher.
for _, v := range w.Proofs {
t.Answers = append(t.Answers, seatSubject(w, "proof", v+".*"))
}
}
}
}
t.Publish = unique(t.Publish)
t.Subscribe = unique(t.Subscribe)
t.Answers = unique(t.Answers)
t.Records = unique(t.Records)
sort.Slice(t.Workers, func(i, j int) bool { return t.Workers[i].Consumer < t.Workers[j].Consumer })
return t
}
// grants is the bus permissions seat traffic needs: the subjects themselves, and the JetStream API a
// worker is pulled and acknowledged through and a record is read through.
func (t SeatTraffic) grants() (pub, sub []string) {
pub = append(pub, t.Publish...)
sub = append(sub, t.Subscribe...)
sub = append(sub, t.Answers...)
for _, w := range t.Workers {
pub = append(pub,
"$JS.API.CONSUMER.INFO."+w.Stream+"."+w.Consumer,
"$JS.API.CONSUMER.MSG.NEXT."+w.Stream+"."+w.Consumer,
"$JS.ACK."+w.Stream+"."+w.Consumer+".>")
}
pub = append(pub, t.Records...)
return pub, sub
}
// isNewTraffic says whether a seat carries any of the rules above, so a seat that carries none is
// composed exactly as before them.
func (s Seat) isNewTraffic() bool {
return s.Kinded || len(s.ByCaller) > 0 || len(s.Proofs) > 0 || len(s.Records) > 0
}
// plainVerbs is a seat's accepts or emits with those the rules above compose taken out: a verb named by
// its caller and every verb of a kinded bench are composed by SeatTrafficOf and nowhere else.
func plainVerbs(s Seat, verbs []string) []string {
if s.Kinded {
return nil
}
var out []string
for _, v := range verbs {
if !namesVerb(s.ByCaller, v) {
out = append(out, v)
}
}
return out
}
// SeatTrafficObjects is the work queues and workers the seat traffic of every composed user implies
// (novox/hq ADR 0259 §3): a queue for each seat a holder takes work from, and each holder's worker on it —
// one per kind on a kinded bench, filtered to that kind, so the kinds never take each other's work. Only
// seats carrying the rules above; the mesh's own seats' queues are RaiseSeats'.
func SeatTrafficObjects(users []Principal) ([]Stream, []Consumer) {
streams := map[string]Stream{}
consumers := map[string]Consumer{}
add := func(module string, holds []Seat) {
for _, w := range SeatTrafficOf(module, holds, nil, nil).Workers {
seat := ""
for _, s := range holds {
if seatStreamName(s.Name) == w.Stream {
seat = s.Name
}
}
streams[w.Stream] = Stream{
Name: w.Stream,
Subjects: []string{"mesh.seat." + seat + ".accept.>"},
Retention: RetentionWorkQueue,
MaxAge: 7 * 24 * 60 * 60,
Why: "work submitted to the " + seat + " seat; its holders take it, each kind its own, and it queues while nobody does",
}
consumers[w.Consumer] = Consumer{
Name: w.Consumer,
Stream: w.Stream,
Filters: []string{w.Filter},
AckWaitSeconds: 60,
// No bound on redelivery: a channel away for a day keeps its work, offered again later and
// later by its holder's runtime (novox/hq ADR 0259; the correctness review of 2026-10-08).
MaxDeliver: 0,
Why: module + " holds " + seat + "; it pulls one ask at a time and acknowledges once it has " +
"recorded it, so a crash redelivers rather than loses",
}
}
}
for _, p := range users {
switch p.Kind {
case KindModule:
add(p.Module, p.Holds)
case KindNodeTools:
for _, d := range p.Carries {
add(d.Module, d.Holds)
}
}
}
var ss []Stream
for _, s := range streams {
ss = append(ss, s)
}
sort.Slice(ss, func(i, j int) bool { return ss[i].Name < ss[j].Name })
var cs []Consumer
for _, c := range consumers {
cs = append(cs, c)
}
sort.Slice(cs, func(i, j int) bool { return cs[i].Name < cs[j].Name })
return ss, cs
}
// trustedTraffic is why a module's seat traffic is the trusted holder's (novox/hq ADR 0259 §8), or "": it
// says a seat's event to one caller each (a warrant), or holds a kind of a kinded bench that proves its sender.
func trustedTraffic(d Declared) string {
for _, s := range d.Holds {
for _, e := range s.Emits {
if namesVerb(s.ByCaller, e) {
return "it says " + s.Name + "'s " + e + " to one caller each"
}
}
if s.Kinded && namesVerb(s.Capabilities, "verified-sender") {
return "it holds " + s.Name + " of kind " + s.Kind + ", which proves its sender"
}
}
return ""
}
// TrafficQueues is the work queue of every seat naming its caller or its kind that accepts work, held or not
// (novox/hq ADR 0259 §3): what is submitted before a holder is assigned waits for it.
func TrafficQueues(seats []Seat) []Stream {
var out []Stream
seen := map[string]bool{}
for _, s := range seats {
if len(s.Accepts) == 0 || !s.isNewTraffic() || seen[s.Name] {
continue
}
seen[s.Name] = true
out = append(out, Stream{Name: seatStreamName(s.Name), Subjects: []string{"mesh.seat." + s.Name + ".accept.>"},
Retention: RetentionWorkQueue, MaxAge: 7 * 24 * 60 * 60,
Why: "work submitted to the " + s.Name + " seat; its holders take it, each kind its own, and it queues while nobody does"})
}
sort.Slice(out, func(i, j int) bool { return out[i].Name < out[j].Name })
return out
}
-390
View File
@@ -1,390 +0,0 @@
package broker
import (
"strings"
"testing"
)
// The seats of novox/hq ADR 0259 §3, as the messenger declares them.
func operatorChannel() Seat {
return Seat{Name: "operator-channel", Scope: "mesh", Accepts: []string{"ask", "cancel"},
Emits: []string{"decided"}, Serves: []string{"open", "history", "notify"},
ByCaller: []string{"ask", "cancel", "decided"}, Records: []string{"messenger_asks"}}
}
func channelSeat(kind string) Seat {
return Seat{Name: "channel", Scope: "mesh", Accepts: []string{"show", "edit", "send"}, Kinded: true, Kind: kind,
DeclaredBy: "messenger"}
}
func intakeSeat(kind string) Seat {
return Seat{Name: "intake", Scope: "mesh", Emits: []string{"choice", "link"}, Proofs: []string{"code"},
Kinded: true, Kind: kind, DeclaredBy: "messenger"}
}
func allowed(patterns []string, subject string) bool {
for _, p := range patterns {
if subjectMatches(p, subject) {
return true
}
}
return false
}
func perms(t *testing.T, p Principal) Permissions {
t.Helper()
got, err := PermissionsFor(p)
if err != nil {
t.Fatal(err)
}
return got
}
func TestAnAskerAsksAndHearsUnderItsOwnNameOnly(t *testing.T) {
asker := Principal{Kind: KindModule, Node: "anchor", Module: "mesh-delivery", Uses: []Seat{operatorChannel()}}
got := perms(t, asker)
for _, s := range []string{
"mesh.seat.operator-channel.accept.ask.mesh-delivery",
"mesh.seat.operator-channel.accept.cancel.mesh-delivery",
"$JS.API.DIRECT.GET.KV_messenger_asks.$KV.messenger_asks.mesh-delivery.a1",
} {
if !allowed(got.Publish, s) {
t.Errorf("an asker may not publish %s", s)
}
}
for _, s := range []string{
"mesh.seat.operator-channel.accept.ask.mesh-controller",
"mesh.seat.operator-channel.accept.ask.*",
"mesh.seat.operator-channel.event.decided.mesh-delivery",
"$JS.API.DIRECT.GET.KV_messenger_asks.$KV.messenger_asks.mesh-controller.a1",
"$KV.messenger_asks.mesh-delivery.a1",
} {
if allowed(got.Publish, s) {
t.Errorf("an asker may publish %s, which is not its own to submit", s)
}
}
if !allowed(got.Subscribe, "mesh.seat.operator-channel.event.decided.mesh-delivery") {
t.Error("an asker does not hear its own warrants")
}
if allowed(got.Subscribe, "mesh.seat.operator-channel.event.decided.mesh-controller") {
t.Error("an asker hears another asker's warrants")
}
// And its own consumer carries its warrants, so a restart catches up.
c, ok := ConsumerFor(asker)
if !ok || !allowed(c.Filters, "mesh.seat.operator-channel.event.decided.mesh-delivery") {
t.Errorf("the asker's consumer does not carry its warrants: %v", c.Filters)
}
}
func TestOnlyTheHolderPublishesAWarrant(t *testing.T) {
users, err := Users(Records{
Nodes: []string{"anchor"},
Assigned: map[string][]Declared{"anchor": {
{Module: "messenger", Holds: []Seat{operatorChannel()}, Uses: []Seat{channelSeat("")},
Watches: []Seat{{Name: "intake", Emits: []string{"choice", "link"}, Kinded: true, Proofs: []string{"code"}, DeclaredBy: "messenger"}}},
{Module: "mesh-delivery", Uses: []Seat{operatorChannel()}},
{Module: "telegram", Holds: []Seat{channelSeat("telegram"), intakeSeat("telegram")}},
}},
})
if err != nil {
t.Fatal(err)
}
for _, u := range users {
got := perms(t, u)
says := allowed(got.Publish, "mesh.seat.operator-channel.event.decided.mesh-delivery")
if says != (u.Module == "messenger") {
t.Errorf("%s %s publish a warrant", u.Username(), map[bool]string{true: "may", false: "may not"}[says])
}
}
}
func TestTheHolderTakesEveryCallersAskThroughItsWorker(t *testing.T) {
got := perms(t, Principal{Kind: KindModule, Node: "anchor", Module: "messenger", Holds: []Seat{operatorChannel()}})
if !allowed(got.Subscribe, "mesh.seat.operator-channel.accept.ask.mesh-delivery") {
t.Error("the router does not take an ask")
}
for _, s := range []string{
"$JS.API.CONSUMER.MSG.NEXT.SEAT_OPERATOR_CHANNEL.SEAT_OPERATOR_CHANNEL_worker",
"$JS.ACK.SEAT_OPERATOR_CHANNEL.SEAT_OPERATOR_CHANNEL_worker.x",
"mesh.seat.operator-channel.event.decided.mesh-controller",
} {
if !allowed(got.Publish, s) {
t.Errorf("the router may not publish %s", s)
}
}
if allowed(got.Publish, "mesh.seat.operator-channel.accept.ask.messenger") {
t.Error("the holder may ask its own seat without using it")
}
}
func TestAKindedHolderReachesItsOwnKindAndNoOther(t *testing.T) {
got := perms(t, Principal{Kind: KindModule, Node: "anchor", Module: "telegram",
Holds: []Seat{channelSeat("telegram"), intakeSeat("telegram")}})
for _, s := range []string{
"mesh.seat.intake.event.choice.telegram", "mesh.seat.intake.event.link.telegram",
"mesh.seat.intake.proof.code.telegram",
"$JS.API.CONSUMER.MSG.NEXT.SEAT_CHANNEL.SEAT_CHANNEL_TELEGRAM_worker",
} {
if !allowed(got.Publish, s) {
t.Errorf("telegram may not publish %s", s)
}
}
for _, s := range []string{
"mesh.seat.intake.event.choice.desktop", "mesh.seat.intake.proof.code.desktop",
"mesh.seat.channel.accept.show.telegram",
"$JS.API.CONSUMER.MSG.NEXT.SEAT_CHANNEL.SEAT_CHANNEL_DESKTOP_worker",
"mesh.seat.operator-channel.event.decided.mesh-delivery",
} {
if allowed(got.Publish, s) {
t.Errorf("telegram may publish %s", s)
}
}
if !allowed(got.Subscribe, "mesh.seat.channel.accept.show.telegram") ||
allowed(got.Subscribe, "mesh.seat.channel.accept.show.desktop") {
t.Error("telegram does not take exactly its own kind's work")
}
if allowed(got.Subscribe, "mesh.seat.intake.proof.code.telegram") {
t.Error("a channel answers its own proofs")
}
}
func TestTheWatcherAnswersProofsAndHearsEveryKind(t *testing.T) {
got := perms(t, Principal{Kind: KindModule, Node: "anchor", Module: "messenger",
Uses: []Seat{channelSeat("")},
Watches: []Seat{{Name: "intake", Emits: []string{"choice"}, Kinded: true, Proofs: []string{"code"}, DeclaredBy: "messenger"}}})
for _, s := range []string{"mesh.seat.intake.event.choice.telegram", "mesh.seat.intake.proof.code.desktop"} {
if !allowed(got.Subscribe, s) {
t.Errorf("the router does not hear %s", s)
}
}
if !allowed(got.Publish, "mesh.seat.channel.accept.show.telegram") {
t.Error("the router cannot send a channel its work")
}
if allowed(got.Publish, "mesh.seat.intake.event.choice.telegram") || allowed(got.Publish, "mesh.seat.intake.proof.code.telegram") {
t.Error("the router may say a channel's answer or proof")
}
}
func TestNoStreamKeepsAProof(t *testing.T) {
users, _ := Users(Records{Nodes: []string{"anchor"}, Assigned: map[string][]Declared{"anchor": {
{Module: "telegram", Holds: []Seat{channelSeat("telegram"), intakeSeat("telegram")}},
{Module: "messenger", Holds: []Seat{operatorChannel()}},
}}})
streams, _ := SeatTrafficObjects(users)
streams = append(streams, MeshStreams()...)
for _, s := range streams {
for _, subject := range s.Subjects {
if subjectMatches(subject, "mesh.seat.intake.proof.code.telegram") {
t.Errorf("%s keeps a proof (%s)", s.Name, subject)
}
}
}
}
func TestEachKindHasAWorkerOfItsOwn(t *testing.T) {
users, _ := Users(Records{Nodes: []string{"anchor"}, Assigned: map[string][]Declared{"anchor": {
{Module: "telegram", Holds: []Seat{channelSeat("telegram")}},
{Module: "desk-channel", Holds: []Seat{channelSeat("desktop")}},
{Module: "messenger", Holds: []Seat{operatorChannel()}},
}}})
streams, workers := SeatTrafficObjects(users)
names := map[string]string{}
for _, w := range workers {
names[w.Name] = strings.Join(w.Filters, ",")
}
want := map[string]string{
"SEAT_CHANNEL_TELEGRAM_worker": "mesh.seat.channel.accept.*.telegram",
"SEAT_CHANNEL_DESKTOP_worker": "mesh.seat.channel.accept.*.desktop",
"SEAT_OPERATOR_CHANNEL_worker": "mesh.seat.operator-channel.accept.>",
}
for n, f := range want {
if names[n] != f {
t.Errorf("worker %s filters %q, want %q", n, names[n], f)
}
}
if len(streams) != 2 {
t.Errorf("want the queues of channel and operator-channel, got %v", streams)
}
}
func TestTheRuntimeIsGrantedTheUnionAndTheMembershipEachModulesShare(t *testing.T) {
telegram := Declared{Module: "telegram", Holds: []Seat{channelSeat("telegram"), intakeSeat("telegram")}}
desk := Declared{Module: "desk-channel", Holds: []Seat{channelSeat("desktop"), intakeSeat("desktop")}}
got := perms(t, Principal{Kind: KindNodeTools, Node: "anchor", Module: RuntimeModule, Carries: []Declared{telegram, desk}})
for _, s := range []string{"mesh.seat.intake.event.choice.telegram", "mesh.seat.intake.event.choice.desktop"} {
if !allowed(got.Publish, s) {
t.Errorf("the runtime may not publish %s for a module it carries", s)
}
}
m := MembershipFor("anchor", telegram, Placements{})
if m.SeatTraffic == nil || !allowed(m.SeatTraffic.Publish, "mesh.seat.intake.event.choice.telegram") ||
allowed(m.SeatTraffic.Publish, "mesh.seat.intake.event.choice.desktop") {
t.Errorf("telegram's membership does not list exactly its own kind: %+v", m.SeatTraffic)
}
if plain := MembershipFor("anchor", Declared{Module: "plain"}, Placements{}); plain.SeatTraffic != nil {
t.Error("a module with no such seat is given seat traffic")
}
}
func TestASeatWithoutTheNewRulesIsComposedAsBefore(t *testing.T) {
old := Seat{Name: "node-build-agent", Scope: "node", Accepts: []string{"build"}, Emits: []string{"built"}}
got := perms(t, Principal{Kind: KindModule, Node: "anchor", Module: "builder", Holds: []Seat{old}})
for _, s := range []string{"mesh.seat.node-build-agent.event.built",
"$JS.API.CONSUMER.MSG.NEXT.SEAT_NODE_BUILD_AGENT.SEAT_NODE_BUILD_AGENT_worker"} {
if !allowed(got.Publish, s) {
t.Errorf("an old seat's holder lost %s", s)
}
}
if !allowed(got.Subscribe, "mesh.seat.node-build-agent.accept.build") {
t.Error("an old seat's holder lost its accept")
}
}
// The router learns which channel is which, and what each promises, from the controller's membership:
// the claims, never a channel's word (ADR 0259 §5).
func TestTheRoutersMembershipNamesEveryKindAndItsCapabilities(t *testing.T) {
tg := channelSeat("telegram")
tg.Capabilities = []string{"choice", "verified-sender"}
desk := channelSeat("desktop")
desk.Capabilities = []string{"choice"}
router := Declared{Module: "messenger", Holds: []Seat{operatorChannel()}, Uses: []Seat{channelSeat("")}}
records := Records{Nodes: []string{"anchor", "laptop"}, Assigned: map[string][]Declared{
"anchor": {router, {Module: "telegram", Holds: []Seat{tg}, RunsAs: "telegram"}},
"laptop": {{Module: "desk-channel", Holds: []Seat{desk}}},
}, RootFree: map[string]bool{"anchor": true}}
where := PlacementsOf(records, nil)
m := MembershipFor("anchor", router, where)
if m.SeatTraffic == nil || len(m.SeatTraffic.Kinds) != 2 {
t.Fatalf("the router is not told the kinds: %+v", m.SeatTraffic)
}
byKind := map[string]KindHeld{}
for _, k := range m.SeatTraffic.Kinds {
byKind[k.Kind] = k
}
if k := byKind["telegram"]; k.Module != "telegram" || k.Node != "anchor" || !namesVerb(k.Capabilities, "verified-sender") {
t.Errorf("telegram is %+v", k)
}
if k := byKind["desktop"]; k.Module != "desk-channel" || namesVerb(k.Capabilities, "verified-sender") {
t.Errorf("the desk is %+v", k)
}
if other := MembershipFor("anchor", Declared{Module: "mesh-delivery", Uses: []Seat{operatorChannel()}}, where); other.SeatTraffic != nil && len(other.SeatTraffic.Kinds) > 0 {
t.Error("an asker is told the channels")
}
}
// novox/hq ADR 0259 §8: only the bench's own router answers its proofs and puts work on a kind's queue.
func TestOnlyTheBenchsRouterAnswersProofsAndSubmitsWork(t *testing.T) {
other := perms(t, Principal{Kind: KindModule, Node: "anchor", Module: "eavesdropper",
Uses: []Seat{channelSeat("")},
Watches: []Seat{{Name: "intake", Emits: []string{"choice"}, Kinded: true, Proofs: []string{"code"}, DeclaredBy: "messenger"}}})
if allowed(other.Subscribe, "mesh.seat.intake.proof.code.telegram") {
t.Error("a watcher that is not the router answers codes")
}
if allowed(other.Publish, "mesh.seat.channel.accept.show.telegram") {
t.Error("a user that is not the router puts work on a kind's queue")
}
if !allowed(other.Subscribe, "mesh.seat.intake.event.choice.telegram") {
t.Error("a watcher no longer hears the bench's events")
}
}
// novox/hq ADR 0259 §8: the machine's runtime runs as the operator's account; it never carries a module
// that says warrants or speaks for a kind proving its sender, and such a module has its own account.
func TestTheMachinesRuntimeNeverCarriesATrustedHolder(t *testing.T) {
tg := channelSeat("telegram")
tg.Capabilities = []string{"choice", "verified-sender"}
for name, d := range map[string]Declared{
"the router": {Module: "messenger", Holds: []Seat{operatorChannel()}},
"a verified channel": {Module: "telegram", Holds: []Seat{tg}},
} {
if _, err := PermissionsFor(Principal{Kind: KindNodeTools, Node: "anchor", Module: RuntimeModule,
Carries: []Declared{d}}); err == nil || !strings.Contains(err.Error(), "an account of its own") {
t.Errorf("%s was composed into the machine's runtime: %v", name, err)
}
}
desk := channelSeat("desktop")
desk.Capabilities = []string{"choice"}
if _, err := PermissionsFor(Principal{Kind: KindNodeTools, Node: "anchor", Module: RuntimeModule,
Carries: []Declared{{Module: "desk-channel", Holds: []Seat{desk}}}}); err != nil {
t.Errorf("a channel proving nothing was refused: %v", err)
}
users, err := Users(Records{Nodes: []string{"anchor"}, Assigned: map[string][]Declared{"anchor": {
{Module: RuntimeModule}, {Module: "telegram", Holds: []Seat{tg}, RunsAs: "telegram"},
{Module: "messenger", Holds: []Seat{operatorChannel()}, RunsAs: "messenger"},
}}})
if err != nil {
t.Fatal(err)
}
for _, u := range users {
if u.Kind == KindNodeTools {
for _, d := range u.Carries {
if d.RunsAs != "" {
t.Errorf("the machine's runtime carries %s", d.Module)
}
}
if _, err := PermissionsFor(u); err != nil {
t.Errorf("the runtime could not be composed: %v", err)
}
}
}
}
// novox/hq ADR 0259 §8: verified-sender reaches the router only from a holder of its own account, on a machine
// root-free when composed, with the router's own machine root-free too (the review of 2026-10-09, H3).
func TestVerifiedSenderIsBelievedOnlyFromAHolderOfItsOwnAccount(t *testing.T) {
if got := placedCapabilities([]string{"choice", "verified-sender"}, "", true); namesVerb(got, "verified-sender") {
t.Errorf("a carried holder keeps verified-sender: %v", got)
}
if got := placedCapabilities([]string{"choice", "verified-sender"}, "telegram", true); !namesVerb(got, "verified-sender") {
t.Errorf("a holder of its own account on a root-free machine lost verified-sender: %v", got)
}
if got := placedCapabilities([]string{"choice", "verified-sender"}, "telegram", false); namesVerb(got, "verified-sender") ||
!namesVerb(got, "choice") {
t.Errorf("a holder on a machine not root-free keeps verified-sender, or lost the rest: %v", got)
}
}
// The kinds the router is told carry verified-sender only while the channel's machine and the router's are
// both root-free as composed; no record of a pass is no pass, and neither is a router placed nowhere.
func TestVerifiedSenderNeedsTheChannelsAndTheRoutersMachinesRootFree(t *testing.T) {
tg := channelSeat("telegram")
tg.Capabilities = []string{"choice", "verified-sender"}
router := Declared{Module: "messenger", Holds: []Seat{operatorChannel()}, RunsAs: "messenger"}
telegram := Declared{Module: "telegram", Holds: []Seat{tg}, RunsAs: "telegram"}
verified := func(r Records) bool {
for _, k := range PlacementsOf(r, nil).Kinds {
if k.Kind == "telegram" {
return namesVerb(k.Capabilities, "verified-sender")
}
}
t.Fatal("telegram not placed")
return false
}
same := func(free map[string]bool) Records {
return Records{Nodes: []string{"anchor"}, Assigned: map[string][]Declared{"anchor": {router, telegram}}, RootFree: free}
}
apart := func(free map[string]bool) Records {
return Records{Nodes: []string{"anchor", "relay"},
Assigned: map[string][]Declared{"anchor": {router}, "relay": {telegram}}, RootFree: free}
}
if !verified(same(map[string]bool{"anchor": true})) {
t.Error("both on one root-free machine: verified-sender withheld")
}
if verified(same(nil)) {
t.Error("no record of a pass, and verified-sender kept")
}
if verified(apart(map[string]bool{"relay": true})) {
t.Error("the router's machine not root-free, and verified-sender kept")
}
if verified(apart(map[string]bool{"anchor": true})) {
t.Error("the channel's machine not root-free, and verified-sender kept")
}
if !verified(apart(map[string]bool{"anchor": true, "relay": true})) {
t.Error("both machines root-free: verified-sender withheld")
}
noRouter := Records{Nodes: []string{"relay"}, Assigned: map[string][]Declared{"relay": {telegram}},
RootFree: map[string]bool{"relay": true}}
if verified(noRouter) {
t.Error("no router placed, and verified-sender kept")
}
}
-11
View File
@@ -363,19 +363,8 @@ var ControllerFollows = []string{
// seat to check before it merges — every machine of the facts snapshot composed with the change. // seat to check before it merges — every machine of the facts snapshot composed with the change.
// Appended, because the index is a name. // Appended, because the index is a name.
moduleEventSubject("gitea", "pull.updated"), moduleEventSubject("gitea", "pull.updated"),
// **The operator's answers to what the controller asked** (novox/hq ADR 0259): the router's warrant, or
// the end of an ask without one, said to the controller alone under its own name. On the stream, so a
// controller that was away hears what was decided meanwhile. Appended, because the index is a name.
DecidedSubject,
} }
// AsksSeat is the seat an ask is made on and its warrant heard from (novox/hq ADR 0259): the router's.
const AsksSeat = "operator-channel"
// DecidedSubject is where the router says the controller's warrants: the seat's event named by the
// controller as its caller.
var DecidedSubject = seatEventSubject(AsksSeat, "decided."+ControllerSeat)
// The provider standing events, by their local names. Written here as well as in the catalogue // The provider standing events, by their local names. Written here as well as in the catalogue
// (catalogue.ProvisionerEvents), which this package cannot import; a test keeps them agreeing. // (catalogue.ProvisionerEvents), which this package cannot import; a test keeps them agreeing.
const ( const (
+3 -4
View File
@@ -24,8 +24,8 @@ accounts {
jetstream: enabled jetstream: enabled
users = [ users = [
{ user: "controller", password: "$2a$11$cccccccccccccccccccccc", permissions: { { user: "controller", password: "$2a$11$cccccccccccccccccccccc", permissions: {
publish: { allow: ["$JS.ACK.CONTROL.controller.>", "$JS.ACK.DEAD_LETTER_NOTICES.controller.>", "$JS.ACK.EVENTS.controller.>", "$JS.API.>", "$KV.SEAT_MESH_BUILD_MACHINE_cancelled.>", "$KV.SEAT_NODE_BUILD_AGENT_cancelled.>", "$KV.mesh-controller_asked.>", "$KV.mesh-controller_calls.>", "$KV.mesh-controller_condition-history.>", "$KV.mesh-controller_conditions.>", "$KV.mesh-controller_hand-acts.>", "$KV.mesh-controller_lease.>", "$SRV.INFO", "_INBOX.enrol.>", "mesh.again.>", "mesh.assignment.>", "mesh.events.dead.>", "mesh.mod.*.tool.>", "mesh.node.>", "mesh.seat.mesh-build-machine.accept.>", "mesh.seat.mesh-build-machine.tool.>", "mesh.seat.mesh-controller.event.applied", "mesh.seat.mesh-controller.event.built-before", "mesh.seat.mesh-controller.event.checked", "mesh.seat.mesh-controller.event.condition-changed", "mesh.seat.mesh-controller.event.condition-cleared", "mesh.seat.mesh-controller.event.condition-raised", "mesh.seat.mesh-controller.event.doctor-heartbeat", "mesh.seat.mesh-controller.event.healer-acted", "mesh.seat.mesh-controller.event.plan-moved", "mesh.seat.mesh-controller.event.refused", "mesh.seat.mesh-controller.event.rolled-back", "mesh.seat.mesh-controller.event.secret-replaced", "mesh.seat.mesh-controller.tool.plans", "mesh.seat.mesh-delivery.tool.close", "mesh.seat.mesh-delivery.tool.release", "mesh.seat.mesh-delivery.tool.stalled", "mesh.seat.mesh-delivery.tool.stop", "mesh.seat.node-backup.tool.backed-up.*", "mesh.seat.node-backup.tool.now.*", "mesh.seat.node-build-agent.accept.>", "mesh.seat.node-build-agent.tool.>", "mesh.seat.node-intrusion-prevention.tool.banned.*", "mesh.seat.node-launcher.tool.secret.*", "mesh.seat.node-service-manager.tool.restart.*"] } publish: { allow: ["$JS.ACK.CONTROL.controller.>", "$JS.ACK.DEAD_LETTER_NOTICES.controller.>", "$JS.ACK.EVENTS.controller.>", "$JS.API.>", "$KV.SEAT_MESH_BUILD_MACHINE_cancelled.>", "$KV.SEAT_NODE_BUILD_AGENT_cancelled.>", "$KV.mesh-controller_calls.>", "$KV.mesh-controller_condition-history.>", "$KV.mesh-controller_conditions.>", "$KV.mesh-controller_hand-acts.>", "$KV.mesh-controller_lease.>", "$SRV.INFO", "_INBOX.enrol.>", "mesh.again.>", "mesh.assignment.>", "mesh.events.dead.>", "mesh.mod.*.tool.>", "mesh.node.>", "mesh.seat.mesh-build-machine.accept.>", "mesh.seat.mesh-build-machine.tool.>", "mesh.seat.mesh-controller.event.applied", "mesh.seat.mesh-controller.event.built-before", "mesh.seat.mesh-controller.event.checked", "mesh.seat.mesh-controller.event.condition-changed", "mesh.seat.mesh-controller.event.condition-cleared", "mesh.seat.mesh-controller.event.condition-raised", "mesh.seat.mesh-controller.event.doctor-heartbeat", "mesh.seat.mesh-controller.event.healer-acted", "mesh.seat.mesh-controller.event.plan-moved", "mesh.seat.mesh-controller.event.refused", "mesh.seat.mesh-controller.event.rolled-back", "mesh.seat.mesh-controller.event.secret-replaced", "mesh.seat.mesh-delivery.tool.close", "mesh.seat.mesh-delivery.tool.stalled", "mesh.seat.node-backup.tool.backed-up.*", "mesh.seat.node-backup.tool.now.*", "mesh.seat.node-build-agent.accept.>", "mesh.seat.node-build-agent.tool.>", "mesh.seat.node-intrusion-prevention.tool.banned.*"] }
subscribe: { allow: ["$JS.API.>", "$JS.EVENT.ADVISORY.CONSUMER.DELETED.>", "$JS.EVENT.ADVISORY.CONSUMER.MAX_DELIVERIES.>", "$SRV.INFO", "$SRV.INFO.mesh-controller", "$SRV.INFO.mesh-controller.>", "$SRV.PING", "$SRV.PING.mesh-controller", "$SRV.PING.mesh-controller.>", "$SRV.STATS", "$SRV.STATS.mesh-controller", "$SRV.STATS.mesh-controller.>", "_DELIVER.controller", "_DELIVER.controller.>", "_INBOX.controller.>", "mesh.control.>", "mesh.mod.*.event.provisioner.failing", "mesh.mod.*.event.provisioner.recovered", "mesh.mod.*.event.provisioner.retirement", "mesh.mod.gitea.event.pull.merged", "mesh.mod.gitea.event.pull.updated", "mesh.mod.mesh-catalog.event.catching-up", "mesh.mod.mesh-catalog.event.upgraded", "mesh.seat.mesh-build-machine.event.built", "mesh.seat.mesh-controller.tool.>", "mesh.seat.node-build-agent.event.built", "mesh.seat.operator-channel.event.decided.mesh-controller"] } subscribe: { allow: ["$JS.API.>", "$JS.EVENT.ADVISORY.CONSUMER.DELETED.>", "$JS.EVENT.ADVISORY.CONSUMER.MAX_DELIVERIES.>", "$SRV.INFO", "$SRV.INFO.mesh-controller", "$SRV.INFO.mesh-controller.>", "$SRV.PING", "$SRV.PING.mesh-controller", "$SRV.PING.mesh-controller.>", "$SRV.STATS", "$SRV.STATS.mesh-controller", "$SRV.STATS.mesh-controller.>", "_DELIVER.controller", "_DELIVER.controller.>", "_INBOX.controller.>", "mesh.control.>", "mesh.mod.*.event.provisioner.failing", "mesh.mod.*.event.provisioner.recovered", "mesh.mod.*.event.provisioner.retirement", "mesh.mod.gitea.event.pull.merged", "mesh.mod.gitea.event.pull.updated", "mesh.mod.mesh-catalog.event.catching-up", "mesh.mod.mesh-catalog.event.upgraded", "mesh.seat.mesh-build-machine.event.built", "mesh.seat.mesh-controller.tool.>", "mesh.seat.node-build-agent.event.built"] }
allow_responses: { max: 1, ttl: "1m" } allow_responses: { max: 1, ttl: "1m" }
} } } }
{ user: "enrol.one", password: "$2a$11$eeeeeeeeeeeeeeeeeeeeee", permissions: { { user: "enrol.one", password: "$2a$11$eeeeeeeeeeeeeeeeeeeeee", permissions: {
@@ -34,8 +34,7 @@ accounts {
} } } }
{ user: "node.one", password: "$2a$11$nnnnnnnnnnnnnnnnnnnnnn", permissions: { { user: "node.one", password: "$2a$11$nnnnnnnnnnnnnnnnnnnnnn", permissions: {
publish: { allow: ["$JS.ACK.NODES.one.>", "$JS.API.CONSUMER.INFO.NODES.one", "$SRV.PING.node-tools.one", "mesh.control.one.>"] } publish: { allow: ["$JS.ACK.NODES.one.>", "$JS.API.CONSUMER.INFO.NODES.one", "$SRV.PING.node-tools.one", "mesh.control.one.>"] }
subscribe: { allow: ["_DELIVER.one", "_DELIVER.one.>", "_INBOX.node.one.>", "mesh.node.one.ask.hand-over", "mesh.node.one.ask.report", "mesh.node.one.ask.setuid-search", "mesh.node.one.declare"] } subscribe: { allow: ["_DELIVER.one", "_DELIVER.one.>", "_INBOX.node.one.>", "mesh.node.one.ask.report", "mesh.node.one.declare"] }
allow_responses: { max: 1, ttl: "1m" }
} } } }
{ user: "one.nats", password: "$2a$11$bbbbbbbbbbbbbbbbbbbbbb", permissions: { { user: "one.nats", password: "$2a$11$bbbbbbbbbbbbbbbbbbbbbb", permissions: {
publish: { allow: ["$JS.API.STREAM.INFO.*", "$JS.API.STREAM.NAMES", "$JS.API.STREAM.SNAPSHOT.*", "$JS.SNAPSHOT.ACK.>"] } publish: { allow: ["$JS.API.STREAM.INFO.*", "$JS.API.STREAM.NAMES", "$JS.API.STREAM.SNAPSHOT.*", "$JS.SNAPSHOT.ACK.>"] }
+5 -33
View File
@@ -50,9 +50,6 @@ type Declared struct {
// Checks are the module's own tools its health asks, each `<module>.<tool>` (novox/hq ADR 0240, to-be // Checks are the module's own tools its health asks, each `<module>.<tool>` (novox/hq ADR 0240, to-be
// 48 §3): the machine's node-engine asks them of its own node tools, and is granted that and no more. // 48 §3): the machine's node-engine asks them of its own node tools, and is granted that and no more.
Checks []string Checks []string
// RunsAs is the account the module runs as in a runtime of its own (novox/hq ADR 0259 §8): it is never
// carried by the machine's runtime, and reaches the bus on its own account.
RunsAs string
} }
// Records is what composing a user list needs to know about the mesh, and nothing more. // Records is what composing a user list needs to know about the mesh, and nothing more.
@@ -70,10 +67,6 @@ type Records struct {
// Interchangeable is each module whose definition says its instances are the same anywhere // Interchangeable is each module whose definition says its instances are the same anywhere
// (ADR 0160), which decides whether the module's plain subject is issued to every instance. // (ADR 0160), which decides whether the module's plain subject is issued to every instance.
Interchangeable map[string]bool Interchangeable map[string]bool
// RootFree is each machine judged root-free when this was composed (novox/hq ADR 0259 §8): it names an
// account agents run as, judged unable to become root by its node-engine, and serves no login shell
// execute. A machine absent is not free: no record of a pass is no pass.
RootFree map[string]bool
} }
// Users is every user the composed file should contain, in the order it will be written. // Users is every user the composed file should contain, in the order it will be written.
@@ -82,7 +75,7 @@ type Records struct {
// is a mesh that cannot be told anything, and there is no state of the records in which that is // is a mesh that cannot be told anything, and there is no state of the records in which that is
// correct. // correct.
func Users(r Records) ([]Principal, error) { func Users(r Records) ([]Principal, error) {
out := []Principal{{Kind: KindController, Uses: asksSeatOf(r)}} out := []Principal{{Kind: KindController}}
for _, node := range sortedCopy(r.Nodes) { for _, node := range sortedCopy(r.Nodes) {
witness := false witness := false
@@ -127,13 +120,10 @@ func Users(r Records) ([]Principal, error) {
}) })
} }
if runtimeHere { if runtimeHere {
var carried []Declared out = append(out, Principal{
for _, d := range r.Assigned[node] { Kind: KindNodeTools, Node: node, Module: RuntimeModule,
if d.RunsAs == "" { Carries: append([]Declared(nil), r.Assigned[node]...),
carried = append(carried, d) })
}
}
out = append(out, Principal{Kind: KindNodeTools, Node: node, Module: RuntimeModule, Carries: carried})
} }
} }
for _, node := range sortedCopy(r.Enrolling) { for _, node := range sortedCopy(r.Enrolling) {
@@ -199,21 +189,3 @@ func sortedNames(in map[string][]string) []string {
// controllerModule is the controller's module: the machine assigned it witnesses its upgrades. // controllerModule is the controller's module: the machine assigned it witnesses its upgrades.
const controllerModule = "mesh-controller" const controllerModule = "mesh-controller"
// asksSeatOf is the seat an ask is made on, as its holder declares it (novox/hq ADR 0259): the controller
// asks the operator through it like any other user, and is granted what its declaration names for a caller.
// None while nothing holds it.
func asksSeatOf(r Records) []Seat {
for _, node := range sortedCopy(r.Nodes) {
for _, d := range r.Assigned[node] {
for _, s := range d.Holds {
if s.Name == AsksSeat && namesVerb(s.ByCaller, "ask") {
seat := s
seat.Kind, seat.Capabilities = "", nil
return []Seat{seat}
}
}
}
}
return nil
}
-12
View File
@@ -84,18 +84,6 @@ func kvOf(bucket string) []string { return []string{"$KV." + bucket + ".>"} }
var WritersTable = []WriterRow{ var WritersTable = []WriterRow{
{State: "a machine's declaration", Writer: "controller (lease holder)", KeptIn: "the bus, last per subject", {State: "a machine's declaration", Writer: "controller (lease holder)", KeptIn: "the bus, last per subject",
Others: "read", Subjects: []string{"mesh.node.*.declare"}, Writes: isController}, Others: "read", Subjects: []string{"mesh.node.*.declare"}, Writes: isController},
// The operator's hand-over of a directory used as found, asked of the machine's engine at the controller's
// terminal (novox/hq issue 356). One publisher; and because a responder can still reach the subject through a
// reply, the ask is signed with the mesh's key and the engine verifies it — the row bounds who is granted the
// publish, the signature who is believed.
{State: "a hand-over asked of a machine", Writer: "controller, at its terminal", KeptIn: "the machine, beside its state",
Others: "the engine verifies the mesh's signature and records it, or refuses",
Subjects: []string{"mesh.node.*.ask.hand-over"}, Writes: isController},
// The operator asking a machine's engine for a fresh search for setuid programs, at the controller's
// terminal (novox/hq issue 361): signed as a hand-over is, under a signing context of its own.
{State: "a fresh setuid search asked of a machine", Writer: "controller, at its terminal",
KeptIn: "the machine, which throws its last search away", Others: "the engine verifies the mesh's signature and starts it, or refuses",
Subjects: []string{"mesh.node.*.ask.setuid-search"}, Writes: isController},
{State: "a machine's applied state and its report", Writer: "the node-engine's apply queue", {State: "a machine's applied state and its report", Writer: "the node-engine's apply queue",
KeptIn: "the machine; the report on the bus", Others: "the reconcile and a delivery enqueue, never apply", KeptIn: "the machine; the report on the bus", Others: "the reconcile and a delivery enqueue, never apply",
// And its health statement between reports (novox/hq ADR 0240): the same writer stating the same // And its health statement between reports (novox/hq ADR 0240): the same writer stating the same
-36
View File
@@ -15,8 +15,6 @@ import (
// to the table; one dropped from either fails. // to the table; one dropped from either fails.
var designRows = []string{ var designRows = []string{
"a machine's declaration", "a machine's declaration",
"a hand-over asked of a machine",
"a fresh setuid search asked of a machine",
"a machine's applied state and its report", "a machine's applied state and its report",
"the controller lease", "the controller lease",
"plans and their tiers", "plans and their tiers",
@@ -152,37 +150,3 @@ func TestSubjectsOverlap(t *testing.T) {
} }
} }
} }
// **A hand-over asked of a machine has one publisher, the controller** (novox/hq issue 356): a grant that lets any
// other principal publish it — a node, the node tools, a module — is refused at composition, naming the state.
// (Who the engine believes is the signature's; this bounds who is granted the publish.)
func TestAHandOverAskHasOnePublisher(t *testing.T) {
for _, p := range []Principal{
{Kind: KindNode, Node: "laptop"},
{Kind: KindNodeTools, Node: "laptop", Module: RuntimeModule},
{Kind: KindModule, Node: "laptop", Module: "notes"},
} {
err := CheckWriters(p, []string{"mesh.node.laptop.ask.hand-over"})
if err == nil || !strings.Contains(err.Error(), "a hand-over asked of a machine") {
t.Errorf("%s may publish a hand-over: %v", p.Username(), err)
}
}
if err := CheckWriters(Principal{Kind: KindController}, []string{"mesh.node.>"}); err != nil {
t.Fatalf("the controller may not ask a hand-over: %v", err)
}
}
// **A fresh setuid search asked of a machine has one publisher, the controller** (novox/hq issue 361), as a
// hand-over has.
func TestASetuidSearchAskHasOnePublisher(t *testing.T) {
for _, p := range []Principal{
{Kind: KindNode, Node: "laptop"},
{Kind: KindNodeTools, Node: "laptop", Module: RuntimeModule},
{Kind: KindModule, Node: "laptop", Module: "notes"},
} {
err := CheckWriters(p, []string{AskSetuidSearchSubject("laptop")})
if err == nil || !strings.Contains(err.Error(), "a fresh setuid search asked of a machine") {
t.Errorf("%s may ask a setuid search: %v", p.Username(), err)
}
}
}
-202
View File
@@ -1,202 +0,0 @@
package builder
import (
"context"
"os"
"path/filepath"
"slices"
"strings"
"testing"
)
// What a build says it was made from, as files (novox/hq ADR 0267), and what a build compiling a Go
// program is handed.
// onTrunk answers the trunk's questions as a clone of a commit on main would, or off it.
type onTrunk struct {
*recorded
off bool
// behind is a commit on the trunk that is not its head: an older commit built by hand.
behind bool
}
func (o onTrunk) run(ctx context.Context, dir, name string, args ...string) (string, error) {
if name == "git" && len(args) > 0 && args[0] == "symbolic-ref" {
return "origin/main\n", nil
}
if name == "git" && len(args) > 1 && args[0] == "rev-parse" && args[1] == "origin/main" && o.behind {
return "feedfacefeedfacefeedfacefeedfacefeedface\n", nil
}
if name == "git" && len(args) > 0 && args[0] == "merge-base" && o.off {
return "", os.ErrNotExist
}
return compiling{o.recorded}.run(ctx, dir, name, args...)
}
// aSharedRepository is a repository holding two programs that share a package, as the controller's does.
func aSharedRepository(readme, shared string) map[string]string {
return map[string]string{
"go.mod": "module example.com/ctl\n\ngo 1.22\n",
"go.sum": "",
"README.md": readme,
"cmd/ctl/main.go": "package main\n\nimport _ \"example.com/ctl/internal/shared\"\n\nfunc main() {}\n",
"proxy/main.go": "package main\n\nimport _ \"example.com/ctl/internal/shared\"\n\nfunc main() {}\n",
"internal/shared/s.go": "package shared\n\nconst S = " + shared + "\n",
"internal/only/o.go": "package only\n",
}
}
const aProxy = `{"module":"route-proxy","version":"1",
"build":{"artifacts":[
{"name":"server","kind":"image","from":"Dockerfile","compiles":"proxy",
"context":{"repository":"https://forge.invalid/ctl.git","ref":"main"}},
{"name":"trust","kind":"upstream","from":"alpine@sha256:` + "3333333333333333333333333333333333333333333333333333333333333333" + `"}]}}`
func buildTheProxy(t *testing.T, context_ map[string]string, off bool, behind ...bool) (Result, *recorded, string) {
t.Helper()
r := &recorded{
contents: map[string]string{"modules/route-proxy/" + ManifestName: aProxy, "modules/route-proxy/Dockerfile": "FROM scratch\nCOPY . .\n", "modules/route-proxy/README.md": "x"},
secondary: map[string]map[string]string{"https://forge.invalid/ctl.git": context_},
}
workspace := t.TempDir()
got, err := Build(context.Background(), onTrunk{r, off, len(behind) > 0 && behind[0]}.run, r,
"https://forge.invalid/catalogue.git", "modules/route-proxy", "", workspace, nil, Npmrc{}, GitCredential{}, nil)
if err != nil {
t.Fatal(err)
}
return got, r, workspace
}
func TestAnImageCompilingGoIsHandedItsBuildSourceAndSaysIt(t *testing.T) {
got, r, workspace := buildTheProxy(t, aSharedRepository("one", "1"), false)
// Built in the narrowed tree, which holds the program's closure and nothing else.
at := ""
for i, line := range r.ran {
if strings.HasPrefix(line, "docker build ") {
at = r.dirs[i]
}
}
if filepath.Base(at) != "narrow-server" {
t.Fatalf("docker build ran in %q, not the narrowed build source", at)
}
for file, want := range map[string]bool{"proxy/main.go": true, "internal/shared/s.go": true, "go.mod": true,
"cmd/ctl/main.go": false, "internal/only/o.go": false, "README.md": false} {
_, err := os.Stat(filepath.Join(workspace, "narrow-server", filepath.FromSlash(file)))
if (err == nil) != want {
t.Errorf("%s handed to the recipe: %v, wanted %v", file, err == nil, want)
}
}
// Said per repository: its own, the manifest and the recipe; the context's, the closure.
if len(got.Sources) != 2 {
t.Fatalf("sources %+v", got.Sources)
}
own, ctx := got.Sources[0], got.Sources[1]
if own.Repository != "" || !slices.Equal(own.Paths, []string{"modules/route-proxy/Dockerfile", "modules/route-proxy/module.json"}) {
t.Errorf("its own build source: %+v", own)
}
if ctx.Repository != "https://forge.invalid/ctl.git" || ctx.Ref != "main" {
t.Errorf("the context's build source names %q at %q", ctx.Repository, ctx.Ref)
}
for file, want := range map[string]bool{"proxy/main.go": true, "internal/shared/s.go": true, "go.mod": true,
"cmd/ctl/main.go": false, "README.md": false} {
if SourceHolds(ctx.Paths, file) != want {
t.Errorf("the context's build source holds %s: %v, wanted %v (%v)", file, !want, want, ctx.Paths)
}
}
// **The fingerprint is over the build source** (rule 5): a change outside it is one build, inside it another.
readme, _, _ := buildTheProxy(t, aSharedRepository("two", "1"), false)
if readme.Source != got.Source {
t.Errorf("a README of the context changed the fingerprint: %s %s", got.Source, readme.Source)
}
shared, _, _ := buildTheProxy(t, aSharedRepository("one", "2"), false)
if shared.Source == got.Source {
t.Error("a change to the program's closure kept its fingerprint")
}
}
// A build off the trunk, or of a trunk commit that is not its head, says no build source: the planner maps
// a merge onto the trunk head's, and an older commit's closure lacks what was imported since.
func TestABuildOffTheTrunksHeadSaysNoBuildSource(t *testing.T) {
got, _, _ := buildTheProxy(t, aSharedRepository("one", "1"), true)
if len(got.Sources) != 0 {
t.Fatalf("a build off the trunk said %+v", got.Sources)
}
got, _, _ = buildTheProxy(t, aSharedRepository("one", "1"), false, true)
if len(got.Sources) != 0 {
t.Fatalf("a build of an older trunk commit said %+v", got.Sources)
}
}
// An archive of the module's whole directory holds every file of it.
func TestAnArchiveOfTheWholeDirectoryHoldsIt(t *testing.T) {
manifest := `{"module":"look","version":"1","build":{"artifacts":[{"name":"all","kind":"archive","from":"."}]},
"resources":[{"id":"files","type":"archive","path":"/opt/look","artifact":"all"}]}`
r := &recorded{contents: map[string]string{"modules/look/" + ManifestName: manifest, "modules/look/a/b.css": "x"}}
got, err := Build(context.Background(), onTrunk{recorded: r}.run, r,
"https://forge.invalid/catalogue.git", "modules/look", "", t.TempDir(), nil, Npmrc{}, GitCredential{}, nil)
if err != nil {
t.Fatal(err)
}
if len(got.Sources) != 1 || !SourceHolds(got.Sources[0].Paths, "modules/look/a/b.css") ||
SourceHolds(got.Sources[0].Paths, "modules/other/x") {
t.Fatalf("sources %+v", got.Sources)
}
}
// A recipe reading past its build source fails, naming what it could not find — in the real docker build;
// here, the file is simply not in the tree it is handed, which is what makes that so.
func TestAnImageCompilingANonexistentPackageFails(t *testing.T) {
r := &recorded{
contents: map[string]string{ManifestName: strings.Replace(aProxy, `"compiles":"proxy"`, `"compiles":"nowhere"`, 1),
"Dockerfile": "FROM scratch\n"},
secondary: map[string]map[string]string{"https://forge.invalid/ctl.git": aSharedRepository("one", "1")},
}
_, err := Build(context.Background(), onTrunk{recorded: r}.run, r,
"https://forge.invalid/catalogue.git", "", "", t.TempDir(), nil, Npmrc{}, GitCredential{}, nil)
if err == nil || !strings.Contains(err.Error(), "nowhere") {
t.Fatalf("a package that is not there built: %v", err)
}
}
// A Go bundle of a module built from its repository's root says its import closure, and the manifest;
// an image that compiles nothing it was told of leaves the module's source whole, and says none.
func TestAGoBundleSaysItsClosureAndAnUntoldImageNothing(t *testing.T) {
files := aSharedRepository("one", "1")
manifest := `{"module":"ctl","version":"1","build":{"artifacts":[
{"name":"controller","kind":"bundle","language":"go","system":"arch","from":"cmd/ctl","binary":"ctl"}]},
"resources":[{"id":"controller","type":"process","name":"ctl","artifact":"controller","run":["./ctl"]}]}`
r := &recorded{contents: map[string]string{ManifestName: manifest}}
for k, v := range files {
r.contents[k] = v
}
held := map[string]string{"mesh-tools-go/build": "registry.invalid/mesh-tools-go/build@sha256:" + strings.Repeat("b", 64)}
got, err := Build(context.Background(), onTrunk{recorded: r}.run, r,
"https://forge.invalid/ctl.git", "", "", t.TempDir(), held, Npmrc{}, GitCredential{}, nil)
if err != nil {
t.Fatal(err)
}
if len(got.Sources) != 1 || got.Sources[0].Repository != "" {
t.Fatalf("sources %+v", got.Sources)
}
for file, want := range map[string]bool{"cmd/ctl/main.go": true, "internal/shared/s.go": true, ManifestName: true,
"go.sum": true, "proxy/main.go": false, "README.md": false, "internal/only/o.go": false} {
if SourceHolds(got.Sources[0].Paths, file) != want {
t.Errorf("%s: held %v, wanted %v (%v)", file, !want, want, got.Sources[0].Paths)
}
}
untold := `{"module":"ctl","version":"1","build":{"artifacts":[
{"name":"server","kind":"image","from":"Dockerfile"}]}}`
r = &recorded{contents: map[string]string{ManifestName: untold, "Dockerfile": "FROM scratch\n"}}
got, err = Build(context.Background(), onTrunk{recorded: r}.run, r,
"https://forge.invalid/ctl.git", "", "", t.TempDir(), nil, Npmrc{}, GitCredential{}, nil)
if err != nil {
t.Fatal(err)
}
if len(got.Sources) != 0 {
t.Fatalf("an image compiling nothing it was told of said a build source: %+v", got.Sources)
}
}
+5 -113
View File
@@ -11,7 +11,6 @@ import (
"io" "io"
"os" "os"
"os/exec" "os/exec"
"path"
"path/filepath" "path/filepath"
"regexp" "regexp"
"sort" "sort"
@@ -88,23 +87,6 @@ type Result struct {
// pin the build. Two builds with one fingerprint are one build, whatever digests they made // pin the build. Two builds with one fingerprint are one build, whatever digests they made
// (novox/hq issue 280). // (novox/hq issue 280).
Source string Source string
// Sources are what this build was made from, as files (novox/hq ADR 0267 rule 1): per repository, the
// entries a changed file is tested against (SourceHolds). The module's own repository has an empty
// Repository. Said only for a build of a commit on the trunk, and only for a repository whose every
// artifact's build source is known — a Go program's import closure, an archive's directory, an image's
// recipe and the package it compiles; for any other, nothing is said and the whole of what the build
// sees stays its source, as before.
Sources []BuildSource
}
// BuildSource is the build source a build read in one repository (novox/hq ADR 0267).
type BuildSource struct {
// Repository and Ref are a context's, as the manifest names it; empty for the module's own.
Repository string
Ref string
// Paths are the entries, relative to the repository's root (SourceHolds).
Paths []string
} }
// GitCredential is the forge credential a clone may present when the server asks for one. // GitCredential is the forge credential a clone may present when the server asks for one.
@@ -224,10 +206,6 @@ func build(ctx context.Context, run Runner, publish Publisher,
// What it is made from, for its source fingerprint: the module's own tree first. // What it is made from, for its source fingerprint: the module's own tree first.
src := newSourceInputs(manifest.Module) src := newSourceInputs(manifest.Module)
src.prefix = strings.Trim(filepath.ToSlash(filepath.Clean(path)), "/")
if src.prefix == "." {
src.prefix = ""
}
if src.tree, err = gitTree(ctx, run, tree, path); err != nil { if src.tree, err = gitTree(ctx, run, tree, path); err != nil {
src.notPinned("its tree could not be named: " + err.Error()) src.notPinned("its tree could not be named: " + err.Error())
} }
@@ -320,23 +298,9 @@ func build(ctx context.Context, run Runner, publish Publisher,
if fingerprint == "" { if fingerprint == "" {
say("source", "no source fingerprint: %s", orNoTree(src.unpinned)) say("source", "no source fingerprint: %s", orNoTree(src.unpinned))
} }
// **Only a build of the trunk's head says its build source** (novox/hq ADR 0267): the planner maps the
// next merge onto the build source of the newest build, and a branch's closure — or an older trunk
// commit's, built by hand — is not the trunk's. Nor does a build whose context was not its trunk's head.
var sources []BuildSource
if trunk != "" && onTrunk && src.contextsAtHead && atTrunkHead(ctx, run, tree, commit, trunk) {
sources = src.buildSources()
for _, s := range sources {
where := "its own repository"
if s.Repository != "" {
where = s.Repository
}
say("source", "%d path(s) of %s", len(s.Paths), where)
}
}
return Result{Manifest: resolved, Commit: commit, Built: built, return Result{Manifest: resolved, Commit: commit, Built: built,
Against: against(within, manifest, stoodOn), Read: readBy(manifest), Source: fingerprint, Against: against(within, manifest, stoodOn), Read: readBy(manifest), Source: fingerprint,
Trunk: trunk, OnTrunk: onTrunk, Branches: branches, Sources: sources}, nil Trunk: trunk, OnTrunk: onTrunk, Branches: branches}, nil
} }
// branchesHolding is every branch of a fresh clone's origin the commit is on, without `origin/`. // branchesHolding is every branch of a fresh clone's origin the commit is on, without `origin/`.
@@ -381,28 +345,6 @@ func trunkOf(ctx context.Context, run Runner, clone, commit string) (string, boo
return trunk, err == nil return trunk, err == nil
} }
// atTrunkHead is whether a clone's commit is its trunk's head as the clone holds it.
func atTrunkHead(ctx context.Context, run Runner, clone, commit, trunk string) bool {
head, err := run(ctx, clone, "git", "rev-parse", "origin/"+trunk)
if err != nil {
return false
}
at, err := run(ctx, clone, "git", "rev-parse", commit)
if err != nil {
return false
}
return strings.TrimSpace(head) != "" && strings.TrimSpace(head) == strings.TrimSpace(at)
}
// cleanEntry is a path of the module's directory as an entry: cleaned, relative, `.` for the directory.
func cleanEntry(p string) string {
c := strings.Trim(path.Clean("/"+filepath.ToSlash(p)), "/")
if c == "" {
return "."
}
return c
}
// orNoTree is why a build has no source fingerprint, for its log. // orNoTree is why a build has no source fingerprint, for its log.
func orNoTree(why string) string { func orNoTree(why string) string {
if why == "" { if why == "" {
@@ -706,51 +648,15 @@ func one(ctx context.Context, run Runner, publish Publisher,
} else if src != nil { } else if src != nil {
src.contexts[a.Name] = t src.contexts[a.Name] = t
} }
buildDir = cloned // docker build accepts -f outside the context it is given; the recipe stays exactly
if t, _ := trunkOf(ctx, run, cloned, "HEAD"); t == "" || !atTrunkHead(ctx, run, cloned, "HEAD", t) { // where it was read from and validated against, absolute so the working directory
src.contextOffHead() // switching to the cloned context does not change which file that is.
}
}
// docker build accepts -f outside the context it is given; the recipe stays exactly where it was
// read from and validated against, absolute so a context elsewhere does not change which file
// that is.
if buildDir != tree || a.Compiles != "" {
absRecipe, err := filepath.Abs(filepath.Join(tree, a.From)) absRecipe, err := filepath.Abs(filepath.Join(tree, a.From))
if err != nil { if err != nil {
return catalogue.Built{}, fmt.Errorf("%s: %s's recipe: %w", module, a.Name, err) return catalogue.Built{}, fmt.Errorf("%s: %s's recipe: %w", module, a.Name, err)
} }
recipePath = absRecipe recipePath = absRecipe
} buildDir = cloned
// **An image that compiles a Go program is handed its build source and nothing else** (novox/hq
// ADR 0267 rules 1 and 3): the program's import closure, read from the context it is built in, so a
// merge elsewhere in that repository is no change to it — and a recipe that copies a file outside
// it fails here, naming the file, rather than building from something no merge is mapped onto.
if a.Compiles != "" {
paths, err := GoBuildSource(buildDir, a.Compiles)
if err != nil {
return catalogue.Built{}, fmt.Errorf("%s: %s compiles %s, whose build source cannot be read: %w",
module, a.Name, a.Compiles, err)
}
narrowed := filepath.Join(workspace, "narrow-"+a.Name)
sum, err := narrowTree(buildDir, narrowed, paths)
if err != nil {
return catalogue.Built{}, fmt.Errorf("%s: handing %s its build source: %w", module, a.Name, err)
}
say("image", "%s is handed its build source: %d path(s) of %s", a.Name, len(paths), a.Compiles)
if a.Context != nil {
src.contexts[a.Name] = sum
src.readIn(*a.Context, paths)
} else {
src.ownHas(paths...)
}
buildDir = narrowed
} else if a.Context != nil {
src.readWhole(*a.Context)
}
if a.Compiles != "" || a.Context != nil {
src.ownHas(cleanEntry(a.From))
} else {
src.ownWhole()
} }
invocation := append([]string{"build", "-f", recipePath, "-t", local}, args...) invocation := append([]string{"build", "-f", recipePath, "-t", local}, args...)
if a.Target != "" { if a.Target != "" {
@@ -802,18 +708,6 @@ func one(ctx context.Context, run Runner, publish Publisher,
if src != nil { if src != nil {
src.toolchains[a.Name] = toolchainOf(chain, base) src.toolchains[a.Name] = toolchainOf(chain, base)
} }
// A Go program's build source is its import closure (novox/hq ADR 0267 rule 1). Not read, it is the
// module's whole directory, as before — said, so the wider plan has a reason a person can find.
if chain.Language == "go" {
if paths, err := GoBuildSource(tree, a.From); err != nil {
say("bundle", "%s's build source is its whole directory: %v", a.Name, err)
src.ownWhole()
} else {
src.ownHas(paths...)
}
} else {
src.ownWhole()
}
if chain.Language == "typescript" { if chain.Language == "typescript" {
if own, _ := ownDependencies(tree); len(own) > 0 { if own, _ := ownDependencies(tree); len(own) > 0 {
src.notPinned(a.Name + " resolves packages of its own at build time") src.notPinned(a.Name + " resolves packages of its own at build time")
@@ -860,7 +754,6 @@ func one(ctx context.Context, run Runner, publish Publisher,
// there is no Publisher call — the container itself publishes, with the credential the // there is no Publisher call — the container itself publishes, with the credential the
// build was handed. // build was handed.
say("package", "building and publishing %s (%s)", a.Name, a.Language) say("package", "building and publishing %s (%s)", a.Name, a.Language)
src.ownWhole()
src.notPinned(a.Name + " is a package, built from what the registry holds when it is built") src.notPinned(a.Name + " is a package, built from what the registry holds when it is built")
reference, err := publishPackage(ctx, run, module, tree, a, npmrc, say) reference, err := publishPackage(ctx, run, module, tree, a, npmrc, say)
if err != nil { if err != nil {
@@ -870,7 +763,6 @@ func one(ctx context.Context, run Runner, publish Publisher,
return catalogue.Built{Name: a.Name, Kind: a.Kind, Reference: reference}, nil return catalogue.Built{Name: a.Name, Kind: a.Kind, Reference: reference}, nil
case catalogue.ArtifactArchive: case catalogue.ArtifactArchive:
src.ownHas(cleanEntry(a.From) + "/**")
body, err := pack(filepath.Join(tree, a.From)) body, err := pack(filepath.Join(tree, a.From))
if err != nil { if err != nil {
return catalogue.Built{}, fmt.Errorf("%s: packing %s failed: %w", module, a.Name, err) return catalogue.Built{}, fmt.Errorf("%s: packing %s failed: %w", module, a.Name, err)
+1 -6
View File
@@ -543,11 +543,6 @@ func passedSoFar(ran []string) string {
return strings.Join(ran, ", ") + " passed; " return strings.Join(ran, ", ") + " passed; "
} }
// SomeManifestsEnv tells the judge's module check that the manifests it is given are only those a change touches,
// so a seat another module of the repository declares is a note there, not a refusal (novox/hq issue 364). A judge
// that predates it reads nothing of it and refuses as before.
const SomeManifestsEnv = "MESH_MODULE_CHECK_SOME=1"
// gateLayer runs the gate: the touched manifests through `module check`, every machine composed with the // gateLayer runs the gate: the touched manifests through `module check`, every machine composed with the
// change, and the replays of what the mesh runs. It answers the gate's verdict and summary. // change, and the replays of what the mesh runs. It answers the gate's verdict and summary.
func gateLayer(ctx context.Context, spec CheckSpec, tree, root, gate, verdictFile string, env []string, func gateLayer(ctx context.Context, spec CheckSpec, tree, root, gate, verdictFile string, env []string,
@@ -567,7 +562,7 @@ func gateLayer(ctx context.Context, spec CheckSpec, tree, root, gate, verdictFil
checked := func(dir string) (string, error) { checked := func(dir string) (string, error) {
var own tail var own tail
cmd := exec.CommandContext(ctx, "docker", LabelledArgs("docker", cmd := exec.CommandContext(ctx, "docker", LabelledArgs("docker",
inToolchain(dir, append(append([]string{}, env...), SomeManifestsEnv), append([]string{gate, "module", "check"}, manifests...)...), spec.ID)...) inToolchain(dir, env, append([]string{gate, "module", "check"}, manifests...)...), spec.ID)...)
inItsOwnGroup(cmd) inItsOwnGroup(cmd)
w := io.MultiWriter(out, &own) w := io.MultiWriter(out, &own)
cmd.Stdout, cmd.Stderr = w, w cmd.Stdout, cmd.Stderr = w, w
-528
View File
@@ -1,528 +0,0 @@
package builder
import (
"bufio"
"crypto/sha256"
"encoding/hex"
"errors"
"fmt"
"go/parser"
"go/token"
"io"
"io/fs"
"os"
"path"
"path/filepath"
"sort"
"strconv"
"strings"
)
// A Go program's build source (novox/hq ADR 0267 rule 1): the files it is built from, derived from its
// import closure rather than listed by hand, because a list drifts from the imports it describes and a
// path missing from it is a real change missed — worse than a needless rebuild.
//
// **The closure read here is never narrower than `go list -deps`.** Every .go file of a package that is
// not a test is read, whatever its build constraint, so the imports are the union over every system and
// tag; a directory is held whole (but for its tests), so a file added to a package is in it; an embed is
// held by the directory its pattern starts in, everything below it. Read with the standard library's
// parser and no toolchain: the build machine carries none, and a closure that needed the network to
// read would be one a build could not say offline.
//
// A build source is a list of entries, relative to the root the build sees:
//
// dir/ a Go package's directory: every file directly in it but its tests (`*_test.go`)
// dir/** everything below a directory (an embed)
// ** the whole tree
// file one file
//
// The root package's directory is `./`.
// GoPackageDirEntry is the entry for a Go package's directory.
func goPackageDirEntry(dir string) string {
if dir == "" || dir == "." {
return "./"
}
return dir + "/"
}
// SourceHolds is whether a changed file — a path relative to the root the build source was read in — is
// in that build source.
func SourceHolds(entries []string, file string) bool {
file = strings.TrimPrefix(path.Clean("/"+strings.TrimSpace(file)), "/")
for _, e := range entries {
switch {
case e == "**":
return true
case strings.HasSuffix(e, "/**"):
dir := strings.TrimSuffix(e, "/**")
if dir == "." || dir == "" || file == dir || strings.HasPrefix(file, dir+"/") {
return true
}
case strings.HasSuffix(e, "/"):
dir := strings.TrimSuffix(e, "/")
parent := path.Dir(file)
if (dir == "." && parent == ".") || parent == dir {
if !strings.HasSuffix(file, "_test.go") {
return true
}
}
case e == file:
return true
}
}
return false
}
// GoBuildSource is the build source of the Go program whose main package is pkg, a directory relative to
// root: the directories of every package of its import closure inside root, the embeds those packages
// name, its module's go.mod, go.sum and vendor/modules.txt, and a go.work wherever one would be read. An
// entry for a file that does not exist is kept: creating it is a change to the build.
//
// Refused — so the build source is not narrowed, and nothing is missed — when the closure cannot be told
// from the files: no go.mod holds the package, a go.work is present, a local replace leaves root, a file
// does not parse, or a cgo preamble reaches outside its directory.
func GoBuildSource(root, pkg string) ([]string, error) {
root, err := filepath.Abs(root)
if err != nil {
return nil, err
}
rel := path.Clean(strings.TrimPrefix(filepath.ToSlash(strings.TrimSpace(pkg)), "/"))
if rel == ".." || strings.HasPrefix(rel, "../") {
return nil, fmt.Errorf("the package %q leaves the tree it is built from", pkg)
}
if info, err := os.Stat(filepath.Join(root, filepath.FromSlash(rel))); err != nil || !info.IsDir() {
return nil, fmt.Errorf("%q is not a directory of the tree it is built from", pkg)
}
// The module holding the package: the nearest go.mod at or above it, within root.
modRoot := ""
for dir := rel; ; dir = path.Dir(dir) {
if _, err := os.Stat(filepath.Join(root, filepath.FromSlash(dir), "go.mod")); err == nil {
modRoot = dir
break
}
if dir == "." {
break
}
}
if modRoot == "" {
return nil, fmt.Errorf("no go.mod holds %q within the tree it is built from", pkg)
}
entries := map[string]bool{}
file := func(dir, name string) {
entries[strings.TrimPrefix(path.Join(dir, name), "./")] = true
}
file(modRoot, "go.mod")
file(modRoot, "go.sum")
file(modRoot, "vendor/modules.txt")
// A workspace changes how every import resolves; one present is not read past, one created later is a
// change to the build.
for dir := modRoot; ; dir = path.Dir(dir) {
file(dir, "go.work")
file(dir, "go.work.sum")
if _, err := os.Stat(filepath.Join(root, filepath.FromSlash(dir), "go.work")); err == nil {
return nil, fmt.Errorf("%s holds a go.work, and a workspace's imports are not read here", path.Join(dir, "go.work"))
}
if dir == "." {
break
}
}
modPath, replaces, err := readGoMod(filepath.Join(root, filepath.FromSlash(modRoot), "go.mod"))
if err != nil {
return nil, err
}
vendored := false
if _, err := os.Stat(filepath.Join(root, filepath.FromSlash(modRoot), "vendor", "modules.txt")); err == nil {
vendored = true
}
// resolve is the directories, relative to root, an import may be read from: none for the standard
// library and for a module outside the tree, which go.mod and go.sum pin. A vendored module replaced
// by a local directory is both — vendor/ under -mod=vendor, the directory under -mod=mod — and both
// are held, so neither way of building it is missed.
resolve := func(importPath string) ([]string, error) {
within := func(prefix, dir string) (string, bool) {
if importPath == prefix {
return dir, true
}
if rest, ok := strings.CutPrefix(importPath, prefix+"/"); ok {
return path.Join(dir, rest), true
}
return "", false
}
if dir, ok := within(modPath, modRoot); ok {
return []string{dir}, nil
}
var dirs []string
for _, r := range replaces {
if sub, ok := within(r.from, ""); ok {
target := path.Clean(path.Join(modRoot, r.to))
if target == ".." || strings.HasPrefix(target, "../") {
return nil, fmt.Errorf("go.mod replaces %s with %s, outside the tree it is built from", r.from, r.to)
}
dirs = append(dirs, path.Join(target, sub))
// Its go.mod states what it requires, read when it is built from there.
entries[path.Join(target, "go.mod")] = true
break
}
}
first, _, _ := strings.Cut(importPath, "/")
if len(dirs) == 0 && !strings.Contains(first, ".") {
return nil, nil // the standard library
}
if vendored {
dirs = append(dirs, path.Join(modRoot, "vendor", importPath))
}
return dirs, nil
}
seen := map[string]bool{}
queue := []string{rel}
for len(queue) > 0 {
dir := queue[0]
queue = queue[1:]
if seen[dir] {
continue
}
seen[dir] = true
entries[goPackageDirEntry(dir)] = true
// A go.mod made between the module's root and a package moves that package out of the module.
for up := dir; up != modRoot && up != "." && up != "/" && !strings.HasPrefix(up, "../"); up = path.Dir(up) {
file(up, "go.mod")
}
listing, err := os.ReadDir(filepath.Join(root, filepath.FromSlash(dir)))
if err != nil {
// A package that is not there fails the build that imports it; its directory is held, so
// adding it is a change.
continue
}
for _, f := range listing {
name := f.Name()
// **C and assembly beside Go** may include files from below the package's directory: the
// directory is held whole, and an include reaching above it is refused.
if !f.IsDir() && nativeSource(name) {
entries[strings.TrimPrefix(dir+"/**", "./")] = true
if dir == "." {
entries["**"] = true
}
if err := includesStayWithin(filepath.Join(root, filepath.FromSlash(dir), name)); err != nil {
return nil, fmt.Errorf("%s: %w", path.Join(dir, name), err)
}
continue
}
if f.IsDir() || !strings.HasSuffix(name, ".go") || strings.HasSuffix(name, "_test.go") {
continue
}
full := filepath.Join(root, filepath.FromSlash(dir), name)
imports, embeds, err := goFileReads(full)
if err != nil {
return nil, fmt.Errorf("%s: %w", path.Join(dir, name), err)
}
for _, ip := range imports {
targets, err := resolve(ip)
if err != nil {
return nil, err
}
for _, target := range targets {
if !seen[target] {
queue = append(queue, target)
}
}
}
for _, e := range embeds {
entries[path.Join(dir, e)+"/**"] = true
if !strings.ContainsAny(e, "*?[\\") {
entries[path.Join(dir, e)] = true
}
}
}
}
out := make([]string, 0, len(entries))
for e := range entries {
out = append(out, e)
}
sort.Strings(out)
return out, nil
}
// goReplace is one local replacement in go.mod: an import path read from a directory.
type goReplace struct{ from, to string }
// readGoMod is a go.mod's module path and its replacements by a local directory. A replacement by another
// module version is resolved by go.sum, which the build source holds.
func readGoMod(file string) (string, []goReplace, error) {
f, err := os.Open(file)
if err != nil {
return "", nil, err
}
defer f.Close()
var module string
var replaces []goReplace
inReplace := false
scanner := bufio.NewScanner(f)
for scanner.Scan() {
line := scanner.Text()
if i := strings.Index(line, "//"); i >= 0 {
line = line[:i]
}
line = strings.TrimSpace(line)
switch {
case line == "":
continue
case inReplace && line == ")":
inReplace = false
continue
case strings.HasPrefix(line, "module "):
module = unquoteGoMod(strings.TrimSpace(strings.TrimPrefix(line, "module")))
continue
case line == "replace (":
inReplace = true
continue
case strings.HasPrefix(line, "replace "):
line = strings.TrimSpace(strings.TrimPrefix(line, "replace"))
case !inReplace:
continue
}
left, right, found := strings.Cut(line, "=>")
if !found {
continue
}
from := strings.Fields(left)
to := strings.Fields(right)
if len(from) == 0 || len(to) == 0 {
continue
}
target := unquoteGoMod(to[0])
// A local replacement is a path: ./, ../ or absolute. Anything else names a module version.
if strings.HasPrefix(target, "./") || strings.HasPrefix(target, "../") || target == "." || target == ".." {
replaces = append(replaces, goReplace{from: unquoteGoMod(from[0]), to: target})
} else if strings.HasPrefix(target, "/") {
return "", nil, fmt.Errorf("go.mod replaces %s with %s, outside the tree it is built from", from[0], target)
}
}
if err := scanner.Err(); err != nil {
return "", nil, err
}
if module == "" {
return "", nil, fmt.Errorf("%s names no module", file)
}
// The longest replacement first, so a replaced sub-path wins over its parent.
sort.SliceStable(replaces, func(i, j int) bool { return len(replaces[i].from) > len(replaces[j].from) })
return module, replaces, nil
}
func unquoteGoMod(s string) string {
if u, err := strconv.Unquote(s); err == nil {
return u
}
return s
}
// goFileReads is what one Go file makes its build read: the packages it imports, and the directories its
// //go:embed patterns start in, relative to its own directory ("." for the directory itself).
//
// **A cgo preamble reaching outside its directory is refused**: a header included by a relative path, or
// a flag naming ${SRCDIR}/.., is a file of the build no import names. Inside the directory it is held
// already.
func goFileReads(file string) (imports, embeds []string, err error) {
src, err := os.ReadFile(file)
if err != nil {
return nil, nil, err
}
fset := token.NewFileSet()
parsed, err := parser.ParseFile(fset, file, src, parser.ImportsOnly|parser.ParseComments)
if err != nil {
return nil, nil, err
}
for _, spec := range parsed.Imports {
ip, err := strconv.Unquote(spec.Path.Value)
if err != nil {
return nil, nil, err
}
if ip == "C" {
// The preamble is the comment before the import; only its #include and #cgo lines read files.
for _, cg := range parsed.Comments {
if cg.End() > spec.Pos() {
continue
}
for _, line := range strings.Split(cg.Text(), "\n") {
line = strings.TrimSpace(line)
if (strings.HasPrefix(line, "#include") || strings.HasPrefix(line, "#cgo")) && strings.Contains(line, "..") {
return nil, nil, errors.New("its cgo preamble names a path outside its directory: " + line)
}
}
}
// A cgo file may include from below its directory: the directory is held whole.
embeds = append(embeds, ".")
continue
}
imports = append(imports, ip)
}
// //go:embed directives may stand anywhere in the file, so the whole text is read for them.
scanner := bufio.NewScanner(strings.NewReader(string(src)))
scanner.Buffer(make([]byte, 0, 64*1024), 4*1024*1024)
for scanner.Scan() {
line := strings.TrimSpace(scanner.Text())
rest, ok := strings.CutPrefix(line, "//go:embed")
if !ok || (rest != "" && rest[0] != ' ' && rest[0] != '\t') {
continue
}
patterns, err := embedPatterns(rest)
if err != nil {
return nil, nil, err
}
for _, p := range patterns {
embeds = append(embeds, embedRoot(p))
}
}
return imports, embeds, scanner.Err()
}
// nativeSource is a file the Go command compiles or links beside Go: C, C++, Objective-C, Fortran,
// assembly, their headers and a system object.
func nativeSource(name string) bool {
switch strings.ToLower(path.Ext(name)) {
case ".c", ".h", ".cc", ".cpp", ".cxx", ".hh", ".hpp", ".hxx", ".m", ".s", ".sx", ".f", ".f90", ".for", ".syso":
return true
}
return false
}
// includesStayWithin refuses a native source whose #include names a path above its directory.
func includesStayWithin(file string) error {
body, err := os.ReadFile(file)
if err != nil {
return err
}
for _, line := range strings.Split(string(body), "\n") {
line = strings.TrimSpace(line)
if strings.HasPrefix(line, "#") && strings.Contains(line, "include") && strings.Contains(line, "..") {
return errors.New("it includes a path outside its directory: " + line)
}
}
return nil
}
// embedPatterns splits a //go:embed line's patterns: separated by spaces, each possibly quoted.
func embedPatterns(s string) ([]string, error) {
var out []string
s = strings.TrimSpace(s)
for s != "" {
var p string
switch s[0] {
case '"', '`':
q, err := strconv.QuotedPrefix(s)
if err != nil {
return nil, fmt.Errorf("an embed pattern does not parse: %w", err)
}
if p, err = strconv.Unquote(q); err != nil {
return nil, err
}
s = s[len(q):]
default:
end := strings.IndexAny(s, " \t")
if end < 0 {
end = len(s)
}
p, s = s[:end], s[end:]
}
out = append(out, p)
s = strings.TrimSpace(s)
}
return out, nil
}
// embedRoot is the directory an embed pattern starts in, relative to the package: its leading elements
// without a wildcard. A pattern naming a file or a directory outright is held as itself.
func embedRoot(pattern string) string {
pattern = strings.TrimPrefix(pattern, "all:")
var kept []string
for _, el := range strings.Split(pattern, "/") {
if strings.ContainsAny(el, "*?[\\") {
break
}
kept = append(kept, el)
}
if len(kept) == 0 {
return "."
}
return path.Clean(strings.Join(kept, "/"))
}
// narrowTree copies into dst the files of src a build source holds, and nothing else — never `.git` — and
// returns a fingerprint of what it copied: each file's path, mode and content, hashed in path order. **A
// build handed only its build source cannot read past it** (novox/hq ADR 0267 rule 3): a recipe that
// copies a file outside it fails, naming the file, where it would have built and been missed.
func narrowTree(src, dst string, entries []string) (string, error) {
if err := os.RemoveAll(dst); err != nil {
return "", err
}
if err := os.MkdirAll(dst, 0o755); err != nil {
return "", err
}
var lines []string
err := filepath.WalkDir(src, func(p string, d fs.DirEntry, err error) error {
if err != nil {
return err
}
rel, err := filepath.Rel(src, p)
if err != nil {
return err
}
rel = filepath.ToSlash(rel)
if d.IsDir() {
if d.Name() == ".git" {
return filepath.SkipDir
}
return nil
}
if !SourceHolds(entries, rel) {
return nil
}
out := filepath.Join(dst, filepath.FromSlash(rel))
if err := os.MkdirAll(filepath.Dir(out), 0o755); err != nil {
return err
}
info, err := d.Info()
if err != nil {
return err
}
if info.Mode()&fs.ModeSymlink != 0 {
// A link in the repository is copied as the link it is, and what it names said in the
// fingerprint.
target, err := os.Readlink(p)
if err != nil {
return err
}
lines = append(lines, fmt.Sprintf("%s link %s", rel, target))
return os.Symlink(target, out)
}
if !info.Mode().IsRegular() {
return nil
}
in, err := os.Open(p)
if err != nil {
return err
}
defer in.Close()
w, err := os.OpenFile(out, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, info.Mode().Perm())
if err != nil {
return err
}
sum := sha256.New()
if _, err := io.Copy(io.MultiWriter(w, sum), in); err != nil {
w.Close()
return err
}
if err := w.Close(); err != nil {
return err
}
lines = append(lines, fmt.Sprintf("%s %o %s", rel, info.Mode().Perm()&0o111, hex.EncodeToString(sum.Sum(nil))))
return nil
})
if err != nil {
return "", err
}
sort.Strings(lines)
sum := sha256.Sum256([]byte(strings.Join(lines, "\n")))
return "narrow:" + hex.EncodeToString(sum[:]), nil
}
-318
View File
@@ -1,318 +0,0 @@
package builder
import (
"os"
"os/exec"
"path/filepath"
"strings"
"testing"
)
// A Go program's build source is its import closure (novox/hq ADR 0267 rule 1): never narrower than what
// `go build` reads, whatever the system, the tags, the vendoring or the embeds.
// aGoTree writes files under a fresh directory and returns it.
func aGoTree(t *testing.T, files map[string]string) string {
t.Helper()
root := t.TempDir()
for name, body := range files {
full := filepath.Join(root, filepath.FromSlash(name))
if err := os.MkdirAll(filepath.Dir(full), 0o755); err != nil {
t.Fatal(err)
}
if err := os.WriteFile(full, []byte(body), 0o644); err != nil {
t.Fatal(err)
}
}
return root
}
// aProgram is a module whose program imports its own packages, a vendored module, a local replacement
// that is vendored too, an embed and a package only one system builds; beside them, a package nothing
// imports and one only a test imports.
var aProgram = map[string]string{
"go.mod": "module example.com/fix\n\ngo 1.22\n\nrequire (\n\texample.org/dep v1.0.0\n\texample.net/local v0.0.0\n)\n\n" +
"replace example.net/local => ./third_party/local\n",
"go.sum": "",
"vendor/modules.txt": "# example.net/local v0.0.0 => ./third_party/local\n## explicit; go 1.22\nexample.net/local/pkg\n" +
"# example.org/dep v1.0.0\n## explicit; go 1.22\nexample.org/dep\nexample.org/dep/sub\n# example.net/local => ./third_party/local\n",
"vendor/example.org/dep/dep.go": "package dep\n\nimport _ \"example.org/dep/sub\"\n",
"vendor/example.org/dep/sub/sub.go": "package sub\n",
"vendor/example.org/other/other.go": "package other\n",
"vendor/example.net/local/pkg/p.go": "package pkg\n",
"third_party/local/go.mod": "module example.net/local\n\ngo 1.22\n",
"third_party/local/pkg/p.go": "package pkg\n",
"cmd/prog/main.go": "package main\n\nimport (\n\t\"fmt\"\n\n\t_ \"example.com/fix/emb\"\n\t\"example.com/fix/lib\"\n" +
"\t_ \"example.net/local/pkg\"\n\t_ \"example.org/dep\"\n)\n\nfunc main() { fmt.Println(lib.X) }\n",
"lib/lib.go": "package lib\n\nconst X = 1\n",
"lib/lib_plan9.go": "//go:build plan9\n\npackage lib\n\nimport _ \"example.com/fix/plan9only\"\n",
"lib/lib_test.go": "package lib\n\nimport _ \"example.com/fix/testonly\"\n",
"emb/emb_amd64.s": "",
"lib/sub/sub.go": "package sub\n",
"plan9only/p.go": "package plan9only\n",
"testonly/t.go": "package testonly\n",
"unrelated/u.go": "package unrelated\n",
"emb/emb.go": "package emb\n\nimport \"embed\"\n\n//go:embed static/*\nvar Static embed.FS\n\n" +
"//go:embed \"a b.txt\"\nvar Text string\n",
"emb/static/index.html": "x",
"emb/static/deep/style.css": "x",
"emb/a b.txt": "x",
"README.md": "x",
}
func TestAGoProgramsBuildSourceIsItsImportClosure(t *testing.T) {
root := aGoTree(t, aProgram)
got, err := GoBuildSource(root, "cmd/prog")
if err != nil {
t.Fatal(err)
}
for _, c := range []struct {
file string
held bool
why string
}{
{"cmd/prog/main.go", true, "the program itself"},
{"cmd/prog/helper.go", true, "a file added to the program's package"},
{"lib/lib.go", true, "a package it imports"},
{"emb/emb_amd64.s", true, "assembly beside a package's Go"},
{"lib/lib_plan9.go", true, "a file one system builds"},
{"plan9only/p.go", true, "what a file one system builds imports"},
{"emb/static/index.html", true, "an embedded file"},
{"emb/static/deep/style.css", true, "an embedded file below the pattern's directory"},
{"emb/a b.txt", true, "an embed named outright, quoted"},
{"vendor/example.org/dep/dep.go", true, "a vendored package it imports"},
{"vendor/example.org/dep/sub/sub.go", true, "what a vendored package imports"},
{"vendor/example.net/local/pkg/p.go", true, "a replaced module, as vendored"},
{"third_party/local/pkg/p.go", true, "a replaced module, at its directory"},
{"third_party/local/go.mod", true, "a replaced module's requirements"},
{"go.mod", true, "the module's requirements"},
{"go.sum", true, "the module's sums"},
{"vendor/modules.txt", true, "the vendored modules"},
{"go.work", true, "a workspace, were one made"},
{"lib/lib_test.go", false, "a test is not built"},
{"testonly/t.go", false, "a package only a test imports"},
{"lib/sub/sub.go", false, "a package below an imported one, not imported"},
{"unrelated/u.go", false, "a package nothing imports"},
{"vendor/example.org/other/other.go", false, "a vendored package nothing imports"},
{"README.md", false, "a file no build reads"},
} {
if SourceHolds(got, c.file) != c.held {
t.Errorf("%s (%s): held %v, wanted %v\n %v", c.file, c.why, !c.held, c.held, got)
}
}
// **Never narrower than go list -deps**: every package go names, and every file it compiles or embeds,
// is held. Where no go command is at hand, said, not passed.
goCmd, err := exec.LookPath("go")
if err != nil {
t.Skip("NOT COMPARED: no go command to list the closure with")
}
list := exec.Command(goCmd, "list", "-deps", "-f",
`{{if not .Standard}}{{$d := .Dir}}{{range .GoFiles}}{{$d}}/{{.}}
{{end}}{{range .SFiles}}{{$d}}/{{.}}
{{end}}{{range .EmbedFiles}}{{$d}}/{{.}}
{{end}}{{end}}`, "./cmd/prog")
list.Dir = root
list.Env = append(os.Environ(), "GOFLAGS=-mod=vendor", "GOPROXY=off", "GOWORK=off", "GOOS=linux", "GOARCH=amd64")
out, err := list.CombinedOutput()
if err != nil {
t.Fatalf("go list: %v\n%s", err, out)
}
real, _ := filepath.EvalSymlinks(root)
for _, line := range strings.Split(strings.TrimSpace(string(out)), "\n") {
if line == "" {
continue
}
rel, err := filepath.Rel(real, line)
if err != nil || strings.HasPrefix(rel, "..") {
rel, _ = filepath.Rel(root, line)
}
if !SourceHolds(got, filepath.ToSlash(rel)) {
t.Errorf("go list builds %s, and the build source does not hold it", rel)
}
}
}
// A file added to the program's import closure moves its build source with it: the closure read again
// grows by the package.
func TestTheBuildSourceGrowsWithAnImport(t *testing.T) {
files := map[string]string{}
for k, v := range aProgram {
files[k] = v
}
before, err := GoBuildSource(aGoTree(t, files), "cmd/prog")
if err != nil {
t.Fatal(err)
}
if SourceHolds(before, "unrelated/u.go") {
t.Fatal("held before it was imported")
}
files["cmd/prog/more.go"] = "package main\n\nimport _ \"example.com/fix/unrelated\"\n"
after, err := GoBuildSource(aGoTree(t, files), "cmd/prog")
if err != nil {
t.Fatal(err)
}
if !SourceHolds(after, "unrelated/u.go") {
t.Fatalf("an import added did not grow the build source: %v", after)
}
}
// What cannot be read is refused, so the build source is not narrowed and nothing is missed.
func TestABuildSourceThatCannotBeReadIsRefused(t *testing.T) {
for _, c := range []struct {
what string
files map[string]string
pkg string
says string
}{
{"no go.mod", map[string]string{"cmd/p/main.go": "package main\n"}, "cmd/p", "no go.mod"},
{"a workspace", map[string]string{"go.mod": "module x\n", "go.work": "go 1.22\n", "p/main.go": "package main\n"},
"p", "go.work"},
{"a local replacement outside the tree", map[string]string{
"go.mod": "module x\n\nreplace y => ../y\n", "p/main.go": "package main\n\nimport _ \"y\"\n"}, "p", "outside"},
{"a cgo header outside the directory", map[string]string{
"go.mod": "module x\n", "p/main.go": "package main\n\n// #include \"../h/h.h\"\nimport \"C\"\n"}, "p", "cgo"},
{"an assembly include above its directory", map[string]string{"go.mod": "module x\n", "p/main.go": "package main\n",
"p/a_amd64.s": "#include \"../h/textflag.h\"\n"}, "p", "outside"},
{"a file that does not parse", map[string]string{"go.mod": "module x\n", "p/main.go": "package main\n\nimport (\n"},
"p", "main.go"},
{"a package that leaves the tree", map[string]string{"go.mod": "module x\n"}, "../elsewhere", "leaves"},
} {
_, err := GoBuildSource(aGoTree(t, c.files), c.pkg)
if err == nil || !strings.Contains(err.Error(), c.says) {
t.Errorf("%s: %v, wanted a refusal saying %q", c.what, err, c.says)
}
}
// A cgo header in the package's own directory is held already, and is no refusal.
if _, err := GoBuildSource(aGoTree(t, map[string]string{"go.mod": "module x\n",
"p/main.go": "package main\n\n// #include \"h.h\"\nimport \"C\"\n"}), "p"); err != nil {
t.Errorf("a header in the package's directory was refused: %v", err)
}
}
func TestABuildSourceHoldsWhatItsEntriesSay(t *testing.T) {
entries := []string{"./", "cmd/p/", "web/**", "go.mod"}
for file, want := range map[string]bool{
"main.go": true, "main_test.go": false, "cmd/p/x.go": true, "cmd/p/x_test.go": false, "cmd/p/sub/y.go": false,
"cmd/px/x.go": false, "web/a/b/c.css": true, "web": true, "webx/a": false, "go.mod": true, "docs/x.md": false,
"/cmd/p/x.go": true, "cmd/p/../q/x.go": false,
} {
if SourceHolds(entries, file) != want {
t.Errorf("%s: held %v, wanted %v", file, !want, want)
}
}
if !SourceHolds([]string{"**"}, "anything/at/all") {
t.Error("the whole tree does not hold a file")
}
}
// **A build handed its build source reads nothing else** (rule 3): the narrowed tree holds the source's
// files and no others — never .git — and its fingerprint changes with them and only with them.
func TestANarrowedTreeHoldsTheBuildSourceAndNothingElse(t *testing.T) {
files := map[string]string{}
for k, v := range aProgram {
files[k] = v
}
files[".git/HEAD"] = "ref: refs/heads/main\n"
src := aGoTree(t, files)
entries, err := GoBuildSource(src, "cmd/prog")
if err != nil {
t.Fatal(err)
}
dst := filepath.Join(t.TempDir(), "narrow")
one, err := narrowTree(src, dst, entries)
if err != nil {
t.Fatal(err)
}
for file, want := range map[string]bool{"cmd/prog/main.go": true, "lib/lib.go": true, "emb/static/deep/style.css": true,
"lib/lib_test.go": false, "unrelated/u.go": false, "README.md": false, ".git/HEAD": false} {
_, err := os.Stat(filepath.Join(dst, filepath.FromSlash(file)))
if (err == nil) != want {
t.Errorf("%s: copied %v, wanted %v", file, err == nil, want)
}
}
// Outside the build source: one fingerprint.
files["README.md"] = "changed"
files["unrelated/u.go"] = "package unrelated\n\nconst Changed = 1\n"
again, err := narrowTree(aGoTree(t, files), filepath.Join(t.TempDir(), "n"), entries)
if err != nil || again != one {
t.Fatalf("a change outside the build source changed its fingerprint: %s %s %v", one, again, err)
}
// Inside it: another.
files["lib/lib.go"] = "package lib\n\nconst X = 2\n"
moved, err := narrowTree(aGoTree(t, files), filepath.Join(t.TempDir(), "n"), entries)
if err != nil || moved == one {
t.Fatalf("a change inside the build source kept its fingerprint: %s %v", moved, err)
}
}
// This repository's own programs: the route proxy's build source is not the controller's, and neither
// holds the other's command.
func TestThisRepositorysProgramsHaveBuildSourcesOfTheirOwn(t *testing.T) {
root := filepath.Join("..", "..")
proxy, err := GoBuildSource(root, "examples/route-proxy")
if err != nil {
t.Fatal(err)
}
controller, err := GoBuildSource(root, "cmd/mesh-controller")
if err != nil {
t.Fatal(err)
}
builder, err := GoBuildSource(root, "cmd/mesh-builder")
if err != nil {
t.Fatal(err)
}
for _, c := range []struct {
entries []string
name string
file string
held bool
}{
{proxy, "the route proxy", "examples/route-proxy/main.go", true},
{proxy, "the route proxy", "internal/broker/broker.go", true},
{proxy, "the route proxy", "cmd/mesh-controller/main.go", false},
{proxy, "the route proxy", "internal/conditions/condition.go", false},
{proxy, "the route proxy", "README.md", false},
{controller, "the controller", "cmd/mesh-controller/main.go", true},
{controller, "the controller", "internal/conditions/condition.go", true},
{controller, "the controller", "internal/inventory/migrations/0001-nodes.sql", true},
{controller, "the controller", "examples/route-proxy/main.go", false},
{controller, "the controller", "cmd/mesh-builder/main.go", false},
{controller, "the controller", "README.md", false},
{builder, "the build seat's program", "cmd/mesh-builder/main.go", true},
{builder, "the build seat's program", "internal/conditions/condition.go", false},
{builder, "the build seat's program", "cmd/mesh-controller/main.go", false},
} {
if SourceHolds(c.entries, c.file) != c.held {
t.Errorf("%s: %s held %v, wanted %v", c.name, c.file, !c.held, c.held)
}
}
}
// C or assembly beside Go holds its package's directory whole — an include may name a file below it — and a
// go.mod made between the module's root and a package is a change.
func TestNativeSourcesHoldTheirDirectoryWhole(t *testing.T) {
got, err := GoBuildSource(aGoTree(t, map[string]string{"go.mod": "module x\n", "cmd/p/main.go": "package main\n\nimport _ \"x/lib/asm\"\n",
"lib/asm/a.go": "package asm\n", "lib/asm/a_amd64.s": "#include \"inc/textflag.h\"\n", "lib/asm/inc/textflag.h": ""}), "cmd/p")
if err != nil {
t.Fatal(err)
}
for file, want := range map[string]bool{"lib/asm/inc/textflag.h": true, "lib/asm/a_amd64.s": true, "lib/go.mod": true,
"lib/asm/go.mod": true, "cmd/go.mod": true, "other/go.mod": false} {
if SourceHolds(got, file) != want {
t.Errorf("%s: held %v, wanted %v (%v)", file, !want, want, got)
}
}
}
// A cgo file may include from below its directory with nothing native beside it: its directory is held whole.
func TestACgoFileHoldsItsDirectoryWhole(t *testing.T) {
got, err := GoBuildSource(aGoTree(t, map[string]string{"go.mod": "module x\n",
"p/main.go": "package main\n\n// #include \"inc/x.h\"\nimport \"C\"\n", "p/inc/x.h": ""}), "p")
if err != nil {
t.Fatal(err)
}
if !SourceHolds(got, "p/inc/x.h") {
t.Fatalf("a header a cgo preamble includes is not held: %v", got)
}
}
+1 -145
View File
@@ -9,8 +9,6 @@ import (
"path/filepath" "path/filepath"
"sort" "sort"
"strings" "strings"
"github.com/novox/mesh-controller/internal/catalogue"
) )
// A build's source fingerprint: what it was made from, hashed (novox/hq issue 280). // A build's source fingerprint: what it was made from, hashed (novox/hq issue 280).
@@ -51,152 +49,10 @@ type sourceInputs struct {
toolchains map[string]string toolchains map[string]string
// unpinned is why this build has no fingerprint: empty when it has one. // unpinned is why this build has no fingerprint: empty when it has one.
unpinned string unpinned string
// prefix is the module's directory within its repository, empty at the root.
prefix string
// own is the module's build source in its own repository, relative to the module's directory, and
// whole when an artifact's is not known (novox/hq ADR 0267).
own map[string]bool
ownIsAll bool
// read is, per context (repository and ref), the build source read there; nil for one read whole.
read map[string]map[string]bool
readAs map[string]catalogue.ArtifactContext
// contextsAtHead is false once a context was cloned at something other than its trunk's head: its
// closure is not the one the next merge there meets, and the build says no build source.
contextsAtHead bool
}
// contextOffHead says a context was not its trunk's head.
func (s *sourceInputs) contextOffHead() {
if s != nil {
s.contextsAtHead = false
}
} }
func newSourceInputs(module string) *sourceInputs { func newSourceInputs(module string) *sourceInputs {
return &sourceInputs{module: module, contexts: map[string]string{}, toolchains: map[string]string{}, return &sourceInputs{module: module, contexts: map[string]string{}, toolchains: map[string]string{}}
own: map[string]bool{}, read: map[string]map[string]bool{}, readAs: map[string]catalogue.ArtifactContext{},
contextsAtHead: true}
}
// ownHas adds entries, relative to the module's directory, to its build source in its own repository.
func (s *sourceInputs) ownHas(entries ...string) {
if s == nil {
return
}
for _, e := range entries {
s.own[e] = true
}
}
// ownWhole says an artifact's build source in the module's own repository is not known: the module's
// whole directory is its source, as it was before.
func (s *sourceInputs) ownWhole() {
if s != nil {
s.ownIsAll = true
}
}
func contextKey(c catalogue.ArtifactContext) string { return c.Repository + "#" + c.Ref }
// readIn adds entries to the build source read in a context; one read whole stays whole.
func (s *sourceInputs) readIn(c catalogue.ArtifactContext, entries []string) {
if s == nil {
return
}
key := contextKey(c)
s.readAs[key] = c
set, known := s.read[key]
if known && set == nil {
return
}
if set == nil {
set = map[string]bool{}
s.read[key] = set
}
for _, e := range entries {
set[e] = true
}
}
// readWhole says a context is read whole by an artifact.
func (s *sourceInputs) readWhole(c catalogue.ArtifactContext) {
if s == nil {
return
}
key := contextKey(c)
s.readAs[key] = c
s.read[key] = nil
}
// buildSources is what the build says it was made from, per repository: its own (module.json always,
// and every artifact's) unless an artifact's is not known, and each context not read whole.
func (s *sourceInputs) buildSources() []BuildSource {
if s == nil {
return nil
}
var out []BuildSource
if !s.ownIsAll {
own := []string{withPrefix(s.prefix, ManifestName)}
for e := range s.own {
own = append(own, withPrefix(s.prefix, e))
}
sort.Strings(own)
out = append(out, BuildSource{Paths: compactSorted(own)})
}
var keys []string
for k := range s.read {
keys = append(keys, k)
}
sort.Strings(keys)
for _, k := range keys {
set := s.read[k]
if set == nil {
continue
}
var paths []string
for e := range set {
paths = append(paths, e)
}
sort.Strings(paths)
c := s.readAs[k]
out = append(out, BuildSource{Repository: c.Repository, Ref: c.Ref, Paths: paths})
}
return out
}
// withPrefix is an entry relative to the module's directory made relative to its repository's root.
func withPrefix(prefix, entry string) string {
switch {
case entry == "." || entry == "./":
entry = ""
case entry == "**" || entry == "./**" || entry == "/**":
if prefix == "" {
return "**"
}
return prefix + "/**"
}
entry = strings.TrimPrefix(entry, "./")
if prefix == "" {
if entry == "" {
return "./"
}
return entry
}
if entry == "" {
return prefix + "/"
}
return prefix + "/" + entry
}
func compactSorted(in []string) []string {
var out []string
for i, e := range in {
if i == 0 || e != in[i-1] {
out = append(out, e)
}
}
return out
} }
// notPinned marks the build as one its source does not pin; the first reason stands. // notPinned marks the build as one its source does not pin; the first reason stands.
+5 -37
View File
@@ -173,7 +173,11 @@ func (m Manifest) Resolve(built []Built) (Manifest, error) {
// had — so re-composing a declaration moves nothing, where a commit would move the // had — so re-composing a declaration moves nothing, where a commit would move the
// path of an identical binary and recreate everything that reads it. // path of an identical binary and recreate everything that reads it.
for key, value := range filled { for key, value := range filled {
filled[key] = withVersion(value, versionOf(artifact.Digest)) text, isText := value.(string)
if !isText || !strings.Contains(text, versionRef) {
continue
}
filled[key] = strings.ReplaceAll(text, versionRef, versionOf(artifact.Digest))
} }
default: default:
return Manifest{}, fmt.Errorf("%s: %q is a %q, and an artifact is %q, %q, %q or %q", return Manifest{}, fmt.Errorf("%s: %q is a %q, and an artifact is %q, %q, %q or %q",
@@ -185,31 +189,6 @@ func (m Manifest) Resolve(built []Built) (Manifest, error) {
return out, nil return out, nil
} }
// withVersion is a resource's value with `${version}` filled: in a string, and in each string of a map —
// a process's env, where the controller is told which build it is (novox/hq issue 352). Anything else is
// left as it is.
func withVersion(value any, version string) any {
switch v := value.(type) {
case string:
if strings.Contains(v, versionRef) {
return strings.ReplaceAll(v, versionRef, version)
}
case map[string]any:
out := make(map[string]any, len(v))
for k, x := range v {
out[k] = withVersion(x, version)
}
return out
case map[string]string:
out := make(map[string]string, len(v))
for k, x := range v {
out[k] = strings.ReplaceAll(x, versionRef, version)
}
return out
}
return value
}
// checkBuild is the manifest's own account of what it builds. // checkBuild is the manifest's own account of what it builds.
func (b *Build) problems(module string) []string { func (b *Build) problems(module string) []string {
if b == nil { if b == nil {
@@ -322,17 +301,6 @@ func (b *Build) problems(module string) []string {
"everything else brings its own recipe", module, a.Name, a.Kind)) "everything else brings its own recipe", module, a.Name, a.Kind))
} }
} }
if a.Compiles != "" {
if a.Kind != ArtifactImage {
problems = append(problems, fmt.Sprintf(
"%s: %q is a %q and names a Go package it compiles. Only an image's recipe is "+
"told which; a bundle names its package in from (novox/hq ADR 0267)", module, a.Name, a.Kind))
} else if c := strings.TrimSpace(a.Compiles); strings.HasPrefix(c, "/") || c == ".." ||
strings.HasPrefix(c, "../") || strings.Contains(c, "/../") || strings.HasSuffix(c, "/..") {
problems = append(problems, fmt.Sprintf(
"%s: %q compiles %q, which leaves the tree it is built in", module, a.Name, a.Compiles))
}
}
// An upstream image is named, not read from the repository, so the path rule does not // An upstream image is named, not read from the repository, so the path rule does not
// apply to it — and applying it anyway would refuse every reference with a registry host // apply to it — and applying it anyway would refuse every reference with a registry host
// in it. // in it.
-21
View File
@@ -180,24 +180,3 @@ func TestAResourceNamingAPackageIsRefused(t *testing.T) {
t.Fatal("a resource backed by a package was accepted; a package is not a resource") t.Fatal("a resource backed by a package was accepted; a package is not a resource")
} }
} }
// An image names the Go package its recipe compiles (novox/hq ADR 0267): within the tree it is built in,
// and only an image says one.
func TestOnlyAnImageNamesThePackageItCompilesWithinItsTree(t *testing.T) {
m, err := ParseManifest([]byte(`{"module":"a","version":"1","build":{"artifacts":[
{"name":"x","kind":"image","from":"Dockerfile","compiles":"cmd/x"}]}}`))
if err != nil || m.Build.Artifacts[0].Compiles != "cmd/x" {
t.Fatalf("an image naming its package was refused or lost it: %v", err)
}
for _, c := range []struct{ artifact, says string }{
{`{"name":"x","kind":"archive","from":"files","compiles":"cmd/x"}`, "Only an image"},
{`{"name":"x","kind":"image","from":"Dockerfile","compiles":"../x"}`, "leaves the tree"},
{`{"name":"x","kind":"image","from":"Dockerfile","compiles":"/x"}`, "leaves the tree"},
{`{"name":"x","kind":"image","from":"Dockerfile","compiles":"a/../../x"}`, "leaves the tree"},
} {
_, err := ParseManifest([]byte(`{"module":"a","version":"1","build":{"artifacts":[` + c.artifact + `]}}`))
if err == nil || !strings.Contains(err.Error(), c.says) {
t.Errorf("%s: %v, wanted a refusal saying %q", c.artifact, err, c.says)
}
}
}
+1 -15
View File
@@ -1101,23 +1101,9 @@ func (r Resolution) compose(with Rendering, owner map[string]string,
} }
owner[fmt.Sprint(process["id"])] = RuntimeModule owner[fmt.Sprint(process["id"])] = RuntimeModule
out = append(out, process) out = append(out, process)
// And a runtime of its own for each module of its own account, after it (novox/hq ADR 0259 §8). // What each module's bundles are given is read as the account the runtime runs as.
owns, err := r.ownRuntimes(with)
if err != nil {
return nil, err
}
for _, p := range owns {
id := fmt.Sprint(p["id"])
owner[id] = strings.TrimSuffix(id, "."+OwnRuntimeID())
out = append(out, p)
}
// What each module's bundles are given is read as the account the runtime runs as — not what a
// module of its own account is given, which its own account reads.
words := map[string]map[string]string{} words := map[string]map[string]string{}
for _, m := range r.Modules { for _, m := range r.Modules {
if m.RunsAs != "" {
continue
}
w, err := bundleWords(m, with) w, err := bundleWords(m, with)
if err != nil { if err != nil {
return nil, err return nil, err
-16
View File
@@ -78,22 +78,6 @@ func graphicalSessionSeats() []Seat {
"description": "the lines to choose between, in order"}, "description": "the lines to choose between, in order"},
"prompt": map[string]any{"type": "string", "description": "what the menu asks (optional)"}, "prompt": map[string]any{"type": "string", "description": "what the menu asks (optional)"},
}}}, }}},
// A value the operator types and nobody sees (novox/hq ADR 0259 §10): a hidden prompt whose answer
// is sealed to the asker's key, so it is never plaintext on the bus or in any call's record.
// **Optional while its holders catch up** (ADR 0246): rofi serves it once this is live.
{Name: "secret", Optional: true, Description: "Ask the operator for a value in a prompt that " +
"does not show what is typed, and answer it sealed to the key the asker gives — never in " +
"the clear — or cancelled when the prompt was dismissed or not answered in time.",
// By name, never by words (the review of 2026-10-09, M4): the holder writes the prompt from the
// module, the secret and the machine, and says the controller asks — the bus lets nobody else
// ask it (broker.ControllerOnly) — so no caller puts words of its own before the operator.
Input: schema(map[string]string{
"module": "the module whose own secret is asked for",
"secret": "the own secret's name",
"node": "the machine the module runs on",
"seal_to": "the asker's public sealing key: the answer is sealed to it",
"timeout_seconds": "give up after this long (optional)",
}, []string{"module", "secret", "node", "seal_to"})},
}}, }},
{Name: NotifierSeat, Scope: ScopeNode, Decision: decided, Serves: []Verb{ {Name: NotifierSeat, Scope: ScopeNode, Decision: decided, Serves: []Verb{
{Name: "send", Description: "Show the operator a notification.", {Name: "send", Description: "Show the operator a notification.",
+1 -1
View File
@@ -16,7 +16,7 @@ func TestTheGraphicalSessionsSeatsAreTheMeshsOwnWithTheirVerbs(t *testing.T) {
DisplayServerSeat: {"displays", "layout"}, DisplayServerSeat: {"displays", "layout"},
DisplaySessionSeat: {"reload", "workspaces", "windows"}, DisplaySessionSeat: {"reload", "workspaces", "windows"},
TerminalEmulatorSeat: {"open"}, TerminalEmulatorSeat: {"open"},
LauncherSeat: {"menu", "secret"}, LauncherSeat: {"menu"},
NotifierSeat: {"send", "history"}, NotifierSeat: {"send", "history"},
LockScreenSeat: {"lock"}, LockScreenSeat: {"lock"},
ClipboardSeat: {"history", "copy"}, ClipboardSeat: {"history", "copy"},
-168
View File
@@ -1,168 +0,0 @@
package catalogue
import (
"strings"
"testing"
)
// The router of novox/hq ADR 0259: it declares the operator's seat and the two kinded benches.
func router() Manifest {
return Manifest{Module: "messenger", Tools: []string{"open", "history", "notify"},
State: []StateDeclaration{{Name: "asks"}}, RunsAs: "messenger", SecretsOwner: "messenger",
DefinesSeats: []SeatDeclaration{
{Name: "operator-channel", Scope: ScopeMesh, Accepts: []string{"ask", "cancel"}, Emits: []string{"decided"},
ByCaller: []string{"ask", "cancel", "decided"}, Records: []string{"asks"},
Serves: []Verb{{Name: "open"}, {Name: "history"}, {Name: "notify"}}},
{Name: "channel", Scope: ScopeMesh, Kinded: true, Accepts: []string{"show", "edit", "send"}},
{Name: "intake", Scope: ScopeMesh, Kinded: true, Emits: []string{"choice", "link"}, Proofs: []string{"code"}},
},
Claims: []Claim{{Name: "operator-channel", Scope: ScopeMesh}},
Uses: []string{"channel"}}
}
func aChannel(module, kind string) Manifest {
return Manifest{Module: module, Claims: []Claim{
{Name: "channel", Scope: ScopeMesh, Kind: kind}, {Name: "intake", Scope: ScopeMesh, Kind: kind}}}
}
func TestTwoChannelsOfDifferentKindsHoldTheBenches(t *testing.T) {
shelf := Shelf{"messenger": router(), "telegram": aChannel("telegram", "telegram"),
"desk-channel": aChannel("desk-channel", "desktop")}
if got := problemsFor(t, shelf); got != "" {
t.Fatalf("two kinds were refused: %s", got)
}
for _, m := range shelf {
if got := declaredSeatProblems(m); len(got) > 0 {
t.Fatalf("%s: %v", m.Module, got)
}
}
}
func TestASecondClaimOfOneKindIsRefused(t *testing.T) {
got := problemsFor(t, Shelf{"messenger": router(), "telegram": aChannel("telegram", "telegram"),
"telegram-two": aChannel("telegram-two", "telegram")})
if !strings.Contains(got, `of kind "telegram", which telegram already claims`) {
t.Fatalf("a second holder of one kind stood: %s", got)
}
}
func TestAKindedBenchNeedsAKindAndNoOtherSeatTakesOne(t *testing.T) {
got := problemsFor(t, Shelf{"messenger": router(), "nameless": aChannel("nameless", "")})
if !strings.Contains(got, "claims the kinded bench channel and names no kind") {
t.Fatalf("a claim without a kind stood: %s", got)
}
odd := Manifest{Module: "odd", Claims: []Claim{{Name: "operator-channel", Scope: ScopeMesh, Kind: "telegram"}}}
got = problemsFor(t, Shelf{"messenger": router(), "odd": odd})
if !strings.Contains(got, "only a kinded bench takes a kind") {
t.Fatalf("a kind on a seat that is not kinded stood: %s", got)
}
dotted := problemsFor(t, Shelf{"messenger": router(), "dotted": aChannel("dotted", "a.b")})
if !strings.Contains(dotted, "not a usable name") {
t.Fatalf("a kind that would widen a subject stood: %s", dotted)
}
}
func TestOnlyChannelAndIntakeAreKinded(t *testing.T) {
m := Manifest{Module: "x", DefinesSeats: []SeatDeclaration{{Name: "pager", Kinded: true, Accepts: []string{"page"}}}}
if got := strings.Join(declaredSeatProblems(m), "; "); !strings.Contains(got, "only channel and intake are kinded") {
t.Fatalf("another kinded bench was declared: %s", got)
}
}
func TestTheNewRulesAreHeldToWhatTheSeatSays(t *testing.T) {
m := Manifest{Module: "x", DefinesSeats: []SeatDeclaration{{Name: "thing", Accepts: []string{"do"},
ByCaller: []string{"undo"}, Proofs: []string{"code"}, Records: []string{"nothing"}}}}
got := strings.Join(declaredSeatProblems(m), "; ")
for _, want := range []string{"names thing.undo by its caller, which the seat neither accepts nor emits",
"declares proofs on thing, which is not kinded", `read its records "nothing", which it keeps no state of`} {
if !strings.Contains(got, want) {
t.Errorf("not refused: %q in %s", want, got)
}
}
}
// Two kinds on one machine are two holders, and one kind on two machines is a second claimant.
func TestEachKindIsItsOwnHolderWhenResolved(t *testing.T) {
modules := []Manifest{aChannel("telegram", "telegram"), aChannel("desk-channel", "desktop")}
held, problems := checkClaims(modules, Node{Name: "anchor"}, nil, nil)
if len(problems) > 0 || len(held) != 4 {
t.Fatalf("two kinds on one machine: held %v, problems %v", held, problems)
}
_, problems = checkClaims([]Manifest{aChannel("telegram", "telegram")}, Node{Name: "home"}, held, nil)
if len(problems) == 0 {
t.Fatal("one kind held on two machines was not refused")
}
}
// A channel's capabilities come from the fixed vocabulary, and only a kinded claim carries any.
func TestCapabilitiesAreTheVocabularysAndOnlyOnAKindedClaim(t *testing.T) {
good := aChannel("telegram", "telegram")
good.Claims[0].Capabilities = []string{"deliver", "choice", "verified-sender", "max-length:4096"}
good.RunsAs = "telegram"
if got := problemsFor(t, Shelf{"messenger": router(), "telegram": good}); got != "" {
t.Fatalf("the vocabulary was refused: %s", got)
}
bad := aChannel("telegram", "telegram")
bad.Claims[0].Capabilities = []string{"trusted", "max-length:lots"}
got := problemsFor(t, Shelf{"messenger": router(), "telegram": bad})
for _, w := range []string{`"trusted"`, `"max-length:lots"`} {
if !strings.Contains(got, w+", which channel-capabilities/1 does not have") {
t.Errorf("%s was not refused: %s", w, got)
}
}
odd := Manifest{Module: "odd", Claims: []Claim{{Name: "operator-channel", Scope: ScopeMesh, Capabilities: []string{"deliver"}}}}
if got := problemsFor(t, Shelf{"messenger": router(), "odd": odd}); !strings.Contains(got, "only a kinded bench's claim carries them") {
t.Errorf("capabilities on a seat that is not kinded stood: %s", got)
}
}
// novox/hq ADR 0259 §8: a module saying warrants, or speaking for a kind that proves its sender, runs as an
// account of its own — never carried by the machine's runtime, which runs as the operator's account.
func TestATrustedHolderMustRunAsAnAccountOfItsOwn(t *testing.T) {
r := router()
r.RunsAs = ""
if got := problemsFor(t, Shelf{"messenger": r}); !strings.Contains(got, "messenger must run as an account of its own") {
t.Errorf("a router on the machine's runtime stood: %s", got)
}
tg := aChannel("telegram", "telegram")
tg.Claims[0].Capabilities = []string{"choice", "verified-sender"}
if got := problemsFor(t, Shelf{"messenger": router(), "telegram": tg}); !strings.Contains(got, "telegram must run as an account of its own") {
t.Errorf("a verified channel on the machine's runtime stood: %s", got)
}
desk := aChannel("desk-channel", "desktop")
desk.Claims[0].Capabilities = []string{"choice"}
if got := problemsFor(t, Shelf{"messenger": router(), "desk-channel": desk}); got != "" {
t.Errorf("a channel proving nothing was held to it: %s", got)
}
// A kind that is `private` shows a link's code, which links an account as the operator: its holder is
// trusted with it, so it runs as its own account too (the confirmation review of 2026-10-09).
private := aChannel("desk-channel", "desktop")
private.Claims[0].Capabilities = []string{"choice", "private"}
if got := problemsFor(t, Shelf{"messenger": router(), "desk-channel": private}); !strings.Contains(got, "desk-channel must run as an account of its own") {
t.Errorf("a private channel on the machine's runtime stood: %s", got)
}
}
func TestRunsAsIsAnAccountOfTheModulesOwn(t *testing.T) {
ok := Manifest{Module: "telegram", RunsAs: "telegram", SecretsOwner: "telegram",
OwnSecrets: OwnSecrets{"broker": {Path: "/var/lib/telegram/broker"}},
Resources: []map[string]any{{"id": "account", "type": "user", "name": "telegram"}}}
if got := RunsAsProblems(ok); len(got) != 0 {
t.Fatalf("a sound runs-as was refused: %v", got)
}
for want, change := range map[string]func(*Manifest){
"never root": func(m *Manifest) { m.RunsAs, m.SecretsOwner = "root", "root" },
"not an account name": func(m *Manifest) { m.RunsAs = "${machine:account}" },
"which it does not make": func(m *Manifest) { m.Resources = nil },
"declares no own secret": func(m *Manifest) { m.OwnSecrets = nil },
"they are the account's own": func(m *Manifest) { m.SecretsOwner = "" },
} {
m := ok
m.Resources = append([]map[string]any(nil), ok.Resources...)
m.OwnSecrets = OwnSecrets{"broker": {Path: "/x"}}
change(&m)
if got := strings.Join(RunsAsProblems(m), "; "); !strings.Contains(got, want) {
t.Errorf("want %q, got %q", want, got)
}
}
}
-25
View File
@@ -64,13 +64,6 @@ type Claim struct {
// text/template over one piece — its fields and `module` — in the tool's own grammar (novox/hq ADR // text/template over one piece — its fields and `module` — in the tool's own grammar (novox/hq ADR
// 0255). The data is the mesh's, the format the holder's, as a module's facts template is. // 0255). The data is the mesh's, the format the holder's, as a module's facts template is.
Renders map[string]string `json:"renders,omitempty"` Renders map[string]string `json:"renders,omitempty"`
// Kind is the kind this module holds a kinded bench as (novox/hq ADR 0234 §2, ADR 0259): `telegram`,
// `desktop`. Refused on any other seat, and a second claim of one kind is refused.
Kind string `json:"kind,omitempty"`
// Capabilities are what a channel of this kind promises, from the fixed vocabulary
// channel-capabilities/1 (novox/hq ADR 0234 §2): the router judges an answer by these, read from the
// controller's record of this claim and never from the channel.
Capabilities []string `json:"capabilities,omitempty"`
} }
// ServesFor is what this claim offers a seat's protocol: the verbs it names, else the module's // ServesFor is what this claim offers a seat's protocol: the verbs it names, else the module's
@@ -647,13 +640,6 @@ type Manifest struct {
// cannot use. // cannot use.
SecretsOwner string `json:"secrets-owner,omitempty"` SecretsOwner string `json:"secrets-owner,omitempty"`
// RunsAs is the account this module's tools bundle runs as, in a runtime of its own on a bus account of
// its own (novox/hq ADR 0259 §8): never the machine's runtime, which runs as the operator's account and
// carries every module on the machine. The account is one the module makes (a `user` resource of that
// name), owns its secrets (`secrets-owner`), and is neither root nor the operator's. Required of a module
// that says a warrant, or speaks for a channel kind that proves its sender.
RunsAs string `json:"runs-as,omitempty"`
// Keeps is where this module wants every operator-sealed secret in the mesh written — the // Keeps is where this module wants every operator-sealed secret in the mesh written — the
// vault's field, and so far nobody else's (novox/hq ADR 0085, amended). // vault's field, and so far nobody else's (novox/hq ADR 0085, amended).
// //
@@ -905,16 +891,6 @@ type Artifact struct {
// image built from this same module's own repository, the same as every other artifact. // image built from this same module's own repository, the same as every other artifact.
Context *ArtifactContext `json:"context,omitempty"` Context *ArtifactContext `json:"context,omitempty"`
// Compiles is the Go package an image's recipe compiles, relative to the tree it is built in — its
// context, or the module's directory (novox/hq ADR 0267 rule 1).
//
// **What a build is made from is derived, never listed.** The build seat reads the package's import
// closure and hands the recipe that and nothing else, so a merge elsewhere in a shared repository is
// no change to this image, and a recipe copying a file outside the closure fails by name instead of
// building from something no merge is mapped onto. Empty for an image that compiles no Go: its whole
// tree is its build source, as before.
Compiles string `json:"compiles,omitempty"`
// System is the operating system this artifact is compiled for, for a bundle whose output is a // System is the operating system this artifact is compiled for, for a bundle whose output is a
// binary rather than portable code (novox/hq ADR 0142). // binary rather than portable code (novox/hq ADR 0142).
// //
@@ -1644,7 +1620,6 @@ func ParseManifest(raw []byte) (Manifest, error) {
// prefix. Whether a seat anybody names exists, and whether a holder answers for it, are // prefix. Whether a seat anybody names exists, and whether a holder answers for it, are
// facts about the catalogue and are checked at registration (CatalogueProblems). // facts about the catalogue and are checked at registration (CatalogueProblems).
problems = append(problems, declaredSeatProblems(m)...) problems = append(problems, declaredSeatProblems(m)...)
problems = append(problems, RunsAsProblems(m)...)
if m.Computed != "" && len(m.Resources) > 0 { if m.Computed != "" && len(m.Resources) > 0 {
// One or the other. A module that both ships files and has them computed would leave // One or the other. A module that both ships files and has them computed would leave
// nobody able to say where a given file came from. // nobody able to say where a given file came from.
+3 -13
View File
@@ -120,16 +120,6 @@ type Held struct {
Node string Node string
Module string Module string
Site string Site string
// Kind is the kind a kinded bench is held as (novox/hq ADR 0234 §2): each kind is its own holder.
Kind string
}
// heldKey is what one holder holds: the seat, and its kind on a kinded bench.
func heldKey(claim, kind string) string {
if kind == "" {
return canonicalSeat(claim)
}
return canonicalSeat(claim) + "/" + kind
} }
// Resolution is what a node should run, and why. // Resolution is what a node should run, and why.
@@ -884,7 +874,7 @@ func checkClaims(modules []Manifest, node Node, elsewhere []Held, holdings []Hel
// **One seat under either of its names** (novox/hq ADR 0122): a manifest registered before // **One seat under either of its names** (novox/hq ADR 0122): a manifest registered before
// a rename claims the former name, and one written after it the current — two claimants of // a rename claims the former name, and one written after it the current — two claimants of
// one seat, compared by the seat they resolve to and not by how each spelled it. // one seat, compared by the seat they resolve to and not by how each spelled it.
seat := heldKey(c.Name, c.Kind) seat := canonicalSeat(c.Name)
if other, taken := byScope[scope][seat]; taken { if other, taken := byScope[scope][seat]; taken {
problems = append(problems, fmt.Sprintf( problems = append(problems, fmt.Sprintf(
"%s and %s both claim %q, and only one thing may hold it per %s", "%s and %s both claim %q, and only one thing may hold it per %s",
@@ -893,14 +883,14 @@ func checkClaims(modules []Manifest, node Node, elsewhere []Held, holdings []Hel
} }
byScope[scope][seat] = m.Module byScope[scope][seat] = m.Module
held = append(held, Held{Claim: c.Name, Scope: scope, Node: node.Name, held = append(held, Held{Claim: c.Name, Scope: scope, Node: node.Name,
Module: m.Module, Site: node.Site, Kind: c.Kind}) Module: m.Module, Site: node.Site})
} }
} }
// And against the rest of the mesh, for the scopes that reach past this machine. // And against the rest of the mesh, for the scopes that reach past this machine.
for _, h := range held { for _, h := range held {
for _, e := range elsewhere { for _, e := range elsewhere {
if e.Node == node.Name || heldKey(e.Claim, e.Kind) != heldKey(h.Claim, h.Kind) || e.Scope != h.Scope { if e.Node == node.Name || canonicalSeat(e.Claim) != canonicalSeat(h.Claim) || e.Scope != h.Scope {
continue continue
} }
switch h.Scope { switch h.Scope {
+1 -10
View File
@@ -51,8 +51,7 @@ func TestTheResolverForwardsToFixedUpstreamsAndNeverReadsResolvConf(t *testing.T
"\nno-resolv\n", "\nserver=1.1.1.1\n", "\nserver=8.8.8.8\n", "\nno-resolv\n", "\nserver=1.1.1.1\n", "\nserver=8.8.8.8\n",
// The private address and loopback, never a LAN's (novox/hq ADR 0194): a device that is not a // The private address and loopback, never a LAN's (novox/hq ADR 0194): a device that is not a
// member cannot reach what the mesh's names point at. // member cannot reach what the mesh's names point at.
"\nlisten-address=127.0.0.1\n", "\nlisten-address=${machine:address}\n", "\nlisten-address=127.0.0.1\n", "\nlisten-address=${machine:address}\n", "\nbind-dynamic\n",
// No hosts file and no operator's files: the mesh's resolver answers every node (ADR 0199). // No hosts file and no operator's files: the mesh's resolver answers every node (ADR 0199).
"\nno-hosts\n", "\nno-hosts\n",
"\nconf-file=" + m.Facts["zones"].Path + "\n", "\nconf-file=" + m.Facts["zones"].Path + "\n",
@@ -63,14 +62,6 @@ func TestTheResolverForwardsToFixedUpstreamsAndNeverReadsResolvConf(t *testing.T
t.Errorf("the resolver's configuration lacks %q:\n%s", strings.TrimSpace(want), config) t.Errorf("the resolver's configuration lacks %q:\n%s", strings.TrimSpace(want), config)
} }
} }
// Bound to its addresses, one way or the other. mesh-catalog PR 161 (novox/hq issue 348) moves it from
// bind-dynamic, which closed the private address's listener when a bridge teardown failed its re-read
// of the machine's addresses, to bind-interfaces. This test reads the catalogue beside it, which may be
// on either side of that merge, so it takes both; once the catalogue's main has it, bind-dynamic is
// refused here.
if !strings.Contains(config, "\nbind-interfaces\n") && !strings.Contains(config, "\nbind-dynamic\n") {
t.Errorf("the resolver's configuration binds neither by bind-interfaces nor by bind-dynamic:\n%s", config)
}
// By address and never by interface: dnsmasq admits a query by the interface it arrives on // By address and never by interface: dnsmasq admits a query by the interface it arrives on
// when told one, and a container's query to the private address arrives on the runtime's // when told one, and a container's query to the private address arrives on the runtime's
// bridge — `interface=mesh0` dropped every such query, silently (novox/hq issue 110). // bridge — `interface=mesh0` dropped every such query, silently (novox/hq issue 110).

Some files were not shown because too many files have changed in this diff Show More