Compare 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
167 changed files with 822 additions and 16758 deletions
-3
View File
@@ -306,9 +306,6 @@ func answer(ctx context.Context, publisher builder.Publisher, on, workspace stri
for _, r := range built.Read {
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))
}
}
+1 -16
View File
@@ -5,7 +5,6 @@ import (
"errors"
"fmt"
"os"
"strings"
"sync"
"time"
@@ -180,21 +179,7 @@ func (a *actor) release() {
// holderOf is this process as the lease's holder.
func holderOf(instance string) lease.Holder {
host, _ := os.Hostname()
build := runningBuild()
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))
return lease.Holder{Instance: instance, Host: host, Build: version}
}
// 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
// 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, now time.Time) (named, confined bool, why string, err error) {
func agentConfined(ctx context.Context, inv *inventory.Inventory, node string) (named, confined bool, why string, err error) {
n, err := inv.NodeByName(ctx, node)
if err != nil {
return false, false, "", err
@@ -62,7 +61,7 @@ func agentConfined(ctx context.Context, inv *inventory.Inventory, node string, n
if err != nil {
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
}
@@ -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
// search before that is itself the urgent condition: the longest a search is expected to take (link.RootSearchQuiet,
// the engine's own value; novox/hq issue 361 — the search runs to completion, with no bound of its own), counted
// from when this controller first saw it waiting, never from the engine's start. Quiet raises nothing; it never
// makes the agent account confined, which only a healthy verdict from a complete, fresh search does.
const searchQuietFor = link.RootSearchQuiet
// search before that is itself the urgent condition: the engine's bound on one search (link.RootSearchBound, the
// engine's own value), counted from when this controller first saw it waiting, never from the engine's start.
const searchQuietFor = link.RootSearchBound
// The kinds of an agent account's verdict, for the quiet a search earns.
const (
@@ -209,10 +206,10 @@ func probeAgentAccounts(ctx context.Context, d *doctor) ([]conditions.Observatio
if confined {
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
// 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 {
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",
orNoneKnown(n.Account))}
}
_, confined, why, err := agentConfined(ctx, inv, n.Name, time.Now())
_, confined, why, err := agentConfined(ctx, inv, n.Name)
if err != nil {
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)}
+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 {
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)
}
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") {
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
// older than the engine lets one judge — the agent account is not judged, and the search runs to its end with no
// bound (novox/hq issue 361). DA does not raise that as urgent within searchQuietFor, counted from when this
// controller first saw it waiting — never from the engine's own "since", which a restart resets, so an engine
// restarted in a loop does not keep it quiet. The account is still not confined, and `node show` still says not
// judged; a search that failed, or a way to root found, is urgent at once.
// After every node-engine restart its search for setuid programs runs for up to its bound, and the agent account
// is not judged until it ends. DA does not raise that as urgent while the search is within its bound, counted from
// when this controller first saw it waiting — never from the engine's own "since", which a restart resets, so an
// engine restarted in a loop does not keep it quiet. The account is still not confined, and `node show` still
// says not judged; a search that failed, or a way to root found, is urgent at once.
func TestASearchStillRunningAfterARestartIsNotUrgent(t *testing.T) {
if searchQuietFor != rootsearch.Quiet {
t.Fatalf("the quiet is %s and the node-engine's %s: they are one value", searchQuietFor, rootsearch.Quiet)
}
// 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)
if searchQuietFor != rootsearch.Bound {
t.Fatalf("the quiet is %s and the node-engine's bound %s: they are one value", searchQuietFor, rootsearch.Bound)
}
open := aMesh(t)
ctx := t.Context()
@@ -252,7 +246,7 @@ func TestASearchStillRunningAfterARestartIsNotUrgent(t *testing.T) {
if found := say(link.StateUnknown, running); len(found) != 0 {
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)
}
// The engine restarted again and again, each statement's own since fresh: the controller's clock runs on.
-937
View File
@@ -1,937 +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; one the router refused is asked again after a wait
// that grows with each refusal in a row, at most half an hour, so a refusal is never believed longer. 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
// askRefusedRetryMost is the longest a refusal is believed without asking again (novox/hq issue 369): the
// router refused while its channels had not yet said they could send, the channels could send twenty
// minutes later, and the controller repeated that refusal for eleven hours. A refused ask is asked again
// after askEvery, then twice as long after each refusal in a row, never longer than this — and at once when
// the channels change.
askRefusedRetryMost = 30 * time.Minute
// askRefusedCounted is how far back refusals in a row are counted for that wait.
askRefusedCounted = 6 * time.Hour
// askVerdictWait is how long an ask made again after a refusal waits for the router's word before it is
// taken as taken: the router refuses an ask as it reads it, so a refusal comes within seconds. Meanwhile
// the condition saying it was not delivered stands as it was, neither cleared nor raised again.
askVerdictWait = 2 * time.Minute
)
// refusedRetryAfter is how long a part refused n times in a row waits before it is asked again.
func refusedRetryAfter(n int) time.Duration {
wait := askEvery
for i := 1; i < n && wait < askRefusedRetryMost; i++ {
wait *= 2
}
return min(wait, askRefusedRetryMost)
}
// 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 asked
// again at once when the condition's answers or the channels change, and otherwise after a wait that grows
// with each refusal in a row (refusedRetryAfter, novox/hq issue 369).
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"`
// Proposal is a settings layer proposed through a verb (proposals.go, novox/hq ADR 0277): about no
// condition, set on Approve by the serving controller itself, and left alone by the reconciling of conditions.
Proposal *settingsProposal `json:"proposal,omitempty"`
}
// ofACondition says an ask is one of a condition's: not a rehearsal, not a proposal.
func (r asked) ofACondition() bool { return !r.Rehearsal && r.Proposal == nil }
// 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
// setLayer sets a proposed settings layer on the operator's warrant (novox/hq ADR 0277); nil cannot.
setLayer setOnWarrant
// 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
// What is open and not a condition's — a rehearsal, a proposal — still counts toward what the router holds
// open for the controller (askMostOpen); expired unanswered, it is kept so, as the router says it too.
otherOpen := 0
for _, r := range all {
if r.State == askOpen && !r.ofACondition() && !now.Before(r.Ask.Expires) {
if _, err := a.store.Change(ctx, r.ID, func(x *asked) bool {
if x.State != askOpen || x.Acted != "" {
return false
}
x.State, x.Ended, x.Acted = string(asks.OutcomeExpired), now, "nothing: the ask expired unanswered"
return true
}); err != nil {
return err
}
continue
}
if r.State == askOpen && !r.ofACondition() {
otherOpen++
}
if r.State == askOpen && r.ofACondition() {
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: for a condition's ask, and for a proposal's or a
// rehearsal's alike.
if a.routerRecord != nil {
var lookedUp []asked
for _, r := range byCondition {
lookedUp = append(lookedUp, r)
}
for _, r := range all {
if r.State == askOpen && !r.ofACondition() {
lookedUp = append(lookedUp, r)
}
}
for _, r := range lookedUp {
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.ofACondition() {
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
}
}
}
// Refusals in a row, by part: those since the last ask that was not refused, within askRefusedCounted.
// superseded: a part asked since its last refusal, by an ask the router did not refuse — open, answered,
// expired or cancelled. Its refusal is history then, never said again (the review of PR 198: an answered
// retry brought the refusal back).
inRow, superseded := map[string]int{}, map[string]asked{}
for k, last := range refused {
for _, r := range all {
if partKey(r.Condition, r.Part) == k && r.State != string(asks.OutcomeRefused) && r.State != askUnsent &&
r.Opened.After(last.Opened) && r.Opened.After(superseded[k].Opened) {
superseded[k] = r
}
}
var since time.Time
for _, r := range all {
if partKey(r.Condition, r.Part) == k && r.State != string(asks.OutcomeRefused) && r.State != askUnsent &&
!r.Opened.After(last.Opened) && r.Opened.After(since) {
since = r.Opened
}
}
for _, r := range all {
if partKey(r.Condition, r.Part) == k && r.State == string(asks.OutcomeRefused) && r.Opened.After(since) &&
now.Sub(r.Opened) < askRefusedCounted {
inRow[k]++
}
}
}
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 := otherOpen
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) {
cur, held := byCondition[key]
ended := r.Ended
if ended.IsZero() {
ended = r.Opened
}
later, asked := superseded[key]
waiting := !held && !asked && r.Channels == channels && now.Sub(ended) < refusedRetryAfter(inRow[key])
// Asked again now (the wait over), or lately and the router's word not in yet: said as it was until
// that word, so the condition neither clears nor is raised again at each try.
retrying := !held && !asked && !waiting
verdictDue := held && asked && later.ID == cur.ID && now.Sub(cur.Opened) < askVerdictWait
if waiting || retrying || verdictDue {
if !saidUnasked {
unasked, saidUnasked = append(unasked, c), true
}
if r.Warrant != nil && r.Warrant.Words != "" {
refusedWords = append(refusedWords, r.Warrant.Words)
}
}
if waiting {
continue // refused lately, and nothing it was refused for has changed: asked again after the wait
}
}
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: "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 || r.Proposal != nil // a rehearsal and a proposal are 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
outcome := "done"
switch {
case r.Rehearsal && act.Verb == rehearsalVerb:
// A rehearsal's answer performs nothing: it is recorded below as the operator's decision.
case r.Proposal != nil && act.Verb == proposalVerb:
// A proposed settings layer, set by this controller itself on Approve (novox/hq ADR 0277): the act's
// digest of the values is held to the record's own values before anything is set.
outcome, acted = a.decideProposal(ctx, *r, act, w)
if acted == nil && outcome != "" && !strings.HasPrefix(outcome, "nothing") {
outcome = "done: " + outcome
}
case act.Arguments["silence"] != "":
acted = a.silence(ctx, act.Arguments["silence"], conditions.MaxSilence, byWords(w), why)
default:
acted = a.call(ctx, act, args)
}
ended := a.now()
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)
}
}
-810
View File
@@ -1,810 +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 at once; issue 369: nor is the refusal believed
// for ever — it is asked again after a wait that doubles with each refusal in a row, and at once when the
// channels change.
func TestAnAskTheRouterRefusedIsAskedAgainAfterAGrowingWait(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()}
refuse := func() {
t.Helper()
sent := r.asksSent(t)
refusal, _ := json.Marshal(asks.Warrant{Ask: sent[len(sent)-1].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())
first := r.asksSent(t)[0]
refuse()
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)
}
// Each wait: nothing asked before it ends, asked once when it has.
for i, wait := range []time.Duration{time.Minute, 2 * time.Minute, 4 * time.Minute, 8 * time.Minute,
16 * time.Minute, 30 * time.Minute, 30 * time.Minute} {
before := len(r.asksSent(t))
r.now = r.now.Add(wait - 10*time.Second)
_ = r.a.reconcile(context.Background())
if n := len(r.asksSent(t)); n != before {
t.Fatalf("refusal %d: asked again before %s", i+1, wait)
}
r.now = r.now.Add(10 * time.Second)
_ = r.a.reconcile(context.Background())
if n := len(r.asksSent(t)); n != before+1 {
t.Fatalf("refusal %d: not asked again after %s (%d asks)", i+1, wait, n)
}
refuse()
}
before := len(r.asksSent(t))
channels = "channel/telegram=telegram@anchor[choice,verified-sender]own:true"
_ = r.a.reconcile(context.Background())
if n := len(r.asksSent(t)); n != before+1 {
t.Errorf("not asked again once the channels changed: %d", n-before)
}
}
// 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)
}
}
// novox/hq issue 369: the router refused while its channels had not yet said they could send; they could twenty
// minutes later, and "Questions for you not delivered" repeated that refusal for eleven hours, escalating on it.
// The ask is made again; while the router's word on it is awaited the condition stands unchanged (neither
// cleared nor raised again); once the router took it, the condition clears; a new refusal is said in its own
// words.
func TestARefusalIsAskedAgainAndTheUndeliveredConditionClearsOnceTaken(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] }
r.a.channels = func(context.Context) string { return "channel/telegram=telegram@anchor[choice]own:true" }
r.open = []conditions.Condition{unitsCondition()}
refuse := func(words string) {
t.Helper()
sent := r.asksSent(t)
refusal, _ := json.Marshal(asks.Warrant{Ask: sent[len(sent)-1].ID, Asker: "mesh-controller",
Outcome: asks.OutcomeRefused, Words: words, At: r.now})
if err := r.a.Decided(context.Background(), refusal); err != nil {
t.Fatal(err)
}
}
_ = r.a.reconcile(context.Background())
refuse("no channel can carry any of its answers now: telegram: telegram has not said whether it can send")
_ = r.a.reconcile(context.Background())
if got := last(); len(got) != 1 || !strings.Contains(got[0].Summary, "telegram has not said") {
t.Fatalf("the refusal, said as %+v", got)
}
// The wait ends: asked again; until the router's word on it is in, the condition stands as it was.
r.now = r.now.Add(askEvery)
_ = r.a.reconcile(context.Background())
if n := len(r.asksSent(t)); n != 2 {
t.Fatalf("not asked again: %d asks", n)
}
if got := last(); len(got) != 1 {
t.Fatalf("cleared while the router's word on the new ask was awaited: the condition would flap")
}
r.now = r.now.Add(askVerdictWait / 2)
_ = r.a.reconcile(context.Background())
if got := last(); len(got) != 1 {
t.Fatalf("cleared inside the verdict wait: the condition would flap")
}
// Refused again, now for a reason of today: said in those words.
refuse("no channel can carry any of its answers now: telegram: no account is linked")
_ = r.a.reconcile(context.Background())
if got := last(); len(got) != 1 || !strings.Contains(got[0].Summary, "no account is linked") ||
strings.Contains(got[0].Summary, "has not said") {
t.Fatalf("an old refusal's words repeated: %+v", got)
}
// Asked again after the doubled wait, and taken: no refusal comes, and the condition clears.
r.now = r.now.Add(2 * askEvery)
_ = r.a.reconcile(context.Background())
if n := len(r.asksSent(t)); n != 3 {
t.Fatalf("not asked again after the doubled wait: %d asks", n)
}
r.now = r.now.Add(askVerdictWait)
_ = r.a.reconcile(context.Background())
if got := last(); len(got) != 0 {
t.Fatalf("taken, and still said undelivered: %+v", got)
}
if n := len(r.asksSent(t)); n != 3 {
t.Fatalf("an ask taken was asked again: %d asks", n)
}
// The operator answers it; the condition stays open (its fix takes a while): the old refusal is not said again.
taken := r.asksSent(t)[2]
r.store.Change(context.Background(), taken.ID, func(x *asked) bool {
x.State, x.Ended = string(asks.OutcomeChosen), r.now
return true
})
for i := 0; i < 5; i++ {
r.now = r.now.Add(askEvery)
_ = r.a.reconcile(context.Background())
if got := last(); len(got) != 0 {
t.Fatalf("an answered ask brought its old refusal back: %+v", got)
}
}
// Its explanation opens with one verdict, not two.
r.open = append(r.open, heldCondition())
_ = r.a.reconcile(context.Background())
refuse("no channel can carry any of its answers now")
_ = r.a.reconcile(context.Background())
if e := conditions.Verdict(last()[0].Needs, last()[0].Explanation); strings.Count(e, "Needs you") != 1 {
t.Errorf("the explanation says its verdict twice: %q", e)
}
}
-339
View File
@@ -1,339 +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),
// A proposed settings layer is set by this controller itself on the warrant (novox/hq ADR 0277).
setLayer: setLayerIn(open),
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)
}
File diff suppressed because it is too large Load Diff
-760
View File
@@ -1,760 +0,0 @@
package main
import (
"bytes"
"context"
"encoding/json"
"io"
"os"
"slices"
"strings"
"testing"
"time"
"github.com/novox/mesh-controller/internal/catalogue"
"github.com/novox/mesh-controller/internal/inventory"
"github.com/novox/mesh-controller/internal/link"
)
// The suite's tests of a merge, but for the merge window's own (this file's), plan the merge as it is heard: a
// window of nothing closes at once, so a merge with no walk open is cut into its walk at once, as every merge
// was planned before novox/hq ADR 0276. The window's tests set it through the controller's settings.
func init() { mergeWindowDefault = 0 }
// windowed is a mesh whose controller's settings give a merge window of 90 seconds and at most 10 minutes, as its
// settings file says them, and
// modules built from the catalogue (app, notes) and from three repositories of their own.
func windowed(t *testing.T) *stores {
t.Helper()
open := aMesh(t)
ctx := t.Context()
inv := open.inventory
settings := t.TempDir() + "/merge-window.json"
if err := os.WriteFile(settings, []byte(`{"merge-window": "90s", "merge-window-at-most": "10m"}`), 0o600); err != nil {
t.Fatal(err)
}
t.Setenv(mergeWindowFileVar, settings)
if err := inv.RegisterModule(ctx, catalogue.Manifest{Module: "mesh-controller", Version: "1"},
inventory.Source{Repository: "novox/mesh-controller", Seat: "git", Ref: "main", BuiltFrom: "c0",
Head: "c0"}); err != nil {
t.Fatal(err)
}
for _, m := range []struct{ module, repository, path string }{
{"app", "novox/mesh-catalog", "modules/app"},
{"notes", "novox/mesh-catalog", "modules/notes"},
{"one", "novox/one", ""},
{"two", "novox/two", ""},
{"three", "novox/three", ""},
} {
if err := inv.RegisterModule(ctx, catalogue.Manifest{Module: m.module, Version: "1"},
inventory.Source{Repository: m.repository, Seat: "git", Path: m.path, Ref: "main", BuiltFrom: "c0",
Head: "c0"}); err != nil {
t.Fatal(err)
}
}
return open
}
// t0 is the moment a test's clock starts: merges are dated by it, and heard after it.
var t0 = time.Now().UTC().Truncate(time.Second)
// catalogueMerge is a merge of the catalogue changing one module's directory, made at a moment.
func catalogueMerge(commit, module string, made time.Time) link.SourceMoved {
return link.SourceMoved{Owner: "novox", Repo: "mesh-catalog", Base: "main", Commit: commit,
MergedAt: made.Format(time.RFC3339Nano), Paths: []string{"modules/" + module + "/module.json"},
ModuleDirs: []string{"modules/" + module}, ModuleDirsSaid: true}
}
// repoMerge is a merge of a repository of one module's own.
func repoMerge(repo, commit string, made time.Time) link.SourceMoved {
return link.SourceMoved{Owner: "novox", Repo: repo, Base: "main", Commit: commit,
MergedAt: made.Format(time.RFC3339Nano), Paths: []string{"main.go"}}
}
// hear is a merge heard at a moment.
func hear(t *testing.T, open *stores, m link.SourceMoved, now time.Time) {
t.Helper()
if err := (following{open: open}).hearMerge(t.Context(), m, now); err != nil {
t.Fatal(err)
}
}
// cutAt is the cutter looking at a moment.
func cutAt(t *testing.T, open *stores, now time.Time) {
t.Helper()
if err := cutBatchesHeld(t.Context(), open, now); err != nil {
t.Fatal(err)
}
}
// walks is every plan record that is a walk, newest first, and the batches not yet cut.
func walks(t *testing.T, open *stores) (walks, batches []inventory.Plan) {
t.Helper()
recent, err := open.inventory.RecentPlans(t.Context(), 50)
if err != nil {
t.Fatal(err)
}
for _, p := range recent {
if p.Batch() {
batches = append(batches, p)
} else {
walks = append(walks, p)
}
}
return walks, batches
}
// The replay of issue 362: two catalogue merges 18 seconds apart are one batch, cut once into one walk at the
// later commit, which names both merges — the earlier carried by the later.
func TestTwoMergesSecondsApartAreOneWalkAtTheLaterCommit(t *testing.T) {
open := windowed(t)
asked := asksWithPaths(t)
claude := catalogueMerge("553b7191claude", "app", t0)
dunst := catalogueMerge("48bda475dunst", "notes", t0.Add(17*time.Second))
hear(t, open, claude, t0.Add(time.Second))
hear(t, open, dunst, t0.Add(18*time.Second))
if ws, bs := walks(t, open); len(ws) != 0 || len(bs) != 1 {
t.Fatalf("within the window: %d walk(s), %d batch(es)", len(ws), len(bs))
}
cutAt(t, open, t0.Add(18*time.Second+89*time.Second))
if ws, _ := walks(t, open); len(ws) != 0 {
t.Fatalf("cut before the window closed: %+v", ws)
}
cutAt(t, open, t0.Add(18*time.Second+90*time.Second))
ws, bs := walks(t, open)
if len(ws) != 1 || len(bs) != 0 {
t.Fatalf("after the window: %d walk(s), %d batch(es)", len(ws), len(bs))
}
w := ws[0]
if w.Commit != dunst.Commit || len(w.Commits) != 1 || w.Commits[0].Commit != dunst.Commit {
t.Fatalf("the walk is at %s %+v, not the later commit", w.Commit, w.Commits)
}
for _, m := range []string{"app", "notes"} {
if _, in := w.Modules[m]; !in {
t.Fatalf("the walk does not build %s, which one of its merges moved: %v", m, w.Modules)
}
}
want := []inventory.PlanMerge{{Repository: "novox/mesh-catalog", Commit: claude.Commit, Carried: dunst.Commit},
{Repository: "novox/mesh-catalog", Commit: dunst.Commit}}
if w.Delivery == nil || !slices.Equal(w.Delivery.Merges, want) {
t.Fatalf("the walk answers %+v, want %+v", w.Delivery, want)
}
if len(*asked) != 2 || (*asked)[0][2] != "main" || (*asked)[1][2] != "main" {
t.Fatalf("the walk did not ask its modules at the branch, which holds the commit it carries: %v", *asked)
}
// Heard again, as the bus may hand it over twice: never a second merge, nor a second walk.
hear(t, open, claude, t0.Add(5*time.Minute))
if ws, bs := walks(t, open); len(ws) != 1 || len(bs) != 0 {
t.Fatalf("a merge heard twice made %d walk(s) and %d batch(es)", len(ws), len(bs))
}
}
// Merges of three repositories in one window are one walk, one commit for each.
func TestThreeRepositoriesInOneWindowAreOneWalk(t *testing.T) {
open := windowed(t)
asksRecorded(t)
for i, r := range []string{"one", "two", "three"} {
hear(t, open, repoMerge(r, "c-"+r, t0.Add(time.Duration(i)*20*time.Second)),
t0.Add(time.Duration(i)*20*time.Second+time.Second))
}
cutAt(t, open, t0.Add(41*time.Second+90*time.Second))
ws, bs := walks(t, open)
if len(ws) != 1 || len(bs) != 0 {
t.Fatalf("%d walk(s), %d batch(es)", len(ws), len(bs))
}
w := ws[0]
if len(w.Commits) != 3 || len(w.Delivery.Merges) != 3 {
t.Fatalf("the walk carries %+v and answers %+v", w.Commits, w.Delivery.Merges)
}
for _, r := range []string{"one", "two", "three"} {
if w.CommitOf("novox/"+r) != "c-"+r {
t.Fatalf("the walk carries %s at %q", r, w.CommitOf("novox/"+r))
}
if _, in := w.Modules[r]; !in {
t.Fatalf("the walk does not build %s: %v", r, w.Modules)
}
}
// Each module's ask is recorded at its own repository's commit.
for _, r := range []string{"one", "two", "three"} {
q, found, err := open.inventory.BuildRequestByID(t.Context(), w.Modules[r].Build)
if err != nil || !found || q.Commit != "c-"+r {
t.Fatalf("%s's build was not recorded at its own commit: %+v %v %v", r, q, found, err)
}
}
}
// Each merge restarts the window: a merge at second 80 keeps the batch open until second 170; the window
// closes at most ten minutes after its first merge however busy.
func TestAMergeRestartsTheWindowAndTheMaximumClosesIt(t *testing.T) {
open := windowed(t)
asksWithPaths(t)
hear(t, open, repoMerge("one", "c1", t0), t0)
hear(t, open, repoMerge("two", "c2", t0.Add(80*time.Second)), t0.Add(80*time.Second))
cutAt(t, open, t0.Add(100*time.Second))
if ws, _ := walks(t, open); len(ws) != 0 {
t.Fatal("the window closed 90 seconds after its first merge, not after its last")
}
cutAt(t, open, t0.Add(170*time.Second))
if ws, _ := walks(t, open); len(ws) != 1 || len(ws[0].Commits) != 2 {
t.Fatalf("the window did not close 90 seconds after its last merge: %+v", ws)
}
busy := windowed(t)
asksWithPaths(t)
var last time.Time
for i := 0; i < 12; i++ {
last = t0.Add(time.Duration(i) * time.Minute)
repo := []string{"one", "two", "three"}[i%3]
hear(t, busy, repoMerge(repo, "c"+string(rune('a'+i)), last), last)
if ws, _ := walks(t, busy); len(ws) > 0 {
if i != 10 {
t.Fatalf("a busy window closed at merge %d (minute %d), not at its maximum", i, i)
}
break
}
}
ws, _ := walks(t, busy)
if len(ws) != 1 {
t.Fatalf("ten minutes of merges a minute apart were never cut: %d walk(s)", len(ws))
}
}
// `plans` lists the batch being assembled first, in the operator's words: how long is left, when at the latest,
// what is grouped, and that the plan is not yet calculated. It says so on the bus as plan-moved, too.
func TestTheAssemblingBatchIsShown(t *testing.T) {
open := windowed(t)
asksWithPaths(t)
var said []inventory.Plan
was := inventory.PlanSaved
inventory.PlanSaved = func(p inventory.Plan) { said = append(said, p) }
t.Cleanup(func() { inventory.PlanSaved = was })
now := time.Now().UTC()
hear(t, open, catalogueMerge("553b7191claude", "app", now.Add(-20*time.Second)), now.Add(-19*time.Second))
hear(t, open, catalogueMerge("48bda475dunst", "notes", now.Add(-2*time.Second)), now.Add(-time.Second))
hear(t, open, repoMerge("one", "a6bc0931one", now), now)
out := captured(t, func() error { return plansCommand(t.Context(), nil) })
first := strings.SplitN(out, "\n", 2)[0]
for _, want := range []string{"assembling: ", " s left (at the latest ", "grouped: novox/mesh-catalog@48bda475 " +
"(answers 553b7191), novox/one@a6bc0931; plan not yet calculated"} {
if !strings.Contains(first, want) {
t.Fatalf("plans' first line %q does not say %q", first, want)
}
}
if len(said) == 0 || said[len(said)-1].State != inventory.PlanAssembling || said[len(said)-1].Delivery.Batch == nil ||
len(said[len(said)-1].Delivery.Merges) != 3 {
t.Fatalf("the batch was not said as assembling with its merges: %+v", said)
}
}
// captured is what a command printed.
func captured(t *testing.T, run func() error) string {
t.Helper()
r, w, err := os.Pipe()
if err != nil {
t.Fatal(err)
}
was := os.Stdout
os.Stdout = w
runErr := run()
os.Stdout = was
_ = w.Close()
var b bytes.Buffer
_, _ = io.Copy(&b, r)
if runErr != nil {
t.Fatal(runErr)
}
return b.String()
}
// One walk at a time: a merge heard while a walk runs joins the next batch, which is cut when the walk ends;
// the walk that started is never superseded.
func TestAMergeDuringAWalkJoinsTheNextBatch(t *testing.T) {
open := windowed(t)
asksWithPaths(t)
hear(t, open, repoMerge("one", "c1", t0), t0)
cutAt(t, open, t0.Add(90*time.Second))
ws, _ := walks(t, open)
if len(ws) != 1 || !ws[0].Open() {
t.Fatalf("the first batch was not cut: %+v", ws)
}
first := ws[0]
hear(t, open, repoMerge("two", "c2", t0.Add(2*time.Minute)), t0.Add(2*time.Minute))
cutAt(t, open, t0.Add(5*time.Minute))
ws, bs := walks(t, open)
if len(ws) != 1 || len(bs) != 1 || bs[0].State != inventory.PlanQueued || bs[0].Delivery.Batch.Behind != first.ID {
t.Fatalf("the merge during the walk: %d walk(s), batches %+v", len(ws), bs)
}
if got, _ := open.inventory.PlanByID(t.Context(), first.ID); !got.Open() {
t.Fatalf("the started walk was %s by the next merge", got.State)
}
if !strings.HasPrefix(batchWords(bs[0], t0.Add(5*time.Minute)), "queued behind "+first.ID) {
t.Fatalf("a queued batch reads %q", batchWords(bs[0], t0.Add(5*time.Minute)))
}
first.State = inventory.PlanDone
if err := open.inventory.SavePlan(t.Context(), &first); err != nil {
t.Fatal(err)
}
cutAt(t, open, t0.Add(6*time.Minute))
ws, bs = walks(t, open)
if len(ws) != 2 || len(bs) != 0 || ws[0].Commit != "c2" {
t.Fatalf("the queued batch was not cut when the walk ended: %+v %+v", ws, bs)
}
}
// A walk waiting for its delivery's word is folded into the next batch's walk, which answers its merges; it
// names the walk that took it over.
func TestAWaitingWalkIsFoldedIntoTheNextBatch(t *testing.T) {
open := windowed(t)
asksWithPaths(t)
ctx := t.Context()
if err := open.inventory.RegisterModule(ctx, catalogue.Manifest{Module: "mesh-delivery", Version: "1",
Claims: []catalogue.Claim{{Name: catalogue.DeliverySeat, Scope: catalogue.ScopeMesh}}},
inventory.Source{Repository: "novox/mesh-catalog", Seat: "git", Path: "modules/mesh-delivery", Ref: "main",
BuiltFrom: "c0", Head: "c0"}); err != nil {
t.Fatal(err)
}
if _, err := open.inventory.Assign(ctx, "anchor", "mesh-delivery"); err != nil {
t.Fatal(err)
}
hear(t, open, repoMerge("one", "c1", t0), t0)
cutAt(t, open, t0.Add(90*time.Second))
ws, _ := walks(t, open)
if len(ws) != 1 || !ws[0].Waiting() {
t.Fatalf("the walk does not wait for its word: %+v", ws)
}
waiting := ws[0]
hear(t, open, repoMerge("two", "c2", t0.Add(2*time.Minute)), t0.Add(2*time.Minute))
cutAt(t, open, t0.Add(2*time.Minute+90*time.Second))
ws, bs := walks(t, open)
if len(bs) != 0 || len(ws) != 2 {
t.Fatalf("%d walk(s), %d batch(es)", len(ws), len(bs))
}
folded, err := open.inventory.PlanByID(ctx, waiting.ID)
if err != nil {
t.Fatal(err)
}
newer := ws[0]
if folded.State != inventory.PlanSuperseded || folded.Delivery.TakenOverBy != newer.ID {
t.Fatalf("the waiting walk is %s, taken over by %q", folded.State, folded.Delivery.TakenOverBy)
}
if len(newer.Delivery.Merges) != 2 || newer.CommitOf("novox/one") != "c1" || newer.CommitOf("novox/two") != "c2" {
t.Fatalf("the newer walk answers %+v", newer.Delivery.Merges)
}
if _, in := newer.Modules["one"]; !in {
t.Fatalf("what the folded walk was to build is not built: %v", newer.Modules)
}
}
// A merge heard after a later merge of its branch was walked is answered by that walk when it builds all it
// moves — named at once on a walk done — and joins the next batch when it does not.
func TestALateMergeIsAnsweredByTheWalkOfTheLaterOne(t *testing.T) {
open := windowed(t)
asksWithPaths(t)
ctx := t.Context()
later := catalogueMerge("48bda475dunst", "notes", t0.Add(17*time.Second))
hear(t, open, later, t0.Add(18*time.Second))
cutAt(t, open, t0.Add(2*time.Minute))
ws, _ := walks(t, open)
w := ws[0]
w.State = inventory.PlanDone
if err := open.inventory.SavePlan(ctx, &w); err != nil {
t.Fatal(err)
}
// Moves only notes, which the done walk built from the branch after it: answered there, at once.
hear(t, open, catalogueMerge("553b7191early", "notes", t0), t0.Add(15*time.Minute))
got, _ := open.inventory.PlanByID(ctx, w.ID)
if got.State != inventory.PlanDone || len(got.Delivery.Merges) != 2 ||
got.Delivery.Merges[0] != (inventory.PlanMerge{Repository: "novox/mesh-catalog", Commit: "553b7191early",
Carried: later.Commit}) {
t.Fatalf("the late merge is not named by the walk that carried it: %+v", got.Delivery.Merges)
}
// Moves app, which that walk never built: the next batch walks it.
hear(t, open, catalogueMerge("1111aaaaapp", "app", t0.Add(time.Second)), t0.Add(16*time.Minute))
_, bs := walks(t, open)
if len(bs) != 1 || bs[0].Delivery.Merges[0].Commit != "1111aaaaapp" {
t.Fatalf("a late merge moving what the walk never built did not join the next batch: %+v", bs)
}
}
// A failed walk marks nothing delivered: its earlier merges are walked alone, on their own commit, the newest
// first; the first delivered answers the older ones, and the search stops.
func TestAFailedWalkWalksItsEarlierMergesAlone(t *testing.T) {
open := windowed(t)
asked := asksWithPaths(t)
ctx := t.Context()
// Made before the cut marks their modules seen, as merges are: walked again, they are news all the same.
oldest := catalogueMerge("aaaa0001", "app", t0.Add(-3*time.Minute))
middle := catalogueMerge("bbbb0002", "app", t0.Add(-3*time.Minute+10*time.Second))
newest := catalogueMerge("cccc0003", "notes", t0.Add(-3*time.Minute+20*time.Second))
for i, m := range []link.SourceMoved{oldest, middle, newest} {
hear(t, open, m, t0.Add(time.Duration(i)*10*time.Second+time.Second))
}
cutAt(t, open, t0.Add(2*time.Minute))
ws, _ := walks(t, open)
failed := ws[0]
failed.State, failed.Note = inventory.PlanFailed, "notes failed to build in tier 0"
if err := open.inventory.SavePlan(ctx, &failed); err != nil {
t.Fatal(err)
}
cutAt(t, open, t0.Add(3*time.Minute))
got, _ := open.inventory.PlanByID(ctx, failed.ID)
if got.State != inventory.PlanFailed || len(got.Delivery.Merges) != 3 {
t.Fatalf("the failed walk's record changed: %s %+v", got.State, got.Delivery.Merges)
}
ws, _ = walks(t, open)
alone := ws[0]
if alone.ID == failed.ID || alone.Commit != middle.Commit || len(alone.Delivery.Merges) != 1 ||
alone.Delivery.Merges[0] != (inventory.PlanMerge{Repository: "novox/mesh-catalog", Commit: middle.Commit}) {
t.Fatalf("the newest earlier merge was not walked alone on its own commit: %+v", alone)
}
if _, in := alone.Modules["app"]; !in || (*asked)[len(*asked)-1] != [3]string{"novox/mesh-catalog", "modules/app",
middle.Commit} {
t.Fatalf("the merge walked alone does not build app at its own commit: %v, asked %v", alone.Modules, *asked)
}
// Retried, the failed walk would name merges the search answers now.
if _, err := retryPlan(ctx, open, failed.ID); err == nil || !strings.Contains(err.Error(), "walked alone") {
t.Fatalf("a failed walk whose merges are searched was retried: %v", err)
}
alone.State = inventory.PlanDone
if err := open.inventory.SavePlan(ctx, &alone); err != nil {
t.Fatal(err)
}
cutAt(t, open, t0.Add(4*time.Minute))
ws, bs := walks(t, open)
if ws[0].ID != alone.ID || len(bs) != 0 {
t.Fatalf("the search went on after a merge was delivered: %+v", ws[0])
}
done, _ := open.inventory.PlanByID(ctx, alone.ID)
if len(done.Delivery.Merges) != 2 || done.Delivery.Merges[0] != (inventory.PlanMerge{Repository: "novox/mesh-catalog",
Commit: oldest.Commit, Carried: middle.Commit}) {
t.Fatalf("the oldest merge is not answered by the walk that delivered the one after it: %+v", done.Delivery.Merges)
}
// A stopped walk starts no search: a person ended it.
hear(t, open, catalogueMerge("dddd0004", "app", t0.Add(5*time.Minute)), t0.Add(5*time.Minute))
hear(t, open, catalogueMerge("eeee0005", "notes", t0.Add(5*time.Minute+time.Second)), t0.Add(5*time.Minute+time.Second))
cutAt(t, open, t0.Add(8*time.Minute))
ws, _ = walks(t, open)
if _, err := stopWalk(ctx, open.inventory, ws[0].ID, "mesh-delivery for jochen", "not now"); err != nil {
t.Fatal(err)
}
cutAt(t, open, t0.Add(9*time.Minute))
if again, _ := walks(t, open); again[0].ID != ws[0].ID {
t.Fatalf("a stopped walk's earlier merge was walked alone: %+v", again[0])
}
}
// A delivery group's order holds inside the walk (ADR 0249 unchanged): merges of two repositories from one head
// branch name, the node-engine's before the controller's, are tiered so; without the group, one tier.
func TestAGroupsOrderBecomesTiersInsideTheWalk(t *testing.T) {
for _, grouped := range []bool{true, false} {
open := windowed(t)
asksWithPaths(t)
ctx := t.Context()
for _, m := range []string{"mesh-host"} {
if err := open.inventory.RegisterModule(ctx, catalogue.Manifest{Module: m, Version: "1"},
inventory.Source{Repository: "novox/" + m, Seat: "git", Ref: "main", BuiltFrom: "c0", Head: "c0"}); err != nil {
t.Fatal(err)
}
}
host := repoMerge("mesh-host", "h1", t0)
ctl := repoMerge("mesh-controller", "k1", t0.Add(time.Second))
host.Head, ctl.Head = "feat/together", "feat/together"
if !grouped {
ctl.Head = "feat/alone"
}
hear(t, open, ctl, t0.Add(2*time.Second))
hear(t, open, host, t0.Add(3*time.Second))
cutAt(t, open, t0.Add(2*time.Minute))
ws, _ := walks(t, open)
if len(ws) != 1 {
t.Fatalf("%d walks", len(ws))
}
tiers := ws[0].Tiers
if grouped {
if len(tiers) != 2 || !slices.Equal(tiers[0], []string{"mesh-host"}) ||
!slices.Equal(tiers[1], []string{"mesh-controller"}) {
t.Fatalf("the group's order is not the walk's tiers: %v", tiers)
}
} else if len(tiers) != 1 {
t.Fatalf("two merges of no group were ordered: %v", tiers)
}
}
}
// A restarted controller resumes the window where it stood: the batch, its merges and their times are in the
// store, a merge handed over again is not doubled, and the batch is cut when its window closes.
func TestABatchSurvivesARestart(t *testing.T) {
open := windowed(t)
asksWithPaths(t)
hear(t, open, repoMerge("one", "c1", t0), t0)
hear(t, open, repoMerge("two", "c2", t0.Add(30*time.Second)), t0.Add(30*time.Second))
// A new controller: nothing of the first one's but the store.
again, err := openStores(t.Context())
if err != nil {
t.Fatal(err)
}
t.Cleanup(again.Close)
hear(t, again, repoMerge("one", "c1", t0), t0.Add(60*time.Second)) // the bus hands it over again
cutAt(t, again, t0.Add(119*time.Second))
if ws, bs := walks(t, again); len(ws) != 0 || len(bs) != 1 || len(bs[0].Delivery.Merges) != 2 {
t.Fatalf("the restarted controller's batch: %d walk(s), %+v", len(ws), bs)
}
cutAt(t, again, t0.Add(120*time.Second))
ws, bs := walks(t, again)
if len(ws) != 1 || len(bs) != 0 || len(ws[0].Delivery.Merges) != 2 {
t.Fatalf("the restarted controller did not cut the batch at its window: %+v %+v", ws, bs)
}
}
// The window's settings reach the controller in its settings file, read at every look; one that says no
// duration, or no file, is the default.
func TestTheWindowIsTheControllersSetting(t *testing.T) {
file := t.TempDir() + "/merge-window.json"
t.Setenv(mergeWindowFileVar, file)
for _, c := range []struct {
content string
window, atMost time.Duration
}{
{`{"merge-window": "60s", "merge-window-at-most": "5m"}`, time.Minute, 5 * time.Minute},
{`{"merge-window": 30, "merge-window-at-most": ""}`, 30 * time.Second, mergeWindowAtMostDefault},
{`{"merge-window": "soon"}`, mergeWindowDefault, mergeWindowAtMostDefault},
} {
if err := os.WriteFile(file, []byte(c.content), 0o600); err != nil {
t.Fatal(err)
}
if w, m := mergeWindowOf(); w != c.window || m != c.atMost {
t.Errorf("%s reads as %s and %s, want %s and %s", c.content, w, m, c.window, c.atMost)
}
}
t.Setenv(mergeWindowFileVar, file+".gone")
if w, m := mergeWindowOf(); w != mergeWindowDefault || m != mergeWindowAtMostDefault {
t.Errorf("no file reads as %s and %s", w, m)
}
}
// S16 names every merge a waiting walk answers, never one commit a supersession may have ended (issue 362).
func TestAWaitingWalkNamesTheMergesItAnswers(t *testing.T) {
f := calm(t0)
f.waits = []waitFacts{{id: "plan-2", repository: "novox/mesh-catalog", commit: "48bda475dunst", awaits: "mesh-delivery",
since: t0.Add(-31 * time.Minute), merges: []inventory.PlanMerge{
{Repository: "novox/mesh-catalog", Commit: "553b7191claude", Carried: "48bda475dunst"},
{Repository: "novox/mesh-catalog", Commit: "48bda475dunst"}}}}
got := watchWaits(f)
if len(got) != 1 || !strings.Contains(got[0].Summary, "novox/mesh-catalog@553b7191") ||
!strings.Contains(got[0].Summary, "novox/mesh-catalog@48bda475") {
t.Fatalf("the waiting walk does not name both merges: %+v", got)
}
}
// A merge heard after a later merge of its branch was cut is named by that walk however its modules read since
// the cut, in a repository of one module (review of this change); a walk that never built what it moves names
// nothing, and the merge joins the next batch.
func TestALateMergeOfAOneModuleRepositoryIsNamed(t *testing.T) {
open := windowed(t)
asksWithPaths(t)
ctx := t.Context()
c2 := repoMerge("one", "c2", t0.Add(-50*time.Second))
hear(t, open, c2, t0)
cutAt(t, open, t0.Add(2*time.Minute))
ws, _ := walks(t, open)
w := ws[0]
w.State = inventory.PlanDone
if err := open.inventory.SavePlan(ctx, &w); err != nil {
t.Fatal(err)
}
hear(t, open, repoMerge("one", "c1", t0.Add(-60*time.Second)), t0.Add(15*time.Minute))
got, _ := open.inventory.PlanByID(ctx, w.ID)
if len(got.Delivery.Merges) != 2 || got.Delivery.Merges[0] != (inventory.PlanMerge{Repository: "novox/one",
Commit: "c1", Carried: "c2"}) {
t.Fatalf("the late merge was not named by the walk that carried it: %+v", got.Delivery.Merges)
}
// A walk of two that built only two: a late merge of one is not its to name.
hear(t, open, repoMerge("two", "d2", t0.Add(16*time.Minute)), t0.Add(16*time.Minute))
cutAt(t, open, t0.Add(18*time.Minute))
ws, _ = walks(t, open)
w = ws[0]
delete(w.Modules, "two")
w.State = inventory.PlanDone
if err := open.inventory.SavePlan(ctx, &w); err != nil {
t.Fatal(err)
}
hear(t, open, repoMerge("two", "d1", t0.Add(15*time.Minute)), t0.Add(20*time.Minute))
if got, _ := open.inventory.PlanByID(ctx, w.ID); len(got.Delivery.Merges) != 1 {
t.Fatalf("a walk that never built what the late merge moves named it: %+v", got.Delivery.Merges)
}
if _, bs := walks(t, open); len(bs) != 1 || bs[0].Delivery.Merges[0].Commit != "d1" {
t.Fatalf("the late merge did not join the next batch: %+v", bs)
}
}
// One walk at a time holds against the delivery's word too: a waiting walk is not let go while another started.
func TestAWaitingWalkIsNotLetGoBesideAStartedOne(t *testing.T) {
open := windowed(t)
asksWithPaths(t)
ctx := t.Context()
hear(t, open, repoMerge("one", "c1", t0), t0)
cutAt(t, open, t0.Add(2*time.Minute))
started, _ := walks(t, open)
waiting := inventory.Plan{ID: "plan-waiting", Repository: "novox/two", Branch: "main", Commit: "d1", Created: t0,
State: inventory.PlanBuilding, Tiers: [][]string{{"two"}}, Modules: map[string]*inventory.PlanModule{"two": {}},
Delivery: &inventory.PlanDelivery{Awaits: catalogue.DeliverySeat}}
if err := open.inventory.SavePlan(ctx, &waiting); err != nil {
t.Fatal(err)
}
if _, err := letGo(ctx, open.inventory, waiting.ID, catalogue.DeliverySeat, "its turn"); err == nil ||
!strings.Contains(err.Error(), started[0].ID) {
t.Fatalf("a waiting walk was let go beside %s: %v", started[0].ID, err)
}
}
// A file a merge of the batch removed and a later one brought back is not removed; one removed last is.
func TestARemovedFileIsTheLastMergesWord(t *testing.T) {
ev := func(commit string, paths, removed []string) inventory.BatchedMerge {
m := link.SourceMoved{Owner: "novox", Repo: "one", Base: "main", Commit: commit, Paths: paths, Removed: removed}
b, _ := json.Marshal(m)
return inventory.BatchedMerge{Repository: "novox/one", Branch: "main", Commit: commit, Event: b,
Merged: t0.Add(time.Duration(len(commit)) * time.Second)}
}
got := combinedMerges([]inventory.BatchedMerge{
ev("a", []string{"x/module.json", "y/module.json"}, []string{"x/module.json", "y/module.json"}),
ev("bb", []string{"x/module.json"}, nil),
ev("ccc", []string{"z.go"}, nil)})
if len(got) != 1 || got[0].Commit != "ccc" || !slices.Equal(got[0].Removed, []string{"y/module.json"}) ||
len(got[0].Paths) != 3 {
t.Fatalf("combined as %+v", got)
}
}
// The catch-up hands over what the bus lost after its branch's later merges were cut, and the record keeps it:
// an earlier merge is named by the walk that carried it; a merge made before a cut and heard after it, which
// no later merge carries, joins the next batch — neither reads as history and is dropped (review of this change).
func TestTheCatchUpKeepsWhatACutMadeHistory(t *testing.T) {
open := windowed(t)
asksWithPaths(t)
ctx := t.Context()
base := time.Now().UTC().Add(-time.Hour).Truncate(time.Second)
hear(t, open, repoMerge("one", "c2", base.Add(10*time.Second)), base.Add(11*time.Second))
cutAt(t, open, base.Add(2*time.Minute))
ws, _ := walks(t, open)
w := ws[0]
w.State = inventory.PlanDone
if err := open.inventory.SavePlan(ctx, &w); err != nil {
t.Fatal(err)
}
lost := []link.AnnouncedMerge{
{SourceMoved: repoMerge("one", "c1", base), At: base.Add(time.Second)},
{SourceMoved: repoMerge("one", "c3lost", base.Add(20*time.Second)), At: base.Add(21 * time.Second)},
}
catalogued := func(ctx context.Context) ([]inventory.Entry, map[string][]inventory.ReadRepository, error) {
entries, err := open.inventory.Catalogued(ctx)
if err != nil {
return nil, nil, err
}
read, err := readForPlanning(ctx, open.inventory)
return entries, read, err
}
if err := catchUpOnMerges(ctx, time.Now(), unheardMerges{announcedList(lost), open.inventory}, catalogued,
(following{open}).SourceMoved, t.Logf); err != nil {
t.Fatal(err)
}
got, _ := open.inventory.PlanByID(ctx, w.ID)
if len(got.Delivery.Merges) != 2 || got.Delivery.Merges[0] != (inventory.PlanMerge{Repository: "novox/one",
Commit: "c1", Carried: "c2"}) {
t.Fatalf("the earlier merge the catch-up handed over is not named by the walk that carried it: %+v",
got.Delivery.Merges)
}
if _, kept, err := open.inventory.MergeOf(ctx, "novox/one", "c3lost"); err != nil || !kept {
t.Fatalf("the merge made before the cut and heard after it was dropped: %v %v", kept, err)
}
}
// A merge on the controller's own path never shares a batch with one that waits for mesh-delivery's word
// (decided during the build, 2026-10-10): the two are batches of their own, cut one after the other; a walk on
// the own path folds no waiting catalogue walk but batches its merges again behind it; the catalogue's walk
// still waits for the word, so no catalogue delivery skips its turn.
func TestAMergeOnTheControllersPathNeverSharesABatch(t *testing.T) {
open := windowed(t)
asked := asksWithPaths(t)
ctx := t.Context()
if err := open.inventory.RegisterModule(ctx, catalogue.Manifest{Module: "mesh-delivery", Version: "1",
Claims: []catalogue.Claim{{Name: catalogue.DeliverySeat, Scope: catalogue.ScopeMesh}}},
inventory.Source{Repository: "novox/mesh-catalog", Seat: "git", Path: "modules/mesh-delivery", Ref: "main",
BuiltFrom: "c0", Head: "c0"}); err != nil {
t.Fatal(err)
}
if _, err := open.inventory.Assign(ctx, "anchor", "mesh-delivery"); err != nil {
t.Fatal(err)
}
hear(t, open, catalogueMerge("aaaa0001", "app", t0), t0)
hear(t, open, repoMerge("mesh-controller", "k1", t0.Add(10*time.Second)), t0.Add(10*time.Second))
hear(t, open, catalogueMerge("bbbb0002", "notes", t0.Add(20*time.Second)), t0.Add(20*time.Second))
_, bs := walks(t, open)
var ownBatch, catalogueBatch inventory.Plan
for _, b := range bs {
if b.OwnPath() {
ownBatch = b
} else {
catalogueBatch = b
}
}
if len(bs) != 2 || ownBatch.ID == "" || catalogueBatch.ID == "" || len(catalogueBatch.Delivery.Merges) != 2 ||
len(ownBatch.Delivery.Merges) != 1 {
t.Fatalf("a controller merge and two catalogue merges in one window: %+v", bs)
}
if !strings.Contains(batchWords(ownBatch, t0.Add(30*time.Second)), "own path") {
t.Fatalf("the own-path batch does not say so: %q", batchWords(ownBatch, t0.Add(30*time.Second)))
}
// The catalogue batch, the older, is cut first and waits for its word; the controller's batch is cut next:
// the waiting walk is batched again behind it, not folded into it.
cutAt(t, open, t0.Add(2*time.Minute))
ws, _ := walks(t, open)
if len(ws) != 1 || !ws[0].Waiting() {
t.Fatalf("the catalogue batch was not cut into a waiting walk: %+v", ws)
}
catalogueWalk := ws[0]
cutAt(t, open, t0.Add(2*time.Minute+5*time.Second))
ws, bs = walks(t, open)
var ownWalk inventory.Plan
for _, w := range ws {
if w.Open() {
ownWalk = w
}
}
if ownWalk.ID == "" || ownWalk.Waiting() || ownWalk.CommitOf("novox/mesh-controller") != "k1" {
t.Fatalf("the controller's batch was not cut into a started walk: %+v", ws)
}
for _, m := range []string{"app", "notes"} {
if _, in := ownWalk.Modules[m]; in {
t.Fatalf("the controller's walk builds %s, a catalogue module: it skipped its turn", m)
}
}
for _, a := range *asked {
if a[1] == "modules/app" || a[1] == "modules/notes" {
t.Fatalf("a catalogue module was asked without the word: %v", *asked)
}
}
folded, _ := open.inventory.PlanByID(ctx, catalogueWalk.ID)
if folded.State != inventory.PlanSuperseded || len(bs) != 1 || folded.Delivery.TakenOverBy != bs[0].ID ||
bs[0].State != inventory.PlanQueued || bs[0].Delivery.Batch.Behind != ownWalk.ID || bs[0].OwnPath() ||
len(bs[0].Delivery.Merges) != 2 {
t.Fatalf("the waiting catalogue walk is %s (taken over by %q); batches %+v", folded.State,
folded.Delivery.TakenOverBy, bs)
}
// The controller's walk done, the catalogue batch is cut and waits for its word with both merges.
ownWalk.State = inventory.PlanDone
if err := open.inventory.SavePlan(ctx, &ownWalk); err != nil {
t.Fatal(err)
}
cutAt(t, open, t0.Add(3*time.Minute))
ws, bs = walks(t, open)
if len(bs) != 0 || ws[0].ID != folded.Delivery.TakenOverBy || !ws[0].Waiting() || len(ws[0].Delivery.Merges) != 2 {
t.Fatalf("the catalogue batch was not cut into a waiting walk once the controller's ended: %+v %+v", ws, bs)
}
for _, m := range []string{"app", "notes"} {
if _, in := ws[0].Modules[m]; !in {
t.Fatalf("the catalogue walk does not build %s: %v", m, ws[0].Modules)
}
}
}
-103
View File
@@ -1,103 +0,0 @@
package main
import (
"context"
"fmt"
"time"
"github.com/novox/mesh-controller/internal/conditions"
"github.com/novox/mesh-controller/internal/inventory"
)
// **Every wait of a batch has a bound and a condition** (novox/hq ADR 0276 decision 9): a batch still
// assembling a minute past its maximum while no walk is open is a cut the controller failed to make (S18),
// and a batch waiting behind an open walk longer than that walk's bound waits on a walk gone wrong (S19).
// The kinds S18 and S19 raise.
const (
kindBatchNotCut = "batch-not-cut"
kindBatchBehindWalk = "batch-behind-walk"
)
// batchFacts is one batch not yet cut, as the watchdogs read it.
type batchFacts struct {
id, state, grouped string
// atMost is when its window closes at the latest; closed when it closed.
atMost, closed time.Time
// behind is the open walk it waits behind, and walkBound that walk's bound: a tier's bound for each of its
// tiers.
behind string
walkBound time.Duration
}
// gatherBatches is every batch not yet cut, with the bound of the walk it waits behind.
func gatherBatches(ctx context.Context, inv *inventory.Inventory, now time.Time) ([]batchFacts, error) {
batches, err := inv.Batches(ctx)
if err != nil || len(batches) == 0 {
return nil, err
}
bounds, err := measuredTierBounds(ctx, inv, now)
if err != nil {
return nil, err
}
var out []batchFacts
for _, b := range batches {
f := batchFacts{id: b.ID, state: b.State, grouped: groupedWords(b)}
if w := b.Delivery; w != nil && w.Batch != nil {
f.atMost, f.closed, f.behind = w.Batch.AtMost, w.Batch.ClosesAt, w.Batch.Behind
if f.atMost.Before(f.closed) {
f.closed = f.atMost
}
}
if f.behind != "" {
if walk, err := inv.PlanByID(ctx, f.behind); err == nil {
f.walkBound = bounds.of(walk.Repository) * time.Duration(max(1, len(walk.Tiers)))
}
}
out = append(out, f)
}
return out, nil
}
// watchBatchesNotCut is S18: a batch still assembling a minute past its maximum, while no walk is open.
func watchBatchesNotCut(f *signalFacts) []conditions.Observation {
var out []conditions.Observation
for _, b := range f.batches {
if b.state != inventory.PlanAssembling || b.atMost.IsZero() || f.now.Sub(b.atMost) <= batchLateAfter {
continue
}
late := f.now.Sub(b.atMost)
out = append(out, conditions.Observation{Scope: conditions.ScopePlan, ID: b.id, Kind: kindBatchNotCut,
Severity: conditions.Warning,
Summary: fmt.Sprintf("the batch %s is still assembling %s past its latest close (%s), with no walk open: "+
"the controller did not cut it; grouped: %s", b.id, ago(late), b.atMost.UTC().Format(time.RFC3339), b.grouped),
Said: fmt.Sprintf("assembling since %s past its maximum", ago(late)),
Headline: "Merged changes are not being delivered",
Explanation: fmt.Sprintf("Merges collected for one delivery should have been planned %s ago and were not. "+
"Nothing is lost; the mesh keeps them until it plans them.", humanDuration(late)),
Resolved: "Merged changes are being delivered again"})
}
return out
}
// watchBatchesBehind is S19: a batch waiting behind an open walk longer than that walk's bound, naming it.
func watchBatchesBehind(f *signalFacts) []conditions.Observation {
var out []conditions.Observation
for _, b := range f.batches {
if b.state != inventory.PlanQueued || b.behind == "" || b.walkBound <= 0 || f.now.Sub(b.closed) <= b.walkBound {
continue
}
in := f.now.Sub(b.closed)
out = append(out, conditions.Observation{Scope: conditions.ScopePlan, ID: b.id, Kind: kindBatchBehindWalk,
Severity: conditions.Warning,
Summary: fmt.Sprintf("the batch %s has waited %s behind the walk %s, longer than that walk's bound (%s); "+
"grouped: %s; `plans %s` says where that walk stands", b.id, ago(in), b.behind, ago(b.walkBound),
b.grouped, b.behind),
Said: fmt.Sprintf("queued behind %s for %s", b.behind, ago(in)),
Headline: "Merged changes wait behind a slow delivery",
Explanation: fmt.Sprintf("Merges collected for the next delivery have waited %s for the delivery before "+
"them, which is taking longer than it should. Nothing is lost.", humanDuration(in)),
Resolved: "Merged changes no longer wait behind a slow delivery"})
}
return out
}
-4
View File
@@ -166,10 +166,6 @@ func buildFrom(result link.BuildResult) inventory.Build {
for _, r := range result.Read {
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
for _, made := range result.Made {
announced = append(announced, inventory.Artifact{
+1 -5
View File
@@ -4,7 +4,6 @@ import (
"context"
"encoding/json"
"errors"
"fmt"
"hash/fnv"
"os"
"os/exec"
@@ -12,7 +11,6 @@ import (
"strings"
"sync"
"testing"
"time"
"github.com/novox/mesh-controller/internal/inventory"
"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) {
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
// 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",
failed := onTrunk("build-1791600000000000000", "novox/mesh-catalog", "git", "modules/sudo",
map[string]any{"module": "sudo", "version": "2"})
failed.Commit = "badbadbad0123456"
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 {
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
// the modules after it in the list hear nothing (novox/hq issue 208, where this runs on each send).
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 {
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))
@@ -147,37 +130,6 @@ func moduleConsumers(ctx context.Context, inv *inventory.Inventory) ([]broker.Mo
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.
func moduleConsumerCount(ctx context.Context, inv *inventory.Inventory) (int, error) {
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
// 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.
// 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 {
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
// 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.
//
// **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)
sort.Strings(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)
failed++
}
failed += len(problems)
// Between the manifests too: an identity against the bounds of the provisions it wants, which
// 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 {
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:
// 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")
changedText, err := git("diff", "--name-only", "origin/"+*base+"...HEAD")
if err != nil {
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())
}
}
// 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 {
return err
}
read, err := readForPlanning(ctx, inv)
read, err := inv.ReadRepositories(ctx)
if err != nil {
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...) },
// What status leads with changed: composed again soon (a nudge outside the serving controller
// does nothing).
Changed: func() { statusFrom.nudge(); askerFrom.nudge() },
Changed: statusFrom.nudge,
// Written under the lease, carrying its epoch (novox/hq to-be 45 §6).
Epoch: func() (uint64, error) { return theLease.epoch(context.WithoutCancel(ctx)) }}), nil
}
+1 -17
View File
@@ -103,16 +103,6 @@ func letGo(ctx context.Context, inv *inventory.Inventory, id, by, why string) (i
return p, fmt.Errorf("%s was let go by %s at %s already", p.ID, p.Delivery.By,
p.Delivery.Go.Local().Format("15:04:05"))
}
// **One walk at a time** (novox/hq ADR 0276): a walk that started is open, so this one waits for its end.
open, err := inv.OpenPlans(ctx)
if err != nil {
return p, err
}
for _, q := range open {
if q.ID != p.ID && q.Release == nil && !q.Waiting() {
return p, fmt.Errorf("%s is open (%s): one walk at a time — %s is let go once it ended", q.ID, q.Named(), p.ID)
}
}
now := time.Now().UTC()
p.Delivery.Go, p.Delivery.By, p.Delivery.Why = &now, by, why
p.Note = "let go by " + by + "; its first tier is asked next"
@@ -584,12 +574,6 @@ func deliveryCommand(ctx context.Context, args []string) error {
if walks, err = inv.OpenPlans(ctx); err != nil {
return err
}
// And the batch being assembled (novox/hq ADR 0276): never a walk to link or let go, shown.
batches, err := inv.Batches(ctx)
if err != nil {
return err
}
walks = append(walks, batches...)
recent, err := inv.RecentPlans(ctx, *limit)
if err != nil {
return err
@@ -620,7 +604,7 @@ func theGraph(ctx context.Context, inv *inventory.Inventory) ([]inventory.Entry,
if err != nil {
return nil, nil, nil, err
}
read, err := readForPlanning(ctx, inv)
read, err := inv.ReadRepositories(ctx)
if err != nil {
return nil, nil, nil, err
}
@@ -136,13 +136,12 @@ func TestTheDeliveryOwnerIsAskedOverTheBus(t *testing.T) {
if err != nil {
t.Fatal(err)
}
// And release and stop, which the operator's warrant chooses (novox/hq ADR 0259).
for _, verb := range []string{"stalled", "close", "release", "stop"} {
for _, verb := range []string{"stalled", "close"} {
if !slices.Contains(granted.Publish, link.SeatToolSubject(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)
}
conn, err := nats.Connect(testbus.URL(t))
+5 -8
View File
@@ -227,9 +227,7 @@ func TestAMergeWaitsForItsDeliverysWordAndThePersonsWordWorksWithoutIt(t *testin
t.Fatalf("with no holder on record the walk waited (%v) or asked %v", p.Waiting(), *asked)
}
// The holder on record: the next merge waits, asking nothing, and an advance asks nothing either. One walk
// is open at a time (novox/hq ADR 0276): the first ends before the next merge's batch is cut.
finish(p.ID)
// The holder on record: the next merge waits, asking nothing, and an advance asks nothing either.
if _, err := inv.Assign(ctx, "anchor", "mesh-delivery"); err != nil {
t.Fatal(err)
}
@@ -261,11 +259,10 @@ func TestAMergeWaitsForItsDeliverysWordAndThePersonsWordWorksWithoutIt(t *testin
t.Fatalf("the word was not kept: %+v %v", got.Delivery, err)
}
// The delivery's owner's own merge never waits for it. A walk that started is never taken over (novox/hq
// ADR 0276): the merge's batch is cut once it ended.
finish(p.ID)
// The delivery's owner's own merge never waits for it — and takes over what the older walk had not
// built (app, folded in: ADR 0218), which goes with it on the controller's own path.
p = merge("c3cccccccc", "modules/mesh-delivery/main.go")
if p.Waiting() || len(*asked) != 3 {
if p.Waiting() || len(*asked) != 4 {
t.Fatalf("mesh-delivery's own walk waited for mesh-delivery: %v %v", p.Waiting(), *asked)
}
@@ -283,7 +280,7 @@ func TestAMergeWaitsForItsDeliverysWordAndThePersonsWordWorksWithoutIt(t *testin
}
advanceHeld(ctx, open)
got, _ = inv.PlanByID(ctx, p.ID)
if got.Waiting() || !strings.HasPrefix(got.Delivery.By, "a person") || len(*asked) < 4 {
if got.Waiting() || !strings.HasPrefix(got.Delivery.By, "a person") || len(*asked) < 5 {
t.Fatalf("a person's word did not start the walk: %+v, asked %v", got.Delivery, *asked)
}
-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 " +
"newest statement, unable to become root without a person", From: "ADR 0266, ADR 0259 §8",
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",
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
+1 -12
View File
@@ -205,7 +205,7 @@ func gatherFacts(ctx context.Context, open *stores, busVersion string) (snapshot
if err != nil {
return snapshot.Facts{}, err
}
read, err := readForPlanning(ctx, inv)
read, err := inv.ReadRepositories(ctx)
if err != nil {
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,
Manifest: raw}
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))
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)
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,
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 " +
"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) {
+4 -158
View File
@@ -87,9 +87,6 @@ const (
healthWaiting
healthNotYet
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.
@@ -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
// not (issue 318 review): the only move whose wait for a new login is excused.
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
@@ -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,
short(r.From), r.Outcome, r.Why)
}
if why, superseded := f.supersededOn(module, machine); superseded {
return healthSuperseded, why
}
r, said := f.reports[machine]
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)
case r.Outcome == inventory.OutcomeFailed || r.Outcome == inventory.OutcomeRefused:
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)
}
// **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
// 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).
// raised since the judging began. The gate's own are not evidence about the build.
if f.judged {
if f.openErr != nil {
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
// reading, not a fault raised since the send: the gate reads it from the statement below (ADR 0254,
// 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
}
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 ||
slices.Contains(c.Subject.Also, machine))
// 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)
continue
}
@@ -477,21 +436,6 @@ func judgeMoves(ctx context.Context, open *stores, g *inventory.PlanGate, pairs
return "", err
}
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
// machine judged, apart from what is wrong with a module there, and never pinned on the module the
// 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 {
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) {
g.Broken = append(g.Broken, j.module)
if g.BrokenWhy == "" {
@@ -638,40 +575,6 @@ func judgeMoves(ctx context.Context, open *stores, g *inventory.PlanGate, pairs
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
// for a person, never another's (issue 318 review).
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))
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 {
notBack(err.Error())
return
@@ -940,53 +833,6 @@ func gateFailed(ctx context.Context, open *stores, p *inventory.Plan, module str
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
// 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.
+1 -2
View File
@@ -113,10 +113,9 @@ func aGateMesh(t *testing.T) *gateMesh {
}
for node, h := range g.health {
if h == healthNotYet {
// Not reported on the send: neither the send made last, nor the gate's own (issue 352).
f.rolledBack[node] = nil
r := f.reports[node]
r.Current, r.Declared, r.ReportedSequence = false, "", 0
r.Current = false
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).
{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},
// Stopping or starting a walk the operator chose on a warrant (novox/hq ADR 0259) is their decision.
{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 stop"},
{Verb: "plans close"},
// 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
// a warrant (ADR 0259).
{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)"},
// not trusted with it — either is a repair the owner should have made.
{Verb: "plans go"},
{Verb: "broker consumer-reset"},
// 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).
@@ -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
// 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.
// 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",
DecidedFor: []string{causeLeakedInLogs}},
}
@@ -251,9 +241,6 @@ func handActCommand(ctx context.Context, args []string) error {
if len(args) > 0 && args[0] == "drill" {
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], "-") {
return errors.New("hand-act record <what> --why <text> --cause <word> | hand-act drill <what> --why <text> " +
"| 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())
}
}
+1 -1
View File
@@ -625,7 +625,7 @@ func planStale(ctx context.Context, inv *inventory.Inventory, p inventory.Plan)
}
for _, newer := range recent {
if newer.ID == p.ID || !newer.Created.After(p.Created) || !repositoryMatches(newer.Repository, p.Repository) ||
newer.Branch != p.Branch || newer.State == inventory.PlanSuperseded || newer.Batch() {
newer.Branch != p.Branch || newer.State == inventory.PlanSuperseded {
continue
}
return inventory.PlanSuperseded, fmt.Sprintf("superseded by %s (%s %s), a newer merge of the same repository "+
-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)
}
}
-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())
}
}
undecided := undecidedParts(f)
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
}
why := "no machine says it any more"
@@ -117,59 +116,6 @@ func judgeNetworks(ctx context.Context, inv *inventory.Inventory, k *conditions.
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.
type pointed struct {
from string
-2
View File
@@ -78,8 +78,6 @@ func run() error {
return rotateCommand(ctx, args[1:])
case "ask":
return askCommand(ctx, args[1:])
case "rehearse":
return rehearseCommand(ctx, args[1:])
case "builds":
return buildsCommand(ctx, args[1:])
// 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,
Source: inventory.Source{Repository: mod.Repository, Path: mod.Path, BuiltFrom: mod.Commit}})
for _, r := range mod.Reads {
entry := 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})
}
read[mod.Name] = append(read[mod.Name], inventory.ReadRepository{Repository: r})
}
}
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 != "" {
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]] {
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])}
@@ -161,12 +155,8 @@ func runForMeshCLI(ctx context.Context, node string, asked link.CLIAsked, v cliV
}
cmd := selfCommand(ctx, line)
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
// fails saying so (ADR 0272 §5).
// No standard input: a command that reads one gets nothing, and fails saying so (ADR 0272 §5).
cmd.Stdin = nil
if len(asked.Stdin) > 0 {
cmd.Stdin = bytes.NewReader(asked.Stdin)
}
var stdout, stderr bytes.Buffer
cmd.Stdout, cmd.Stderr = &stdout, &stderr
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"},
}
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"}
}
@@ -39,13 +39,13 @@ func TestMeshCLIIsTheTerminalOnlyForTheControlNodesOperator(t *testing.T) {
refused string
why string
}{
{"the control-node's operator", "control", cliAsked("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 account", "control", cliAsked("agent", 1001, "status"), control, false, "operator account (operator) only", ""},
{"root", "control", cliAsked("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 the mesh does not know", "elsewhere", cliAsked("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"},
{"the control-node's operator", "control", asked("operator", 1000, "status"), control, true, "", "the controller's terminal"},
{"another node's operator", "laptop", asked("operator", 1000, "status"), control, false, "", "agents on laptop may run as operator"},
{"another account", "control", asked("agent", 1001, "status"), control, false, "operator account (operator) only", ""},
{"root", "control", asked("root", 0, "status"), control, false, "never root", ""},
{"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", asked("operator", 1000, "status"), control, false, "not a node this mesh knows", ""},
{"two control-nodes", "control", asked("operator", 1000, "status"), []string{"control", "laptop"}, false, "", "2 control-nodes"},
}
for _, c := range cases {
v := judgeCLI(c.node, c.asked, cliNodes, c.control)
@@ -70,7 +70,7 @@ func TestTheTerminalRunsWithoutAVerbAndAnOrdinaryCallNamesMeshCLI(t *testing.T)
t.Setenv(servedVar, "1")
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 {
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)
}
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" {
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")
ctx := context.Background()
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" {
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"`) {
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"} {
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 {
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 {
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) }
t.Cleanup(func() { cliJournal = was })
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"})
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")
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") {
@@ -213,7 +213,7 @@ func TestAnOrdinaryLineRunsNothingTheCommandVerbWouldRefuse(t *testing.T) {
if _, err := ordinaryLine(line); err == nil {
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 {
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"`) {
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") {
t.Fatalf("the terminal's line ran as %s", got)
}
+4 -58
View File
@@ -40,50 +40,6 @@ type merges interface {
AnnouncedMerges(ctx context.Context, since time.Time) ([]link.AnnouncedMerge, error)
}
// unheardMerges is the announcements of merges the controller has not heard (novox/hq ADR 0276): a merge kept
// in a batch is not acted on until its batch is cut, which can be past mergeGrace behind a long walk, and is
// not missed for that.
type unheardMerges struct {
merges
inv *inventory.Inventory
}
func (u unheardMerges) AnnouncedMerges(ctx context.Context, since time.Time) ([]link.AnnouncedMerge, error) {
all, err := u.merges.AnnouncedMerges(ctx, since)
if err != nil {
return nil, err
}
var out []link.AnnouncedMerge
for _, a := range all {
_, kept, err := u.inv.MergeOf(ctx, a.Owner+"/"+a.Repo, a.Commit)
if err != nil {
return nil, err
}
if !kept {
out = append(out, a)
}
}
return out, nil
}
// owedToTheRecord says a merge read as history is still owed to the record (novox/hq ADR 0276): the merges
// reader knows, when it keeps them (unheardMerges).
func owedToTheRecord(ctx context.Context, announced merges, m link.SourceMoved, entries []inventory.Entry,
read map[string][]inventory.ReadRepository) bool {
u, ok := announced.(unheardMerges)
if !ok {
return false
}
merged, err := time.Parse(time.RFC3339Nano, m.MergedAt)
if err != nil {
return false
}
raw := m
raw.MergedAt = ""
owed, err := owedLate(ctx, u.inv, m, merged, wouldMove(raw, entries, read))
return err == nil && owed
}
// catchingUpOnMerges reads back the forge's announcements on a timer, until the context ends.
func catchingUpOnMerges(ctx context.Context, open *stores, announced merges) {
f := following{open}
@@ -92,7 +48,7 @@ func catchingUpOnMerges(ctx context.Context, open *stores, announced merges) {
if err != nil {
return nil, nil, err
}
read, err := readForPlanning(ctx, open.inventory)
read, err := open.inventory.ReadRepositories(ctx)
return entries, read, err
}
failing := ""
@@ -105,7 +61,7 @@ func catchingUpOnMerges(ctx context.Context, open *stores, announced merges) {
case <-tick.C:
}
watchedMerges.begin()
err := catchUpOnMerges(ctx, time.Now(), unheardMerges{announced, open.inventory}, catalogued, f.SourceMoved, func(format string, args ...any) {
err := catchUpOnMerges(ctx, time.Now(), announced, catalogued, f.SourceMoved, func(format string, args ...any) {
fmt.Printf(format+"\n", args...)
})
// What the pass found is what S5 says (novox/hq to-be 45 §3); a pass that could not read
@@ -146,15 +102,10 @@ func catchUpOnMerges(ctx context.Context, now time.Time, announced merges,
return err
}
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 {
continue
}
if len(wouldMove(a.SourceMoved, entries, read)) == 0 && !owedToTheRecord(ctx, announced, a.SourceMoved, entries, read) {
if len(wouldMove(a.SourceMoved, entries, read)) == 0 {
continue
}
if entries, read, err = catalogued(ctx); err != nil {
@@ -162,12 +113,7 @@ func catchUpOnMerges(ctx context.Context, now time.Time, announced merges,
}
moves := wouldMove(a.SourceMoved, entries, read)
if len(moves) == 0 {
if !owedToTheRecord(ctx, announced, a.SourceMoved, entries, read) {
continue
}
raw := a.SourceMoved
raw.MergedAt = ""
moves = wouldMove(raw, entries, read)
continue
}
var names []string
for _, e := range moves {
+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,
namesWords(plain, 3)),
Needs: needs,
Actions: moduleActions(node, rs),
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)
}
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 "+
"(`nox node hand-over %s <directory>` on the control-node)", machine, module, strings.Join(found, ", "), machine))
said = append(said, fmt.Sprintf("on %s, %s uses %s as found and waits for a person to hand it over "+
"(`mesh-host hand-over <directory>` at the machine)", machine, module, strings.Join(found, ", ")))
}
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. "+
"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)
// Plain words (ADR 0253): the line itself — `nox node hand-over <node> <path>` on the control-node (ADR 0272,
// 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.Needs = fmt.Sprintf("on %s, run mesh-host hand-over with the directory's path as root.", node)
o.Resolved = fmt.Sprintf("%s's directory on %s is the mesh's", module, node)
o.Actions = nil
return o
+17 -72
View File
@@ -10,12 +10,10 @@ import (
"os"
"sort"
"strings"
"time"
"github.com/novox/mesh-controller/internal/broker"
"github.com/novox/mesh-controller/internal/catalogue"
"github.com/novox/mesh-controller/internal/inventory"
"github.com/novox/mesh-controller/internal/link"
"github.com/novox/mesh-controller/internal/overlay"
)
@@ -399,9 +397,8 @@ func assignCommand(ctx context.Context, verb string, args []string) error {
func settingsCommand(ctx context.Context, args []string) error {
if len(args) == 0 {
return errors.New("settings show <module> [--node <node>] [--history], settings set <module> <file> " +
"[--node <node>] [--replace], settings clear <module> [--node <node>], settings preferences " +
"[<module>] [--node <node>], settings propose <module> <file | --clear> [--node <node>] [--replace], " +
"or settings proposals [<id>]")
"[--node <node>] [--replace], settings clear <module> [--node <node>], or settings preferences " +
"[<module>] [--node <node>]")
}
open, err := openStores(ctx)
if err != nil {
@@ -415,38 +412,12 @@ func settingsCommand(ctx context.Context, args []string) error {
// **What a set removes is refused unless meant** (novox/hq ADR 0217). A layer is replaced whole,
// and on 2026-10-05 setting one placement dropped a machine's whole layer for a module without a
// word (novox/hq issue 304). Adding and changing keys needs nothing; removing one needs this.
replace := set.Bool("replace", false, "for set and propose: remove the keys the new layer does not name")
replace := set.Bool("replace", false, "for set: remove the keys the new layer does not name")
history := set.Bool("history", false, "for show: the layers this one replaced, the latest first")
clear := set.Bool("clear", false, "for propose: propose that the layer be removed")
positionals, err := parseAround(set, args[1:])
if err != nil {
return err
}
switch args[0] {
case "propose":
// A trusted setting, proposed by anyone and set only on the operator's warrant (novox/hq ADR 0277):
// the stores are opened there, so the proposal and the verb's refusals below never meet.
if len(positionals) < 1 || len(positionals) > 2 {
return errors.New("settings propose <module> <settings.json | {…}> [--node <node>] [--replace], or settings propose <module> --clear [--node <node>]")
}
values := ""
if len(positionals) == 2 {
values = positionals[1]
}
return proposeCommand(ctx, positionals[0], *node, values, *clear, *replace)
case "proposals":
if len(positionals) > 1 || *node != "" || *clear || *replace || *history {
return errors.New("settings proposals [<id>]")
}
id := ""
if len(positionals) == 1 {
id = positionals[0]
}
return proposalsCommand(ctx, id)
}
if *clear {
return errors.New("--clear is for settings propose; a layer is cleared with settings clear")
}
where := "the whole mesh"
if *node != "" {
@@ -484,7 +455,7 @@ func settingsCommand(ctx context.Context, args []string) error {
"removal is meant (novox/hq ADR 0217). Nothing was changed",
positionals[0], where, strings.Join(removed, ", "), positionals[0], nodeFlag(*node))
}
if err := inv.SetSettingsBy(ctx, *node, positionals[0], values, setByWords()); err != nil {
if err := inv.SetSettings(ctx, *node, positionals[0], values); err != nil {
return err
}
fmt.Printf("%s on %s:\n", positionals[0], where)
@@ -525,12 +496,8 @@ func settingsCommand(ctx context.Context, args []string) error {
}
for _, p := range past {
shown, _ := json.MarshalIndent(p.Values, " ", " ")
by := ""
if p.SetBy != "" {
by = "; " + p.SetBy
}
fmt.Printf("%s on %s, until %s (%s%s):\n %s\n", positionals[0], where,
p.ReplacedAt.Local().Format("2006-01-02 15:04:05"), p.ReplacedBy, by, shown)
fmt.Printf("%s on %s, until %s (%s):\n %s\n", positionals[0], where,
p.ReplacedAt.Local().Format("2006-01-02 15:04:05"), p.ReplacedBy, shown)
}
return nil
}
@@ -546,14 +513,6 @@ func settingsCommand(ctx context.Context, args []string) error {
return err
}
fmt.Println(string(shown))
// And who set it (novox/hq ADR 0277): a layer the operator approved on their phone says so.
if setBy, setAt, has, err := inv.LayerOrigin(ctx, *node, positionals[0]); err != nil {
return err
} else if has && setBy != "" {
fmt.Printf(" %s\n", setBy)
} else if has {
fmt.Printf(" set at %s; who set it was not kept\n", setAt.Local().Format("2006-01-02 15:04"))
}
}
// Every value the module gives a default or a layer sets, and where it came from (novox/hq
// ADR 0262): the default, the mesh's layer, or this node's. Said after the layer, which stays
@@ -647,26 +606,17 @@ func settingsCommand(ctx context.Context, args []string) error {
if err := refuseTerminalSettingsThroughAVerb(ctx, inv, before, nil, positionals[0], where); err != nil {
return err
}
if err := inv.ClearSettingsBy(ctx, *node, positionals[0], setByWords()); err != nil {
if err := inv.ClearSettings(ctx, *node, positionals[0]); err != nil {
return err
}
fmt.Printf("%s on %s is back to what the module says\n", positionals[0], where)
return nil
default:
return fmt.Errorf("settings has no %q; it has show, set, clear, preferences, propose and proposals", args[0])
return fmt.Errorf("settings has no %q; it has show, set, clear and preferences", args[0])
}
}
// setByWords is who sets a layer from this process, as the layer keeps it (novox/hq ADR 0277): the caller at the
// controller's terminal, or through which verb.
func setByWords() string {
if verb, through := throughAVerb(); through {
return "set by " + link.Caller() + " through " + verb + " at " + time.Now().Local().Format("2006-01-02 15:04")
}
return "set at the controller's terminal by " + link.Caller() + " at " + time.Now().Local().Format("2006-01-02 15:04")
}
// describeEffective says each setting's value on a machine or the whole mesh, where it came from, and
// the module's default when a layer overrides it.
func describeEffective(module, where string, values []catalogue.SettingSource) string {
@@ -1157,13 +1107,10 @@ func refuseTerminalSettingsThroughAVerb(ctx context.Context, inv *inventory.Inve
}
// A trusted mergeable file takes any key, so its module's whole layer is the terminal's (novox/hq issue 340).
if files := catalogue.TrustedMergeable(shelf[module]); len(files) > 0 && !sameLayer(before, after) {
return fmt.Errorf("the settings of %s on %s are set at the controller's terminal (`mesh-cli` on the control-node) or on "+
"the operator's warrant, never through a verb alone (this line came through %q): %s merges whatever key a "+
"layer sets into a file root or a consumer trusts, so any key could point the module at a listener of the "+
"caller's, and whoever may call a verb includes agents (novox/hq issue 340; a file nothing trusts says "+
"\"trusted\": false). Propose it instead: the settings verb with propose puts the exact values to the "+
"operator on a channel that proves who answers, and the layer is set on their Approve (novox/hq ADR 0277). "+
"Nothing was changed",
return fmt.Errorf("the settings of %s on %s are set at the controller's terminal only (`mesh-cli` on the control-node), never through a verb (this "+
"line came through %q): %s merges whatever key a layer sets into a file root or a consumer trusts, so "+
"any key could point the module at a listener of the caller's, and whoever may call a verb includes "+
"agents (novox/hq issue 340; a file nothing trusts says \"trusted\": false). Nothing was changed",
module, where, verb, strings.Join(files, ", "))
}
for _, key := range catalogue.TerminalKeys(shelf[module]) {
@@ -1172,13 +1119,11 @@ func refuseTerminalSettingsThroughAVerb(ctx context.Context, inv *inventory.Inve
if string(was) == string(now) {
continue
}
return fmt.Errorf("%s of %s on %s is set at the controller's terminal (`mesh-cli` on the control-node) or on the "+
"operator's warrant, never through a verb alone (this line came through %q): it says where root creates and "+
"owns a module's directories, which of the machine's paths are mounted into its container, what the mesh's "+
"consumers trust, or what a file root or a person's session obeys takes, and whoever may call a verb "+
"includes agents (novox/hq issue 339; issue 340 for a mergeable file's own keys). Propose it instead: the "+
"settings verb with propose puts the exact values to the operator on a channel that proves who answers, and "+
"the layer is set on their Approve (novox/hq ADR 0277). Nothing was changed", key, module, where, verb)
return fmt.Errorf("%s of %s on %s is set at the controller's terminal only (`mesh-cli` on the control-node), never through a verb (this "+
"line came through %q): it says where root creates and owns a module's directories, which of "+
"the machine's paths are mounted into its container, what the mesh's consumers trust, or what a file "+
"root or a person's session obeys takes, and whoever may call a verb includes agents (novox/hq issue 339; "+
"issue 340 for a mergeable file's own keys). Nothing was changed", key, module, where, verb)
}
return nil
}
+2 -2
View File
@@ -426,10 +426,10 @@ func overlayShow(ctx context.Context, open *stores) error {
tunnel.Interface, tunnel.Range, tunnel.Port)
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; "+
"`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:
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():
fmt.Print(" not dialable")
}
+2 -156
View File
@@ -10,7 +10,6 @@ import (
"io"
"net"
"os"
"path/filepath"
"strings"
"time"
@@ -18,7 +17,6 @@ import (
"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"
"github.com/novox/mesh-controller/internal/token"
)
@@ -30,7 +28,7 @@ import (
func nodeCommand(ctx context.Context, args []string) error {
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)
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>.
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:
return fmt.Errorf("node has no %q; it has add, list, show, public-domain, account, agent-account, hand-over "+
"and setuid-search", args[0])
return fmt.Errorf("node has no %q; it has add, list, show, public-domain, account and agent-account", 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).
func addNode(ctx context.Context, inv *inventory.Inventory, args []string) error {
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 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)
}
}
@@ -285,7 +285,7 @@ func TestAChangeInsideAModuleIsThatModulesHeldOrNot(t *testing.T) {
{"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), ""},
} {
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)
}
}
+4 -59
View File
@@ -362,17 +362,6 @@ var plainWordings = map[string]func(conditions.Observation) words{
Explanation: "Its walk across the machines has not moved for longer than usual. Nothing is lost.",
Resolved: "Resolved: the delivery moves again"}
}),
kindBatchNotCut: worded(func(o conditions.Observation) words {
return words{Headline: "Merged changes are not being delivered",
Explanation: "Merges collected for one delivery should have been planned and were not. Nothing is lost.",
Resolved: "Resolved: the merged changes are being delivered"}
}),
kindBatchBehindWalk: worded(func(o conditions.Observation) words {
return words{Headline: "Merged changes wait behind a slow delivery",
Explanation: "Merges collected for the next delivery wait for the delivery before them, which is taking " +
"longer than it should. Nothing is lost.",
Resolved: "Resolved: the merged changes no longer wait"}
}),
kindWalkWaiting: worded(func(o conditions.Observation) words {
return words{Headline: "A delivery is waiting to start",
Explanation: "A merged change is built, and mesh-delivery (the module that decides when a delivery goes " +
@@ -691,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.
// 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 {
if severity == conditions.Urgent {
return "start it, or stop it, " + FromMeshMCPServer
@@ -700,20 +687,6 @@ func waitingNeeds(severity conditions.Severity) string {
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
// 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).
@@ -728,35 +701,11 @@ func moduleNeeds(node string, rs []inventory.ResourceHealth) string {
}
}
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 ""
}
// 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
// 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
@@ -827,19 +776,15 @@ func stalledWords(l stalledLine, o conditions.Observation) (headline, explanatio
long = "for " + humanDuration(d)
}
if o.Resolver == conditions.ResolverOperator {
// Asked of the operator, approved on a channel that proves who answered (novox/hq ADR 0259); the
// router says where each can be answered, so the words do not.
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": ""}}
// Words only: releasing or stopping a delivery is not an acknowledgement, so no desk click
// performs it (ADR 0258).
switch 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":
needs = "merge its pull request, or close it."
default:
needs, actions = "stop it "+FromMeshMCPServer, []conditions.Action{stop}
needs = "stop it " + FromMeshMCPServer
}
}
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)
}
// Past four hours it is urgent, and asks the operator to start or stop it (novox/hq ADR 0259): the plan's
// own verbs, approved, which the controller performs on the warrant. The router says where to answer.
// Past four hours it is urgent, and offers the controller's own answers.
f.waits[0].since = now.Add(-5 * time.Hour)
got = watchWaits(f)
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 "+
"module that decides when a delivery goes out) has not let it start for 5 hours, so mesh-delivery may "+
"be stuck.", "Start", "Stop")
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)
}
}
"be stuck.")
// Many modules are counted, not listed in the headline.
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
// account's own service manager (exit-code)". Restarting is not an acknowledgement: it is asked of the
// operator at the approve level (novox/hq ADR 0259), so a desk click never performs it (ADR 0258).
// account's own service manager (exit-code)". Restarting is not an acknowledgement, so it is said in words
// and offered as no answer (ADR 0258).
func TestAModuleUnhealthyAsksForARestartInWords(t *testing.T) {
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()}})
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. "+
"openrazer on g14 is not healthy: its service openrazer-daemon stopped with an error. It clears as soon "+
"as it runs again.", "Restart")
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)
}
"as it runs again.")
// 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",
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"}})
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.",
"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
+12 -5
View File
@@ -512,6 +512,15 @@ func sortedKeysOf(m map[string]string) []string {
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
// 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) {
@@ -1239,11 +1248,9 @@ func planCommand(ctx context.Context, args []string) error {
if err != nil {
return err
}
// 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).
reportLeftOut(args[0], declared)
return writePlanDiff(ctx, open.inventory, args[0], body)
return sayPlanDiff(args[0], declared, func() error {
return writePlanDiff(ctx, open.inventory, args[0], body)
})
}
if *asJSON {
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)
}
}
+1 -25
View File
@@ -164,13 +164,12 @@ func retryRefusal(p inventory.Plan, plans []inventory.Plan) error {
case p.Open():
return fmt.Errorf("%s is still %s; nothing in it failed to retry — `rebuild <module>` asks one module again", p.ID, p.State)
}
if len(failedIn(p)) > 0 {
if q, found := newerOpenPlan(p, plans); found {
return fmt.Errorf("%s supersedes it: a newer merge of %s (%s at %s) is open, and retrying %s would build "+
"what that one replaced", q.ID, q.Repository, q.ID, short(q.Commit), p.ID)
}
return oneWalkAtATime(p, plans)
return nil
}
stopped := stoppedRollouts(p)
if len(stopped) == 0 {
@@ -194,17 +193,6 @@ func retryRefusal(p inventory.Plan, plans []inventory.Plan) error {
"the older build back", m, q.ID, q.State, q.Repository, short(q.Commit), p.ID)
}
}
return oneWalkAtATime(p, plans)
}
// oneWalkAtATime refuses a retry while another walk is open, started or waiting for its word (novox/hq ADR
// 0276): a walk retried beside it would be two walks at once.
func oneWalkAtATime(p inventory.Plan, plans []inventory.Plan) error {
for _, q := range plans {
if q.ID != p.ID && q.Open() && q.Release == nil {
return fmt.Errorf("%s is open (%s): one walk at a time — retry %s once it ended", q.ID, q.Named(), p.ID)
}
}
return nil
}
@@ -276,18 +264,6 @@ func retryPlan(ctx context.Context, open *stores, id string) (string, error) {
return "", err
}
plans = append(plans, recent...)
// **A failed walk whose earlier merges are walked alone is not retried** (novox/hq ADR 0276): the search for
// the merge that brought the failure answers them now, and a retried walk would name them twice.
if p.Delivery != nil && len(p.Delivery.Merges) > 0 {
kept, err := inv.MergesOf(ctx, p.ID)
if err != nil {
return "", err
}
if len(kept) < len(p.Delivery.Merges) {
return "", fmt.Errorf("%s's earlier merges are walked alone, to find which one brought its failure: "+
"those walks answer them; a newer merge, or `rebuild <module>`, builds again", p.ID)
}
}
if err := retryRefusal(p, plans); err != nil {
return "", err
}
+21 -41
View File
@@ -15,16 +15,16 @@ import (
// inventory.Dependencies (dependenciesOf over the records), and the merge planned by reachOfMerge — the
// 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,
// issues 338 and 363): each build records the build source it said, and a merge is mapped onto those of the
// newest builds. A build that said none (P below, as every build before ADR 0267) is read as before: P moves
// on any merge to the repository it packages, though through no edge. The rows tagged 338 held the opposite
// until ADR 0267 was built.
func TestASharedRepositoryIsPlannedFromTheRecordedBuildSources(t *testing.T) {
// **The repository rows are CURRENT BEHAVIOUR, documented — not the rule the operator states**
// (novox/hq issue 338, and the decision pending on it): a build that read a repository gives its module a
// packages edge to every module built from that repository, and mergeCandidates moves it on any merge to
// that repository, whatever the files. So a change to C alone, or to a README, moves the module that
// packages C's repository. ADR 0238 §3 records exactly that today ("a repository a recipe names"); the
// expectations marked 338 change with that decision.
func TestASharedRepositoryIsPlannedFromTheRecordsAsItIsToday(t *testing.T) {
inv := inventory.ForTest(t)
ctx := t.Context()
asked := time.Now().Add(-time.Hour)
sourcesOf := map[string][]inventory.BuildSource{}
register := func(m catalogue.Manifest, repository, path string, against []string, read []inventory.ReadRepository) {
t.Helper()
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)
}
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,
Sources: sourcesOf[m.Module]}); err != nil {
Module: m.Module, Commit: "old", On: "builder", Path: path, Against: against, Read: read, Asked: asked}); err != nil {
t.Fatal(err)
}
}
@@ -41,16 +40,7 @@ func TestASharedRepositoryIsPlannedFromTheRecordedBuildSources(t *testing.T) {
agent := catalogue.Manifest{Module: "build-agent", Version: "1",
Claims: []catalogue.Claim{{Name: "node-build-agent", Scope: catalogue.ScopeNode}}}
// The shape of issue 338, each build saying its build source (ADR 0267).
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...)}}
// The shape of issue 338.
register(catalogue.Manifest{Module: "mesh-controller", Version: "1"}, "novox/mesh-controller", "", nil, nil)
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)
@@ -91,15 +81,13 @@ func TestASharedRepositoryIsPlannedFromTheRecordedBuildSources(t *testing.T) {
if !reflect.DeepEqual(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.
for _, e := range edges {
if e.Kind == inventory.EdgePackages {
t.Errorf("a packages edge was drawn (ADR 0267 rule 4): %v", e)
}
}
// Each kind derived from its record: built against (stands-on), build.on (declared), read (packages).
for _, want := range []inventory.Edge{
dep("d", inventory.EdgeStandsOn, "a"),
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"),
} {
found := false
@@ -117,23 +105,15 @@ func TestASharedRepositoryIsPlannedFromTheRecordedBuildSources(t *testing.T) {
want string
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},
{"C alone: C, and P, read whole as before; P after nothing", "one",
[]string{"modules/c/x.go"}, "c,p", false},
{"a README of the repository P packages: P, read whole as before", "one",
[]string{"README.md"}, "p", false},
{"C alone: C, and P, which packages C's repository", "one",
[]string{"modules/c/x.go"}, "c,p", true},
{"a README of the repository P packages: P moves, nothing built from it does", "one",
[]string{"README.md"}, "p", true},
{"the dependent's repository: D alone", "two", []string{"d/main.go"}, "d", false},
{"a README of the controller's repository: no module", "mesh-controller",
[]string{"README.md"}, "", 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},
{"a README of the controller's repository: all three, three tiers", "mesh-controller",
[]string{"README.md"}, "mesh-controller | build-agent | route-proxy", true},
{"the route proxy's directory in the catalogue: it alone", "mesh-catalog",
[]string{"modules/route-proxy/module.json"}, "route-proxy", false},
{"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 {
tag := ""
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)
}
+45 -278
View File
@@ -1,15 +1,12 @@
package main
import (
"encoding/json"
"fmt"
"math/rand/v2"
"sort"
"strings"
"testing"
"time"
snapshot "github.com/novox/mesh-controller/internal/facts"
"github.com/novox/mesh-controller/internal/inventory"
"github.com/novox/mesh-controller/internal/link"
)
@@ -24,7 +21,7 @@ import (
// kind widens the plan orders the tiers
// stands-on 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
// on, and for the controller whose worker it binds
// 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",
edges: []inventory.Edge{dep("f", standsOn, "d"), dep("d", standsOn, "a")},
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")},
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).
{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"},
{what: "declared orders", edges: []inventory.Edge{dep("x", declared, "a")},
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")},
repo: "one", paths: []string{"modules/a/x"}, want: "a"},
{what: "packages widens, into the same tier", edges: []inventory.Edge{dep("x", packages, "a")},
repo: "one", paths: []string{"modules/a/x"}, want: "a,x"},
{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"},
{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"},
{what: "a diamond of mixed kinds orders on the ordering side only",
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.
{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,
// issue 338, issue 363). The controller is built from its repository's root as a Go bundle; the route proxy
// and the build seat's holder build images whose context is that repository and which name the package they
// 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.
// **CURRENT BEHAVIOUR, documented — not the rule the operator states.** novox/hq issue 338 (a module
// built from a shared repository moves on every merge to it) and the decision pending on it would change
// every row here. Today:
//
// The build sources are this repository's own programs as GoBuildSource reads them (held to that by
// TestThisRepositorysProgramsHaveBuildSourcesOfTheirOwn in internal/builder), cut to what the rows need.
func TestASharedRepositoryMovesOnlyWhatItsBuildSourceHolds(t *testing.T) {
// - mesh-controller is built from its repository's root, so every file of that repository touches it;
// - route-proxy and build-agent package the whole of that repository (a build context), so the build
// 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 controllerRepo = "http://forge.internal:20000/novox/mesh-controller.git"
entries := []inventory.Entry{
@@ -246,26 +249,7 @@ func TestASharedRepositoryMovesOnlyWhatItsBuildSourceHolds(t *testing.T) {
fromRepo("route-proxy", catalogueRepo, "modules/route-proxy"),
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{
"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"}},
"route-proxy": {{Repository: "novox/mesh-controller", Ref: "main"}},
}
@@ -273,176 +257,51 @@ func TestASharedRepositoryMovesOnlyWhatItsBuildSourceHolds(t *testing.T) {
for _, c := range []struct {
what, repo string
paths []string
read map[string][]inventory.ReadRepository
want string
unread string
}{
// The operator's acceptance: a merge of the controller's own code plans the controller alone.
{"the controller's own command: the controller alone (338)", "mesh-controller",
[]string{"cmd/mesh-controller/main.go"}, read, "mesh-controller", ""},
{"a package only the controller builds from: the controller alone (338)", "mesh-controller",
[]string{"internal/conditions/condition.go", "internal/conditions/bus.go"}, read, "mesh-controller", ""},
{"a test beside the controller's command: nothing is built from it", "mesh-controller",
[]string{"cmd/mesh-controller/main_test.go"}, read, "", "cmd/mesh-controller/main_test.go"},
{"a README of the controller's repository: no module (338)", "mesh-controller",
[]string{"README.md"}, read, "", "README.md"},
{"the route proxy's program alone: the route proxy alone (338)", "mesh-controller",
[]string{"examples/route-proxy/main.go"}, read, "route-proxy", ""},
{"the build seat's program alone: its holder alone", "mesh-controller",
[]string{"cmd/mesh-builder/main.go"}, read, "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", ""},
// The live three-tier plan of 2026-10-08 (issue 338), in the worker-of order (issue 206) that
// TestAMergeIsPlannedInTiersAlongTheThreeKindsOfDependency's controller case holds too.
{"a README of the controller's repository moves all three, in three tiers", "mesh-controller",
[]string{"README.md"}, "mesh-controller | build-agent | route-proxy"},
{"the controller's own code: the same", "mesh-controller",
[]string{"cmd/mesh-controller/main.go"}, "mesh-controller | build-agent | route-proxy"},
{"the route proxy's program alone: the same, the controller with it", "mesh-controller",
[]string{"examples/route-proxy/main.go"}, "mesh-controller | build-agent | route-proxy"},
// In the catalogue, where they live, the rule is path-precise.
{"the route proxy's directory in the catalogue: it alone", "mesh-catalog",
[]string{"modules/route-proxy/module.json"}, "route-proxy"},
{"the build agent's directory: it alone, nothing it builds", "mesh-catalog",
[]string{"modules/build-agent/module.json"}, "build-agent"},
{"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 {
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 {
t.Errorf("%s: unread %q, wanted %q", c.what, u, c.unread)
if c.repo == "mesh-controller" && strings.Join(r.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
// sorts them: no packages edge (novox/hq ADR 0267 rule 4).
// sorts them.
var sharedRepositoryEdges = []inventory.Edge{
dep("build-agent", inventory.EdgePackages, "mesh-controller"),
dep("build-agent", inventory.EdgeWorkerOf, "mesh-controller"),
dep("gitea", inventory.EdgeBuiltBy, "build-agent"),
dep("mesh-controller", 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
// 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,
// for a module built from the root), and everything reachable from them along stands-on and declared —
// never along packages (novox/hq ADR 0267), built-by or worker-of;
// for a module built from the root), and everything reachable from them along stands-on, declared and
// 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
// depended on in an earlier tier;
// - no cycle is said.
@@ -451,7 +310,7 @@ var sharedRepositoryEdges = []inventory.Edge{
func TestAPlanIsTheTouchedModulesAndWhatIsReachableAlongTheWideningEdges(t *testing.T) {
kinds := []string{inventory.EdgeStandsOn, inventory.EdgeDeclared, inventory.EdgePackages,
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,
inventory.EdgeBuiltBy: true, inventory.EdgeWorkerOf: true}
// 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
}
// **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))
} else {
// 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.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,
@@ -641,8 +637,31 @@ const (
// discoverHolders asks the bus's discovery who serves what, and answers seat → machine for every
// endpoint a seat's verb is served on.
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{}
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 {
seat, node := e.Metadata["seat"], e.Metadata["node"]
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][info.ID] = true
})
return out, err
}
// 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
return out, nil
}
// 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
}
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)
if err != nil {
return nil, err
@@ -1140,26 +1103,6 @@ func probeCoreBuilds(ctx context.Context, d *doctor) ([]conditions.Observation,
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
// 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.
-833
View File
@@ -1,833 +0,0 @@
package main
// A trusted setting proposed through a verb and set only on the operator's warrant (novox/hq ADR 0277).
//
// mesh-controller settings propose <module> <values.json | {…}> [--node <node>] [--replace]
// mesh-controller settings propose <module> --clear [--node <node>]
// mesh-controller settings proposals [<id>]
//
// Whoever the bus admits may PROPOSE a settings layer — the keys issue 339 made the terminal's (`places`,
// `accesses`, what a provider serves, what a trusted file asks for) among them. A proposal changes nothing: it is
// kept in the controller's own asks (broker.AskedBucket, written by the controller alone) and asked of the
// operator through the operator channel as an ask whose two answers, Approve and Decline, are both at the level
// approve, so only a channel that proves who answered (Telegram, today) carries either. The serving controller
// acts on the warrant as on any other of its asks (asker.Decided): once, for the ask it holds, the option it
// offered, and only when the act about to be performed — the module, the machine, the digest of the exact values,
// the layer they replace, whether a removal is meant — is the one the option bound when the operator was shown
// it. Then it sets the layer as `settings set` at the terminal does, with the same judgement, keeps who approved
// it beside the layer (`settings` says it back), and records the warrant in the hand-act log on the bus, where a
// person's decisions are read; the router edits the ask on every channel to its outcome. No seat event of its
// own: nothing consumes one, and the installer's first user list in the node-engine's repository names every
// controller event, so one would cost a node-engine change for a fact without a reader. The push afterwards is a
// separate act, as it is for a layer set at the terminal.
//
// **What the operator reads** is the module, the machine, each key with its exact new value and, for a changed
// key, the value it replaces. A value that may not leave the mesh (an address, a path, a secret's shape: the
// router's content rule, outward.Check) is shown with that part replaced by ‹address›, ‹path› or ‹withheld›, the
// ask says so, and the whole is read with `settings proposals <id>` — whose fingerprint must be the one on the
// phone. Fail closed: a proposal nothing can carry to the operator is refused at once, in words, and never left
// waiting for an answer that cannot come.
//
// **On a channel that proves who answers, the values are shown WHOLE** (novox/hq issue 383, the operator's
// decision of 2026-10-10): a mount point, a share name or a private address is the thing being approved and must
// be readable, so the ask carries them whole beside the explanation (asks.Ask.Whole), the router shows that only
// on a kind that verifies its sender, and only a value shaped like a secret is withheld there. The message is the
// headline, one line per key, who proposed it and when; the fingerprint and how to read the proposal whole are
// the Details answer's (asks.Ask.Details). The masking stays for conditions and for channels that prove nothing.
import (
"context"
"crypto/sha256"
"encoding/hex"
"encoding/json"
"errors"
"fmt"
"os"
"regexp"
"sort"
"strconv"
"strings"
"time"
"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"
"github.com/novox/mesh-controller/internal/outward"
)
// settingsProposal is one proposed change to a module's settings layer, as the controller's ask keeps it
// (asked.Proposal). Every field the ask is composed from is here, so the serving controller composes the same ask
// the proposer did, and the warrant's digest holds to it.
type settingsProposal struct {
Module string `json:"module"`
// Node is the machine, or empty for the whole mesh.
Node string `json:"node,omitempty"`
// Values is the layer proposed, whole; Clear says the layer is removed instead.
Values map[string]any `json:"values,omitempty"`
Clear bool `json:"clear,omitempty"`
// Replace says the keys the layer had and Values do not name are meant to go (novox/hq ADR 0217).
Replace bool `json:"replace,omitempty"`
// Before is the layer as it stood when the proposal was made, and HadLayer whether there was one: what the
// operator was shown the change against, and what must still stand when the warrant is acted on.
Before map[string]any `json:"before,omitempty"`
HadLayer bool `json:"had-layer"`
// From is who proposed it, as the bus named the caller; At is when.
From string `json:"from"`
At time.Time `json:"at"`
// Digest is the digest of Values (layerDigest), BeforeDigest of Before.
Digest string `json:"digest"`
BeforeDigest string `json:"before-digest"`
}
// proposalVerb is the verb a proposal's answers bind: the controller's own settings, performed by itself.
const proposalVerb = askerName + ".settings"
// The two answers, both at the level approve.
const (
answerApprove = "approve"
answerDecline = "decline"
)
// layerDigest is the digest a proposal binds: SHA-256 over the layer's canonical JSON (Go sorts a map's keys), an
// absent layer and an empty one alike.
func layerDigest(values map[string]any) string {
if values == nil {
values = map[string]any{}
}
raw, _ := json.Marshal(values)
sum := sha256.Sum256(raw)
return "sha256:" + hex.EncodeToString(sum[:])
}
// fingerprint is a digest as the operator is shown it: its first 24 hexadecimal digits in groups of four, so no
// word of it is long enough for the router to take for a secret.
func fingerprint(digest string) string {
hexed := strings.TrimPrefix(digest, "sha256:")
if len(hexed) < 24 {
return hexed
}
var groups []string
for i := 0; i < 24; i += 4 {
groups = append(groups, hexed[i:i+4])
}
return strings.Join(groups, " ")
}
// where is the layer in words: "shanks" or "the whole mesh".
func (p settingsProposal) where() string {
if p.Node == "" {
return "the whole mesh"
}
return p.Node
}
// about is what the ask is about, a key without spaces: the module's layer on the machine, or on the mesh.
func (p settingsProposal) about() string {
if p.Node == "" {
return "settings." + p.Module + ".mesh"
}
return "settings." + p.Module + "." + p.Node
}
// actions are the proposal's two answers as the controller keeps them (asked.Actions): each binds the exact act
// through boundAct — the verb, the machine, the level and every argument, the values' digest among them.
func (p settingsProposal) actions(id string) []conditions.Action {
args := func(answer string) map[string]string {
return map[string]string{"proposal": id, "answer": answer, "module": p.Module, "node": p.Node,
"values": p.Digest, "before": p.BeforeDigest, "had-layer": strconv.FormatBool(p.HadLayer),
"replace": strconv.FormatBool(p.Replace), "clear": strconv.FormatBool(p.Clear), "from": p.From,
"at": p.At.UTC().Format(time.RFC3339Nano)}
}
return []conditions.Action{
{Label: "Approve", Verb: proposalVerb, Machine: p.Node, Level: conditions.LevelApprove, Arguments: args(answerApprove)},
{Label: "Decline", Verb: proposalVerb, Machine: p.Node, Level: conditions.LevelApprove, Arguments: args(answerDecline)},
}
}
// ask is the proposal's ask, composed from it alone, as the router is sent it: the headline, the explanation
// with the change shown under the content rule, and the two options with their bound acts.
func (p settingsProposal) ask(id string, machines []string) (asks.Ask, map[string]int) {
verb := "Set"
if p.Clear {
verb = "Clear"
}
headline := fmt.Sprintf("%s %s on %s?", verb, p.Module, p.where())
if len([]rune(headline)) > asks.HeadlineLength {
headline = fmt.Sprintf("%s settings on %s?", verb, p.where())
}
if len([]rune(headline)) > asks.HeadlineLength {
headline = verb + " settings?"
}
// The message (issue 383): the change, one line per key, then who proposed it and when — the headline says
// what is set where, and the router adds what every ask says. The fingerprint and the how-to are Details'.
compose := func(change string) string {
var b strings.Builder
if p.Clear && !p.HadLayer {
b.WriteString("There is no layer to remove; approving changes nothing.\n")
} else {
b.WriteString(change + "\n")
}
fmt.Fprintf(&b, "Proposed by %s at %s.", sayable(p.From, machines), p.At.Local().Format("15:04 on 2 Jan"))
return b.String()
}
shown, shownWhole := p.change(machines, false)
whole, _ := p.change(machines, true)
var d strings.Builder
fmt.Fprintf(&d, "Fingerprint %s.\n", fingerprint(p.Digest))
if !shownWhole {
d.WriteString("On a channel that does not prove who answers, these values are shown as ‹address›, ‹path› or ‹withheld›.\n")
}
fmt.Fprintf(&d, "Read it whole, with this fingerprint: settings proposals %s at the controller's terminal, or "+
"mesh-controller.settings with proposal %s through the mesh MCP server.\n", id, id)
if p.Clear {
d.WriteString("Approved, the layer is removed at once and the machine takes it at its next push.")
} else {
d.WriteString("Approved, the layer is set at once and the machine takes it at its next push.")
}
q := asks.Ask{ID: id, Headline: headline, Explanation: compose(shown), Who: asks.Operator,
Expires: p.At.Add(askApproveFor), OnExpiry: "the proposal is discarded; nothing changes",
About: p.about(), Details: d.String()}
if whole != shown {
// Only where a value was masked: an ask whose values all pass the content rule shows the same everywhere.
q.Whole = compose(whole)
}
options := map[string]int{}
for i, act := range p.actions(id) {
binds, _ := asks.ActDigest(boundAct(act))
oid := optionID(act.Label)
options[oid] = i
does := "the settings are set; nothing is pushed yet"
if p.Clear {
does = "the layer is removed; nothing is pushed yet"
}
if act.Arguments["answer"] == answerDecline {
does = "nothing changes; the proposal is discarded"
}
q.Options = append(q.Options, asks.Option{ID: oid, Label: act.Label, Does: does, Level: asks.Level(act.Level),
Binds: binds})
}
return q, options
}
// shownMost is the most of a change the phone is shown, in bytes; the rest is read whole with `settings proposals`.
const shownMost = 1400
// change is what changes, line by line, and whether every value was shown whole: "+ key: value" added,
// "~ key: value (was: old)" changed, "- key (was: old)" removed, and the count of keys unchanged. Each value is
// shown under the content rule (sayableValue), or — exact, for a channel that proves who answers — as it is,
// withheld only when shaped like a secret (wholeValue).
func (p settingsProposal) change(machines []string, exact bool) (string, bool) {
whole := true
say := func(v any) string {
s, w := sayableValue(v, machines)
if exact {
s, w = wholeValue(v, machines)
}
whole = whole && w
return s
}
var lines []string
if p.Clear {
was := leaves(p.Before, "")
for _, k := range layerKeys(was) {
lines = append(lines, fmt.Sprintf("- %s: %s", k, say(was[k])))
}
} else {
added, changed, removed := settingsChange(p.Before, p.Values)
was, now := leaves(p.Before, ""), leaves(p.Values, "")
for _, k := range added {
lines = append(lines, fmt.Sprintf("+ %s: %s", k, say(now[k])))
}
for _, k := range changed {
lines = append(lines, fmt.Sprintf("~ %s: %s (was: %s)", k, say(now[k]), say(was[k])))
}
for _, k := range removed {
lines = append(lines, fmt.Sprintf("- %s (was: %s)", k, say(was[k])))
}
unchanged := len(now) - len(added) - len(changed)
switch {
case len(lines) == 0 && unchanged > 0:
lines = append(lines, fmt.Sprintf("= nothing changes: the %d key(s) are as they stand", unchanged))
case unchanged > 0:
lines = append(lines, fmt.Sprintf("= %d key(s) unchanged", unchanged))
case len(lines) == 0:
lines = append(lines, "= the layer has no keys")
}
}
out := strings.Join(lines, "\n")
if len(out) > shownMost {
cut := shownMost
for cut > 0 && out[cut] != '\n' {
cut--
}
out = out[:cut] + fmt.Sprintf("\n… NOT SHOWN IN FULL here: %d more bytes", len(strings.Join(lines, "\n"))-cut)
whole = false
}
return out, whole
}
func layerKeys(m map[string]any) []string {
keys := make([]string, 0, len(m))
for k := range m {
keys = append(keys, k)
}
sort.Strings(keys)
return keys
}
// The shapes a value is shown without, in place: an address with what follows it up to a separator, and a
// path. Replaced in place rather than word by word, so "recalbox=smb://host/share@/mnt/recalbox" is shown as
// "recalbox=‹address›@‹path›" and keeps its shape.
var (
shownURL = regexp.MustCompile(`(?i)\b[a-z][a-z0-9+.-]*://[^\s@"',;)\]}]*`)
shownIPv4 = regexp.MustCompile(`\b\d{1,3}(\.\d{1,3}){3}(:\d+)?\b`)
shownEmail = regexp.MustCompile(`[A-Za-z0-9._%+-]+@[A-Za-z0-9-]+(\.[A-Za-z0-9-]+)*\.[A-Za-z]{2,}`)
shownPath = regexp.MustCompile(`(^|[\s=@,;:"'(\[{])((?:~|\.{1,2})?/[^\s"',;:)\]}]*)`)
)
// Markers for what is not shown.
const (
markAddress = "‹address›"
markPath = "‹path›"
markWithheld = "‹withheld›"
)
// sayableValue is a value as the phone is shown it, and whether it was shown whole. A string is shown bare; any
// other value as JSON.
func sayableValue(v any, machines []string) (string, bool) {
text, ok := v.(string)
if !ok {
raw, err := json.Marshal(v)
if err != nil {
return markWithheld, false
}
text = string(raw)
}
if text == "" {
return `""`, true
}
out := sayable(text, machines)
return out, out == text
}
// wholeValue is a value as a channel that proves who answers is shown it (asks.Ask.Whole), and whether it was
// shown whole: as it is, unless it is shaped like a secret (outward.Secret), which is withheld whole — never in
// part, so no half of a key reaches the phone.
func wholeValue(v any, machines []string) (string, bool) {
text, ok := v.(string)
if !ok {
raw, err := json.Marshal(v)
if err != nil {
return markWithheld, false
}
text = string(raw)
}
if text == "" {
return `""`, true
}
if _, ok := outward.Secret(text, machines...); !ok {
return markWithheld, false
}
return text, true
}
// sayable is a text with what may not leave the mesh replaced in place by a marker; what the markers cannot make
// pass is withheld whole.
func sayable(text string, machines []string) string {
if _, ok := outward.Check(text, machines...); ok {
return text
}
out := shownURL.ReplaceAllString(text, markAddress)
out = shownEmail.ReplaceAllString(out, markAddress)
out = shownIPv4.ReplaceAllString(out, markAddress)
out = shownPath.ReplaceAllString(out, "${1}"+markPath)
// Then word by word, as the router reads them: a host name, a path the shapes above missed, a secret's shape.
words := strings.FieldsFunc(out, func(r rune) bool {
return r == ' ' || r == '\t' || r == '\n' || strings.ContainsRune("\"'`()[]{}<>,;|", r)
})
for _, w := range words {
if refusal, ok := outward.Check(w, machines...); !ok {
mark := markWithheld
switch refusal.Class {
case "address":
mark = markAddress
case "path":
mark = markPath
}
out = strings.ReplaceAll(out, w, mark)
}
}
if _, ok := outward.Check(out, machines...); ok {
return out
}
out = strings.ReplaceAll(outward.Scrub(out, markWithheld, machines...), "(withheld)", markWithheld)
if _, ok := outward.Check(out, machines...); ok {
return out
}
return markWithheld
}
// ---- proposing ---------------------------------------------------------------------------------------
// proposer is the propose command's reaches, given so a test needs no store and no bus.
type proposer struct {
// layer reads a module's layer as it stands.
layer func(ctx context.Context, node, module string) (map[string]any, bool, error)
// judge judges the layer as SetSettings would, keeping nothing.
judge func(ctx context.Context, node, module string, values map[string]any) error
// machines are the mesh's machine names: allowed in the ask's words.
machines func(ctx context.Context) ([]string, error)
store askedStore
publish func(ctx context.Context, subject string, body []byte, id string) error
// routerHere and grantHeld are the asker's own judgements of whether an ask can be carried; nil is yes.
routerHere func(ctx context.Context) (bool, error)
grantHeld func(ctx context.Context) (bool, string, error)
// routerRecord reads the router's record of an ask: its state, or "" for none.
routerRecord func(ctx context.Context, id string) (string, error)
now func() time.Time
caller string
// waitFor and waitEvery bound how long propose waits for the router's word on the new ask.
waitFor time.Duration
waitEvery time.Duration
}
// proposeInput is what is proposed.
type proposeInput struct {
module string
node string
values map[string]any
clear bool
replace bool
}
// atTheTerminalInstead names the other way, said whenever a proposal is refused for want of a channel.
func atTheTerminalInstead(in proposeInput) string {
if in.clear {
return "the operator may clear it at the controller's terminal instead: mesh-cli settings clear " + in.module + nodeFlag(in.node)
}
return "the operator may set it at the controller's terminal instead: mesh-cli settings set " + in.module + " '{…}'" + nodeFlag(in.node)
}
// propose keeps a proposal in the controller's asks and asks the operator; it answers the words said to the
// caller. Nothing is set here.
func (pr proposer) propose(ctx context.Context, in proposeInput) (string, error) {
refuse := func(format string, args ...any) (string, error) {
return "", fmt.Errorf(format+". Nothing was proposed", args...)
}
if in.module == "" {
return refuse("a proposal names a module")
}
if !in.clear && in.values == nil {
return refuse("a proposal gives the values, or says --clear")
}
now := pr.now()
before, had, err := pr.layer(ctx, in.node, in.module)
if err != nil {
return "", err
}
p := settingsProposal{Module: in.module, Node: in.node, Values: in.values, Clear: in.clear, Replace: in.replace,
Before: before, HadLayer: had, From: pr.caller, At: now, BeforeDigest: layerDigest(before)}
if in.clear {
// A clear binds the layer it removes: its digest is the fingerprint the operator reads.
p.Values, p.Digest = nil, layerDigest(before)
} else {
// Judged now, as SetSettings judges, so the operator is never asked about a layer the mesh would refuse —
// and judged again when the warrant is acted on.
if err := pr.judge(ctx, in.node, in.module, in.values); err != nil {
return refuse("%v", err)
}
_, _, removed := settingsChange(before, in.values)
if len(removed) > 0 && !in.replace {
return refuse("%s on %s: this layer would no longer set %s. A layer is replaced whole; read it with "+
"`settings show %s%s` and include what should stay, or add --replace if the removal is meant "+
"(novox/hq ADR 0217)", in.module, p.where(), strings.Join(removed, ", "), in.module, nodeFlag(in.node))
}
p.Digest = layerDigest(in.values)
}
var machines []string
if pr.machines != nil {
if machines, err = pr.machines(ctx); err != nil {
return "", err
}
}
id := newProposalID()
q, options := p.ask(id, machines)
if err := q.Check(now); err != nil {
return refuse("%v", err)
}
words := []string{q.Headline, q.Explanation, q.OnExpiry}
for _, o := range q.Options {
words = append(words, o.Label, o.Does)
}
if refusal, ok := outward.Check(strings.Join(words, "\n"), machines...); !ok {
return refuse("the ask's words would carry %s, which may not leave the mesh, and the change could not be "+
"shown without it; %s", refusal, atTheTerminalInstead(in))
}
// The whole words are shown only where the sender is proven, and never a secret's shape: wholeValue withholds
// one, and the router would refuse the ask if one were left, so it is refused here first, in words.
if refusal, ok := outward.Secret(q.Whole, machines...); !ok {
return refuse("the change shown whole would carry %s, which may not leave the mesh on any channel; %s",
refusal, atTheTerminalInstead(in))
}
// Fail closed, before anything is kept: no router, no grant, no channel means no ask.
if pr.routerHere != nil {
here, err := pr.routerHere(ctx)
if err != nil {
return "", err
}
if !here {
return refuse("no router takes the controller's asks (no module holding the operator channel is assigned), "+
"so the operator cannot be asked; %s", atTheTerminalInstead(in))
}
}
if pr.grantHeld != nil {
held, why, err := pr.grantHeld(ctx)
if err != nil {
return "", err
}
if !held {
return refuse("the operator cannot be asked yet: %s; %s", why, atTheTerminalInstead(in))
}
}
all, err := pr.store.All(ctx)
if err != nil {
return "", err
}
open := 0
for _, r := range all {
if r.State != askOpen || !now.Before(r.Ask.Expires) {
continue
}
if r.Proposal != nil && r.Proposal.Module == p.Module && r.Proposal.Node == p.Node && r.Proposal.Digest == p.Digest &&
r.Proposal.Clear == p.Clear {
return refuse("the same change is already proposed as %s and waits for the operator's answer until %s; "+
"`settings proposals` lists it", r.ID, r.Ask.Expires.Local().Format("15:04"))
}
open++
}
if open >= askMostOpen {
return refuse("%d questions already wait for the operator's answer, and the operator is asked at most %d at "+
"once; `settings proposals` lists the proposals among them", open, askMostOpen)
}
// Kept before it is published, as the asker keeps every ask, so a warrant always finds it.
if err := pr.store.Create(ctx, asked{ID: id, Condition: q.About, Ask: q, Actions: p.actions(id), Options: options,
State: askOpen, Opened: now, Proposal: &p}); err != nil {
return "", fmt.Errorf("the proposal could not be kept in the controller's asks, so the operator was not asked: %w", err)
}
body, err := json.Marshal(q)
if err != nil {
return "", err
}
if err := pr.publish(ctx, asks.AskSubject(askerName), body, "ask."+id); err != nil {
_, _ = pr.store.Change(ctx, id, func(x *asked) bool {
if x.State != askOpen || x.Acted != "" {
return false
}
x.State, x.Ended, x.Acted = askUnsent, pr.now(), "nothing: it could not be published: "+err.Error()
return true
})
return "", fmt.Errorf("the operator could not be asked (%v); the proposal %s is kept and will never become "+
"active. Propose it again, or %s", err, id, atTheTerminalInstead(in))
}
// The router's word, before answering: it refuses at once an ask no channel can carry (the serving controller
// hears that and ends the ask here), and records one it took.
taken := "the router has not said yet whether it took the ask; `settings proposals` shows where it stands"
for deadline := time.Now().Add(pr.waitFor); time.Now().Before(deadline); {
if r, err := pr.store.Get(ctx, id); err == nil && r != nil && r.State != askOpen {
why := r.Acted
if r.Warrant != nil && r.Warrant.Words != "" {
why = r.Warrant.Words
}
return "", fmt.Errorf("the router did not ask the operator: the ask %s %s (%s). Nothing changes; %s",
id, r.State, why, atTheTerminalInstead(in))
}
if pr.routerRecord != nil {
if state, err := pr.routerRecord(ctx, id); err == nil && state != "" {
taken = "the router took the ask and shows it on the channels that can carry it"
break
}
}
time.Sleep(pr.waitEvery)
}
var b strings.Builder
fmt.Fprintf(&b, "proposal %s: %s\n", id, q.Headline)
fmt.Fprintf(&b, " %s\n", taken)
fmt.Fprintf(&b, " the operator is asked on a channel that proves who answers, and reads the change with fingerprint %s\n",
fingerprint(p.Digest))
fmt.Fprintf(&b, " until %s nothing changes: the layer is set only on Approve, and discarded on Decline or at expiry\n",
q.Expires.Local().Format("2006-01-02 15:04"))
fmt.Fprintf(&b, " `settings proposals %s` shows it whole; once approved, `push %s` sends it", id, pushWord(p.Node))
return b.String(), nil
}
func pushWord(node string) string {
if node == "" {
return "--behind"
}
return node
}
func newProposalID() string {
return "s" + strings.TrimPrefix(newAskID(), "c")
}
// How long propose waits for the router's word on a new ask, and how often it looks.
const (
routerAnswersWithin = 8 * time.Second
routerAnswersEvery = 250 * time.Millisecond
)
// proposeCommand is `settings propose`, on this controller's stores and bus.
func proposeCommand(ctx context.Context, module, node, valuesArg string, clear, replace bool) error {
var values map[string]any
if !clear {
if valuesArg == "" {
return errors.New("settings propose <module> <settings.json | {…}> [--node <node>] [--replace], or settings propose <module> --clear [--node <node>]")
}
var raw []byte
var err error
if strings.HasPrefix(strings.TrimSpace(valuesArg), "{") {
raw = []byte(valuesArg)
} else if raw, err = os.ReadFile(valuesArg); err != nil {
return err
}
if err := json.Unmarshal(raw, &values); err != nil {
return fmt.Errorf("%s is not a settings file: %w", valuesArg, err)
}
} else if valuesArg != "" {
return errors.New("settings propose: --clear takes no values")
}
open, err := openStores(ctx)
if err != nil {
return err
}
defer open.Close()
js, err := aBus()
if err != nil {
return err
}
defer js.Close()
conn := js.Conn()
pr := proposer{
layer: open.inventory.Layer,
judge: open.inventory.JudgeSettings,
machines: func(ctx context.Context) ([]string, error) {
nodes, err := open.inventory.Nodes(ctx)
if err != nil {
return nil, err
}
var names []string
for _, n := range nodes {
names = append(names, n.Name)
}
return names, nil
},
store: busAsked{conn: conn},
publish: func(ctx context.Context, subject string, body []byte, id string) error {
_, err := js.Context().Publish(subject, body, nats.MsgId(id), nats.Context(ctx))
return err
},
routerHere: routerHereIn(open.inventory),
grantHeld: grantHeldIn(open),
routerRecord: func(ctx context.Context, id string) (string, error) {
bucket, err := asksRecords(ctx, open.inventory)
if err != nil || bucket == "" {
return "", err
}
state, _, err := readRouterRecord(ctx, conn, bucket, askerName, id)
return state, err
},
now: time.Now,
caller: link.Caller(),
waitFor: routerAnswersWithin, waitEvery: routerAnswersEvery,
}
words, err := pr.propose(ctx, proposeInput{module: module, node: node, values: values, clear: clear, replace: replace})
if err != nil {
return err
}
fmt.Println(words)
return nil
}
// ---- listing ---------------------------------------------------------------------------------------
// proposalsCommand is `settings proposals [<id>]`: every proposal, newest first, and where each stands; with an
// id, the proposal whole — its values, the layer it was shown against, and its digest.
func proposalsCommand(ctx context.Context, id string) error {
return onTheBus(func(conn *nats.Conn) error {
all, err := busAsked{conn: conn}.All(ctx)
if err != nil {
return err
}
var proposals []asked
for _, r := range all {
if r.Proposal != nil {
proposals = append(proposals, r)
}
}
sort.Slice(proposals, func(i, j int) bool { return proposals[i].Opened.After(proposals[j].Opened) })
if id != "" {
for _, r := range proposals {
if r.ID == id {
fmt.Print(describeProposal(r, time.Now()))
return nil
}
}
return fmt.Errorf("no proposal %s is kept", id)
}
if len(proposals) == 0 {
fmt.Println("no settings have been proposed")
return nil
}
for _, r := range proposals {
fmt.Print(proposalLine(r, time.Now()))
}
return nil
})
}
// proposalState is where a proposal stands, in a word or two.
func proposalState(r asked, now time.Time) string {
switch {
case r.State == askOpen && now.Before(r.Ask.Expires):
return "waiting for the operator until " + r.Ask.Expires.Local().Format("2006-01-02 15:04")
case r.State == askOpen:
return "expired unanswered; discarded"
case r.Acted != "":
return r.State + ": " + r.Acted
}
return r.State
}
func proposalLine(r asked, now time.Time) string {
p := *r.Proposal
what := "set"
if p.Clear {
what = "clear"
}
keys := strings.Join(layerKeys(p.Values), ", ")
if p.Clear {
keys = "the whole layer"
}
return fmt.Sprintf("%s %s %s on %s (%s)\n by %s at %s; fingerprint %s\n %s\n", r.ID, what, p.Module, p.where(),
keys, p.From, p.At.Local().Format("2006-01-02 15:04"), fingerprint(p.Digest), proposalState(r, now))
}
func describeProposal(r asked, now time.Time) string {
p := *r.Proposal
var b strings.Builder
b.WriteString(proposalLine(r, now))
fmt.Fprintf(&b, " digest %s; the layer it was shown against %s\n", p.Digest, p.BeforeDigest)
shownValues, _ := json.MarshalIndent(p.Values, " ", " ")
if p.Clear {
fmt.Fprintf(&b, " clears the layer\n")
} else {
fmt.Fprintf(&b, " values:\n %s\n", shownValues)
}
if p.HadLayer {
shownBefore, _ := json.MarshalIndent(p.Before, " ", " ")
fmt.Fprintf(&b, " the layer as it stood:\n %s\n", shownBefore)
} else {
b.WriteString(" there was no layer\n")
}
if r.Warrant != nil && r.Warrant.By != nil {
fmt.Fprintf(&b, " answered: %s, through %s, at %s\n", r.Warrant.Says(), viaWords(*r.Warrant),
r.Warrant.At.Local().Format("2006-01-02 15:04"))
}
return b.String()
}
// ---- acting on the warrant --------------------------------------------------------------------------
// setOnWarrant performs an approved proposal: the layer set or cleared as `settings set` and `settings clear` at the
// terminal do, with who approved it kept beside the layer. It answers the words of what changed.
type setOnWarrant func(ctx context.Context, p settingsProposal, askID, setBy string) (string, error)
// decideProposal is what the asker does with a warrant for a proposal (asker.Decided): nothing on Decline, and on
// Approve the act only when it is the one the option bound — the digest of the exact values kept here is the one
// in the act, and so the one the ask's option bound and the warrant's ask digest covers.
func (a *asker) decideProposal(ctx context.Context, r asked, act conditions.Action, w asks.Warrant) (string, error) {
p := *r.Proposal
if act.Arguments["answer"] != answerApprove {
return "nothing: the operator declined; the layer is unchanged", nil
}
if a.setLayer == nil {
return "", errors.New("this controller cannot set a layer on a warrant")
}
// The record's own proposal against the act the option bound: the act is composed again from the record —
// its module, machine, values (digested again), the layer they replace, whether a removal is meant, a clear,
// who proposed it and when — and must digest to what the option bound when the operator was shown it. A
// record changed after the ask, in any field, is refused and nothing is set.
again := p
again.Digest, again.BeforeDigest = layerDigest(p.Values), layerDigest(p.Before)
if p.Clear {
again.Digest = layerDigest(p.Before)
}
recomposed := again.actions(r.ID)
chosen := -1
for i, candidate := range recomposed {
if candidate.Arguments["answer"] == act.Arguments["answer"] {
chosen = i
}
}
option, offered := r.Ask.Option(w.Option)
if chosen < 0 || !offered {
return "", fmt.Errorf("the proposal %s offers no answer %q: nothing is set", r.ID, act.Arguments["answer"])
}
if err := option.Performs(boundAct(recomposed[chosen])); err != nil {
return "", fmt.Errorf("the proposal kept for %s is not the one the operator was shown: %v", r.ID, err)
}
setBy := fmt.Sprintf("approved by %s via %s at %s (ask %s, proposed by %s)", byWords(w), viaWords(w),
w.At.Local().Format("2006-01-02 15:04"), r.ID, p.From)
return a.setLayer(ctx, p, r.ID, setBy)
}
// setLayerIn is setOnWarrant on this controller's stores: the layer must still be the one the operator was shown
// the change against (to-be 46 §10, step 7), then it is set with the same judgement as at the terminal.
func setLayerIn(open *stores) setOnWarrant {
return func(ctx context.Context, p settingsProposal, askID, setBy string) (string, error) {
inv := open.inventory
before, had, err := inv.Layer(ctx, p.Node, p.Module)
if err != nil {
return "", err
}
if layerDigest(before) != p.BeforeDigest || had != p.HadLayer {
return "", fmt.Errorf("the layer of %s on %s changed since the operator was shown the change: it is not set; "+
"propose it again", p.Module, p.where())
}
var changed string
if p.Clear {
if err := inv.ClearSettingsBy(ctx, p.Node, p.Module, setBy); err != nil {
return "", err
}
changed = "the layer is removed"
} else {
added, altered, removed := settingsChange(before, p.Values)
if len(removed) > 0 && !p.Replace {
return "", fmt.Errorf("the layer would no longer set %s, and the removal was not meant: nothing is set",
strings.Join(removed, ", "))
}
if err := inv.SetSettingsBy(ctx, p.Node, p.Module, p.Values, setBy); err != nil {
return "", err
}
var parts []string
for _, k := range added {
parts = append(parts, "+ "+k)
}
for _, k := range altered {
parts = append(parts, "~ "+k)
}
for _, k := range removed {
parts = append(parts, "- "+k)
}
changed = strings.Join(parts, ", ")
if changed == "" {
changed = "nothing changed"
}
}
return changed + " (ask " + askID + ")", nil
}
}
-835
View File
@@ -1,835 +0,0 @@
package main
import (
"context"
"encoding/json"
"errors"
"io"
"os"
"slices"
"strings"
"testing"
"time"
"git.novox.be/novox/mesh-sdk/go/asks"
"github.com/novox/mesh-controller/internal/catalogue"
"github.com/novox/mesh-controller/internal/conditions"
"github.com/novox/mesh-controller/internal/outward"
)
// novox/hq ADR 0277: a trusted setting is proposed through a verb by anyone the bus admits, asked of the
// operator at the level approve with the exact change, and set only on the operator's warrant — once, for the
// exact values the option bound; declined, expired or refused, it is discarded; nothing is asked when nothing
// can carry the ask.
// aProposer is the propose path with its reaches faked: a layer as it stands, a judge that records what it
// judged, a memory store, and what was published.
type proposerRig struct {
pr proposer
store memAskedStore
sent []published
judged []map[string]any
before map[string]any
had bool
refusedBy string
now time.Time
}
func newProposerRig(t *testing.T) *proposerRig {
r := &proposerRig{store: memAskedStore{}, now: time.Date(2026, 10, 10, 14, 5, 0, 0, time.UTC)}
r.pr = proposer{
layer: func(_ context.Context, node, module string) (map[string]any, bool, error) {
return r.before, r.had, nil
},
judge: func(_ context.Context, node, module string, values map[string]any) error {
r.judged = append(r.judged, values)
if r.refusedBy != "" {
return errors.New(r.refusedBy)
}
return nil
},
machines: func(context.Context) ([]string, error) { return []string{"anchor", "laptop", "shanks"}, 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
},
now: func() time.Time { return r.now },
caller: "g14/claude-code, through the mesh-controller seat",
waitFor: time.Millisecond,
waitEvery: time.Millisecond,
}
return r
}
func (r *proposerRig) askSent(t *testing.T) asks.Ask {
t.Helper()
if len(r.sent) != 1 || r.sent[0].subject != asks.AskSubject("mesh-controller") {
t.Fatalf("published %+v", r.sent)
}
var q asks.Ask
if err := json.Unmarshal(r.sent[0].body, &q); err != nil {
t.Fatal(err)
}
return q
}
func (r *proposerRig) theProposal(t *testing.T) asked {
t.Helper()
for _, a := range r.store {
if a.Proposal != nil {
return a
}
}
t.Fatal("no proposal is kept")
return asked{}
}
var mountsSources = map[string]any{"sources": "recalbox=smb://nas.lan/recalbox@/mnt/recalbox:ro", "shares": "library=/mnt/library"}
// The ask: at the level approve on both answers, each binding the proposal's act, with every key and its exact new
// value as far as the content rule lets it leave the mesh, the layer it was shown against, and nothing set.
func TestAProposalAsksAtTheLevelApproveWithTheExactChange(t *testing.T) {
r := newProposerRig(t)
r.before, r.had = map[string]any{"shares": "none", "old": "x"}, true
words, err := r.pr.propose(context.Background(), proposeInput{module: "mounts", node: "shanks", values: mountsSources, replace: true})
if err != nil {
t.Fatal(err)
}
q := r.askSent(t)
if err := q.Check(r.now); err != nil {
t.Fatalf("the ask is refused: %v", err)
}
if q.Headline != "Set mounts on shanks?" || q.About != "settings.mounts.shanks" || q.Who != asks.Operator ||
!q.Expires.Equal(r.now.Add(askApproveFor)) {
t.Errorf("the ask: %+v", q)
}
if len(q.Options) != 2 {
t.Fatalf("options %+v", q.Options)
}
for _, o := range q.Options {
if o.Level != asks.Approve || !strings.HasPrefix(o.Binds, "sha256:") {
t.Errorf("the option %s is %s and binds %q", o.ID, o.Level, o.Binds)
}
}
if q.Options[0].Binds == q.Options[1].Binds {
t.Error("Approve and Decline bind the same act")
}
// The message's shape (issue 383): one line per key, then who proposed it and when — and nothing else: the
// fingerprint and the how-to are the Details answer's.
proposedBy := "Proposed by g14/claude-code, through the mesh-controller seat at " + r.now.Local().Format("15:04 on 2 Jan") + "."
if q.Explanation != "+ sources: recalbox=‹address›@‹path›:ro\n~ shares: library=‹path› (was: none)\n- old (was: x)\n"+proposedBy {
t.Errorf("the explanation:\n%s", q.Explanation)
}
for _, leak := range []string{"nas.lan", "/mnt/recalbox", "/mnt/library", "smb://"} {
if strings.Contains(q.Explanation, leak) {
t.Errorf("the explanation carries %q, which may not leave the mesh", leak)
}
}
// Where the sender is proven, the values whole: the mount point and the share are what is approved.
if q.Whole != "+ sources: recalbox=smb://nas.lan/recalbox@/mnt/recalbox:ro\n~ shares: library=/mnt/library (was: none)\n- old (was: x)\n"+proposedBy {
t.Errorf("the whole words:\n%s", q.Whole)
}
for _, line := range []string{
"Fingerprint " + fingerprint(layerDigest(mountsSources)) + ".",
"On a channel that does not prove who answers, these values are shown as ‹address›, ‹path› or ‹withheld›.",
"Read it whole, with this fingerprint: settings proposals " + kept(r).ID + " at the controller's terminal",
"Approved, the layer is set at once and the machine takes it at its next push.",
} {
if !strings.Contains(q.Details, line) {
t.Errorf("Details lack %q:\n%s", line, q.Details)
}
}
for _, howTo := range []string{"ingerprint", "settings proposals", "mesh-controller.settings", "does not prove", "Approved,"} {
if strings.Contains(q.Explanation, howTo) || strings.Contains(q.Whole, howTo) {
t.Errorf("the message carries the how-to %q", howTo)
}
}
all := []string{q.Headline, q.Explanation, q.OnExpiry}
for _, o := range q.Options {
all = append(all, o.Label, o.Does)
}
if refusal, ok := outward.Check(strings.Join(all, "\n"), "anchor", "laptop", "shanks"); !ok {
t.Errorf("the router would refuse the ask: %s", refusal)
}
if refusal, ok := outward.Secret(q.Whole, "anchor", "laptop", "shanks"); !ok {
t.Errorf("the router would refuse the whole words: %s", refusal)
}
// Kept as the controller's own ask, about no condition, with the proposal whole and its digests.
kept := r.theProposal(t)
p := kept.Proposal
if kept.State != askOpen || kept.Ask.Digest() != q.Digest() || p.Module != "mounts" || p.Node != "shanks" ||
p.Digest != layerDigest(mountsSources) || p.BeforeDigest != layerDigest(r.before) || !p.Replace || !p.HadLayer ||
p.From != r.pr.caller || kept.ofACondition() {
t.Errorf("kept %+v / %+v", kept, p)
}
// Each option binds exactly the act the controller will perform (boundAct over the kept action).
for i, act := range kept.Actions {
binds, _ := asks.ActDigest(boundAct(act))
if q.Options[i].Binds != binds || act.Arguments["values"] != p.Digest || act.Arguments["before"] != p.BeforeDigest ||
act.Verb != proposalVerb || act.Level != conditions.LevelApprove {
t.Errorf("the option %s does not bind the kept act: %+v", q.Options[i].ID, act)
}
}
if len(r.judged) != 1 || r.judged[0]["sources"] != mountsSources["sources"] {
t.Errorf("judged %v", r.judged)
}
if !strings.Contains(words, "nothing changes") || !strings.Contains(words, kept.ID) {
t.Errorf("the caller is told: %s", words)
}
}
// A layer for the whole mesh is proposed too.
func TestAMeshWideLayerIsProposed(t *testing.T) {
r := newProposerRig(t)
if _, err := r.pr.propose(context.Background(), proposeInput{module: "notes", values: map[string]any{"x": "1"}}); err != nil {
t.Fatal(err)
}
q := r.askSent(t)
if q.Headline != "Set notes on the whole mesh?" || q.About != "settings.notes.mesh" ||
!strings.HasPrefix(q.Explanation, "+ x: 1\nProposed by ") || q.Whole != "" {
t.Errorf("%+v", q)
}
}
// kept is the one proposal the rig keeps.
func kept(r *proposerRig) asked {
for _, a := range r.store {
if a.Proposal != nil {
return a
}
}
return asked{}
}
// The switch between the masked and the whole change (issue 383): the same change, under the content rule and
// exact, differs in the masked values alone; a value shaped like a secret is withheld in both, whole, with its
// key still named; and a change no value of which is masked carries no whole words of its own.
func TestAProposalShowsItsValuesWholeOnlyWhereTheSenderIsProvenAndNeverASecret(t *testing.T) {
r := newProposerRig(t)
values := map[string]any{"shares": "media=/storage/media", "sources": "recalbox=smb://nas.lan/recalbox@/mnt/recalbox",
"github": "ghp_abcdefghijklmnopqrstuvwxyz0123456789", "cert": "-----BEGIN CERTIFICATE-----", "font": "Inter 13"}
if _, err := r.pr.propose(context.Background(), proposeInput{module: "mounts", node: "shanks", values: values}); err != nil {
t.Fatal(err)
}
q := r.askSent(t)
masked, _ := kept(r).Proposal.change([]string{"anchor", "laptop", "shanks"}, false)
whole, _ := kept(r).Proposal.change([]string{"anchor", "laptop", "shanks"}, true)
if !strings.Contains(q.Explanation, masked) || !strings.Contains(q.Whole, whole) {
t.Fatalf("the ask does not carry the change masked and whole:\n%s\n--\n%s", q.Explanation, q.Whole)
}
for _, line := range []string{"+ cert: ‹withheld›", "+ font: Inter 13", "+ github: ‹withheld›", "+ shares: media=‹path›", "+ sources: recalbox=‹address›@‹path›"} {
if !strings.Contains(masked, line) {
t.Errorf("masked, lacks %q:\n%s", line, masked)
}
}
for _, line := range []string{"+ cert: ‹withheld›", "+ font: Inter 13", "+ github: ‹withheld›", "+ shares: media=/storage/media",
"+ sources: recalbox=smb://nas.lan/recalbox@/mnt/recalbox"} {
if !strings.Contains(whole, line) {
t.Errorf("whole, lacks %q:\n%s", line, whole)
}
}
for _, secret := range []string{"ghp_", "BEGIN CERTIFICATE"} {
if strings.Contains(q.Explanation, secret) || strings.Contains(q.Whole, secret) || strings.Contains(q.Details, secret) {
t.Errorf("the secret %q reaches the phone", secret)
}
}
if _, ok := outward.Secret(q.Whole, "shanks"); !ok {
t.Error("the router would refuse the whole words")
}
// Mutation: the masked and the whole change differ in exactly the lines whose value was masked.
m, w := strings.Split(masked, "\n"), strings.Split(whole, "\n")
if len(m) != len(w) {
t.Fatalf("masked %d lines, whole %d", len(m), len(w))
}
differ := 0
for i := range m {
if m[i] != w[i] {
differ++
if !strings.Contains(m[i], "‹") || strings.Contains(w[i], "‹") {
t.Errorf("the lines differ otherwise than by the mask:\n%s\n%s", m[i], w[i])
}
}
}
if differ != 2 {
t.Errorf("%d lines differ, and the mask covered 2", differ)
}
// No value masked: the message is the same everywhere, and the ask says no whole words.
r2 := newProposerRig(t)
if _, err := r2.pr.propose(context.Background(), proposeInput{module: "dunst", node: "laptop", values: map[string]any{"font-size": 13, "width": 500}}); err != nil {
t.Fatal(err)
}
if q2 := r2.askSent(t); q2.Whole != "" || !strings.HasPrefix(q2.Explanation, "+ font-size: 13\n+ width: 500\nProposed by ") ||
strings.Contains(q2.Details, "does not prove who answers") {
t.Errorf("%+v", q2)
}
}
// A proposal expired unanswered is kept so by the reconciling, and a warrant for it afterwards sets nothing.
func TestAnExpiredProposalIsKeptExpired(t *testing.T) {
r := newAskerRig(t)
calls := withSetLayer(r)
a := aProposalAsked(t, r, "s7", aProposal(r.now))
r.now = r.now.Add(askApproveFor + time.Minute)
if err := r.a.reconcile(context.Background()); err != nil {
t.Fatal(err)
}
if got := r.store["s7"]; got.State != string(asks.OutcomeExpired) || !strings.HasPrefix(got.Acted, "nothing") {
t.Errorf("kept as %+v", got)
}
answerWith(t, r, warrantOn(a, "approve", r.now.Add(-2*time.Minute)))
if len(*calls) != 0 {
t.Errorf("set after expiry: %+v", *calls)
}
}
// A clear is proposed too, and shows the layer it removes.
func TestAClearIsProposedAndShowsWhatItRemoves(t *testing.T) {
r := newProposerRig(t)
r.before, r.had = map[string]any{"places": map[string]any{"data": map[string]any{"path": "/srv/notes", "owner": "1001:1001"}}}, true
if _, err := r.pr.propose(context.Background(), proposeInput{module: "notes", node: "laptop", clear: true}); err != nil {
t.Fatal(err)
}
q := r.askSent(t)
if q.Headline != "Clear notes on laptop?" || !strings.Contains(q.Explanation, "- places.data.owner: 1001:1001") ||
!strings.Contains(q.Explanation, "- places.data.path: ‹path›") || !strings.Contains(q.Whole, "- places.data.path: /srv/notes") ||
!strings.Contains(q.Details, "Approved, the layer is removed at once") {
t.Errorf("%+v", q)
}
if p := r.theProposal(t).Proposal; !p.Clear || p.Digest != layerDigest(r.before) || len(r.judged) != 0 {
t.Errorf("a clear: %+v, judged %v", p, r.judged)
}
}
// What the phone is shown keeps a value's shape and passes the content rule: an address, a path, a secret's
// shape each replaced in place; a value every word of which may leave the mesh shown whole.
func TestAValueIsShownInItsShapeAndPassesTheContentRule(t *testing.T) {
for in, want := range map[any]string{
"recalbox=smb://nas.lan/recalbox@/mnt/recalbox:ro": "recalbox=‹address›@‹path›:ro",
"library=/mnt/library": "library=‹path›",
"none": "none",
"Inter 13": "Inter 13",
"10.77.0.9:53": "‹address›",
"jochen@example.com": "‹address›",
"nas.lan": "‹address›",
"anchor": "anchor",
"-----BEGIN CERTIFICATE-----": "‹withheld›",
42: "42",
true: "true",
} {
got, whole := sayableValue(in, []string{"anchor"})
if got != want {
t.Errorf("%v shown as %q, want %q", in, got, want)
}
if whole != (got == want && !strings.Contains(want, "‹")) {
t.Errorf("%v: whole %v", in, whole)
}
if _, ok := outward.Check(got, "anchor"); !ok {
t.Errorf("%v shown as %q, which the router refuses", in, got)
}
}
for _, v := range []any{[]any{"a", "/etc/x"}, map[string]any{"path": "/srv/x", "owner": "1001:1001"}} {
got, _ := sayableValue(v, nil)
if _, ok := outward.Check(got); !ok || strings.Contains(got, "/srv") || strings.Contains(got, "/etc") {
t.Errorf("%v shown as %q", v, got)
}
}
}
// A proposal that the mesh would refuse to set, or that would silently remove a key, is refused before anybody is
// asked (ADR 0217 holds for a proposal as for a set).
func TestAProposalTheMeshWouldRefuseIsNotAsked(t *testing.T) {
r := newProposerRig(t)
r.before, r.had = map[string]any{"a": 1, "b": 2}, true
_, err := r.pr.propose(context.Background(), proposeInput{module: "notes", node: "laptop", values: map[string]any{"a": 1}})
if err == nil || !strings.Contains(err.Error(), "would no longer set b") || !strings.Contains(err.Error(), "ADR 0217") {
t.Errorf("a silent removal: %v", err)
}
r.refusedBy = "refused: notes on laptop cannot compose"
_, err = r.pr.propose(context.Background(), proposeInput{module: "notes", node: "laptop", values: map[string]any{"a": 1, "b": 3}})
if err == nil || !strings.Contains(err.Error(), "cannot compose") {
t.Errorf("a layer the mesh refuses: %v", err)
}
if len(r.sent) != 0 || len(r.store) != 0 {
t.Errorf("asked anyway: %+v %+v", r.sent, r.store)
}
}
// Fail closed: no router, no grant, a publish that fails, or the router's refusal each leave nothing waiting for
// an answer that cannot come, and name the terminal's line.
func TestAProposalFailsClosedWhenNothingCanCarryIt(t *testing.T) {
r := newProposerRig(t)
in := proposeInput{module: "mounts", node: "shanks", values: mountsSources}
r.pr.routerHere = func(context.Context) (bool, error) { return false, nil }
if _, err := r.pr.propose(context.Background(), in); err == nil || !strings.Contains(err.Error(), "no router") ||
!strings.Contains(err.Error(), "mesh-cli settings set mounts") {
t.Errorf("without a router: %v", err)
}
r.pr.routerHere = nil
r.pr.grantHeld = func(context.Context) (bool, string, error) { return false, "the bus's user list is behind", nil }
if _, err := r.pr.propose(context.Background(), in); err == nil || !strings.Contains(err.Error(), "user list is behind") {
t.Errorf("without the grant: %v", err)
}
if len(r.sent) != 0 || len(r.store) != 0 {
t.Fatalf("asked anyway: %+v %+v", r.sent, r.store)
}
r.pr.grantHeld = nil
r.pr.publish = func(context.Context, string, []byte, string) error { return errors.New("the bus is away") }
if _, err := r.pr.propose(context.Background(), in); err == nil || !strings.Contains(err.Error(), "the bus is away") ||
!strings.Contains(err.Error(), "never become active") {
t.Errorf("a publish that fails: %v", err)
}
if kept := r.theProposal(t); kept.State != askUnsent {
t.Errorf("an unpublished proposal is %s", kept.State)
}
// The router's refusal, heard by the serving controller and kept here, is said to the caller.
r = newProposerRig(t)
r.pr.publish = func(_ context.Context, _ string, _ []byte, id string) error {
ask := strings.TrimPrefix(id, "ask.")
r.store[ask] = func() asked {
a := r.store[ask]
a.State, a.Acted = string(asks.OutcomeRefused), "nothing: the ask refused: no channel can carry any of its answers now"
return a
}()
return nil
}
if _, err := r.pr.propose(context.Background(), in); err == nil || !strings.Contains(err.Error(), "did not ask the operator") ||
!strings.Contains(err.Error(), "no channel can carry") {
t.Errorf("the router's refusal: %v", err)
}
}
// The bounds: at most three asks open for the controller, and the same change not proposed twice.
func TestAProposalIsBounded(t *testing.T) {
r := newProposerRig(t)
in := proposeInput{module: "mounts", node: "shanks", values: mountsSources}
if _, err := r.pr.propose(context.Background(), in); err != nil {
t.Fatal(err)
}
if _, err := r.pr.propose(context.Background(), in); err == nil || !strings.Contains(err.Error(), "already proposed") {
t.Errorf("the same change twice: %v", err)
}
for _, node := range []string{"laptop", "anchor"} {
if _, err := r.pr.propose(context.Background(), proposeInput{module: "mounts", node: node, values: mountsSources}); err != nil {
t.Fatal(err)
}
}
_, err := r.pr.propose(context.Background(), proposeInput{module: "notes", node: "laptop", values: map[string]any{"x": 1}})
if err == nil || !strings.Contains(err.Error(), "3 questions already wait") {
t.Errorf("a fourth: %v", err)
}
if len(r.sent) != 3 {
t.Errorf("published %d", len(r.sent))
}
}
// ---- the warrant ------------------------------------------------------------------------------------
// aProposalAsked keeps a proposal's ask in the asker rig's store, as propose keeps it.
func aProposalAsked(t *testing.T, r *askerRig, id string, p settingsProposal) asked {
t.Helper()
q, options := p.ask(id, nil)
if err := q.Check(r.now); err != nil {
t.Fatal(err)
}
a := asked{ID: id, Condition: q.About, Ask: q, Actions: p.actions(id), Options: options, State: askOpen, Opened: r.now, Proposal: &p}
r.store[id] = a
return a
}
func aProposal(now time.Time) settingsProposal {
before := map[string]any{"shares": "none"}
return settingsProposal{Module: "mounts", Node: "shanks", Values: mountsSources, Replace: true, Before: before, HadLayer: true,
From: "g14/claude-code", At: now, Digest: layerDigest(mountsSources), BeforeDigest: layerDigest(before)}
}
// warrantOn is the router's warrant for a kept ask, choosing an option by id.
func warrantOn(a asked, option string, now time.Time) asks.Warrant {
o, _ := a.Ask.Option(option)
return asks.Warrant{Ask: a.ID, Asker: "mesh-controller", About: a.Ask.About, Outcome: asks.OutcomeChosen, Option: o.ID,
Label: o.Label, Level: o.Level, Channel: "telegram", Proofs: []string{"P1"}, At: now, AskDigest: a.Ask.Digest(),
By: &asks.Person{Who: asks.Operator, Kind: "telegram", Identity: "42", Verified: "user id verified"}}
}
type setCall struct {
p settingsProposal
ask string
setBy string
}
func withSetLayer(r *askerRig) *[]setCall {
var calls []setCall
r.a.setLayer = func(_ context.Context, p settingsProposal, askID, setBy string) (string, error) {
calls = append(calls, setCall{p, askID, setBy})
return "+ sources, ~ shares", nil
}
return &calls
}
// On Approve the layer is set once, with who approved it and through which channel kept beside it, and the warrant
// is recorded as the operator's decision; heard again, nothing more happens.
func TestTheLayerIsSetOnceOnTheOperatorsApproval(t *testing.T) {
r := newAskerRig(t)
calls := withSetLayer(r)
a := aProposalAsked(t, r, "s1", aProposal(r.now))
if err := r.a.reconcile(context.Background()); err != nil {
t.Fatal(err)
}
if got := r.store["s1"]; got.State != askOpen {
t.Fatalf("the reconciling of conditions ended the proposal: %+v", got)
}
w := warrantOn(a, "approve", r.now.Add(time.Hour))
answerWith(t, r, w)
answerWith(t, r, w) // heard again, or replayed
if len(*calls) != 1 {
t.Fatalf("set %d time(s): %+v", len(*calls), *calls)
}
c := (*calls)[0]
if c.ask != "s1" || c.p.Digest != layerDigest(mountsSources) ||
!strings.HasPrefix(c.setBy, "approved by the operator, as telegram identity 42 via telegram (telegram), user id verified at ") ||
!strings.Contains(c.setBy, "(ask s1, proposed by g14/claude-code)") {
t.Errorf("set by %q for %+v", c.setBy, c.p)
}
if len(r.called)+len(r.silenced) != 0 {
t.Errorf("a verb was called: %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 != "s1" ||
!slices.Contains(act.Args, "answer=approve") || !slices.Contains(act.Args, "values="+layerDigest(mountsSources)) ||
!strings.HasPrefix(act.Outcome, "done") || !personsDecision(act) {
t.Errorf("the record: %+v", act)
}
if got := r.store["s1"]; got.State != string(asks.OutcomeChosen) || !strings.HasPrefix(got.Acted, "done") {
t.Errorf("kept as %+v", got)
}
}
// Decline, expiry, the router's refusal, a cancel: nothing is set, and the proposal is recorded as ended.
func TestDeclineExpiryAndRefusalDiscardAProposal(t *testing.T) {
for name, outcome := range map[string]asks.Outcome{"declined": asks.OutcomeChosen, "expired": asks.OutcomeExpired,
"refused": asks.OutcomeRefused, "cancelled": asks.OutcomeCancelled, "replaced": asks.OutcomeReplaced} {
t.Run(name, func(t *testing.T) {
r := newAskerRig(t)
calls := withSetLayer(r)
a := aProposalAsked(t, r, "s2", aProposal(r.now))
w := warrantOn(a, "decline", r.now.Add(time.Hour))
if outcome != asks.OutcomeChosen {
w = asks.Warrant{Ask: a.ID, Asker: "mesh-controller", Outcome: outcome, Words: "its time passed", At: r.now.Add(time.Hour)}
}
answerWith(t, r, w)
if len(*calls) != 0 {
t.Fatalf("set: %+v", *calls)
}
got := r.store["s2"]
if got.State != string(outcome) || got.Acted == "" || !strings.HasPrefix(got.Acted, "nothing") {
t.Errorf("kept as %+v", got)
}
if outcome == asks.OutcomeChosen && (len(r.acts) != 1 || !strings.Contains(r.acts[0].Outcome, "declined")) {
t.Errorf("a decline is a decision too: %+v", r.acts)
}
// An approval after it ended is refused.
answerWith(t, r, warrantOn(a, "approve", r.now.Add(2*time.Hour)))
if len(*calls) != 0 {
t.Fatalf("set after the end: %+v", *calls)
}
})
}
}
// The warrant binds the exact values: a record whose values changed after the ask, an action changed, a warrant for
// another ask's digest, at another level, or after expiry each set nothing.
func TestAWarrantSetsOnlyTheExactValuesTheOperatorWasShown(t *testing.T) {
cases := map[string]func(r *askerRig, a asked) asks.Warrant{
"the values changed in the record": func(r *askerRig, a asked) asks.Warrant {
a.Proposal.Values = map[string]any{"sources": "recalbox=smb://evil/recalbox@/mnt/recalbox", "shares": "library=/mnt/library"}
r.store[a.ID] = a
return warrantOn(a, "approve", r.now.Add(time.Hour))
},
"the layer it was shown against changed in the record": func(r *askerRig, a asked) asks.Warrant {
a.Proposal.Before = map[string]any{"shares": "other"}
r.store[a.ID] = a
return warrantOn(a, "approve", r.now.Add(time.Hour))
},
"the module changed in the record": func(r *askerRig, a asked) asks.Warrant {
a.Proposal.Module = "sshd"
r.store[a.ID] = a
return warrantOn(a, "approve", r.now.Add(time.Hour))
},
"the machine changed in the record": func(r *askerRig, a asked) asks.Warrant {
a.Proposal.Node = "anchor"
r.store[a.ID] = a
return warrantOn(a, "approve", r.now.Add(time.Hour))
},
"the removal became meant in the record": func(r *askerRig, a asked) asks.Warrant {
a.Proposal.Replace = !a.Proposal.Replace
r.store[a.ID] = a
return warrantOn(a, "approve", r.now.Add(time.Hour))
},
"the set became a clear in the record": func(r *askerRig, a asked) asks.Warrant {
a.Proposal.Clear = true
r.store[a.ID] = a
return warrantOn(a, "approve", r.now.Add(time.Hour))
},
"the action's digest was changed": func(r *askerRig, a asked) asks.Warrant {
a.Actions[0].Arguments["values"] = layerDigest(map[string]any{"sources": "x"})
r.store[a.ID] = a
return warrantOn(a, "approve", r.now.Add(time.Hour))
},
"another ask's digest": func(r *askerRig, a asked) asks.Warrant {
w := warrantOn(a, "approve", r.now.Add(time.Hour))
w.AskDigest = "sha256:0000"
return w
},
"at the level acknowledge": func(r *askerRig, a asked) asks.Warrant {
w := warrantOn(a, "approve", r.now.Add(time.Hour))
w.Level = asks.Acknowledge
return w
},
"after expiry": func(r *askerRig, a asked) asks.Warrant {
return warrantOn(a, "approve", r.now.Add(askApproveFor+time.Minute))
},
"nobody chose": func(r *askerRig, a asked) asks.Warrant {
w := warrantOn(a, "approve", r.now.Add(time.Hour))
w.By = nil
return w
},
"another asker's": func(r *askerRig, a asked) asks.Warrant {
w := warrantOn(a, "approve", r.now.Add(time.Hour))
w.Asker = "claude-code"
return w
},
}
for name, tamper := range cases {
t.Run(name, func(t *testing.T) {
r := newAskerRig(t)
calls := withSetLayer(r)
a := aProposalAsked(t, r, "s3", aProposal(r.now))
answerWith(t, r, tamper(r, a))
if len(*calls) != 0 {
t.Fatalf("set: %+v", *calls)
}
if got := r.store["s3"]; strings.HasPrefix(got.Acted, "done") {
t.Errorf("kept as done: %+v", got)
}
})
}
}
// A proposal counts toward what the controller holds open, so a fourth ask is not attempted while three are.
func TestOpenProposalsCountTowardTheAsksHeldOpen(t *testing.T) {
r := newAskerRig(t)
for _, id := range []string{"s4", "s5", "s6"} {
aProposalAsked(t, r, id, aProposal(r.now))
}
r.open = []conditions.Condition{heldCondition()}
if err := r.a.reconcile(context.Background()); err != nil {
t.Fatal(err)
}
if len(r.sent) != 0 {
t.Errorf("asked beyond the bound: %d", len(r.sent))
}
for _, id := range []string{"s4", "s5", "s6"} {
if r.store[id].State != askOpen {
t.Errorf("%s was ended by the reconciling of conditions: %+v", id, r.store[id])
}
}
}
// ---- the verb ---------------------------------------------------------------------------------------
func TestTheSettingsVerbComposesProposeAndProposals(t *testing.T) {
for name, c := range map[string]struct {
args map[string]any
want string
}{
"propose on a machine": {map[string]any{"module": "mounts", "node": "shanks", "values": `{"sources":"x"}`, "propose": "true"},
"settings propose mounts {\"sources\":\"x\"} --node shanks"},
"propose with replace": {map[string]any{"module": "mounts", "values": `{"a":1}`, "propose": "true", "replace": "true"},
"settings propose mounts {\"a\":1} --replace"},
"propose a clear": {map[string]any{"module": "mounts", "node": "shanks", "propose": "true", "clear": "true"},
"settings propose mounts --clear --node shanks"},
"the proposals": {map[string]any{"proposals": "true"}, "settings proposals"},
"one proposal": {map[string]any{"proposal": "s1"}, "settings proposals s1"},
} {
argv, err := argvFor("settings", c.args)
if err != nil || strings.Join(argv, " ") != c.want {
t.Errorf("%s: %v %v", name, argv, err)
}
}
for name, args := range map[string]map[string]any{
"propose without a module": {"values": `{"a":1}`, "propose": "true"},
"propose without values": {"module": "mounts", "propose": "true"},
"propose values and clear": {"module": "mounts", "values": `{"a":1}`, "clear": "true", "propose": "true"},
"proposals with a module": {"proposals": "true", "module": "mounts"},
"proposals and a proposal": {"proposals": "true", "proposal": "s1"},
"a proposal with values": {"proposal": "s1", "values": `{"a":1}`},
"proposals with a listing": {"proposals": "true", "list": "preferences"},
} {
if _, err := argvFor("settings", args); err == nil {
t.Errorf("%s was composed", name)
}
}
// The generic command verb proposes nothing (it is the settings verb's), and lists proposals (a read).
if err := refusedAsTheGenericCommand([]string{"settings", "propose", "mounts", "{}", "--node", "shanks"}); err == nil {
t.Error("the generic command proposed")
}
if err := refusedAsTheGenericCommand([]string{"settings", "proposals"}); err != nil {
t.Errorf("the generic command may not list proposals: %v", err)
}
}
// ---- on the real stores -----------------------------------------------------------------------------
// setLayerIn sets the layer as the terminal does — judged, the removal meant, the history kept — with who approved it
// beside it; and refuses once the layer is no longer the one the operator was shown the change against.
func TestSetOnAWarrantJudgesTheLayerAndKeepsWhoApprovedIt(t *testing.T) {
open := aMesh(t)
ctx := t.Context()
register(t, open, catalogue.Manifest{Module: "notes", Version: "1",
// y is a preference with a default, so a layer may leave it out; x is the operator's own (ADR 0262).
Settings: map[string]catalogue.SettingDeclaration{"y": {Kind: "preference", Default: "10", Why: "a size"}},
Resources: []map[string]any{{"id": "data", "type": "directory", "mode": "0755"},
// A trusted file (unmarked), so its keys are the terminal's — and now the warrant's (novox/hq ADR 0277).
{"id": "rc", "type": "file", "path": "/etc/notes.conf", "mode": "0644", "content": "x = ${setting:x}\ny = ${setting:y}\n"}}})
if _, err := assign(ctx, open, "laptop", "notes"); err != nil {
t.Fatal(err)
}
set := setLayerIn(open)
now := time.Now()
first := map[string]any{"x": "1", "y": "2"}
p := settingsProposal{Module: "notes", Node: "laptop", Values: first, From: "g14/claude-code", At: now,
Digest: layerDigest(first), BeforeDigest: layerDigest(nil)}
changed, err := set(ctx, p, "s9", "approved by the operator, as telegram identity 42 via telegram at 14:05 (ask s9)")
if err != nil || !strings.Contains(changed, "+ x") {
t.Fatalf("%q %v", changed, err)
}
layer, has, err := open.inventory.Layer(ctx, "laptop", "notes")
if err != nil || !has || layer["x"] != "1" {
t.Fatalf("the layer: %v %v %v", layer, has, err)
}
setBy, _, has, err := open.inventory.LayerOrigin(ctx, "laptop", "notes")
if err != nil || !has || !strings.HasPrefix(setBy, "approved by the operator, as telegram identity 42 via telegram") {
t.Fatalf("who set it: %q %v", setBy, err)
}
// Shown by `settings show`, at the terminal and through the verb.
out := captureStdout(t, func() {
if err := atTheTerminal(t, "settings", "show", "notes", "--node", "laptop"); err != nil {
t.Fatal(err)
}
})
if !strings.Contains(out, "approved by the operator, as telegram identity 42 via telegram") {
t.Errorf("settings show says:\n%s", out)
}
// The same proposal again: the layer is no longer the one the operator was shown it against.
if _, err := set(ctx, p, "s9", "approved …"); err == nil || !strings.Contains(err.Error(), "changed since the operator was shown") {
t.Errorf("a stale proposal: %v", err)
}
// A removal not meant is refused; one meant is taken, and the history says who had set the layer.
second := map[string]any{"x": "1"}
q := settingsProposal{Module: "notes", Node: "laptop", Values: second, Before: first, HadLayer: true,
From: "g14/claude-code", At: now, Digest: layerDigest(second), BeforeDigest: layerDigest(first)}
if _, err := set(ctx, q, "s10", "approved …"); err == nil || !strings.Contains(err.Error(), "removal was not meant") {
t.Errorf("a silent removal: %v", err)
}
// A layer the mesh refuses is refused here too, with the same words as at the terminal.
bad := q
bad.Values, bad.Digest = map[string]any{"x": "a\nb", "y": "2"}, layerDigest(map[string]any{"x": "a\nb", "y": "2"})
if _, err := set(ctx, bad, "s11", "approved …"); err == nil || !strings.Contains(err.Error(), "line break") {
t.Errorf("a line break on a warrant: %v", err)
}
if layer, _, _ := open.inventory.Layer(ctx, "laptop", "notes"); layer["y"] != "2" {
t.Fatalf("a refused act changed the layer: %v", layer)
}
q.Replace = true
if _, err := set(ctx, q, "s10", "approved later"); err != nil {
t.Fatal(err)
}
past, err := open.inventory.SettingsHistory(ctx, "laptop", "notes")
if err != nil || len(past) != 1 || !strings.HasPrefix(past[0].SetBy, "approved by the operator") {
t.Errorf("the history: %+v %v", past, err)
}
// And a clear, keeping who cleared it with the copy.
c := settingsProposal{Module: "notes", Node: "laptop", Clear: true, Before: second, HadLayer: true, From: "x", At: now,
Digest: layerDigest(second), BeforeDigest: layerDigest(second)}
if _, err := set(ctx, c, "s12", "approved by the operator at 15:00"); err != nil {
t.Fatal(err)
}
if _, has, _ := open.inventory.Layer(ctx, "laptop", "notes"); has {
t.Error("the layer was not cleared")
}
past, _ = open.inventory.SettingsHistory(ctx, "laptop", "notes")
if len(past) != 2 || !strings.Contains(past[0].SetBy, "cleared approved by the operator at 15:00") {
t.Errorf("the history after a clear: %+v", past)
}
}
// A layer set at the terminal says so, and one set through a verb names the verb (novox/hq ADR 0277).
func TestALayerSaysWhoSetIt(t *testing.T) {
open := aMesh(t)
ctx := t.Context()
register(t, open, catalogue.Manifest{Module: "notes", Version: "1",
Resources: []map[string]any{{"id": "rc", "type": "file", "path": "/etc/notes.conf", "mode": "0644", "trusted": false,
"content": "x = ${setting:x}\n"}}})
if _, err := assign(ctx, open, "laptop", "notes"); err != nil {
t.Fatal(err)
}
if err := atTheTerminal(t, "settings", "set", "notes", `{"x":"1"}`, "--node", "laptop"); err != nil {
t.Fatal(err)
}
setBy, _, _, _ := open.inventory.LayerOrigin(ctx, "laptop", "notes")
if !strings.HasPrefix(setBy, "set at the controller's terminal by ") {
t.Errorf("at the terminal: %q", setBy)
}
if err := throughVerb(t, "settings", map[string]any{"module": "notes", "node": "laptop", "values": `{"x":"2"}`}); err != nil {
t.Fatal(err)
}
setBy, _, _, _ = open.inventory.LayerOrigin(ctx, "laptop", "notes")
if !strings.HasPrefix(setBy, "set by ") || !strings.Contains(setBy, "through settings") {
t.Errorf("through the verb: %q", setBy)
}
}
// A trusted setting is still refused through the settings verb (novox/hq issue 339), and the refusal now names the
// proposal as the way.
func TestATrustedSettingThroughAVerbNamesTheProposal(t *testing.T) {
open := aMesh(t)
ctx := t.Context()
register(t, open, catalogue.Manifest{Module: "notes", Version: "1",
Resources: []map[string]any{{"id": "data", "type": "directory", "mode": "0755"}}})
if _, err := assign(ctx, open, "laptop", "notes"); err != nil {
t.Fatal(err)
}
err := throughVerb(t, "settings", map[string]any{"module": "notes", "node": "laptop",
"values": `{"places":{"data":{"path":"/srv/notes","owner":"1001:1001"}}}`})
if err == nil || !strings.Contains(err.Error(), "issue 339") || !strings.Contains(err.Error(), "propose") {
t.Errorf("%v", err)
}
if _, has, _ := open.inventory.Layer(ctx, "laptop", "notes"); has {
t.Error("a layer was kept")
}
}
// captureStdout runs f and answers what it printed to standard output.
func captureStdout(t *testing.T, f func()) string {
t.Helper()
before := os.Stdout
r, w, err := os.Pipe()
if err != nil {
t.Fatal(err)
}
os.Stdout = w
done := make(chan string)
go func() {
var b strings.Builder
_, _ = io.Copy(&b, r)
done <- b.String()
}()
f()
os.Stdout = before
_ = w.Close()
return <-done
}
+1 -37
View File
@@ -76,21 +76,6 @@ func serve(ctx context.Context) (err error) {
}
defer open.Close()
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)
if err != nil {
@@ -182,9 +167,6 @@ func serve(ctx context.Context) (err error) {
// Open plans move on a timer as well as on outcomes (novox/hq ADR 0162): a tier waiting for
// machines to report moves when they have, and a plan left by a replaced controller resumes.
go planTicker(ctx, open)
// And the merge window (novox/hq ADR 0276): a batch is cut into its walk when the window closes, which no
// merge or outcome says.
go batchCutter(ctx, open)
// And the pending assignments settled on a tick of their own (novox/hq ADR 0261): made once their module
// is registered, ended with why when its build will not register it, raised and cleared as conditions.
// Never by a read.
@@ -1268,14 +1250,11 @@ func issueMemberships(ctx context.Context, open *stores, server *link.Server, se
if err != nil {
return err
}
where := broker.PlacementsOf(records, records.Interchangeable)
bus, ok := server.Bus().(link.OverNATS)
if !ok {
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
// 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
@@ -1616,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))
}
}
// 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
}
+4 -11
View File
@@ -135,19 +135,12 @@ func TestRetryRefusesWhatItCannotResume(t *testing.T) {
if err := retryRefusal(stopped, nil); err == nil || !strings.Contains(err.Error(), "nothing in tier 0") {
t.Errorf("a plan with nothing failed to build was retried: %v", err)
}
// A failed or done plan does not supersede it.
if err := retryRefusal(failed, []inventory.Plan{plan("plan-4", inventory.PlanFailed, at.Add(time.Hour)),
plan("plan-5", inventory.PlanDone, at.Add(time.Hour))}); err != nil {
t.Errorf("refused for a plan that does not supersede it: %v", err)
}
// Another branch's open walk, or an older one, does not supersede it, and is one walk open: one at a time
// (novox/hq ADR 0276).
// Another branch's newer plan, an older one, and a failed one do not supersede it.
other := plan("plan-3", inventory.PlanBuilding, at.Add(time.Hour))
other.Branch = "release"
for _, open := range []inventory.Plan{other, plan("plan-0", inventory.PlanBuilding, at.Add(-time.Hour))} {
if err := retryRefusal(failed, []inventory.Plan{open}); err == nil || !strings.Contains(err.Error(), "one walk at a time") {
t.Errorf("retried beside the open walk %s: %v", open.ID, err)
}
if err := retryRefusal(failed, []inventory.Plan{other, plan("plan-0", inventory.PlanBuilding, at.Add(-time.Hour)),
plan("plan-4", inventory.PlanFailed, at.Add(time.Hour))}); err != nil {
t.Errorf("refused for a plan that does not supersede it: %v", err)
}
}
-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
}
// walkedBy is the open plan that has started walking a module's build — sent it to a first machine, not
// yet passed — and whose walk this move would cross; empty when none does. A move of the same build to a
// machine that plan already sent it is not a crossing: the build is there. A move of **another** build of
// 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 {
// walkedBy is the open plan that has started walking a module's build — sent it to a first machine,
// not yet passed — other than to this machine; empty when none does.
func (f moveFacts) walkedBy(module, node string) string {
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]
if !holds || s == nil || s.FirstAt == nil || s.SentAt != nil {
continue
}
if slices.Contains(s.First, node) && sameCommit(s.Commit, to) {
if !p.Open() || !holds || s == nil || s.FirstAt == nil || s.SentAt != nil || slices.Contains(s.First, node) {
continue
}
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) {
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,
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 {
i := slices.IndexFunc(moves, func(mv inventory.CarriedMove) bool { return mv.Module == o.Module })
switch {
@@ -557,7 +526,7 @@ func waitingMoves(ctx context.Context, open *stores, all bool) (map[string][]inv
return nil, err
}
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)
}
}
@@ -689,7 +658,7 @@ func advanceRelease(ctx context.Context, open *stores, p *inventory.Plan) (bool,
r.Next++
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))
if said := recreationsSaid(moves); said != "" {
p.Note += "; " + said
@@ -724,15 +693,6 @@ func advanceRelease(ctx context.Context, open *stores, p *inventory.Plan) (bool,
r.Next++
r.Gate = 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 = ""
batched, back := batchingRollbacks(ctx)
+90 -116
View File
@@ -138,11 +138,9 @@ func reachableFrom(moved []string, edges []inventory.Edge) []string {
grew = false
for _, e := range edges {
// 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
// packages edge, read from a record made before novox/hq ADR 0267, widens nothing either:
// a shared file moves each module whose build source holds it, directly.
if e.Kind == inventory.EdgeBuiltBy || e.Kind == inventory.EdgeWorkerOf || e.Kind == inventory.EdgePackages ||
e.Kind == edgeGroupOrder {
// nothing it builds, and a new controller changes nothing about the holder it orders —
// what packages the controller's source is already a code edge.
if e.Kind == inventory.EdgeBuiltBy || e.Kind == inventory.EdgeWorkerOf {
continue
}
if in[e.To] && !in[e.From] {
@@ -181,18 +179,9 @@ func hasCycle(tiers [][]string, edges []inventory.Edge) bool {
return false
}
// planOfMerge is the plan one merge produces: the moved modules and everything reachable from them,
// planFor is the plan a merge produces: the moved modules and everything reachable from them,
// tiered, with the merge it answers.
func planOfMerge(m link.SourceMoved, moved []string, edges []inventory.Edge) inventory.Plan {
p := planOfMoves(moved, edges)
merged, _ := time.Parse(time.RFC3339Nano, m.MergedAt)
p.Repository, p.Branch, p.Commit, p.Merged = m.Owner+"/"+m.Repo, m.Base, m.Commit, merged.UTC()
return p
}
// planOfMoves is the walk of a set of moved modules (novox/hq ADR 0162, 0276): they and everything reachable
// from them, tiered along the graph, with no merge named yet.
func planOfMoves(moved []string, edges []inventory.Edge) inventory.Plan {
set := reachableFrom(moved, edges)
tiers := tiersOf(set, edges)
modules := map[string]*inventory.PlanModule{}
@@ -200,14 +189,71 @@ func planOfMoves(moved []string, edges []inventory.Edge) inventory.Plan {
modules[name] = &inventory.PlanModule{}
}
return inventory.Plan{
ID: fmt.Sprintf("plan-%d", time.Now().UnixNano()),
Created: time.Now().UTC(),
State: inventory.PlanBuilding,
Tiers: tiers,
Modules: modules,
ID: fmt.Sprintf("plan-%d", time.Now().UnixNano()),
Repository: m.Owner + "/" + m.Repo,
Branch: m.Base,
Commit: m.Commit,
Created: time.Now().UTC(),
State: inventory.PlanBuilding,
Tiers: tiers,
Modules: modules,
}
}
// supersededBy is what a newer plan takes over from the open plans it supersedes (novox/hq issue
// 254, ADR 0218): the modules they had not finished, and those plans closed as superseded.
//
// **A merge looked at no plan but its own.** Two merges of one repository a few minutes apart were
// two open plans asking for the same modules, each sending machines what it built; and a plan that
// would never move again — waiting on a report that could not come, at 97b1b2b — stayed open for
// ever beside the newer ones, read as work in progress by everyone who looked. The newer merge is the
// newer intent for that repository and branch, so its plan takes over: every open plan of the same
// repository and branch **created before it** — by the time the plans were made, never by comparing
// commits, which have no order of their own — gives up the modules it had not built, and those are
// planned again in the newer plan beside what the newer merge moved.
//
// "Not built" is a module not yet asked, or asked and not answered; **and a module built and not
// yet sent to its machines**, where its policy rolls it out: closed, the older plan would never send
// it, and the catalogue announces no move for a rebuild (issue 189), so the newer plan builds and
// sends it. A build the older plan asked still finishes and registers as any build does — ordered by
// when it was asked (issue 219), so the newer plan's ask, made later, is the one that stands.
//
// 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.
func supersededBy(newer inventory.Plan, open []inventory.Plan, rollsOut func(string) bool) ([]string, []inventory.Plan) {
folded := map[string]bool{}
var closed []inventory.Plan
for _, old := range open {
if old.ID == newer.ID || !old.Open() || !strings.EqualFold(old.Repository, newer.Repository) ||
(old.Branch != "" && old.Branch != newer.Branch) || !old.Created.Before(newer.Created) {
continue
}
var took []string
for name, s := range old.Modules {
if s != nil && s.State == planDeleted {
continue // deleted at its source: nothing to plan again
}
if s == nil || s.State != "built" || (s.SentAt == nil && rollsOut(name)) {
folded[name] = true
took = append(took, name)
}
}
sort.Strings(took)
old.State = inventory.PlanSuperseded
old.Note = fmt.Sprintf("superseded at tier %d by %s (%s at %s)", old.Tier, newer.ID, newer.Repository, short(newer.Commit))
if len(took) > 0 {
old.Note += "; " + strings.Join(took, ", ") + " planned there again"
}
closed = append(closed, old)
}
out := make([]string, 0, len(folded))
for name := range folded {
out = append(out, name)
}
sort.Strings(out)
return out, closed
}
// 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
// the machines running it before the next tier is asked. A base an image stands on need only be
@@ -235,11 +281,8 @@ func gates(p inventory.Plan, edges []inventory.Edge, rollsOut func(string) bool)
seen := map[string]bool{}
var out []string
for _, e := range edges {
// A delivery group's order inside a walk (novox/hq ADR 0276 decision 5) gates as built-by does: the
// member before is running on its machines before the member after is asked.
ordered := e.Kind == edgeGroupOrder ||
e.Kind == inventory.EdgeBuiltBy && !isBaseOf(e.From, e.To, edges, all)
if later[e.From] && inTier[e.To] && ordered && !seen[e.To] && rollsOut(e.To) {
if later[e.From] && inTier[e.To] && e.Kind == inventory.EdgeBuiltBy && !seen[e.To] && rollsOut(e.To) &&
!isBaseOf(e.From, e.To, edges, all) {
seen[e.To] = true
out = append(out, e.To)
}
@@ -330,15 +373,8 @@ func askModule(ctx context.Context, p *inventory.Plan, name string, byName map[s
}
source := buildSource{Repository: e.Source.Repository, Seat: e.Source.Seat}
fmt.Printf(" tier %d: ", p.Tier)
// The branch it follows, never a commit a build once named (novox/hq 04-ISSUES/215): the branch contains every
// commit a batch's walk carries — but for a merge walked alone after a failed walk (novox/hq ADR 0276
// decision 3), built on its own commit to find which merge brought the failure.
ref := followedBranch(e.Source.Ref)
carried := p.CommitOn(e.Source.Repository, ref)
if p.Delivery != nil && p.Delivery.Alone && carried != "" {
ref = carried
}
id, err := askABuild(ctx, source, e.Source.Path, ref)
// The branch it follows, never a commit a build once named (novox/hq 04-ISSUES/215).
id, err := askABuild(ctx, source, e.Source.Path, followedBranch(e.Source.Ref))
if err != nil {
state.State = "failed"
state.Why = err.Error()
@@ -346,14 +382,8 @@ func askModule(ctx context.Context, p *inventory.Plan, name string, byName map[s
p.Note = fmt.Sprintf("%s could not be asked for: %v", name, err)
return
}
// The commit of the module's own repository the walk carries (novox/hq ADR 0276): a batch's walk carries
// one per repository.
commit := carried
if commit == "" {
commit = p.Commit
}
recordAsked(ctx, inventory.BuildRequest{ID: id, Repository: e.Source.Repository, Seat: e.Source.Seat,
Path: e.Source.Path, Ref: followedBranch(e.Source.Ref), Commit: commit, For: "plan"})
Path: e.Source.Path, Ref: followedBranch(e.Source.Ref), Commit: p.Commit, For: "plan"})
state.State = "asked"
state.AskedAt = &now
state.Build = id
@@ -472,12 +502,6 @@ func advancePlans(ctx context.Context, open *stores) {
// advanceHeld is advancePlans for a caller already holding the plans.
func advanceHeld(ctx context.Context, open *stores) {
inv := open.inventory
// A walk that ended lets the next batch be cut at once, not at the cutter's next look (novox/hq ADR 0276).
defer func() {
if err := cutBatchesHeld(ctx, open, time.Now().UTC()); err != nil {
fmt.Printf("batches: %v\n", err)
}
}()
plans, err := inv.OpenPlans(ctx)
if err != nil {
fmt.Printf("plans: cannot read them: %v\n", err)
@@ -735,15 +759,6 @@ func advanceOnce(ctx context.Context, open *stores, p *inventory.Plan,
case inventory.GateFailed:
failFirstSend(ctx, open, p, m, state, state.Gate.Machines, state.Gate.Why, step.rest)
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:
if !state.Gate.Kept {
gatePassed(ctx, open, p, m, state)
@@ -915,9 +930,6 @@ func firstSend(ctx context.Context, open *stores, p *inventory.Plan, node string
}
now := time.Now().UTC()
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 {
s := p.Modules[m]
// What the first machine ran before: what a failed gate puts back (ADR 0236).
@@ -926,7 +938,7 @@ func firstSend(ctx context.Context, open *stores, p *inventory.Plan, node string
}
s.First, s.FirstAt = sent, &now
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 = ""
if m == lead {
s.Gate.Carried = carried
@@ -1085,15 +1097,8 @@ func nextRollout(s inventory.PlanModule, running []string, together bool, report
var waiting, failed []string
for _, n := range s.First {
r, said := byNode[n]
// Only a report about what this plan sent it — or what it was sent after that — says anything about
// this build (novox/hq issue 352); a plan from before sends were kept on the gate reads the report
// 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 {
// Only a report about what it was last sent says anything about this build.
if !said || r.At == nil || !r.Current {
waiting = append(waiting, n)
continue
}
@@ -1185,23 +1190,20 @@ func inTierSince(p inventory.Plan) time.Time {
func planLineWith(p inventory.Plan, now time.Time, pause pauseView, bound time.Duration) string {
where := fmt.Sprintf("tier %d of %d", min(p.Tier+1, len(p.Tiers)), len(p.Tiers))
switch p.State {
case inventory.PlanAssembling, inventory.PlanQueued:
// A batch not yet a walk (novox/hq ADR 0276): what it holds and how long is left.
return batchWords(p, now)
case inventory.PlanDone:
return fmt.Sprintf("%s done, %d tier(s)", p.Named(), len(p.Tiers))
return fmt.Sprintf("%s %s done, %d tier(s)", p.Repository, short(p.Commit), len(p.Tiers))
case inventory.PlanFailed:
return fmt.Sprintf("%s FAILED at %s: %s", p.Named(), where, p.Note)
return fmt.Sprintf("%s %s FAILED at %s: %s", p.Repository, short(p.Commit), where, p.Note)
case inventory.PlanSuperseded:
return fmt.Sprintf("%s %s", p.Named(), p.Note)
return fmt.Sprintf("%s %s %s", p.Repository, short(p.Commit), p.Note)
}
since := now.Sub(inTierSince(p)).Round(time.Second)
if p.Waiting() {
// Waiting for its delivery's word is no lateness of the walk's (novox/hq ADR 0239).
return fmt.Sprintf("%s %s, %s, for %s", p.Named(), where, waitingNote(p), since)
return fmt.Sprintf("%s %s %s, %s, for %s", p.Repository, short(p.Commit), where, waitingNote(p), since)
}
if waiting, paused := pausedWaiting(p, pause, now); paused {
return fmt.Sprintf("%s %s, %s", p.Named(), where, waiting)
return fmt.Sprintf("%s %s %s, %s", p.Repository, short(p.Commit), where, waiting)
}
late := ""
if since > bound {
@@ -1211,7 +1213,7 @@ func planLineWith(p inventory.Plan, now time.Time, pause pauseView, bound time.D
if p.State == inventory.PlanRolling {
what = p.Note
}
return fmt.Sprintf("%s %s, %s for %s%s", p.Named(), where, what, since, late)
return fmt.Sprintf("%s %s %s, %s for %s%s", p.Repository, short(p.Commit), where, what, since, late)
}
// planFailedBuild marks the module a failed build was for when the result names no module: by the
@@ -1478,25 +1480,13 @@ func plansCommand(ctx context.Context, args []string) error {
if err != nil {
return err
}
// **The batch being assembled first** (novox/hq ADR 0276 decision 7): what it holds, how long is left,
// and that its plan is not yet calculated.
batches, err := inv.Batches(ctx)
if err != nil {
return err
}
if len(plans) == 0 && len(batches) == 0 {
if len(plans) == 0 {
fmt.Println("no merge has produced a plan yet")
return nil
}
for _, b := range batches {
fmt.Printf("%-28s %s\n", b.ID, batchWords(b, now))
}
pause := buildSeatPause(ctx, inv, plans)
bounds := readTierBounds(ctx, inv, now)
for _, p := range plans {
if p.Batch() {
continue
}
fmt.Printf("%-28s %s\n", p.ID, planLineWith(p, now, pause, bounds.of(p.Repository)))
}
return nil
@@ -1518,12 +1508,11 @@ func planWhatIf(ctx context.Context, inv *inventory.Inventory, repository string
if err != nil {
return err
}
read, err := readForPlanning(ctx, inv)
read, err := inv.ReadRepositories(ctx)
if err != nil {
return err
}
var from, packaging []inventory.Entry
deleted := map[string]bool{}
named := map[string]bool{}
for _, name := range modules {
named[name] = true
@@ -1533,9 +1522,6 @@ func planWhatIf(ctx context.Context, inv *inventory.Inventory, repository string
// request's check ask it (novox/hq ADR 0238).
r := reachOfMerge(m, entries, read, nil)
from, packaging = append(append([]inventory.Entry{}, r.Touched...), r.Deleted...), r.Packaging
for _, e := range r.Deleted {
deleted[e.Manifest.Module] = true
}
} else {
for _, e := range entries {
switch {
@@ -1562,18 +1548,6 @@ func planWhatIf(ctx context.Context, inv *inventory.Inventory, repository string
}
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))
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{}
for i, tier := range p.Tiers {
fmt.Printf(" tier %d\n", i)
@@ -1591,19 +1565,19 @@ func planWhatIf(ctx context.Context, inv *inventory.Inventory, repository string
rolls[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) {
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
}
+6 -7
View File
@@ -59,18 +59,17 @@ func TestAMergeIsPlannedInTiersAlongTheThreeKindsOfDependency(t *testing.T) {
t.Fatalf("no cycle here: %v", tiers)
}
// The controller alone moved: the controller alone — what packages its repository moves only when the
// change is in its own build source (novox/hq ADR 0267), so a packages edge widens nothing.
if alone := reachableFrom([]string{"mesh-controller"}, edges); len(alone) != 1 {
t.Fatalf("a controller merge rebuilds the controller alone: %v", alone)
// The controller alone moved: the proxy with it, nothing else.
small := reachableFrom([]string{"mesh-controller"}, edges)
if len(small) != 3 {
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,
// 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,
// 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.)
small := reachableFrom([]string{"mesh-controller", "builder", "route-proxy"}, edges)
smallTiers := tiersOf(small, edges)
if got := tiered(smallTiers); got != "mesh-controller | builder | route-proxy" {
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,
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" {
t.Fatalf("%v touched %v", paths, touched)
}
-5
View File
@@ -28,11 +28,6 @@ import (
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
// 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) != "" {
fmt.Printf("verb=%q caller=%q terminal=%v\n", os.Getenv("MESH_VERB"), os.Getenv("MESH_CALLER"), startedAtTheTerminal())
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 == "" {
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)
if err != nil {
return err
-12
View File
@@ -1,8 +1,6 @@
package main
import (
"context"
"strings"
"testing"
"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)
}
}
// 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)
}
}
}
+8 -96
View File
@@ -114,14 +114,6 @@ func declaredArguments(v catalogue.Verb) (names []string, switches map[string]bo
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.
func readArguments(verb string, args map[string]any) (*verbArguments, error) {
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)
}
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:
return nil, fmt.Errorf("%s: %q is text, and was given %T", verb, k, x)
}
@@ -260,10 +239,10 @@ func refusedAsTheGenericCommand(argv []string) error {
// A layer is written through the settings verb, never the generic one (novox/hq issue 339): the
// settings verb is where what a verb may not set is refused, and one route is one set of words.
// The command refuses places and accesses through any verb as well; this says so before it runs.
if argv[0] == "settings" && slices.ContainsFunc(argv[1:], func(w string) bool { return w == "set" || w == "clear" || w == "propose" }) {
return &heldAtTheTerminal{msg: "settings are set, cleared and proposed through the settings verb, not the " +
"generic command; and places and accesses are set at the controller's terminal or on the operator's " +
"warrant (novox/hq issue 339, ADR 0277). Nothing was done"}
if argv[0] == "settings" && slices.ContainsFunc(argv[1:], func(w string) bool { return w == "set" || w == "clear" }) {
return &heldAtTheTerminal{msg: "settings are set and cleared through the settings verb, not the " +
"generic command; and places and accesses only at the controller's terminal (novox/hq issue 339). " +
"Nothing was done"}
}
// The generic verb only reads (novox/hq ADR 0266): what writes has a named verb that composes its own
// line, or is the operator's at the controller's terminal.
@@ -303,8 +282,6 @@ func (a *verbArguments) commandLine() ([]string, error) {
return nil, errors.New("tools is answered from the records, not by a command")
case "dead-letters":
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":
return []string{"status", "--json"}, nil
case "nodes":
@@ -536,11 +513,6 @@ func (a *verbArguments) commandLine() ([]string, error) {
argv = append(argv, "--condition", c)
}
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":
argv := []string{"hand-acts", "--json"}
if d := str("days"); d != "" {
@@ -767,11 +739,6 @@ func (a *verbArguments) commandLine() ([]string, error) {
argv = append(argv, "--probe", p)
}
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":
if p := str("provision"); p != "" {
argv := []string{"rotate", p}
@@ -835,21 +802,6 @@ func (a *verbArguments) commandLine() ([]string, error) {
if str("module") == "" && on("clear") {
return nil, errors.New("settings: a module is needed to clear a layer; name it with module")
}
// The proposals, listed or one whole (novox/hq ADR 0277): a read, needing no module.
if on("proposals") || str("proposal") != "" {
if str("module") != "" || str("values") != "" || str("node") != "" || on("clear") || on("replace") ||
on("history") || str("list") != "" || on("propose") || (on("proposals") && str("proposal") != "") {
return nil, errors.New("settings: proposals lists what was proposed and proposal shows one; either takes nothing else")
}
argv := []string{"settings", "proposals"}
if id := str("proposal"); id != "" {
argv = append(argv, id)
}
return argv, nil
}
if str("module") == "" && on("propose") {
return nil, errors.New("settings: a module is needed to propose a layer; name it with module")
}
if list := str("list"); list != "" || str("module") == "" {
if list != "" && list != "preferences" {
return nil, fmt.Errorf("settings lists %q only; %q is not a listing", "preferences", list)
@@ -870,22 +822,6 @@ func (a *verbArguments) commandLine() ([]string, error) {
}
var argv []string
switch {
case on("propose"):
// A proposal: the values, or clear, put to the operator (novox/hq ADR 0277). Nothing is set here.
argv = []string{"settings", "propose", str("module")}
switch {
case on("clear") && str("values") != "":
return nil, errors.New("settings: a proposal gives values or says clear, not both")
case on("clear"):
argv = append(argv, "--clear")
case str("values") != "":
argv = append(argv, str("values"))
if on("replace") {
argv = append(argv, "--replace")
}
default:
return nil, errors.New("settings: a proposal gives the values, or says clear")
}
case on("clear"):
argv = []string{"settings", "clear", str("module")}
case str("values") != "":
@@ -971,8 +907,6 @@ func repairingCommand(argv []string) string {
return "plans " + argv[1]
case argv[0] == "broker" && len(argv) > 1 && argv[1] == "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":
return "hand-act drill"
case argv[0] == "hand-act":
@@ -1158,9 +1092,6 @@ func seatToolHandlers() (map[string]link.ToolHandler, []string, error) {
if verb == "dead-letters" {
return deadLettersAnswer(ctx, a)
}
if verb == "root-free" {
return rootFreeAnswer(ctx, a.given["machines"], rootClock())
}
if verb == "doctor" {
// 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.
@@ -1251,10 +1182,7 @@ func actsOnAPlan(args map[string]any) bool {
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
// issue 330).
"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}
"dead-letters": true}
// 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
@@ -1478,7 +1406,7 @@ var commandReadForms = map[string]func(rest []string) bool{
"conditions": func(r []string) bool { return flagsOnly(r) || subIn(r, "list", "show", "history") },
"node": func(r []string) bool { return subIn(r, "list", "show") },
"module": func(r []string) bool { return subIn(r, "list") },
"settings": func(r []string) bool { return subIn(r, "show", "preferences", "proposals") },
"settings": func(r []string) bool { return subIn(r, "show", "preferences") },
"retire": func(r []string) bool { return subIn(r, "list") },
"cleanup": func(r []string) bool { return subIn(r, "list") },
"delivery": func(r []string) bool { return subIn(r, "plan", "walks") },
@@ -1538,20 +1466,6 @@ var terminalOnlyCommands = map[string]string{
"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
// 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
@@ -1565,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 "+
"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
// `give` verb's own line: an own secret typed by the operator into the desk's hidden prompt, sealed to this
// 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) {
// Of a secret's commands only rotation, which seals the new value to the machine that uses it.
if argv[0] == "secret" && (len(argv) < 2 || argv[1] != "rotate") {
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 "+
"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",
"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"},
"conditions": {"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", "",
"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)")
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", "",
"with --provider: the name the credential goes by inside <module>, where its manifest keeps "+
"several for <name> (ADR 0094)")
@@ -68,18 +65,6 @@ func secretCommand(ctx context.Context, args []string) error {
return errors.New(secretUsage)
}
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)
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)
return nil
}
// A trusted party's secret is announced before it is kept (the confirmation review of 2026-10-09, N1-give):
// 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)
untilStart, err := open.inventory.AcceptGivenSecret(ctx, node, module, name, value)
if err != nil {
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
// machine and the mesh cannot read it again.
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" +
"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 export [--out <file>]"
@@ -400,8 +369,6 @@ func valueFor(node, module, name, from string) (string, error) {
fmt.Fprintf(os.Stderr,
"reading %s's %q for %s from standard input; it is not echoed anywhere\n",
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')
if err != nil && line == "" {
return "", fmt.Errorf("nothing was given on standard input: %w", err)
@@ -485,23 +452,3 @@ func whoAsked() string {
}
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
}
+3 -33
View File
@@ -84,7 +84,7 @@ const (
// callBounds are the verbs that may run longer than callDefault, and how long (S7).
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,
}
@@ -210,22 +210,6 @@ var signalsTable = []signalRow{
newest: func(f *signalFacts) time.Time {
return newestOf(f.waits, func(w waitFacts) time.Time { return w.since })
}},
{Row: "S18", Signal: "a batch of merges is cut into its walk", Emitter: "controller's merge window",
Trigger: "each batch (novox/hq ADR 0276)",
Bound: "a minute past merge-window-at-most, while no walk is open: the controller failed to cut it",
Kind: kindBatchNotCut, Severity: conditions.Warning, Phase: 3,
needs: func(f *signalFacts) error { return f.plansErr }, watch: watchBatchesNotCut,
newest: func(f *signalFacts) time.Time {
return newestOf(f.batches, func(b batchFacts) time.Time { return b.atMost })
}},
{Row: "S19", Signal: "a batch waiting behind an open walk is cut when it ends", Emitter: "controller's merge window",
Trigger: "each batch closed while a walk is open (novox/hq ADR 0276)",
Bound: "the walk's bound, a tier's bound for each of its tiers, from when the batch closed: naming the walk",
Kind: kindBatchBehindWalk, Severity: conditions.Warning, Phase: 3,
needs: func(f *signalFacts) error { return f.plansErr }, watch: watchBatchesBehind,
newest: func(f *signalFacts) time.Time {
return newestOf(f.batches, func(b batchFacts) time.Time { return b.closed })
}},
{Row: "S17", Signal: "a send held for the bus's planned step is told to a person", Emitter: "controller's plan",
Trigger: "each send refused because it would replace the bus outside its step (novox/hq issue 336)",
Bound: "none: raised at the first refusal, for the operator, naming what waits, the bus build from and to, " +
@@ -388,31 +372,17 @@ func watchWaits(f *signalFacts) []conditions.Observation {
out = append(out, conditions.Observation{Scope: conditions.ScopePlan, ID: w.id, Kind: kindWalkWaiting,
Severity: severity,
Summary: fmt.Sprintf("the walk of %s %s has waited %s for %s's word to start: `mesh-delivery.show` for the "+
"deliveries that landed as %s says why; `plans go %s --why …` starts it by hand", w.repository,
short(w.commit), ago(in), w.awaits, mergesWords(w), w.id),
"delivery that landed as %s says why; `plans go %s --why …` starts it by hand", w.repository,
short(w.commit), ago(in), w.awaits, short(w.commit), w.id),
Said: fmt.Sprintf("waiting since %s for %s", w.since.UTC().Format(time.RFC3339), w.awaits),
Headline: deliveryName(w.modules, w.repository) + " waiting to start",
Explanation: walkWaitingWords(w, in, severity),
Needs: waitingNeeds(severity),
Actions: waitingActions(w.id, severity),
Resolved: deliveryName(w.modules, w.repository) + " no longer waiting"})
}
return out
}
// mergesWords is the merges a waiting walk answers (novox/hq ADR 0276): every delivery it carries, not one
// commit that a supersession may already have ended (issue 362). Its own commit for a walk naming none.
func mergesWords(w waitFacts) string {
if len(w.merges) == 0 {
return short(w.commit)
}
var out []string
for _, m := range w.merges {
out = append(out, m.Repository+"@"+short(m.Commit))
}
return readableList(out)
}
func watchLoop(f *signalFacts) []conditions.Observation {
if f.loop.pending == 0 {
return nil
-22
View File
@@ -142,28 +142,6 @@ var suppressions = map[string]suppression{
since: f.now.Add(-31 * time.Minute)}}
},
},
// A batch still assembling past its maximum with no walk open (novox/hq ADR 0276): a minute's grace.
"S18": {
inside: func(f *signalFacts) {
f.batches = []batchFacts{{id: "plan-3", state: inventory.PlanAssembling, grouped: "novox/app@c0ffee11",
atMost: f.now.Add(-59 * time.Second), closed: f.now.Add(-59 * time.Second)}}
},
past: func(f *signalFacts) {
f.batches = []batchFacts{{id: "plan-3", state: inventory.PlanAssembling, grouped: "novox/app@c0ffee11",
atMost: f.now.Add(-61 * time.Second), closed: f.now.Add(-61 * time.Second)}}
},
},
// A batch queued behind an open walk past that walk's bound, named.
"S19": {
inside: func(f *signalFacts) {
f.batches = []batchFacts{{id: "plan-3", state: inventory.PlanQueued, grouped: "novox/app@c0ffee11",
closed: f.now.Add(-59 * time.Minute), behind: "plan-1", walkBound: time.Hour}}
},
past: func(f *signalFacts) {
f.batches = []batchFacts{{id: "plan-3", state: inventory.PlanQueued, grouped: "novox/app@c0ffee11",
closed: f.now.Add(-61 * time.Minute), behind: "plan-1", walkBound: time.Hour}}
},
},
// A send refused because it would replace the bus outside its planned step: said at its first refusal,
// whatever the bound (novox/hq issue 336). Inside: a new bus build waits, and no send was refused for it.
"S17": {
+99
View File
@@ -0,0 +1,99 @@
package main
import (
"reflect"
"strings"
"testing"
"time"
"github.com/novox/mesh-controller/internal/inventory"
)
// novox/hq issue 254, ADR 0218: a newer plan takes over what the older open plans of its repository
// and branch had not built, and closes them as superseded; another repository's plan, another
// branch's, and a plan made after it are left alone.
func TestANewerPlanSupersedesTheOlderOpenPlansOfItsRepository(t *testing.T) {
at := time.Date(2026, 10, 5, 12, 0, 0, 0, time.UTC)
sent := at.Add(time.Minute)
plan := func(id, repository, branch string, created time.Time, modules map[string]*inventory.PlanModule) inventory.Plan {
return inventory.Plan{ID: id, Repository: repository, Branch: branch, Commit: id + "-commit",
Created: created, State: inventory.PlanRolling, Modules: modules}
}
older := plan("plan-1", "novox/mesh-catalog", "main", at, map[string]*inventory.PlanModule{
"gitea": {State: "built", SentAt: &sent}, // done with: stays done
"keycloak": {State: "asked"}, // asked, not answered: folded
"plex": {}, // not yet asked: folded
"agent": {State: "built"}, // built, rolls out, not sent: folded
"notes": {State: "built"}, // built, records: nothing to send
})
stuck := plan("plan-0", "Novox/Mesh-Catalog", "", at.Add(-time.Hour), map[string]*inventory.PlanModule{
"runtime": {State: "asked"},
})
other := plan("plan-2", "novox/mesh-controller", "main", at, map[string]*inventory.PlanModule{"mesh-controller": {}})
release := plan("plan-3", "novox/mesh-catalog", "release", at, map[string]*inventory.PlanModule{"lemurs": {}})
later := plan("plan-5", "novox/mesh-catalog", "main", at.Add(2*time.Hour), map[string]*inventory.PlanModule{"later": {}})
done := plan("plan-6", "novox/mesh-catalog", "main", at, map[string]*inventory.PlanModule{"finished": {}})
done.State = inventory.PlanDone
newer := plan("plan-4", "novox/mesh-catalog", "main", at.Add(time.Hour), nil)
newer.Commit = "97b1b2b0c0ffee"
rollsOut := func(m string) bool { return m != "notes" }
folded, closed := supersededBy(newer, []inventory.Plan{stuck, older, other, release, later, done, newer}, rollsOut)
if want := []string{"agent", "keycloak", "plex", "runtime"}; !reflect.DeepEqual(folded, want) {
t.Fatalf("folded %v, wanted %v", folded, want)
}
var ids []string
for _, p := range closed {
ids = append(ids, p.ID)
if p.State != inventory.PlanSuperseded || p.Open() {
t.Errorf("%s was left %s", p.ID, p.State)
}
if !strings.Contains(p.Note, "plan-4") || !strings.Contains(p.Note, "97b1b2b0") {
t.Errorf("%s does not name the plan that superseded it: %q", p.ID, p.Note)
}
}
if want := []string{"plan-0", "plan-1"}; !reflect.DeepEqual(ids, want) {
t.Fatalf("superseded %v, wanted %v — another repository, another branch, a later plan and a "+
"finished one are left alone", ids, want)
}
if other.State != inventory.PlanRolling {
t.Fatal("the plan handed in was changed in place")
}
if line := planLine(closed[1], time.Now()); !strings.Contains(line, "superseded") {
t.Fatalf("a superseded plan reads %q", line)
}
}
// novox/hq issue 254: a person closes a plan that will not move again, by its id.
func TestAPersonClosesAStuckPlan(t *testing.T) {
open := aMesh(t)
ctx := t.Context()
stuck := inventory.Plan{ID: "plan-97b1b2b", Repository: "novox/mesh-catalog", Commit: "97b1b2b",
Created: time.Now().UTC(), State: inventory.PlanRolling, Tier: 1, Tiers: [][]string{{"a"}, {"b"}},
Modules: map[string]*inventory.PlanModule{"a": {State: "built"}, "b": {}}}
if err := open.inventory.SavePlan(ctx, &stuck); err != nil {
t.Fatal(err)
}
if err := plansCommand(ctx, []string{"close", stuck.ID}); err == nil || !strings.Contains(err.Error(), "--why") {
t.Fatalf("a plan was closed by hand without saying why: %v", err)
}
if err := plansCommand(ctx, []string{"close", stuck.ID, "--why", "its report will not come"}); err != nil {
t.Fatal(err)
}
closed, err := open.inventory.PlanByID(ctx, stuck.ID)
if err != nil {
t.Fatal(err)
}
if closed.State != inventory.PlanFailed || !strings.Contains(closed.Note, "closed by hand") ||
!strings.Contains(closed.Note, "its report will not come") {
t.Fatalf("the plan was left %s: %q", closed.State, closed.Note)
}
if err := plansCommand(ctx, []string{"close", stuck.ID, "--why", "again"}); err == nil {
t.Fatal("a plan already closed was closed again")
}
if argv, err := argvFor("plans", map[string]any{"close": stuck.ID, "why": "w"}); err != nil ||
!reflect.DeepEqual(argv, []string{"plans", "close", stuck.ID, "--why", "w"}) {
t.Fatalf("the seat's verb does not close a plan: %v %v", argv, err)
}
}
+165 -304
View File
@@ -12,7 +12,6 @@ import (
"sync"
"time"
"github.com/novox/mesh-controller/internal/builder"
"github.com/novox/mesh-controller/internal/catalogue"
"github.com/novox/mesh-controller/internal/inventory"
"github.com/novox/mesh-controller/internal/link"
@@ -298,20 +297,27 @@ func notNow(err error) error {
return err
}
// SourceMoved is the forge announcing a merge. It is put into the open batch (novox/hq ADR 0276), which
// becomes one walk when its merge window closes: hearMerge, and cutBatchesHeld in batches.go.
// SourceMoved is the forge announcing a merge: every module recorded as built from that
// repository and branch is marked as moved to the merge commit, and built — bases first, so a
// module that stands on another's artifact is built after it and not against the old one
// (novox/hq 04-ISSUES/131). Nothing is pushed here: what a finished build does to the machines
// running the module is the upgrade's decision, taken when the catalogue announces it.
func (f following) SourceMoved(ctx context.Context, m link.SourceMoved) error {
return f.hearMerge(ctx, m, time.Now().UTC())
}
// One merge acted on at a time, whoever hands it over: the bus, or the catch-up that reads back
// what the bus did not hand over (novox/hq issue 266). Each judges against what the other wrote.
actingOnMerges.Lock()
defer actingOnMerges.Unlock()
inv := f.open.inventory
entries, err := inv.Catalogued(ctx)
if err != nil {
return notNow(err)
}
read, err := inv.ReadRepositories(ctx)
if err != nil {
return notNow(err)
}
// movesOfMerge is what one merge moves, acted on: every module recorded as built from that repository and
// branch is marked as moved to the merge commit; a module the merge deleted is forgotten or said; a new module
// is asked for and sent nowhere. The names returned are what the walk builds, bases first along the graph
// (novox/hq 04-ISSUES/131). Nothing is built or pushed here: the walk asks its tiers. Called when a batch is
// cut, once per repository, with the latest merge of the repository's branch and every file the batch's
// merges of it changed.
func movesOfMerge(ctx context.Context, inv *inventory.Inventory, m link.SourceMoved, entries []inventory.Entry,
read map[string][]inventory.ReadRepository) ([]string, error) {
from, packaging, already := mergeCandidates(m, entries, read)
if len(from) == 0 && len(packaging) == 0 {
// "Already built from it" and "nothing reads it" are different facts, and reading the first
@@ -319,11 +325,17 @@ func movesOfMerge(ctx context.Context, inv *inventory.Inventory, m link.SourceMo
if already > 0 {
fmt.Printf("%s/%s merged into %s (%.8s); %d module(s) the mesh holds are already built "+
"from it\n", m.Owner, m.Repo, m.Base, m.Commit, already)
return nil, nil
return nil
}
fmt.Printf("%s/%s merged into %s (%.8s); nothing the mesh holds reads it\n",
m.Owner, m.Repo, m.Base, m.Commit)
return nil, 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
// another branch is not part of this merge, and whoever is waiting for its change should read why.
@@ -333,7 +345,7 @@ func movesOfMerge(ctx context.Context, inv *inventory.Inventory, m link.SourceMo
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
// 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.
@@ -343,7 +355,7 @@ func movesOfMerge(ctx context.Context, inv *inventory.Inventory, m link.SourceMo
}
for _, e := range touched {
if err := inv.SourceMoved(ctx, e.Manifest.Module, m.Commit); err != nil {
return nil, notNow(err)
return notNow(err)
}
}
// **A new module is built and registered, and sent nowhere** (novox/hq issue 300): the delivery plan
@@ -355,25 +367,117 @@ func movesOfMerge(ctx context.Context, inv *inventory.Inventory, m link.SourceMo
fmt.Printf("%s/%s merged into %s (%.8s); it changed no module the mesh holds, and adds %s: "+
"built, registered when the build lands, and sent nowhere\n", m.Owner, m.Repo, m.Base, m.Commit,
strings.Join(built, ", "))
return nil, nil
return nil
}
fmt.Printf("%s/%s merged into %s (%.8s); it changed nothing any module the mesh holds is "+
"built from\n", m.Owner, m.Repo, m.Base, m.Commit)
return nil, nil
return nil
}
// Why each is in it (issue 363): the files of its build source the merge changed, or why it is read whole.
// A merge produces a plan the mesh keeps (novox/hq ADR 0162): what moved and everything that
// depends on it, along the catalogue's one dependency relation, sorted into tiers. The plan is
// written before any build is asked; the first tier is asked; this returns. Outcomes advance it.
edges, err := inv.Dependencies(ctx)
if err != nil {
return notNow(err)
}
var movedNames []string
for _, e := range moved {
fmt.Printf(" %s: %s\n", e.Manifest.Module, whyMoved(e, read[e.Manifest.Module], m))
movedNames = append(movedNames, e.Manifest.Module)
}
for _, e := range packaging {
fmt.Printf(" %s reads %s/%s through its build context: its own source record is left where it is\n",
e.Manifest.Module, m.Owner, m.Repo)
// Written and its first tier asked as one act on the plans (novox/hq issue 213): a timer on
// another controller reading it between the two would ask the tier again.
release, err := inv.HoldPlans(ctx, true)
if err != nil {
return notNow(err)
}
var names []string
for _, e := range moved {
names = append(names, e.Manifest.Module)
defer release()
plan := planOfMerge(m, movedNames, edges)
// **A newer plan supersedes the older open plans of this repository and branch** (novox/hq issue
// 254, ADR 0218): what they had not built is planned here again, and they are closed, so one plan
// works a repository's modules at a time and a stuck one ends at the next merge.
working, err := inv.OpenPlans(ctx)
if err != nil {
return notNow(err)
}
return names, nil
rollsOut := func(module string) bool {
u, err := inv.UpgradeOf(ctx, module)
return err == nil && u.RollOut
}
folded, superseded := supersededBy(plan, working, rollsOut)
if len(folded) > 0 {
held := map[string]bool{}
for _, e := range entries {
held[e.Manifest.Module] = true
}
names := map[string]bool{}
for _, name := range movedNames {
names[name] = true
}
var also []string
for _, name := range folded {
// One the catalogue no longer holds would fail the newer plan's ask; it is not this
// merge's to build.
if held[name] && !names[name] {
names[name] = true
movedNames = append(movedNames, name)
also = append(also, name)
}
}
if len(also) > 0 {
again := planOfMerge(m, movedNames, edges)
again.ID, again.Created = plan.ID, plan.Created
plan = again
fmt.Printf(" %s, left unbuilt by an older plan of %s, are planned here again\n",
strings.Join(also, ", "), plan.Repository)
}
}
// **Whether it waits for its delivery's word** (novox/hq ADR 0239): while the mesh-delivery seat has a
// holder on record, a walk that moves no module on the controller's own path is opened and waits.
plan.Delivery = awaitsFor(entries, movedNames)
if hasCycle(plan.Tiers, edges) {
fmt.Printf(" the last tier depends on itself: %s — built together, in no order\n",
strings.Join(plan.Tiers[len(plan.Tiers)-1], ", "))
}
if err := inv.SavePlan(ctx, &plan); err != nil {
return notNow(err)
}
// Closed after the newer plan is kept, never before: a controller replaced between the two leaves
// both open, which the next merge settles, rather than neither.
for _, old := range superseded {
if err := inv.SavePlan(ctx, &old); err != nil {
return notNow(err)
}
fmt.Printf(" %s (%s at %s) is %s\n", old.ID, old.Repository, short(old.Commit), old.Note)
}
var tiers []string
for i, t := range plan.Tiers {
tiers = append(tiers, fmt.Sprintf("%d: %s", i, strings.Join(t, ", ")))
}
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 "))
if len(packaging) > 0 {
var also []string
for _, e := range packaging {
also = append(also, e.Manifest.Module)
}
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() {
plan.Note = waitingNote(plan)
if err := inv.SavePlan(ctx, &plan); err != nil {
return notNow(err)
}
fmt.Printf(" %s waits for %s's word before its first tier is asked\n", plan.ID, plan.Delivery.Awaits)
return nil
}
if err := askTier(ctx, inv, &plan); err != nil {
return notNow(err)
}
if err := inv.SavePlan(ctx, &plan); err != nil {
return notNow(err)
}
return nil
}
// askNewModules asks the build seat for every module a merge adds to the repository — a directory holding a
@@ -451,119 +555,25 @@ func mergeCandidates(m link.SourceMoved, entries []inventory.Entry,
}
from = append(from, e)
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)
}
}
return from, packaging, already
}
// lookedAtCommit is whether a plan that built a module, or is building it, answered this merge commit.
func lookedAtCommit(read []inventory.ReadRepository, commit string) bool {
for _, r := range read {
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.
// wouldMove is the modules built from the merged repository that acting on this merge would mark as
// moved and rebuild — SourceMoved's judgement, made without acting (novox/hq issue 266). Empty for a
// merge already acted on: acting marks each of them as looked at, so the merge then reads as history.
//
// **The ones packaging source from it too** (novox/hq ADR 0267): with a module moved only by the files of
// its build source, a merge can move a packaging module and nothing built from the repository, and a missed
// one of those was never acted on. A packaging module's look is its newest build or plan (lookedAt), so a
// merge acted on for it reads as history once its plan is made.
// **Only the modules built from it, never the ones that merely package source from it.** Acting
// records nothing about those, so a merge acted on would go on reading as unacted for them, and be
// acted on again on every look. A merge that moves both is caught by the first kind, and acting on it
// rebuilds the second as well.
func wouldMove(m link.SourceMoved, entries []inventory.Entry,
read map[string][]inventory.ReadRepository) []inventory.Entry {
from, packaging, _ := mergeCandidates(m, entries, read)
touched, _ := splitDeleted(whatTheMergeTouched(from, entries, m, read), m)
return append(touched, packaging...)
from, _, _ := mergeCandidates(m, entries, read)
touched, _ := splitDeleted(whatTheMergeTouched(from, entries, m), m)
return touched
}
// splitDeleted parts the modules a merge touched into those it changed and those whose manifest it
@@ -647,171 +657,34 @@ func sameRepository(repository string, m link.SourceMoved) bool {
(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
// repository its recipe packages source from. Its ref must be the branch that moved, or unset — the same
// rule a 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.
// readsFrom is whether a module's build read the repository a merge names: the second repository its
// recipe packages source from. Its ref must be the branch that moved, or unset — the same rule a
// module's own source follows.
func readsFrom(read []inventory.ReadRepository, m link.SourceMoved) bool {
for _, r := range read {
if r.Own || !sameRepository(r.Repository, m) || (r.Ref != "" && r.Ref != m.Base) {
continue
}
if len(r.Paths) == 0 || len(m.Paths) == 0 || m.PathsTruncated {
if sameRepository(r.Repository, m) && (r.Ref == "" || r.Ref == m.Base) {
return true
}
for _, p := range m.Paths {
if builder.SourceHolds(r.Paths, p) {
return true
}
}
}
return false
}
// ownSource is the build source a module's newest trunk build said it read in its own repository; nil
// when it said none, and the module's own directory — or, built from the root, its whole repository — is
// its build source, as before (novox/hq ADR 0267).
func ownSource(read []inventory.ReadRepository) []string {
for _, r := range read {
if r.Own && len(r.Paths) > 0 {
return r.Paths
// lastLookAt is the most recent look at this repository by anything built from it.
func lastLookAt(entries []inventory.Entry, m link.SourceMoved) time.Time {
var newest time.Time
for _, e := range entries {
if sameRepository(e.Source.Repository, m) && e.Source.Seen.After(newest) {
newest = e.Source.Seen
}
}
return nil
}
// 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")) {
// Every commit the walk carries (novox/hq ADR 0276): a batch's walk answers one per repository.
for _, c := range p.Carried() {
commits = append(commits, c.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
return newest
}
// 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
// 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,
read map[string][]inventory.ReadRepository) []inventory.Entry {
touched, _, _ := touchedBy(candidates, known, m, read)
func whatTheMergeTouched(candidates, known []inventory.Entry, m link.SourceMoved) []inventory.Entry {
touched, _, _ := touchedBy(candidates, known, m)
return touched
}
@@ -819,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
// 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
// source, where the module's newest trunk build said one (novox/hq ADR 0267): a Go program's import closure,
// 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,
// **A changed file touches exactly the modules whose build reads it.** What a build reads 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
// 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
@@ -843,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
// be narrowed.
func touchedBy(candidates, known []inventory.Entry, m link.SourceMoved,
read map[string][]inventory.ReadRepository) (touched []inventory.Entry, added, unread []string) {
func touchedBy(candidates, known []inventory.Entry, m link.SourceMoved) (touched []inventory.Entry, added, unread []string) {
knownDirs := map[string]bool{}
for _, e := range known {
if !e.Provided && sameRepository(e.Source.Repository, m) {
@@ -879,27 +748,19 @@ func touchedBy(candidates, known []inventory.Entry, m link.SourceMoved,
return candidates, added, nil
}
for _, e := range candidates {
for _, p := range m.Paths {
if readsFile(e, read[e.Manifest.Module], p) {
touched = append(touched, e)
break
}
if strings.Trim(e.Source.Path, "/") == "" || anyInside(m.Paths, e.Source.Path) {
touched = append(touched, e)
}
}
for _, p := range m.Paths {
isRead := newDir["."]
read := newDir["."]
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 {
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.
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 {
if !read {
unread = append(unread, p)
}
}
@@ -958,7 +819,7 @@ func (r mergeReach) Dependents() []string {
func reachOfMerge(m link.SourceMoved, entries []inventory.Entry, read map[string][]inventory.ReadRepository,
edges []inventory.Edge) mergeReach {
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)
r := mergeReach{Touched: kept, Deleted: deleted, Packaging: packaging, Already: already, Added: added, Unread: unread}
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")
}
}
+1 -11
View File
@@ -53,8 +53,6 @@ type signalFacts struct {
plansErr error
// waits are the walks waiting for their delivery's word (S16, novox/hq ADR 0239).
waits []waitFacts
// batches are the batches not yet cut (S18, S19, novox/hq ADR 0276).
batches []batchFacts
loop loopFacts
loopErr error
@@ -161,8 +159,6 @@ type waitFacts struct {
since time.Time
// modules are the modules the walk moves, as a person names the delivery.
modules []string
// merges are the merges it answers (novox/hq ADR 0276), as the deliveries to read are found.
merges []inventory.PlanMerge
}
type loopFacts struct {
@@ -330,9 +326,6 @@ func (w *watchdogs) gather(ctx context.Context) *signalFacts {
f.machines, f.machinesErr = w.gatherMachines(ctx, inv, now)
f.bus, f.busErr = gatherBus(ctx, inv)
f.plans, f.waits, f.plansErr = gatherPlans(ctx, inv, now, f.bus.heldByTheBus())
if f.plansErr == nil {
f.batches, f.plansErr = gatherBatches(ctx, inv, now)
}
f.loop, f.loopErr = w.gatherLoop()
f.mergesPassed, f.merges, f.mergesErr = watchedMerges.last()
if f.mergesErr == nil && !f.mergesPassed.IsZero() && now.Sub(f.mergesPassed) > 3*mergeCatchUpEvery {
@@ -487,7 +480,7 @@ func gatherPlans(ctx context.Context, inv *inventory.Inventory, now time.Time, b
// S16's, whatever mesh-delivery says or does not say.
if p.Waiting() {
waits = append(waits, waitFacts{id: p.ID, repository: p.Repository, commit: p.Commit,
awaits: p.Delivery.Awaits, since: p.Created, modules: planModules(p), merges: p.Delivery.Merges})
awaits: p.Delivery.Awaits, since: p.Created, modules: planModules(p)})
continue
}
_, paused := pausedWaiting(p, pause, now)
@@ -707,9 +700,6 @@ func watchTheMesh(ctx context.Context, open *stores, server *link.Server, bus li
// under the lease and the brake, every act said.
healers := newHealing(open, keeper, bus, server.JetStream())
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)
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,
+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: "build-agent", Kind: inventory.EdgeBuiltBy},
}
// A packages edge recorded before novox/hq ADR 0267 widens nothing: the controller moved alone moves
// alone, and a change to a package all three build from moves all three, each by its own build source.
if alone := reachableFrom([]string{"mesh-controller"}, edges); len(alone) != 1 {
t.Fatalf("the controller alone, whatever packages its repository: %v", alone)
set := reachableFrom([]string{"mesh-controller"}, edges)
if len(set) != 3 {
t.Fatalf("the controller, what packages it, and nothing more: %v", set)
}
set := reachableFrom([]string{"mesh-controller", "build-agent", "route-proxy"}, edges)
tiers := tiersOf(set, edges)
pos := map[string]int{}
for i, tier := range tiers {
+4 -4
View File
@@ -3,14 +3,11 @@ module github.com/novox/mesh-controller
go 1.26.0
require (
git.novox.be/novox/mesh-sdk/go v0.1.13
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/novox/mesh-host v0.0.0
golang.org/x/crypto v0.57.0
golang.org/x/net v0.58.0
golang.org/x/sys v0.48.0
)
require (
@@ -22,9 +19,12 @@ require (
github.com/klauspost/compress v1.20.0 // indirect
github.com/minio/highwayhash v1.0.4 // 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/nuid v1.0.1 // indirect
go.uber.org/automaxprocs v1.6.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/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
// 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.
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-20261009231844-b8c854611812/go.mod h1:K3/xEzVgmrNKLMV2vv4M80MwmPnQNXqvQ4C5Jj0fJT4=
git.novox.be/novox/mesh-sdk/go v0.1.13 h1:Su4JYpZ+zhNovkGA2DgxiZdcuHpjw2tPJzc/7PljhXg=
git.novox.be/novox/mesh-sdk/go v0.1.13/go.mod h1:GFuZUElBZ9A++mxgIKo97aXXo+kV0uJ/UkbhQPPIbrY=
git.novox.be/novox/mesh-host v0.0.0-20261006095519-3e80b7ae325e h1:g9h4QRaAMg5yaJLwqtb0FoOs23DVGUYpW6qvnQ3oY5A=
git.novox.be/novox/mesh-host v0.0.0-20261006095519-3e80b7ae325e/go.mod h1:VlilMCRZ5yyNXg7SNigNBLr0Gt32jrGw5KSNq5JAVYs=
git.novox.be/novox/mesh-host v0.0.0-20261007120832-bdd44154ccac h1:KvnKtJ2rWeIE/t4GweK+JL0OjKSNxsrVP3/nMdpii8o=
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/go.mod h1:FQyySiasQQM8735Ddel3MRojmy4dA1IqCeyJ5jmPMbI=
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.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
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/go.mod h1:Fdz0i5U6CoizGwLda9DttjSk6qlZo25zYNtR+ycvuZA=
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
// allowed to act writes by compare-and-set and renews; its revision when taken is the epoch.
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
// every five. The bucket's age, so the bus forgets a holder that stopped renewing.
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.
func IsControllerBucket(bucket string) bool {
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.
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.
@@ -156,18 +149,6 @@ func (j *JetStream) EnsureControllerBuckets() error {
}); err != nil {
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
}
@@ -1,15 +1,9 @@
package broker
import (
"context"
"slices"
"strings"
"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
@@ -65,34 +59,3 @@ func TestTheWatchedSignalsMayBeSaidAndHeard(t *testing.T) {
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 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.
// And one that hears its own answers on a seat it uses (novox/hq ADR 0259 §3): an asker's warrants.
hearsItsOwn := len(SeatTrafficOf(p.Module, nil, p.Uses, nil).Subscribe) > 0
if p.Kind != KindModule || (len(p.Consumes) == 0 && len(p.Watches) == 0 && !hearsItsOwn) {
if p.Kind != KindModule || (len(p.Consumes) == 0 && len(p.Watches) == 0) {
return Consumer{}, false
}
perms, err := PermissionsFor(p)
-88
View File
@@ -2,7 +2,6 @@ package broker
import (
"encoding/json"
"slices"
"sort"
"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
// module on the machine.
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.
@@ -85,8 +78,6 @@ type Placements struct {
// 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.
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
@@ -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})
}
for _, s := range d.Holds {
if servedOnlyByTheController(s) {
continue // answered by the serving controller alone, never through a membership
}
for _, verb := range s.Serves {
m.Seats = append(m.Seats, SeatServed{Seat: s.Name, Verb: verb, Subject: seatToolSubject(s, verb, 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 {
m.Reaches = map[string][]string{}
for _, t := range d.Invokes {
@@ -171,67 +145,5 @@ func PlacementsOf(r Records, interchangeable map[string]bool) Placements {
for _, nodes := range p.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
}
// 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
}
+12 -189
View File
@@ -63,23 +63,6 @@ type Seat struct {
Emits []string
Serves []string
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
@@ -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.
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
// 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.
//
// 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"},
{Seat: "mesh-delivery", Verb: "close"}, {Seat: "mesh-delivery", Verb: "release"}, {Seat: "mesh-delivery", Verb: "stop"}}
// 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"}}
{Seat: "mesh-delivery", Verb: "close"}}
// 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.
@@ -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.
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
// §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.
@@ -309,11 +227,8 @@ func (p Principal) inbox() string { return "_INBOX." + p.Username() + ".>" }
// Permissions is what a principal may publish and subscribe, and whether it may answer.
type Permissions struct {
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
Publish []string
Subscribe []string
// AllowResponses lets a principal reply to a request it received, on the reply subject that
// request carried, once.
//
@@ -451,28 +366,10 @@ func PermissionsFor(p Principal) (Permissions, error) {
for _, v := range VerbsTheBusStepAsks {
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).
for _, v := range VerbsTheControllerAsksTheDeliveryOwner {
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
// the same discovery the console reads. The question only; the answers come to its own inbox.
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
// `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.
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)}
AskReportSubject(p.Node)}
// 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
// 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
// role is hearing what it announced, not taking part in it.
for _, w := range p.Watches {
if w.Kinded {
continue // composed by SeatTrafficOf below
}
for _, e := range w.Emits {
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.MSG.NEXT."+consumerStream(p)+"."+consumerDurable(p))
// 3. Seats it holds: full participation — but the controller's own seat, whose verbs only the serving
// controller answers, on its own connection (servedOnlyByTheController).
// 3. Seats it holds: full participation.
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
// 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 —
@@ -712,7 +590,7 @@ func PermissionsFor(p Principal) (Permissions, error) {
// seat's inbound subject and watch other modules' traffic, nor publish its outbound
// events and lie about outcomes (design 29 §2).
for _, s := range p.Uses {
for _, a := range plainVerbs(s, s.Accepts) {
for _, a := range s.Accepts {
pub = append(pub, seatSubject(s, "accept", a))
}
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,
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:
// **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
@@ -756,9 +628,6 @@ func PermissionsFor(p Principal) (Permissions, error) {
pub = append(pub, own+".event."+e)
}
for _, s := range d.Holds {
if servedOnlyByTheController(s) {
continue
}
for _, t := range s.Serves {
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),
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)
pub = unique(pub)
}
@@ -864,29 +714,13 @@ func PermissionsFor(p Principal) (Permissions, error) {
if err := CheckWriters(p, pub); err != nil {
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{
Publish: pub,
PublishDeny: deny,
Subscribe: sub,
Publish: pub,
Subscribe: sub,
// 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
// thing, a hand-over on its own subject (novox/hq issue 356), and answers that: the node is its
// machine's engine, root there already, and it is delivered only its own subjects. What it answers is
// 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,
// authority is bounded by having been asked — and so does the controller. A node and a
// person are never asked anything, and are granted nothing here.
AllowResponses: p.Kind == KindModule || p.Kind == KindController || p.Kind == KindNodeTools,
}, 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
// 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.
// 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 {
base := seatSubject(s, "tool", verb)
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())
}
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))
if perms.AllowResponses {
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
// 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
// subject (novox/hq issue 356), and may answer that; a person is never asked.
// module is asked on its own namespace and may answer; a node and a person are never asked.
func TestOnlyWhatCanBeAskedMayAnswer(t *testing.T) {
module, _ := PermissionsFor(Principal{Kind: KindModule, Node: "one", Module: "audit",
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")
}
node, _ := PermissionsFor(Principal{Kind: KindNode, Node: "one", PasswordHash: "x"})
if !node.AllowResponses || !slices.Contains(node.Subscribe, AskHandOverSubject("one")) ||
!slices.Contains(node.Subscribe, AskSetuidSearchSubject("one")) ||
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")
if node.AllowResponses {
t.Fatal("a node was granted the right to answer, and nothing asks a node 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.
// Appended, because the index is a name.
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
// (catalogue.ProvisionerEvents), which this package cannot import; a test keeps them agreeing.
const (
+3 -4
View File
@@ -24,8 +24,8 @@ accounts {
jetstream: enabled
users = [
{ 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.*"] }
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"] }
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"] }
allow_responses: { max: 1, ttl: "1m" }
} }
{ user: "enrol.one", password: "$2a$11$eeeeeeeeeeeeeeeeeeeeee", permissions: {
@@ -34,8 +34,7 @@ accounts {
} }
{ 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.>"] }
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"] }
allow_responses: { max: 1, ttl: "1m" }
subscribe: { allow: ["_DELIVER.one", "_DELIVER.one.>", "_INBOX.node.one.>", "mesh.node.one.ask.report", "mesh.node.one.declare"] }
} }
{ 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.>"] }
+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
// 48 §3): the machine's node-engine asks them of its own node tools, and is granted that and no more.
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.
@@ -70,10 +67,6 @@ type Records struct {
// 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.
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.
@@ -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
// correct.
func Users(r Records) ([]Principal, error) {
out := []Principal{{Kind: KindController, Uses: asksSeatOf(r)}}
out := []Principal{{Kind: KindController}}
for _, node := range sortedCopy(r.Nodes) {
witness := false
@@ -127,13 +120,10 @@ func Users(r Records) ([]Principal, error) {
})
}
if runtimeHere {
var carried []Declared
for _, d := range r.Assigned[node] {
if d.RunsAs == "" {
carried = append(carried, d)
}
}
out = append(out, Principal{Kind: KindNodeTools, Node: node, Module: RuntimeModule, Carries: carried})
out = append(out, Principal{
Kind: KindNodeTools, Node: node, Module: RuntimeModule,
Carries: append([]Declared(nil), r.Assigned[node]...),
})
}
}
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.
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{
{State: "a machine's declaration", Writer: "controller (lease holder)", KeptIn: "the bus, last per subject",
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",
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
-36
View File
@@ -15,8 +15,6 @@ import (
// to the table; one dropped from either fails.
var designRows = []string{
"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",
"the controller lease",
"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"
"os"
"os/exec"
"path"
"path/filepath"
"regexp"
"sort"
@@ -88,23 +87,6 @@ type Result struct {
// pin the build. Two builds with one fingerprint are one build, whatever digests they made
// (novox/hq issue 280).
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.
@@ -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.
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 {
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 == "" {
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,
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/`.
@@ -381,28 +345,6 @@ func trunkOf(ctx context.Context, run Runner, clone, commit string) (string, boo
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.
func orNoTree(why string) string {
if why == "" {
@@ -706,51 +648,15 @@ func one(ctx context.Context, run Runner, publish Publisher,
} else if src != nil {
src.contexts[a.Name] = t
}
buildDir = cloned
if t, _ := trunkOf(ctx, run, cloned, "HEAD"); t == "" || !atTrunkHead(ctx, run, cloned, "HEAD", t) {
src.contextOffHead()
}
}
// 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 != "" {
// docker build accepts -f outside the context it is given; the recipe stays exactly
// where it was read from and validated against, absolute so the working directory
// switching to the cloned context does not change which file that is.
absRecipe, err := filepath.Abs(filepath.Join(tree, a.From))
if err != nil {
return catalogue.Built{}, fmt.Errorf("%s: %s's recipe: %w", module, a.Name, err)
}
recipePath = absRecipe
}
// **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()
buildDir = cloned
}
invocation := append([]string{"build", "-f", recipePath, "-t", local}, args...)
if a.Target != "" {
@@ -802,18 +708,6 @@ func one(ctx context.Context, run Runner, publish Publisher,
if src != nil {
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 own, _ := ownDependencies(tree); len(own) > 0 {
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
// build was handed.
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")
reference, err := publishPackage(ctx, run, module, tree, a, npmrc, say)
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
case catalogue.ArtifactArchive:
src.ownHas(cleanEntry(a.From) + "/**")
body, err := pack(filepath.Join(tree, a.From))
if err != nil {
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; "
}
// 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
// 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,
@@ -567,7 +562,7 @@ func gateLayer(ctx context.Context, spec CheckSpec, tree, root, gate, verdictFil
checked := func(dir string) (string, error) {
var own tail
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)
w := io.MultiWriter(out, &own)
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
}

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