Let placement take an add-node with its enrolment's warrant, so one Approve lets a machine join

ADR 0587 §7: at redeem mesh-enrolment proposes one change, the node with its decided fields (the approved address,
never the hub) and the public halves of its keys, carrying the issue's warrant; mesh-placement checks the warrant
names this node and address and that the caller is mesh-enrolment, and writes the key with no ask of its own
(changing ADR 0567 §2 and §5 for this one change by this one caller). The proposal codec refuses an add-node beside
other changes, without a warrant, or whose warrant names another node or address, and a warrant on any other
proposal; AddNodeRefusal adds the caller's check for the assigner; LayChanges adds the node and refuses one placed
already. Tested as rows of TestPlacement's proposal subtest, no new test function.

(issue 774, 774/S42)

Claude-Session: https://claude.ai/code/session_01NcbgfJ9Gum47zkFKWRaovL
This commit is contained in:
2026-11-02 01:41:03 +01:00
parent 362dddae40
commit 90a5fc5797
3 changed files with 151 additions and 3 deletions
+9 -2
View File
@@ -31,7 +31,8 @@ func placementVerbs() []Verb {
"place (node, module, ports, bindings), take-off (node, module), pin (node, provision, provider {node, module}), " +
"unpin (node, provision), hold (node, module, claim, scope, kind, site), unhold (node, module, claim), decide " +
"(node, field, value: a decided field of the node, its address among them), config (node, module, mesh-wide, " +
"layer, mesh-wide-values, values)",
"layer, mesh-wide-values, values), add-node (node, decided, keys: a joining node, mesh-enrolment's alone, the " +
"proposal's one change, with the enrolment's warrant; ADR 0587 §7)",
"items": map[string]any{"type": "object", "required": []string{"op", "node"}}}
return []Verb{
{Name: PlacementVerbPlacement, Optional: true, Description: "Read placement: one node's key, the nodes one " +
@@ -52,11 +53,17 @@ func placementVerbs() []Verb {
"on one host port, a provider not placed, a seat held once held twice, two nodes with one address, two " +
"hubs, a module the indexer does not hold, a node with no record). Otherwise the nodes it touches compose " +
"it in shadow and answer, and on a passing verdict the operator is asked to approve it. Who proposes is the " +
"caller, as the runtime proves it.",
"caller, as the runtime proves it. An add-node from mesh-enrolment, whose warrant names its node and address, " +
"is written with no ask of its own: its Approve is the enrolment's (ADR 0587 §7).",
Input: map[string]any{"type": "object", "additionalProperties": false, "required": []string{"why", "changes"},
"properties": map[string]any{
"why": map[string]any{"type": "string", "description": "why, shown to the operator with the proposal"},
"changes": changes,
// ADR 0587 §7: an add-node carries the warrant of the enrolment that issued the node's token, whose
// Approve is the proposal's; the holder writes it with no ask of its own.
"warrant": map[string]any{"type": "object", "description": "for an add-node alone: the enrolment's " +
"warrant {node, address, ask, warrant}, naming the change's node and address",
"required": []string{"node", "address", "ask", "warrant"}},
}}},
{Name: PlacementVerbWithdraw, Optional: true, Description: "Withdraw an open proposal: its key and its " +
"candidates are deleted and placement-withdrawn carries it whole. With why.",
+93 -1
View File
@@ -65,6 +65,9 @@ const (
OpDecide = "decide" // set a decided field of the node (NodeDecided by its JSON name), its address among them
OpConfig = "config" // set a module's config placement or values on the node
OpSeed = "seed" // the node's whole placement: the take-over (adopt, §7), never given by a caller
// OpAddNode adds a joining node with its decided fields and the public halves of its keys, on the warrant of the
// enrolment that issued its token (novox/hq ADR 0587 §7): mesh-enrolment's alone, the proposal's one change.
OpAddNode = "add-node"
)
// The states of an open proposal.
@@ -107,6 +110,32 @@ type PlacementProposalChange struct {
Values map[string]any `json:"values,omitempty"`
// Whole is the node's whole placement (OpSeed).
Whole *Placement `json:"whole,omitempty"`
// Decided and Keys are a joining node's (OpAddNode): its decided fields, the address the operator approved among
// them, and the public halves of the keys it presented at redeem.
Decided *NodeDecided `json:"decided,omitempty"`
Keys *JoiningKeys `json:"keys,omitempty"`
}
// JoiningKeys are the public halves of the keys a joining node presented at redeem (ADR 0587 §6, §7): its identity,
// private network, sealing and serving keys, as Redeem carries them. Public halves alone, never a private key.
type JoiningKeys struct {
Identity string `json:"identity"`
Overlay string `json:"overlay,omitempty"`
Sealing string `json:"sealing"`
Serving string `json:"serving,omitempty"`
}
// Digest is the keys' digest, as the enrolment's key holds it (KeysDigest).
func (k JoiningKeys) Digest() string { return KeysDigest(k.Identity, k.Overlay, k.Sealing, k.Serving) }
// AddNodeWarrant is the warrant an add-node carries (ADR 0587 §7): the enrolment's node and address as its issue's ask
// was bound to them, the ask's id, and the warrant's id as the teller wrote it. mesh-placement checks it names the
// change's node and address, and that the caller is mesh-enrolment as the runtime proves it (AddNodeRefusal).
type AddNodeWarrant struct {
Node string `json:"node"`
Address string `json:"address"`
Ask string `json:"ask"`
Warrant string `json:"warrant"`
}
func (c PlacementProposalChange) check() error {
@@ -135,8 +164,19 @@ func (c PlacementProposalChange) check() error {
return fmt.Errorf("delivery: the seed of %s names no placement of that node", c.Node)
}
return c.Whole.check()
case OpAddNode:
if c.Decided == nil || c.Decided.Address == "" || c.Keys == nil || c.Keys.Identity == "" || c.Keys.Sealing == "" {
return fmt.Errorf("delivery: the add-node of %s names no address or no identity and sealing key", c.Node)
}
if c.Decided.Hub {
return fmt.Errorf("delivery: the add-node of %s makes it the hub, which a joining node never is", c.Node)
}
if c.Module != "" || len(c.Ports)+len(c.Bindings) > 0 || c.Whole != nil {
return fmt.Errorf("delivery: the add-node of %s places a module; a module is placed by a proposal of its own", c.Node)
}
return nil
}
return fmt.Errorf("delivery: %q is no change of placement: place, take-off, pin, unpin, hold, unhold, decide or config", c.Op)
return fmt.Errorf("delivery: %q is no change of placement: place, take-off, pin, unpin, hold, unhold, decide, config or add-node", c.Op)
}
// LayChanges is the placements of the nodes the changes name with the changes laid over current, in the order given;
@@ -175,6 +215,13 @@ func LayChanges(current map[string]Placement, changes []PlacementProposalChange)
switch c.Op {
case OpSeed:
p = clonePlacement(*c.Whole)
case OpAddNode:
// A joining node has no key yet (ADR 0587 §7): one that has is no joining node.
if _, has := current[c.Node]; has {
findings = append(findings, fmt.Sprintf("change %d: %s is placed already, so it does not join", i+1, c.Node))
continue
}
p = Placement{V: Version, Node: c.Node, Decided: *c.Decided, Modules: []PlacedModule{}}
case OpPlace:
m := PlacedModule{Module: c.Module, Ports: c.Ports, Bindings: c.Bindings}
if at >= 0 {
@@ -378,6 +425,9 @@ type PlacementProposal struct {
// Again is when `again` asked the holder to judge it now and raise its ask again (§10); the holder does once per
// value.
Again time.Time `json:"again,omitempty"`
// Warrant is the enrolment's warrant an add-node carries (ADR 0587 §7): its Approve is the issue's, so the holder
// writes it with no ask of its own. Only an add-node carries one.
Warrant *AddNodeWarrant `json:"warrant,omitempty"`
// Withdrawn is who withdrew it and why (`withdraw`, §2): the holder then deletes it and its candidates and says
// placement-withdrawn, which only the holder's process can say on its stream.
Withdrawn string `json:"withdrawn,omitempty"`
@@ -419,9 +469,51 @@ func (p PlacementProposal) check() error {
if p.Seed && p.ExportDigest == "" {
return fmt.Errorf("delivery: the seed %s names no export digest", p.ID)
}
return p.checkAddNode()
}
// checkAddNode refuses an add-node that is not the proposal's one change, carries no warrant, or whose warrant names
// another node or address than the change; and a warrant on any other proposal (ADR 0587 §7).
func (p PlacementProposal) checkAddNode() error {
adds := 0
for _, c := range p.Changes {
if c.Op == OpAddNode {
adds++
}
}
if adds == 0 {
if p.Warrant != nil {
return fmt.Errorf("delivery: the proposal %s carries an enrolment's warrant and adds no node", p.ID)
}
return nil
}
if adds != 1 || len(p.Changes) != 1 || p.HandBack || p.Seed {
return fmt.Errorf("delivery: the proposal %s adds a node beside other changes; an add-node is a proposal's one change", p.ID)
}
c, w := p.Changes[0], p.Warrant
if w == nil || w.Ask == "" || w.Warrant == "" {
return fmt.Errorf("delivery: the add-node of %s carries no warrant of its enrolment", c.Node)
}
if w.Node != c.Node || w.Address != c.Decided.Address {
return fmt.Errorf("delivery: the add-node of %s at %s carries the warrant of %s at %s", c.Node, c.Decided.Address,
w.Node, w.Address)
}
return nil
}
// AddNodeRefusal is why the holder of mesh-placement refuses an add-node from caller, the module the runtime proved
// called (ADR 0587 §7), or "": only mesh-enrolment adds a node; what the proposal itself must hold, DecodePlacementProposal
// refuses.
func AddNodeRefusal(p PlacementProposal, caller string) string {
if err := p.checkAddNode(); err != nil {
return err.Error()
}
if p.Warrant != nil && caller != EnrolmentModule {
return fmt.Sprintf("an add-node is mesh-enrolment's alone, and %s proposed it", caller)
}
return ""
}
// EncodePlacementProposal is p as JSON at Version; it refuses what DecodePlacementProposal refuses.
func EncodePlacementProposal(p PlacementProposal) ([]byte, error) {
if err := p.check(); err != nil {
+49
View File
@@ -689,6 +689,55 @@ func TestPlacement(t *testing.T) {
if ChangesDigest(p) == ChangesDigest(seed) {
t.Error("two proposals' changes digest alike")
}
// 774/S42/AC1 (ADR 0587 §7): an add-node carries its enrolment's warrant naming its node and address, as its one
// change, from mesh-enrolment alone; a warrant naming another node or address is refused.
joining := PlacementProposalChange{Op: OpAddNode, Node: "lab", Decided: &NodeDecided{Address: "10.0.0.7"},
Keys: &JoiningKeys{Identity: "id", Sealing: "seal"}}
add := PlacementProposal{ID: id, By: "ace.mesh-enrolment", Why: "lab joins", At: time.Unix(1, 0).UTC(),
Changes: []PlacementProposalChange{joining}, Base: map[string]uint64{"lab": 0}, State: ProposalAnswering,
Warrant: &AddNodeWarrant{Node: "lab", Address: "10.0.0.7", Ask: "a-1", Warrant: "w-1"}}
if raw, err := EncodePlacementProposal(add); err != nil {
t.Errorf("an add-node: %v", err)
} else if back, err := DecodePlacementProposal(raw); err != nil || back.Warrant == nil || back.Changes[0].Keys.Digest() != KeysDigest("id", "", "seal", "") {
t.Errorf("an add-node round trip: %+v, %v", back, err)
}
if AddNodeRefusal(add, EnrolmentModule) != "" || !strings.Contains(AddNodeRefusal(add, AssignerModule), "mesh-enrolment's alone") {
t.Errorf("add-node by caller: %q, %q", AddNodeRefusal(add, EnrolmentModule), AddNodeRefusal(add, AssignerModule))
}
if laid, f := LayChanges(current, add.Changes); len(f) != 0 || laid["lab"].Decided.Address != "10.0.0.7" || len(laid["lab"].Modules) != 0 {
t.Errorf("an add-node laid: %+v, %v", laid, f)
}
if _, f := LayChanges(current, []PlacementProposalChange{{Op: OpAddNode, Node: "g14", Decided: &NodeDecided{Address: "10.0.0.9"},
Keys: &JoiningKeys{Identity: "id", Sealing: "seal"}}}); len(f) != 1 || !strings.Contains(f[0], "placed already") {
t.Errorf("an add-node of a placed node: %v", f)
}
for name, mut := range map[string]func(*PlacementProposal){
"warrant of another node": func(p *PlacementProposal) {
p.Warrant = &AddNodeWarrant{Node: "pi", Address: "10.0.0.7", Ask: "a", Warrant: "w"}
},
"warrant of another address": func(p *PlacementProposal) {
p.Warrant = &AddNodeWarrant{Node: "lab", Address: "10.0.0.8", Ask: "a", Warrant: "w"}
},
"no warrant": func(p *PlacementProposal) { p.Warrant = nil },
"beside another change": func(p *PlacementProposal) {
p.Changes = append(p.Changes, PlacementProposalChange{Op: OpPlace, Node: "lab", Module: "zsh"})
},
"the hub": func(p *PlacementProposal) {
c := joining
c.Decided = &NodeDecided{Address: "10.0.0.7", Hub: true}
p.Changes = []PlacementProposalChange{c}
},
"a warrant on a move": func(p *PlacementProposal) {
p.Changes, p.Base = []PlacementProposalChange{{Op: OpTakeOff, Node: "g14", Module: "zsh"}}, map[string]uint64{"g14": 4}
},
} {
q := add
q.Changes = append([]PlacementProposalChange(nil), add.Changes...)
mut(&q)
if _, err := EncodePlacementProposal(q); err == nil {
t.Errorf("an add-node with %s: taken", name)
}
}
for name, mut := range map[string]func(*PlacementProposal){
"no id": func(p *PlacementProposal) { p.ID = "" },
"no proposer": func(p *PlacementProposal) { p.By = "" },