From c4f37c279a055fd658e394da06604815768959af Mon Sep 17 00:00:00 2001 From: jochen Date: Sun, 25 Oct 2026 20:49:29 +0100 Subject: [PATCH 1/4] The assigner's seat with put, release and puts, the controller's compose verb, a put's key and value, and the view answers no option that authorises (ADR 0546) --- go/catalogue/assigner_seat.go | 41 +++++++++ go/catalogue/seats.go | 2 + go/catalogue/seats_test.go | 6 +- go/catalogue/verbs.go | 13 +++ go/delivery/assigner.go | 14 ++- go/delivery/assigner_test.go | 36 ++++++++ go/delivery/put.go | 148 +++++++++++++++++++++++++++++++ go/delivery/view_answers.go | 8 +- go/delivery/view_answers_test.go | 7 ++ 9 files changed, 269 insertions(+), 6 deletions(-) create mode 100644 go/catalogue/assigner_seat.go create mode 100644 go/delivery/put.go diff --git a/go/catalogue/assigner_seat.go b/go/catalogue/assigner_seat.go new file mode 100644 index 0000000..66d22f6 --- /dev/null +++ b/go/catalogue/assigner_seat.go @@ -0,0 +1,41 @@ +package catalogue + +// The assigner's seat (novox/hq ADR 0546): putting a version of a module on a node is one verb. The module +// mesh-delivery-assigner, which writes every want, holds it once for the mesh and serves its verbs on its own process's +// credential; a put is a key of its own state, `mesh-delivery-assigner_puts`, so where it stands is read and watched there. + +// AssignerSeat is the seat mesh-delivery-assigner holds. +const AssignerSeat = "mesh-delivery-assigner" + +// assignerVerbs are the assigner seat's verbs: a put, its release, and the puts read (ADR 0546 clauses 1, 6 and 9). +func assignerVerbs() []Verb { + return []Verb{ + {Name: "put", Description: "Put one version of a module on one machine (novox/hq ADR 0546): the build at that " + + "fingerprint, the module's bus accounts and grant there, its composition, then the machine's want naming " + + "it, held there until released when hold is true. A module whose moves a person takes (the bus, a module " + + "of policy record) is asked of the operator first, and the bus has its streams snapshotted after the yes. " + + "Answers step by step what was done, or what it waits on and why; the put goes on after the answer, and " + + "`puts` says where it stands.", + Input: schema(map[string]string{ + "node": "the machine, where the module is assigned", + "module": "the module", + "version": "a fingerprint (src2: and 64 hex digits, or at least its first 12 hex digits), or newest", + "hold": "\"true\": the want stays at this version until released; \"false\" (the default): the rules move it again once it runs", + "why": "why, kept with the put and said in any ask", + }, []string{"node", "module", "version", "why"}, "hold")}, + {Name: "release", Description: "End a put on one machine (novox/hq ADR 0546): its hold is taken away and the " + + "assigner's rules write the machine's want again from their next pass. Nothing changes on the machine " + + "until they move it.", + Input: schema(map[string]string{ + "node": "the machine", + "module": "the module", + "why": "why, kept with the put", + }, []string{"node", "module", "why"})}, + {Name: "puts", Description: "Every put and where it stands: its version, hold, why, each step with its time " + + "and words, and what it waits on. Narrowed by machine or module. Read-only.", + Input: schema(map[string]string{ + "node": "one machine", + "module": "one module", + }, nil)}, + } +} diff --git a/go/catalogue/seats.go b/go/catalogue/seats.go index 7337d8a..b662fa6 100644 --- a/go/catalogue/seats.go +++ b/go/catalogue/seats.go @@ -243,6 +243,8 @@ var defaultSeats = append([]Seat{ // machine, and a group of them, recorded and ordered by its holder, which asks the controller for each act. // What it says it says as its module (`mesh-delivery.transition`), through the runtime that launches it. {Name: DeliverySeat, Scope: ScopeMesh, Serves: deliveryVerbs(), Decision: "novox/hq ADR 0239"}, + // Putting a version of a module on a node is one verb, the assigner's, which writes every want (novox/hq ADR 0546). + {Name: AssignerSeat, Scope: ScopeMesh, Serves: assignerVerbs(), Decision: "novox/hq ADR 0546"}, // Deferred renames (novox/hq ADR 0121): these deliver a provision, so renaming them is a // delivering-seat migration with a mesh-wide cascade if a holder stops resolving mid-flight. // They keep their names until that migration is done deliberately, apart from the node-* pass. diff --git a/go/catalogue/seats_test.go b/go/catalogue/seats_test.go index caa5a25..d500ed0 100644 --- a/go/catalogue/seats_test.go +++ b/go/catalogue/seats_test.go @@ -43,7 +43,7 @@ func TestTheSeatsAreAClosedSetAndEachNamesItsDecision(t *testing.T) { delivered[s.Delivers] = s.Name } } - // Forty-six with the three build seats replacing node-build-agent and the retired mesh-build-machine (novox/hq + // Forty-seven with mesh-delivery-assigner (novox/hq ADR 0546); forty-six with the three build seats replacing node-build-agent and the retired mesh-build-machine (novox/hq // ADR 0525 §2, §10); forty-five with node-files (novox/hq ADR 0394); forty-four again once node-reads goes. // Forty-four with node-provisioner (novox/hq ADR 0349 clause 3); forty-three with mesh-provisioner (novox/hq ADR 0349 clause 3); forty-two with mesh-config (novox/hq ADR 0348 §9); forty-one with node-reads (novox/hq ADR 0310); forty with node-nfs-server and node-mounts (novox/hq ADR 0263); thirty-eight with node-resolver (novox/hq ADR 0247); thirty-seven with mesh-delivery (novox/hq ADR 0239); thirty-six since node-resolver-config retired // into node-uplink (novox/hq ADR 0223); thirty-seven @@ -56,8 +56,8 @@ func TestTheSeatsAreAClosedSetAndEachNamesItsDecision(t *testing.T) { // node-container-runtime (ADR 0207); nineteen with node-environment and node-login-shell (ADR 0203, // ADR 0204); seventeen with node-build-agent (ADR 0190). One fewer once the retired // mesh-build-machine row goes, when no registered manifest claims it. - if len(Seats()) != 46 { - t.Errorf("the mesh defines %d seats rather than 46; the set is closed, so a change here is "+ + if len(Seats()) != 47 { + t.Errorf("the mesh defines %d seats rather than 47; the set is closed, so a change here is "+ "a decision (novox/hq ADR 0110): %s", len(Seats()), seatNames()) } } diff --git a/go/catalogue/verbs.go b/go/catalogue/verbs.go index 11f0b3e..e4bc442 100644 --- a/go/catalogue/verbs.go +++ b/go/catalogue/verbs.go @@ -248,6 +248,19 @@ var ControllerVerbs = []Verb{ "node": "the machine that runs the module", "module": "the module's name", }, []string{"node", "module"})}, + // One module composed for one node at one fingerprint, on demand (novox/hq ADR 0546 clause 4): what the assigner's put + // asks when the composition or the grant it needs is not there. Optional until every controller serves it. + {Name: "compose", Description: "Compose one module for one machine at one fingerprint now, and answer it (novox/hq " + + "ADR 0546): the module's bus accounts on that machine that are not minted are issued and sealed (never one minted " + + "before), the bus's user list and the grants are written, then the composition is written into the machine's " + + "compositions with its digest and the judgement of the move to it. Answers the composition's key, digest and " + + "judgement, and whether the grant at that fingerprint names the machine's account. Under the lease only.", + Input: schema(map[string]string{ + "node": "the machine", + "module": "the module, assigned to that machine", + "fp": "the fingerprint, src2: and 64 hex digits; the one the module is registered at when absent", + }, []string{"node", "module"}), + Optional: true}, {Name: "token", Description: "Refused through a verb since novox/hq ADR 0266: the one-time token a machine " + "joins with is answered to its caller, and whoever may call a verb includes agents. Issue it at the " + "controller's terminal: `mesh-controller token issue --new --overlay-key `.", diff --git a/go/delivery/assigner.go b/go/delivery/assigner.go index b009088..4aff2fd 100644 --- a/go/delivery/assigner.go +++ b/go/delivery/assigner.go @@ -181,15 +181,25 @@ type Want struct { Rule int `json:"rule,omitempty"` Backup string `json:"backup,omitempty"` Words string `json:"words,omitempty"` + // Put says the want was written by a put (ADR 0546), and Hold that the put holds it: the rules pass over it until it + // is released. A put's want names no newest fingerprint or rule. + Put bool `json:"put,omitempty"` + Hold bool `json:"hold,omitempty"` } func (w Want) check() error { if w.Removed { - if w.FP != "" || w.Composed != "" || w.Newest != "" || w.Rule != 0 || w.Backup != "" { - return fmt.Errorf("delivery: a removed want names no fingerprint, composition, rule or backup answer") + if w.FP != "" || w.Composed != "" || w.Newest != "" || w.Rule != 0 || w.Backup != "" || w.Put || w.Hold { + return fmt.Errorf("delivery: a removed want names no fingerprint, composition, rule, backup answer or put") } return nil } + if w.Hold && !w.Put { + return fmt.Errorf("delivery: only a put holds a want") + } + if w.Put && (w.Newest != "" || w.Rule != 0) { + return fmt.Errorf("delivery: a put's want is held back by no rule") + } if err := checkFingerprint(w.FP, "a want's fingerprint"); err != nil { return err } diff --git a/go/delivery/assigner_test.go b/go/delivery/assigner_test.go index a63f108..08ecca2 100644 --- a/go/delivery/assigner_test.go +++ b/go/delivery/assigner_test.go @@ -4,6 +4,7 @@ import ( "errors" "strings" "testing" + "time" ) // 1297/B1a: a want key round-trips through its parser; a held key, a have key and a node's own key are refused, @@ -77,6 +78,7 @@ func TestAssignerValuesRoundTrip(t *testing.T) { {FP: fp2of('a'), Composed: hashOf('1')}, {FP: fp2of('a'), Composed: hashOf('1'), Newest: fp2of('b'), Rule: RuleCanary, Words: "the canary ace does not run it healthy yet"}, {Removed: true, Words: "the module left g14"}, + {FP: fp2of('a'), Composed: hashOf('1'), Put: true, Hold: true, Words: "put by the operator: the bus's user list"}, } for _, w := range wants { data, err := EncodeWant(w) @@ -89,6 +91,24 @@ func TestAssignerValuesRoundTrip(t *testing.T) { t.Errorf("want %+v came back %+v, %v", w, got, err) } } + at := time.Date(2026, 10, 25, 21, 0, 0, 0, time.UTC) + p := Put{FP: fp2of('a'), Hold: true, Why: "the bus's user list", By: "shanks.node-tools", At: at}.Said(at, PutAsked, "put asked") + p = p.Said(at, PutAsked, "put asked").Said(at.Add(time.Second), PutWanted, "want written") + p.Composed = hashOf('1') + if len(p.Steps) != 2 || p.Step != PutWanted || PutEnded(p.Step) || !PutEnded(PutReleased) { + t.Errorf("a put's steps are %+v at %s", p.Steps, p.Step) + } + pkey, _ := PutKey("novox", "nats") + if n, m, err := ParsePutKey(pkey); err != nil || n != "novox" || m != "nats" { + t.Errorf("the put key %s reads %s %s, %v", pkey, n, m, err) + } + pdata, err := EncodePut(p) + if err != nil { + t.Fatal(err) + } + if gotP, err := DecodePut(pdata); err != nil || gotP.FP != p.FP || gotP.Step != PutWanted || len(gotP.Steps) != 2 || !gotP.Hold { + t.Errorf("the put came back %+v, %v", gotP, err) + } h := Held{Node: "ace", Words: "its health check failed: connection refused"} data, err := EncodeHeld(h) if err != nil { @@ -128,6 +148,9 @@ func TestAssignerValuesRefused(t *testing.T) { {FP: fp2of('a'), Newest: fp2of('b')}, {FP: fp2of('a'), Newest: fp2of('a'), Rule: 3}, {FP: fp2of('a'), Composed: hashOf('1'), Newest: fp2of('b'), Rule: 8}, + {FP: fp2of('a'), Composed: hashOf('1'), Hold: true}, + {FP: fp2of('a'), Composed: hashOf('1'), Put: true, Newest: fp2of('b'), Rule: 3}, + {Removed: true, Put: true}, } { if _, err := EncodeWant(w); err == nil { t.Errorf("EncodeWant(%+v) accepted", w) @@ -148,6 +171,19 @@ func TestAssignerValuesRefused(t *testing.T) { t.Errorf("EncodeDifference(%+v) accepted", d) } } + for _, p := range []Put{ + {FP: fp2of('a'), Why: "w", Step: "walking"}, + {FP: fp2of('a'), Step: PutAsked}, + {FP: "src2:abc", Why: "w", Step: PutAsked}, + {FP: fp2of('a'), Why: "w", Step: PutWanted, Composed: "sha256:x"}, + } { + if _, err := EncodePut(p); err == nil { + t.Errorf("EncodePut(%+v) accepted", p) + } + } + if _, _, err := ParsePutKey("ace.node"); err == nil { + t.Errorf("a node's own key was read as a put") + } if _, err := DecodeWant([]byte(`{"v":2,"fp":"` + fp2of('a') + `"}`)); !errors.Is(err, ErrUnknownVersion) { t.Errorf("a want of version 2 was read: %v", err) } diff --git a/go/delivery/put.go b/go/delivery/put.go new file mode 100644 index 0000000..5fda955 --- /dev/null +++ b/go/delivery/put.go @@ -0,0 +1,148 @@ +package delivery + +import ( + "encoding/json" + "fmt" + "time" +) + +// A put (novox/hq ADR 0546): one version of a module put on one node by the assigner seat's verb `put`. Each put is the +// key `.` of the assigner's state puts, which holds the version asked, the hold, why, and each step the +// assigner took or waits on, so a reader watches the key to follow it. The want the put writes carries Put (and Hold), +// so the assigner's rules pass over that key while the put holds it. + +// AssignerPutsBucket is the assigner's bucket of puts, `_puts`. +const AssignerPutsBucket = AssignerModule + "_puts" + +// AssignerPutsHistory is the revisions the puts bucket keeps per key: each step of a put is one. +const AssignerPutsHistory = 20 + +// A put's steps, in the order they are taken (ADR 0546 clauses 3 to 9). A put stands at the last step written. +const ( + // PutAsked is a put written by the verb, not taken up by a pass yet. + PutAsked = "asked" + // PutBuild waits for the builder's build at the fingerprint. + PutBuild = "build" + // PutCompose waits for the controller's composition (and grant) at the fingerprint. + PutCompose = "compose" + // PutPerson waits for the operator's answer to the ask about the move. + PutPerson = "person" + // PutSnapshot waits for the bus's streams to be snapshotted after the operator's yes. + PutSnapshot = "snapshot" + // PutWanted is the want written: the node's have has not reached it yet. + PutWanted = "wanted" + // PutHolding is a held put whose have runs it healthy: the want stays until released. + PutHolding = "holding" + // PutDone is a put without hold whose have ran it healthy: the rules write the want again. + PutDone = "done" + // PutReleased is a put released by the verb `release`. + PutReleased = "released" + // PutRefused is a put that cannot go on: a build that failed, an answer no, a snapshot not taken. Its words say why. + PutRefused = "refused" +) + +// PutEnded says a put at this step is over: nothing more is done for it and the rules own the want. +func PutEnded(step string) bool { + return step == PutDone || step == PutReleased || step == PutRefused +} + +// PutStep is one step a put took: when, which, and what was done or waits, in a sentence. +type PutStep struct { + At time.Time `json:"at"` + Step string `json:"step"` + Words string `json:"words"` +} + +// PutStepsKept is how many steps a put keeps, newest last. +const PutStepsKept = 16 + +// Put is the value of a key of AssignerPutsBucket. +type Put struct { + V int `json:"v"` + // FP is the fingerprint put, of the new form. + FP string `json:"fp"` + // Hold keeps the want at FP until released. + Hold bool `json:"hold,omitempty"` + Why string `json:"why"` + // By is who asked, as the bus named the caller; "unproven" when it named none. + By string `json:"by,omitempty"` + At time.Time `json:"at"` + // Step is where the put stands now, one of the Put* steps; Words say it in a sentence. + Step string `json:"step"` + Words string `json:"words,omitempty"` + // Composed is the digest of the composition the want names, once known. + Composed string `json:"composed,omitempty"` + // Ask is the key, in AssignerAsksBucket, of the ask about the move, once asked. + Ask string `json:"ask,omitempty"` + // Snapshot says where the bus's streams were snapshotted, once they were. + Snapshot string `json:"snapshot,omitempty"` + // Steps are the steps taken, oldest first, at most PutStepsKept. + Steps []PutStep `json:"steps,omitempty"` +} + +// Said is p at step with words, the step added to its steps when it differs from the last one. +func (p Put) Said(at time.Time, step, words string) Put { + if p.Step == step && p.Words == words { + return p + } + p.Step, p.Words = step, words + p.Steps = append(append([]PutStep(nil), p.Steps...), PutStep{At: at.UTC(), Step: step, Words: words}) + if len(p.Steps) > PutStepsKept { + p.Steps = p.Steps[len(p.Steps)-PutStepsKept:] + } + return p +} + +func (p Put) check() error { + if err := checkFingerprint(p.FP, "a put's fingerprint"); err != nil { + return err + } + if p.Why == "" { + return fmt.Errorf("delivery: a put says why") + } + switch p.Step { + case PutAsked, PutBuild, PutCompose, PutPerson, PutSnapshot, PutWanted, PutHolding, PutDone, PutReleased, PutRefused: + default: + return fmt.Errorf("delivery: %q is not a step of a put", p.Step) + } + if p.Composed != "" { + return checkDigest(p.Composed, "a put's composition digest") + } + return nil +} + +// PutKey is the key, in AssignerPutsBucket, of the put of module on node: `.`. +func PutKey(node, module string) (string, error) { + return HaveKey(node, module) +} + +// ParsePutKey reads a PutKey. +func ParsePutKey(key string) (node, module string, err error) { + node, module, isNode, err := ParseHaveKey(key) + if err == nil && isNode { + err = fmt.Errorf("delivery: %q is a node's own key, and no put", key) + } + return node, module, err +} + +// EncodePut is p as JSON at Version; it refuses a fingerprint not of the new form, a put that says no why and an +// unknown step. +func EncodePut(p Put) ([]byte, error) { + if err := p.check(); err != nil { + return nil, err + } + p.V = Version + return json.Marshal(p) +} + +// DecodePut reads a put; it refuses an unknown version (ErrUnknownVersion) and what EncodePut refuses. +func DecodePut(data []byte) (Put, error) { + var p Put + if err := decode(data, &p); err != nil { + return Put{}, err + } + if err := p.check(); err != nil { + return Put{}, err + } + return p, nil +} diff --git a/go/delivery/view_answers.go b/go/delivery/view_answers.go index 68561fc..5109830 100644 --- a/go/delivery/view_answers.go +++ b/go/delivery/view_answers.go @@ -283,9 +283,15 @@ func ViewAnswerFits(a ViewAnswer, ask AssignerAsk) error { if a.Asking != ask.Ask.ID { return fmt.Errorf("delivery: the view's answer is to the asking %s, and the ask stands at the asking %s", a.Asking, ask.Ask.ID) } - if _, ok := ask.Ask.Option(a.Option); !ok { + o, ok := ask.Ask.Option(a.Option) + if !ok { return fmt.Errorf("delivery: the view's answer chose %q, which the asking %s does not offer", a.Option, ask.Ask.ID) } + if o.Level != "" && o.Level != asks.Acknowledge { + // An option that authorises is answered on a channel that proves who answered (ADR 0259 §5, ADR 0546 clause 8), + // never in the view, which proves nobody. + return fmt.Errorf("delivery: the view's answer chose %q, an option of level %s, which only a channel that proves who answered takes", a.Option, o.Level) + } return nil } diff --git a/go/delivery/view_answers_test.go b/go/delivery/view_answers_test.go index c069e01..1a9b824 100644 --- a/go/delivery/view_answers_test.go +++ b/go/delivery/view_answers_test.go @@ -158,4 +158,11 @@ func TestViewAnswerFits(t *testing.T) { if err := ViewAnswerFits(ViewAnswer{Asking: first.Ask.ID, Option: "restore"}, first); err == nil { t.Errorf("an option the ask does not offer fits") } + // An option that authorises is never answered in the view (ADR 0546 clause 8). + approving := first + approving.Ask.Options = append([]asks.Option(nil), first.Ask.Options...) + approving.Ask.Options[0].Level = asks.Approve + if err := ViewAnswerFits(answer, approving); err == nil || !strings.Contains(err.Error(), "approve") { + t.Errorf("the view answered an option of level approve: %v", err) + } } From 20a6dc4920f80ed8c30508f8a2bcf95cdb5dcdaa Mon Sep 17 00:00:00 2001 From: jochen Date: Sun, 25 Oct 2026 20:50:51 +0100 Subject: [PATCH 2/4] A grant says whether it names a module's accounts on a node, for the assigner and the compose verb (ADR 0546) --- go/delivery/assigner_test.go | 16 ++++++++++++++++ go/delivery/grants.go | 17 +++++++++++++++++ 2 files changed, 33 insertions(+) diff --git a/go/delivery/assigner_test.go b/go/delivery/assigner_test.go index 08ecca2..6b54e1d 100644 --- a/go/delivery/assigner_test.go +++ b/go/delivery/assigner_test.go @@ -298,4 +298,20 @@ func TestControllerGrant(t *testing.T) { if _, err := EncodeControllerGrant(ControllerGrant{Accounts: []string{""}}); err == nil { t.Errorf("a grant naming an empty account was accepted") } + g := ControllerGrant{Accounts: []string{"ace.mesh-build-go", "shanks.mesh-mail.reader"}} + for _, c := range []struct { + g ControllerGrant + node, module string + names bool + }{ + {g, "ace", "mesh-build-go", true}, + {g, "shanks", "mesh-mail", true}, + {g, "g14", "mesh-build-go", false}, + {g, "ace", "mesh-build", false}, + {ControllerGrant{}, "novox", "nats", true}, + } { + if got := c.g.NamesNode(c.node, c.module); got != c.names { + t.Errorf("%v names %s.%s: %v, want %v", c.g.Accounts, c.node, c.module, got, c.names) + } + } } diff --git a/go/delivery/grants.go b/go/delivery/grants.go index 39e602c..01d5655 100644 --- a/go/delivery/grants.go +++ b/go/delivery/grants.go @@ -73,3 +73,20 @@ func DecodeControllerGrant(data []byte) (ControllerGrant, error) { // SameControllerGrant says whether two grants are the same value. func SameControllerGrant(a, b ControllerGrant) bool { return reflect.DeepEqual(a, b) } + +// NamesNode says whether the grant names module's accounts on node: its own `.` or a process's +// `..`. A grant that names no account (a module that does not speak on the bus) names every +// node. The assigner wants a node only once the grant names its account (issue 1945), and the controller's compose +// verb answers it (ADR 0546 clause 4). +func (g ControllerGrant) NamesNode(node, module string) bool { + if len(g.Accounts) == 0 { + return true + } + own := node + "." + module + for _, a := range g.Accounts { + if a == own || strings.HasPrefix(a, own+".") { + return true + } + } + return false +} From 2f6741b7f8ec2e34e3e5fd57e2a35625f93d17a5 Mon Sep 17 00:00:00 2001 From: jochen Date: Sun, 25 Oct 2026 21:07:58 +0100 Subject: [PATCH 3/4] No seat and no compose verb for the put: the controller takes no change, so the assigner serves the put as its own tools from compositions the controller already writes (ADR 0546) --- go/catalogue/assigner_seat.go | 41 ----------------------------------- go/catalogue/seats.go | 2 -- go/catalogue/seats_test.go | 6 ++--- go/catalogue/verbs.go | 13 ----------- 4 files changed, 3 insertions(+), 59 deletions(-) delete mode 100644 go/catalogue/assigner_seat.go diff --git a/go/catalogue/assigner_seat.go b/go/catalogue/assigner_seat.go deleted file mode 100644 index 66d22f6..0000000 --- a/go/catalogue/assigner_seat.go +++ /dev/null @@ -1,41 +0,0 @@ -package catalogue - -// The assigner's seat (novox/hq ADR 0546): putting a version of a module on a node is one verb. The module -// mesh-delivery-assigner, which writes every want, holds it once for the mesh and serves its verbs on its own process's -// credential; a put is a key of its own state, `mesh-delivery-assigner_puts`, so where it stands is read and watched there. - -// AssignerSeat is the seat mesh-delivery-assigner holds. -const AssignerSeat = "mesh-delivery-assigner" - -// assignerVerbs are the assigner seat's verbs: a put, its release, and the puts read (ADR 0546 clauses 1, 6 and 9). -func assignerVerbs() []Verb { - return []Verb{ - {Name: "put", Description: "Put one version of a module on one machine (novox/hq ADR 0546): the build at that " + - "fingerprint, the module's bus accounts and grant there, its composition, then the machine's want naming " + - "it, held there until released when hold is true. A module whose moves a person takes (the bus, a module " + - "of policy record) is asked of the operator first, and the bus has its streams snapshotted after the yes. " + - "Answers step by step what was done, or what it waits on and why; the put goes on after the answer, and " + - "`puts` says where it stands.", - Input: schema(map[string]string{ - "node": "the machine, where the module is assigned", - "module": "the module", - "version": "a fingerprint (src2: and 64 hex digits, or at least its first 12 hex digits), or newest", - "hold": "\"true\": the want stays at this version until released; \"false\" (the default): the rules move it again once it runs", - "why": "why, kept with the put and said in any ask", - }, []string{"node", "module", "version", "why"}, "hold")}, - {Name: "release", Description: "End a put on one machine (novox/hq ADR 0546): its hold is taken away and the " + - "assigner's rules write the machine's want again from their next pass. Nothing changes on the machine " + - "until they move it.", - Input: schema(map[string]string{ - "node": "the machine", - "module": "the module", - "why": "why, kept with the put", - }, []string{"node", "module", "why"})}, - {Name: "puts", Description: "Every put and where it stands: its version, hold, why, each step with its time " + - "and words, and what it waits on. Narrowed by machine or module. Read-only.", - Input: schema(map[string]string{ - "node": "one machine", - "module": "one module", - }, nil)}, - } -} diff --git a/go/catalogue/seats.go b/go/catalogue/seats.go index b662fa6..7337d8a 100644 --- a/go/catalogue/seats.go +++ b/go/catalogue/seats.go @@ -243,8 +243,6 @@ var defaultSeats = append([]Seat{ // machine, and a group of them, recorded and ordered by its holder, which asks the controller for each act. // What it says it says as its module (`mesh-delivery.transition`), through the runtime that launches it. {Name: DeliverySeat, Scope: ScopeMesh, Serves: deliveryVerbs(), Decision: "novox/hq ADR 0239"}, - // Putting a version of a module on a node is one verb, the assigner's, which writes every want (novox/hq ADR 0546). - {Name: AssignerSeat, Scope: ScopeMesh, Serves: assignerVerbs(), Decision: "novox/hq ADR 0546"}, // Deferred renames (novox/hq ADR 0121): these deliver a provision, so renaming them is a // delivering-seat migration with a mesh-wide cascade if a holder stops resolving mid-flight. // They keep their names until that migration is done deliberately, apart from the node-* pass. diff --git a/go/catalogue/seats_test.go b/go/catalogue/seats_test.go index d500ed0..caa5a25 100644 --- a/go/catalogue/seats_test.go +++ b/go/catalogue/seats_test.go @@ -43,7 +43,7 @@ func TestTheSeatsAreAClosedSetAndEachNamesItsDecision(t *testing.T) { delivered[s.Delivers] = s.Name } } - // Forty-seven with mesh-delivery-assigner (novox/hq ADR 0546); forty-six with the three build seats replacing node-build-agent and the retired mesh-build-machine (novox/hq + // Forty-six with the three build seats replacing node-build-agent and the retired mesh-build-machine (novox/hq // ADR 0525 §2, §10); forty-five with node-files (novox/hq ADR 0394); forty-four again once node-reads goes. // Forty-four with node-provisioner (novox/hq ADR 0349 clause 3); forty-three with mesh-provisioner (novox/hq ADR 0349 clause 3); forty-two with mesh-config (novox/hq ADR 0348 §9); forty-one with node-reads (novox/hq ADR 0310); forty with node-nfs-server and node-mounts (novox/hq ADR 0263); thirty-eight with node-resolver (novox/hq ADR 0247); thirty-seven with mesh-delivery (novox/hq ADR 0239); thirty-six since node-resolver-config retired // into node-uplink (novox/hq ADR 0223); thirty-seven @@ -56,8 +56,8 @@ func TestTheSeatsAreAClosedSetAndEachNamesItsDecision(t *testing.T) { // node-container-runtime (ADR 0207); nineteen with node-environment and node-login-shell (ADR 0203, // ADR 0204); seventeen with node-build-agent (ADR 0190). One fewer once the retired // mesh-build-machine row goes, when no registered manifest claims it. - if len(Seats()) != 47 { - t.Errorf("the mesh defines %d seats rather than 47; the set is closed, so a change here is "+ + if len(Seats()) != 46 { + t.Errorf("the mesh defines %d seats rather than 46; the set is closed, so a change here is "+ "a decision (novox/hq ADR 0110): %s", len(Seats()), seatNames()) } } diff --git a/go/catalogue/verbs.go b/go/catalogue/verbs.go index e4bc442..11f0b3e 100644 --- a/go/catalogue/verbs.go +++ b/go/catalogue/verbs.go @@ -248,19 +248,6 @@ var ControllerVerbs = []Verb{ "node": "the machine that runs the module", "module": "the module's name", }, []string{"node", "module"})}, - // One module composed for one node at one fingerprint, on demand (novox/hq ADR 0546 clause 4): what the assigner's put - // asks when the composition or the grant it needs is not there. Optional until every controller serves it. - {Name: "compose", Description: "Compose one module for one machine at one fingerprint now, and answer it (novox/hq " + - "ADR 0546): the module's bus accounts on that machine that are not minted are issued and sealed (never one minted " + - "before), the bus's user list and the grants are written, then the composition is written into the machine's " + - "compositions with its digest and the judgement of the move to it. Answers the composition's key, digest and " + - "judgement, and whether the grant at that fingerprint names the machine's account. Under the lease only.", - Input: schema(map[string]string{ - "node": "the machine", - "module": "the module, assigned to that machine", - "fp": "the fingerprint, src2: and 64 hex digits; the one the module is registered at when absent", - }, []string{"node", "module"}), - Optional: true}, {Name: "token", Description: "Refused through a verb since novox/hq ADR 0266: the one-time token a machine " + "joins with is answered to its caller, and whoever may call a verb includes agents. Issue it at the " + "controller's terminal: `mesh-controller token issue --new --overlay-key `.", From d4ae4b6790b86b8fabe620e5483c0011ebaed1dd Mon Sep 17 00:00:00 2001 From: jochen Date: Sun, 25 Oct 2026 21:08:10 +0100 Subject: [PATCH 4/4] Say the put's tools, not a seat's verbs, in the put's words (ADR 0546) --- go/delivery/grants.go | 4 ++-- go/delivery/put.go | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/go/delivery/grants.go b/go/delivery/grants.go index 01d5655..884e4f1 100644 --- a/go/delivery/grants.go +++ b/go/delivery/grants.go @@ -76,8 +76,8 @@ func SameControllerGrant(a, b ControllerGrant) bool { return reflect.DeepEqual(a // NamesNode says whether the grant names module's accounts on node: its own `.` or a process's // `..`. A grant that names no account (a module that does not speak on the bus) names every -// node. The assigner wants a node only once the grant names its account (issue 1945), and the controller's compose -// verb answers it (ADR 0546 clause 4). +// node. The assigner wants a node only once the grant names its account (issue 1945), and a put waits for it (ADR +// 0546 clause 4). func (g ControllerGrant) NamesNode(node, module string) bool { if len(g.Accounts) == 0 { return true diff --git a/go/delivery/put.go b/go/delivery/put.go index 5fda955..609d298 100644 --- a/go/delivery/put.go +++ b/go/delivery/put.go @@ -6,7 +6,7 @@ import ( "time" ) -// A put (novox/hq ADR 0546): one version of a module put on one node by the assigner seat's verb `put`. Each put is the +// A put (novox/hq ADR 0546): one version of a module put on one node by the assigner's tool `assigner_put`. Each put is the // key `.` of the assigner's state puts, which holds the version asked, the hold, why, and each step the // assigner took or waits on, so a reader watches the key to follow it. The want the put writes carries Put (and Hold), // so the assigner's rules pass over that key while the put holds it. @@ -19,7 +19,7 @@ const AssignerPutsHistory = 20 // A put's steps, in the order they are taken (ADR 0546 clauses 3 to 9). A put stands at the last step written. const ( - // PutAsked is a put written by the verb, not taken up by a pass yet. + // PutAsked is a put written by the tool, not taken up by a pass yet. PutAsked = "asked" // PutBuild waits for the builder's build at the fingerprint. PutBuild = "build" @@ -35,7 +35,7 @@ const ( PutHolding = "holding" // PutDone is a put without hold whose have ran it healthy: the rules write the want again. PutDone = "done" - // PutReleased is a put released by the verb `release`. + // PutReleased is a put released by the tool `assigner_release`. PutReleased = "released" // PutRefused is a put that cannot go on: a build that failed, an answer no, a snapshot not taken. Its words say why. PutRefused = "refused"