The assigner's state and verb placement are assignments, with its events, tools, views and every caller; the assigner copies the retired state at its start (issue 1966)

Claude-Session: https://claude.ai/code/session_01NcbgfFSnn24M3GcqttoU7i
This commit is contained in:
2026-11-05 22:13:20 +01:00
parent 369ebd092d
commit 0eafb2fe95
164 changed files with 1699 additions and 1498 deletions
@@ -12,10 +12,10 @@ import (
)
// **The accounts the mesh wants, said to the vault** (novox/hq ADR 0561 §2, issue 774). The bus composer derives every
// user from placement, the manifests and the wants; for each account the vault mints (a module's, a process's, a
// user from assignments, the manifests and the wants; for each account the vault mints (a module's, a process's, a
// machine's provisioner client's), the part writes one key of its state `accounts`, keyed by the bus user, with the
// fields the controller's credential carries beside the password. A key is written only when it differs, and deleted
// only when placement no longer names its user and every input of the pass was read: a refused read never retires an
// only when assignments no longer names its user and every input of the pass was read: a refused read never retires an
// account. The vault alone reads the state (buscompose.NamedReaders); it mints each user it holds no value for.
// StateAccounts is the part's state of wanted accounts.
@@ -119,7 +119,7 @@ func (p *Part) Accounts(user string) (any, error) {
}
if !found {
if user != "" {
return nil, fmt.Errorf("no account %s is wanted: placement composes no module, process or provisioner client of that user", user)
return nil, fmt.Errorf("no account %s is wanted: assignments composes no module, process or provisioner client of that user", user)
}
continue
}
@@ -149,7 +149,7 @@ type AccountsWritten struct {
Wanted int `json:"wanted"`
Written []string `json:"written,omitempty"`
Deleted []string `json:"deleted,omitempty"`
// Kept are keys placement no longer names that this pass did not delete, with why.
// Kept are keys assignments no longer names that this pass did not delete, with why.
Kept []string `json:"kept,omitempty"`
}
@@ -198,7 +198,7 @@ func (p *Part) writeAccounts(res *Result, in Inputs, recs buscompose.Records) {
}
switch {
case placed(user):
done.Kept = append(done.Kept, user+": placement still names it (composed with no account this pass)")
done.Kept = append(done.Kept, user+": assignments still names it (composed with no account this pass)")
continue
case len(failed) > 0:
done.Kept = append(done.Kept, user+": a read of this pass failed, so no account is retired")
@@ -21,7 +21,7 @@ import (
)
// **The grants part's first views** (novox/hq ADR 0615 §1, §4 to §6, ADR 0621, ADR 0622; design 25 § 12.2; DLV-F4r,
// DLV-R1). The part reads the assigner's placement and wants and the indexer's manifests, modules and verdicts no longer
// DLV-R1). The part reads the assigner's assignments and wants and the indexer's manifests, modules and verdicts no longer
// from their buckets, but by each owner's first-view tool, asked once at its start, then from the owner's views family
// alone (each event one changed key with its revision), asking once more only on a gap in revisions, never on a timer.
// The verdicts alone are followed on the seat mesh-catalog's manifest-accepted and manifest-refused, which carry the revision
@@ -48,10 +48,10 @@ const (
IndexerModule = "mesh-delivery-indexer"
)
// FirstViews are the five (ADR 0615 §3 as ADR 0622 §1 amends it: the grants part's placement, wants, manifests,
// FirstViews are the five (ADR 0615 §3 as ADR 0622 §1 amends it: the grants part's assignments, wants, manifests,
// modules and verdicts).
var FirstViews = []FirstView{
{Owner: AssignerModule, Tool: AssignerModule + ".placement", State: "placement", Read: ReadPlacement},
{Owner: AssignerModule, Tool: AssignerModule + ".assignments", State: "assignments", Read: ReadAssignments},
{Owner: AssignerModule, Tool: AssignerModule + ".wants", State: "wants", Read: ReadWants},
{Owner: IndexerModule, Tool: IndexerModule + ".manifests", State: "manifests", Read: ReadManifests},
{Owner: IndexerModule, Tool: IndexerModule + ".modules", State: "modules", Read: ReadModules},
@@ -244,7 +244,7 @@ func (v *Views) Statuses() []views.Status {
return out
}
// Again asks the first view of one state (its state name, `placement`, or its tool) once more, or every one when state is
// Again asks the first view of one state (its state name, `assignments`, or its tool) once more, or every one when state is
// empty, each on a goroutine of its own; it answers which it asked, or refuses a state that is none of the five.
func (v *Views) Again(ctx context.Context, state, why string) ([]string, error) {
var asked []string
@@ -257,7 +257,7 @@ func (v *Views) Again(ctx context.Context, state, why string) ([]string, error)
asked = append(asked, fv.Tool)
}
if len(asked) == 0 {
return nil, fmt.Errorf("%q is no first view of the grants part: one of placement, wants, manifests, modules, verdicts, or none for all", state)
return nil, fmt.Errorf("%q is no first view of the grants part: one of assignments, wants, manifests, modules, verdicts, or none for all", state)
}
return asked, nil
}
@@ -459,7 +459,7 @@ func (p *Part) AskFirstViewAgain(caller, state, why string, record func(what, wh
}
}
if which == "" {
return nil, fmt.Errorf("%q is no first view of the grants part: one of placement, wants, manifests, modules, verdicts, or none for all", state)
return nil, fmt.Errorf("%q is no first view of the grants part: one of assignments, wants, manifests, modules, verdicts, or none for all", state)
}
}
if strings.TrimSpace(why) == "" {
@@ -15,13 +15,13 @@ type Watcher interface {
Watch(state string, changed func(key string, value []byte), ended func(error)) error
}
// Followed are the states whose change composes again: placement, the indexer's manifests by module, the resolved
// Followed are the states whose change composes again: assignments, the indexer's manifests by module, the resolved
// config values, the node agents' haves (a machine's first have, and the bus node's loaded list) and the assigner's
// wants (the manifest each machine is wanted to run), and the vault's hashes of the bus accounts.
//
// And the seat mesh-enrolment's keys (ADR 0587 §4, §5): a key at live composes enrol.<node>, and its end, by the holder or by
// the bus's expiry, is a change of the key that takes the user out; no event is needed beside the watch.
var Followed = []string{ReadModules, ReadVerdicts, ReadResolved, ReadHave, ReadWants, ReadAccounts, ReadPlacement, ReadEnrolments,
var Followed = []string{ReadModules, ReadVerdicts, ReadResolved, ReadHave, ReadWants, ReadAccounts, ReadAssignments, ReadEnrolments,
ReadNodeLogins}
// Follower composes again on every change it hears, never on a timer. It never stops for a refusal: a watch it cannot
@@ -34,7 +34,7 @@ type GrantsWritten struct {
Deleted []string `json:"deleted,omitempty"`
// Kept says why a pass wrote and deleted nothing, or left a module out.
Kept []string `json:"kept,omitempty"`
// Missing is each module the placement or an assignment puts on a machine that has no grant after the pass,
// Missing is each module the assignments or an assignment puts on a machine that has no grant after the pass,
// `<module>: <why>`: the assigner's sixth rule wants none of it (MissingGrants). Never passed over in silence: once
// the part's grants are the ones read, it is the condition mesh-grants.grant-missing.
Missing []string `json:"missing,omitempty"`
+23 -23
View File
@@ -19,15 +19,15 @@ import (
"mesh-grants/internal/buscompose"
"git.novox.be/novox/mesh-contracts/go/clients/meshmoduleassignerclient"
"git.novox.be/novox/mesh-contracts/go/clients/nodeengineclient"
"git.novox.be/novox/mesh-contracts/go/seats/meshmoduleassigner"
"git.novox.be/novox/mesh-contracts/go/seats/nodeengine"
"git.novox.be/novox/mesh-contracts/identity-and-access/go/clients/meshvaultclient"
"git.novox.be/novox/mesh-contracts/identity-and-access/go/seats/meshnodeenroller"
"git.novox.be/novox/mesh-contracts/identity-and-access/go/seats/meshvault"
"git.novox.be/novox/mesh-contracts/placement/go/clients/meshmoduleassignerclient"
"git.novox.be/novox/mesh-contracts/placement/go/clients/meshwantsclient"
"git.novox.be/novox/mesh-contracts/placement/go/seats/meshmoduleassigner"
"git.novox.be/novox/mesh-delivery/contracts/go/clients/meshcatalogclient"
"git.novox.be/novox/mesh-delivery/contracts/go/clients/meshwantsclient"
"git.novox.be/novox/mesh-delivery/contracts/go/seats/meshcatalog"
"git.novox.be/novox/mesh-delivery/modules/mesh-delivery-builder/builds"
"git.novox.be/novox/mesh-networking/modules/mesh-wireguard/go/overlay"
@@ -47,9 +47,9 @@ const (
ReadBuilds = "mesh-delivery-builder.builds"
// ReadAccounts is the vault's bcrypt hashes of the bus accounts it holds, by bus user (ADR 0547 clause 9).
ReadAccounts = "mesh-vault.bus-accounts"
// ReadPlacement is the seat mesh-placement's placement (ADR 0567 §1, §9): each node's modules and the seats
// ReadAssignments is the seat mesh-module-assigner's assignments (ADR 0567 §1, §9): each node's modules and the seats
// they hold.
ReadPlacement = buscompose.PlacementRead
ReadAssignments = buscompose.AssignmentsRead
// ReadEnrolments is the seat mesh-enrolment's open enrolments, read as a named reader (ADR 0587 §2, §4): a key at
// live or redeeming composes the user enrol.<node>.
ReadEnrolments = buscompose.EnrolmentsRead
@@ -58,13 +58,13 @@ const (
ReadNodeLogins = buscompose.NodeLoginsRead
)
// Reads is every state the part reads, in the manifest's order. Not the controller's assignments: the seat's placement
// Reads is every state the part reads, in the manifest's order. Not the controller's assignments: the seat's assignments
// alone places modules (novox/hq ADR 0549 §5, ADR 0571 §10; issue 774).
var Reads = []string{ReadResolved, ReadHave, ReadBuilds, ReadAccounts, ReadEnrolments, ReadNodeLogins}
// ViewReads are the owners' states the part reads by their first views and views families, never their buckets
// (firstviews.go, ADR 0615): each read in Reads' place.
var ViewReads = []string{ReadModules, ReadManifests, ReadVerdicts, ReadWants, ReadPlacement}
var ViewReads = []string{ReadModules, ReadManifests, ReadVerdicts, ReadWants, ReadAssignments}
// The bus module's tools the part calls, as its manifest's invokes name them: read-only, each.
const (
@@ -128,13 +128,13 @@ type resolved struct {
// Inputs is what the shadow composes from, read from the bus.
type Inputs struct {
// Nodes is every machine seen: in a node agent's have or the placement.
// Nodes is every machine seen: in a node agent's have or the assignments.
Nodes []string
// Assigned is what the seat's placement puts on each machine, sorted (issue 774: never the controller's assignments).
// Assigned is what the seat's assignments puts on each machine, sorted (issue 774: never the controller's assignments).
Assigned map[string][]string
// Placed is each node's placement as the seat mesh-placement holds it (ADR 0567 §1); empty before any is read.
// Where it is, a seat held once for the mesh is held where placement says, not wherever a module claiming it runs.
Placed map[string]meshmoduleassigner.Placement
// Placed is each node's assignments as the seat mesh-module-assigner holds it (ADR 0567 §1); empty before any is read.
// Where it is, a seat held once for the mesh is held where assignments says, not wherever a module claiming it runs.
Placed map[string]meshmoduleassigner.Assignments
// Manifests is each module's manifest at the indexer's trunk, by module.
Manifests map[string]catalogue.Manifest
// Bound is, by module, the streams of each bus-state entry the bus module said it created and has not dropped, from
@@ -318,33 +318,33 @@ func ReadInputs(r Reader, cache *Manifests) Inputs {
NoFingerprint: map[string]string{}, Running: map[string]catalogue.Manifest{},
Values: map[string]any{}, Gaps: append([]Gap(nil), standingGaps...)}
nodes := map[string]bool{}
// **The seat's placement alone places modules** (novox/hq ADR 0549 §5, ADR 0567 §9; issue 774): the controller's
// **The seat's assignments alone places modules** (novox/hq ADR 0549 §5, ADR 0567 §9; issue 774): the controller's
// assignments are read no more, and the seats held once for the mesh are held where it says. Not read is a failed
// read: nothing is placed, so the list composed past it is not sealed.
if keys, err := r.Keys(ReadPlacement); err != nil {
in.Gaps = append(in.Gaps, Gap{Input: ReadPlacement, Why: "refused or absent: nothing is placed (" + err.Error() + ")", Failed: true})
if keys, err := r.Keys(ReadAssignments); err != nil {
in.Gaps = append(in.Gaps, Gap{Input: ReadAssignments, Why: "refused or absent: nothing is placed (" + err.Error() + ")", Failed: true})
} else {
// Each node's keys in either form, assembled by the seat's one reader (meshmoduleassignerclient.PlacementsOf; research 072
// row 37): its node key and a key per module, or its legacy key `placement.<node>`.
// Each node's keys in either form, assembled by the seat's one reader (meshmoduleassignerclient.AssignmentsOf; research 072
// row 37): its node key and a key per module, or its legacy key `assignments.<node>`.
values := map[string][]byte{}
for _, k := range keys {
raw, found, err := r.Get(ReadPlacement, k)
raw, found, err := r.Get(ReadAssignments, k)
if err != nil || !found {
continue
}
if _, _, _, err := meshmoduleassignerclient.ParseKey(k); err != nil {
in.Gaps = append(in.Gaps, Gap{Input: ReadPlacement + " " + k, Why: "not a placement key, so it places nothing"})
in.Gaps = append(in.Gaps, Gap{Input: ReadAssignments + " " + k, Why: "not an assignments key, so it places nothing"})
continue
}
values[k] = raw
}
placed, err := meshmoduleassignerclient.PlacementsOf(values)
placed, err := meshmoduleassignerclient.AssignmentsOf(values)
if err != nil {
in.Gaps = append(in.Gaps, Gap{Input: ReadPlacement, Why: "a key does not read, so its node is composed from what does: " + err.Error()})
in.Gaps = append(in.Gaps, Gap{Input: ReadAssignments, Why: "a key does not read, so its node is composed from what does: " + err.Error()})
}
for node, p := range placed {
if in.Placed == nil {
in.Placed = map[string]meshmoduleassigner.Placement{}
in.Placed = map[string]meshmoduleassigner.Assignments{}
}
in.Placed[node] = p
}
@@ -372,7 +372,7 @@ func ReadInputs(r Reader, cache *Manifests) Inputs {
nodes[node] = true
}
}
in.Gaps = append(in.Gaps, Gap{Input: "machines", Why: "read from the node agents' haves and the placement: a machine with neither is not seen"})
in.Gaps = append(in.Gaps, Gap{Input: "machines", Why: "read from the node agents' haves and the assignments: a machine with neither is not seen"})
for n := range nodes {
in.Nodes = append(in.Nodes, n)
}
@@ -40,7 +40,7 @@ type Membership struct {
// Memberships composes every assignment's membership from the records (buscompose.MembershipFor, as the controller's
// parked composition pass, mesh-controller 003eee8a, issued them), by `<node>.<module>`. Pure.
func Memberships(r buscompose.Records) map[string]Membership {
where := buscompose.PlacementsOf(r, r.Interchangeable)
where := buscompose.AssignmentsOf(r, r.Interchangeable)
out := map[string]Membership{}
for node, declared := range r.Assigned {
for _, d := range declared {
@@ -7,7 +7,7 @@ import (
"slices"
"strings"
"git.novox.be/novox/mesh-contracts/placement/go/seats/meshmoduleassigner"
"git.novox.be/novox/mesh-contracts/go/seats/meshmoduleassigner"
"git.novox.be/novox/mesh-sdk/go/catalogue"
"mesh-grants/internal/buscompose"
)
@@ -408,14 +408,14 @@ func hearsOf(m catalogue.Manifest, declared map[string]catalogue.Manifest, node
return out, touches, onEvents
}
// heldWherePlaced is a module's holds on a node as the catalogue's placement says them (ADR 0549 §1, §3). A placement's
// heldWherePlaced is a module's holds on a node as the catalogue's assignments says them (ADR 0549 §1, §3). The assignments'
// holds are the holders on record, as catalogue.World's Holdings are: a seat held once for the mesh that some node's
// placement holds is held only where a placement names it (this node's naming this module), so a module claiming it on
// two machines holds it on the one placement names, not on both. A seat no placement holds is held by derivation, by the
// module claiming it wherever it is placed, as it was before placement: the seeded placements hold only the seats the
// assignments holds is held only where assignments names it (this node's naming this module), so a module claiming it on
// two machines holds it on the one assignments names, not on both. A seat no assignments holds is held by derivation, by the
// module claiming it wherever it is placed, as it was before assignments: the seeded assignments hold only the seats the
// controller had on record (twelve on the control-node), and treating their holds as every holder left nine seats with
// no holder in the user list of 2026-10-27 09:21 (issue 1945). A seat of every node is held wherever the module runs.
func heldWherePlaced(holds []buscompose.Seat, placed map[string]meshmoduleassigner.Placement, node, module string) []buscompose.Seat {
func heldWherePlaced(holds []buscompose.Seat, placed map[string]meshmoduleassigner.Assignments, node, module string) []buscompose.Seat {
same := func(x meshmoduleassigner.Holding, h buscompose.Seat) bool {
return (x.Scope == "" || x.Scope == catalogue.ScopeMesh) && sameSeat(x.Claim, h.Name) && x.Kind == h.Kind
}
@@ -239,7 +239,7 @@ type Comparison struct {
Shadow int `json:"shadow"`
Loaded int `json:"loaded"`
// Added are users in the shadow list the loaded list lacks (one left out for want of a credential among them);
// Dropped are loaded users the shadow list lacks; Gone are those of Dropped whose machine or module placement no
// Dropped are loaded users the shadow list lacks; Gone are those of Dropped whose machine or module assignments no
// longer names, which both lists agree are gone.
Added []string `json:"added"`
Dropped []string `json:"dropped"`
@@ -277,7 +277,7 @@ func (c Comparison) Differing() []string {
func (d UserDiff) responsesLost() bool { return strings.HasPrefix(d.Responses, "shadow false") }
// Removing names every user that keeps the lists apart other than by a right the shadow list adds, each with how: a user
// the shadow list adds, a loaded user it drops that placement still names, a publish or subscribe right a loaded user
// the shadow list adds, a loaded user it drops that assignments still names, a publish or subscribe right a loaded user
// would lose, or an allowance of responses lost (ADR 0562 §2). Empty when the lists agree or differ only by added rights.
func (c Comparison) Removing() []string {
var out []string
@@ -286,7 +286,7 @@ func (c Comparison) Removing() []string {
}
for _, u := range c.Dropped {
if !slices.Contains(c.Gone, u) {
out = append(out, u+" (dropped, and placement still names it)")
out = append(out, u+" (dropped, and assignments still names it)")
}
}
for _, d := range c.Differ {
@@ -331,7 +331,7 @@ func (c Comparison) AddedRights() []UserDiff {
return out
}
// Compare compares the shadow list with the loaded one. placed says whether placement still names a loaded user's
// Compare compares the shadow list with the loaded one. placed says whether assignments still names a loaded user's
// machine and module; nil names every one. Pure.
func Compare(shadow, loaded []User, placed func(user string) bool) Comparison {
c := Comparison{Shadow: len(shadow), Loaded: len(loaded), Added: []string{}, Dropped: []string{}, Differ: []UserDiff{}}
@@ -386,7 +386,7 @@ func minus(a, b []string) []string {
return out
}
// Placed says whether placement still names a user's machine and module: a machine's users (`node.<n>`,
// Placed says whether assignments still names a user's machine and module: a machine's users (`node.<n>`,
// `provisioner.<n>`) while the machine is seen, a module's or process's (`<n>.<module>[.<process>]`) while the module is
// assigned there. The controller, enrolment users and persons are never judged gone from here.
func Placed(in Inputs) func(user string) bool {
@@ -447,7 +447,7 @@ func (c Comparison) Excusing(f func(user, subject string, added, subscribe bool)
var seatSubject = regexp.MustCompile(`^mesh\.seat\.([^.]+)\.(?:call|tool)\.([^.]+)(?:\.[^.]+)?$`)
var seatService = regexp.MustCompile(`^\$SRV\.(?:INFO|PING|STATS)\.([^.]+)(?:\.>)?$`)
// ExcusedBy is the excuse of a comparison until placement lives in the catalogue (the operator, 2026-10-26): a verb the
// ExcusedBy is the excuse of a comparison until assignments lives in the catalogue (the operator, 2026-10-26): a verb the
// seat catalogue retired on purpose, which the shadow no longer grants, is no difference; and what the shadow grants more
// for a mesh seat that is claimed on several machines (the standing gap seat holdings: it composes the seat as held on each)
// is named and not counted, for a user on a machine that claims the seat and only as a subscription (what it serves), since