The assigner takes an add-node from mesh-enrolment and writes the joining node with no ask

propose takes the enrolment's warrant {node, address, ask, warrant} and refuses an add-node from any caller but
mesh-enrolment, with no warrant or with a warrant naming another node or address (delivery.AddNodeRefusal); the joining node
counts as having a record for this one change, as ADR 0587 §7 changes ADR 0567 §2. The assigner's process then writes
placement.<node> at once, with no node to answer and no ask raised, and says placement-changed carrying the enrolment's
warrant; the hand-act record of that warrant is the enrolment's, made when it was given. The holder of mesh-enrolment calls
propose with the change, the keys and the warrant read from the router's record of the ask. Tests: rows appended to the
seat's verbs test and a case to the whole-seat integration test; none added, so none to pay for (the budget is paid in
774/S60).

(issue 774, 774/S59)

Claude-Session: https://claude.ai/code/session_01NcbgfJ9Gum47zkFKWRaovL
This commit is contained in:
2026-11-02 01:45:12 +01:00
parent 07d26adcd3
commit 447bcf3a6b
11 changed files with 193 additions and 53 deletions
@@ -1133,4 +1133,30 @@ func TestAProposalIsAnsweredApprovedAndWritten(t *testing.T) {
if failed.Verdict == nil || !strings.Contains(strings.Join(failed.Verdict.Fails, "; "), "laptop (offline) did not answer") {
t.Errorf("the silent node's verdict = %+v", failed.Verdict)
}
// 774/S59 (ADR 0587 §7): once the lease is handed away, a joining node is written on the enrolment's warrant with no node to
// answer and no ask raised, and placement-changed carries that warrant.
b.putJSON(t, buscompose.LeaseBucket, buscompose.ControllerLeaseKey, []byte(`{"handed-to":"mesh-grants","handed-by":"operator"}`))
for len(heard) > 0 {
<-heard
}
joined, err := verbs.doPropose(map[string]any{"why": "enrolment of ace", "warrant": `{"node":"ace","address":"10.9.0.2","ask":"enrol-ace-1","warrant":"enrol-ace-1@2026-10-28T20:00:00Z"}`,
"changes": `[{"op":"add-node","node":"ace","decided":{"address":"10.9.0.2"},"keys":{"identity":"aWQ=","sealing":"c2Vh"}}]`}, "novox.mesh-enrolment")
if err != nil {
t.Fatal(err)
}
select {
case e := <-heard:
if e.Proposal.ID != joined.(proposed).Proposal.ID || e.Warrant == nil || e.Warrant.Warrant != "enrol-ace-1@2026-10-28T20:00:00Z" || len(e.Nodes) != 1 || e.Nodes[0].Node != "ace" {
t.Errorf("placement-changed for the joining node = %+v", e)
}
case <-time.After(15 * time.Second):
t.Fatal("no placement-changed heard for the joining node")
}
if ace, err := b.buckets[delivery.PlacementBucket].Get(b.ctx, "ace.node"); err != nil || !strings.Contains(string(ace.Value()), "10.9.0.2") {
t.Errorf("the joining node's key = %v, %v", ace, err)
}
if akey, _ := delivery.PlacementAskKey(joined.(proposed).Proposal.ID); b.hasKey(delivery.AssignerAsksBucket, akey) || b.hasKey(delivery.PlacementProposalsBucket, joined.(proposed).Proposal.ID) {
t.Error("an ask was raised for the joining node, or its proposal stays open")
}
}
@@ -268,6 +268,10 @@ func (p *passer) stepProposal(ctx context.Context, prop delivery.PlacementPropos
p.endProposal(ctx, prop, "adopt and hand-back are gone since the lease write: placement is the mesh's alone")
return
}
if prop.Warrant != nil {
p.addNode(ctx, prop, revs)
return
}
akey, _ := delivery.PlacementAskKey(prop.ID)
again := !prop.Again.IsZero() && (prop.Verdict == nil || prop.Again.After(prop.Verdict.At))
if prop.State == delivery.ProposalAsked {
@@ -544,6 +548,40 @@ func (p *passer) warranted(ctx context.Context, prop delivery.PlacementProposal,
p.followWrite(prop, answer, changes)
}
// addNode writes a joining node on the warrant of the enrolment that issued its token (ADR 0587 §7): the operator's Approve
// was the issue's, so no node is waited on and no ask is raised. The key is created at the revision it was laid over (none: a
// node with a key is no joining node); the enrolment recorded the warrant in the hand-act log when it was given, so this does
// not record it again; placement-changed carries the warrant. A key written meanwhile ends the proposal, saying so.
func (p *passer) addNode(ctx context.Context, prop delivery.PlacementProposal, revs map[string]uint64) {
current, _ := p.seatPlacements()
if moved(prop, revs) {
p.endProposal(ctx, prop, "a key it was laid over was written since: the enrolment proposes the node again")
return
}
laid, findings := delivery.LayChanges(current, prop.Changes)
node := prop.Changes[0].Node
value, ok := laid[node]
if len(findings) > 0 || !ok {
p.endProposal(ctx, prop, "the node cannot be added: "+strings.Join(findings, "; "))
return
}
rev, err := p.writeNode(ctx, node, &value)
if err != nil {
p.endProposal(ctx, prop, "the node's key was not written: "+err.Error())
return
}
c := delivery.ChangeOf(delivery.Placement{Node: node}, value)
c.Before, c.After = prop.Base[node], rev
p.deleteCandidates(ctx, prop.ID)
p.deletePulses(ctx, prop.ID)
_ = p.kvs[delivery.PlacementProposalsBucket].Delete(ctx, prop.ID)
p.w.put(delivery.PlacementProposalsBucket, prop.ID, nil)
w := prop.Warrant
p.sayChanged(prop, delivery.Answer{Option: assign.OptionYes, Warrant: w.Warrant,
Words: fmt.Sprintf("the operator approved the machine %s at %s through the enrolment's ask %s", w.Node, w.Address, w.Ask)},
[]delivery.PlacementChange{c})
}
// followWrite follows the keys a warrant wrote: the warrant recorded and placement-changed said. No verb of the
// controller is called: the placement is the one source of where modules run (ADR 0567 §8, issue 774).
func (p *passer) followWrite(prop delivery.PlacementProposal, answer delivery.Answer, changes []delivery.PlacementChange) {
@@ -217,9 +217,16 @@ func (s *seatTools) doPropose(args map[string]any, by string) (any, error) {
if err != nil {
return nil, err
}
warrant, err := warrantOf(args["warrant"])
if err != nil {
return nil, err
}
now := s.now()
p, laid, findings := assign.Propose(assign.ProposeIn{ID: delivery.NewProposalID(now), By: by, Why: arg(args, "why"), At: now,
Changes: changes, Current: current, Revisions: revs, Held: held, Recorded: recorded})
Changes: changes, Current: current, Revisions: revs, Held: held, Recorded: recorded, Warrant: warrant})
if refusal := delivery.AddNodeRefusal(p, moduleOfCaller(by)); refusal != "" {
findings = append(findings, refusal)
}
if len(findings) > 0 {
return nil, fmt.Errorf("the proposal is refused: %s", strings.Join(findings, "; "))
}
@@ -229,6 +236,40 @@ func (s *seatTools) doPropose(args map[string]any, by string) (any, error) {
return proposed{Proposal: p, Follow: followProposal(p.ID)}, nil
}
// warrantOf reads propose's warrant, an enrolment's for an add-node: an object, given as one or as its text; none is none.
func warrantOf(v any) (*delivery.AddNodeWarrant, error) {
var raw []byte
switch w := v.(type) {
case nil:
return nil, nil
case string:
if strings.TrimSpace(w) == "" {
return nil, nil
}
raw = []byte(w)
default:
var err error
if raw, err = json.Marshal(w); err != nil {
return nil, err
}
}
var out delivery.AddNodeWarrant
dec := json.NewDecoder(strings.NewReader(string(raw)))
dec.DisallowUnknownFields()
if err := dec.Decode(&out); err != nil {
return nil, fmt.Errorf("the warrant is no enrolment's warrant {node, address, ask, warrant}: %w", err)
}
return &out, nil
}
// moduleOfCaller is the module a caller is, as the runtime proves it: a module's bus user is `<node>.<module>`.
func moduleOfCaller(by string) string {
if _, module, ok := strings.Cut(by, "."); ok {
return module
}
return by
}
// doWithdraw is the verb withdraw (§2): the proposal is marked, and the assigner deletes it and its candidates and says
// placement-withdrawn carrying it whole.
func (s *seatTools) doWithdraw(args map[string]any, by string) (any, error) {
@@ -150,6 +150,27 @@ func TestTheSeatsVerbs(t *testing.T) {
if _, err := s.doPropose(map[string]any{"why": "hold", "changes": `[{"op":"hold","node":"anchor","module":"web","claim":"node-shell","scope":"node"}]`}, "op"); err != nil {
t.Errorf("a hold: %v", err)
}
// 774/S59/AC1 (ADR 0587 §7): an add-node from mesh-enrolment whose warrant names this node and this address is proposed, with
// the node a joining one with no record; from another caller, naming another node or address, or with no warrant it is refused.
addNode := func(by, warrant string) error {
_, err := s.doPropose(map[string]any{"why": "enrolment of ace", "warrant": warrant, "changes": []any{map[string]any{"op": "add-node", "node": "ace",
"decided": map[string]any{"address": "10.9.0.2"}, "keys": map[string]any{"identity": "aWQ=", "sealing": "c2Vh"}}}}, by)
return err
}
good := `{"node":"ace","address":"10.9.0.2","ask":"enrol-ace-1","warrant":"enrol-ace-1@2026-10-28T20:00:00Z"}`
for _, c := range []struct{ by, warrant, want string }{
{"novox.mesh-grants", good, "mesh-enrolment's alone"},
{"novox.mesh-enrolment", `{"node":"g14","address":"10.9.0.2","ask":"a","warrant":"a@t"}`, "carries the warrant of g14"},
{"novox.mesh-enrolment", `{"node":"ace","address":"10.9.0.9","ask":"a","warrant":"a@t"}`, "carries the warrant of ace at 10.9.0.9"},
{"novox.mesh-enrolment", "", "carries no warrant"},
} {
if err := addNode(c.by, c.warrant); err == nil || !strings.Contains(err.Error(), c.want) {
t.Errorf("an add-node by %s with warrant %q: %v, want a refusal saying %q", c.by, c.warrant, err, c.want)
}
}
if err := addNode("novox.mesh-enrolment", good); err != nil {
t.Errorf("an add-node from mesh-enrolment: %v", err)
}
if _, err := s.doHandBack(map[string]any{"why": "back"}, "op"); err == nil || !strings.Contains(err.Error(), "since the lease write") {
t.Errorf("a hand-back: %v", err)
}
@@ -24,6 +24,8 @@ type ProposeIn struct {
Revisions map[string]uint64
Held func(module string) bool
Recorded func(node string) bool
// Warrant is the enrolment's warrant an add-node carries (ADR 0587 §7); only that change has one.
Warrant *delivery.AddNodeWarrant
}
// Propose lays a proposal over the current keys (§2): the proposal (state answering, each changed node's base revision),
@@ -33,7 +35,7 @@ type ProposeIn struct {
// record). A key whose value the proposal leaves as it is changes nothing and is not laid.
func Propose(in ProposeIn) (delivery.PlacementProposal, map[string]*delivery.Placement, []string) {
p := delivery.PlacementProposal{ID: in.ID, By: strings.TrimSpace(in.By), Why: strings.TrimSpace(in.Why), At: in.At.UTC(),
Changes: in.Changes, Base: map[string]uint64{}, State: delivery.ProposalAnswering}
Changes: in.Changes, Warrant: in.Warrant, Base: map[string]uint64{}, State: delivery.ProposalAnswering}
var findings []string
if p.By == "" {
findings = append(findings, "the runtime names no caller: a proposal is made by someone")
@@ -75,6 +77,18 @@ func Propose(in ProposeIn) (delivery.PlacementProposal, map[string]*delivery.Pla
if recorded == nil {
recorded = func(string) bool { return false }
}
// A joining node writes its record only once it holds its account, which the add-node's write gives it: this one change by
// this one caller is the exception to a node with no record (ADR 0587 §7, changing ADR 0567 §2).
joining := map[string]bool{}
for _, c := range in.Changes {
if c.Op == delivery.OpAddNode {
joining[c.Node] = true
}
}
if len(joining) > 0 {
was := recorded
recorded = func(node string) bool { return joining[node] || was(node) }
}
for _, r := range delivery.CheckPlacements(all, held, recorded) {
findings = append(findings, r)
}
@@ -55,7 +55,9 @@ type Holder struct {
// Warrant and Act record in the hand-act log, through the seat mesh-hand-acts: the operator's warrant for an ask, and an
// act done by hand (a revoke).
Warrant func(ask string) error
Act func(what, why, cause string) error
// WarrantOf is the warrant's id as the teller wrote it (the ask's id and when), read from the router's record of the ask.
WarrantOf func(ask string) (string, error)
Act func(what, why, cause string) error
// Loaded says whether the bus node loaded the bus user (ADR 0587 §3: redeem answers once node.<node> is in its list).
Loaded func(user string) (bool, error)
Now func() time.Time
@@ -91,32 +93,13 @@ func (h *Holder) say(event string, body any, id string) error {
return h.Say(event, body, id)
}
// AddNode is what the holder asks mesh-placement to add (ADR 0587 §7): the node, its address, the public halves of its
// keys, and the issue's warrant, which names this node and this address.
// AddNode is what the holder asks mesh-placement to add (ADR 0587 §7): the proposal's one change, a joining node with the
// address the operator approved and the public halves of its keys, and the issue's warrant, which names this node and this
// address (delivery.AddNodeWarrant).
type AddNode struct {
Node string `json:"node"`
Address string `json:"address"`
Why string `json:"why"`
Keys NodeKeys `json:"keys"`
Warrant Warrant `json:"warrant"`
}
// NodeKeys are the public halves of a joining node's keys: identity, private network, sealing and serving, and the
// tunnel it found, as delivery.Redeem presents them.
type NodeKeys struct {
Public string `json:"public_key"`
Overlay string `json:"overlay_key,omitempty"`
Sealing string `json:"sealing_key,omitempty"`
Serving string `json:"serving_key,omitempty"`
Tunnel json.RawMessage `json:"tunnel,omitempty"`
}
// Warrant is the issue's warrant as the proposal carries it: the ask the operator answered, the node and the address it
// named.
type Warrant struct {
Ask string `json:"ask"`
Node string `json:"node"`
Address string `json:"address"`
Why string
Change delivery.PlacementProposalChange
Warrant delivery.AddNodeWarrant
}
// issuing says whether issue is on: only while the controller's lease is handed to a holder that is no controller (ADR 0587
@@ -211,15 +211,17 @@ func newRig(t *testing.T) *rig {
AddNode: func(a AddNode) error {
r.added = append(r.added, a)
if r.placeErr == nil { // placement writes the node as the assigner does on the warrant
r.rd["mesh-delivery-assigner.placement"][a.Node+".node"] = `{"v":1,"node":"` + a.Node + `","decided":{"address":"` + a.Address + `"},"modules":[]}`
n := a.Change.Node
r.rd["mesh-delivery-assigner.placement"][n+".node"] = `{"v":1,"node":"` + n + `","decided":{"address":"` + a.Change.Decided.Address + `"},"modules":[]}`
}
return r.placeErr
},
Warrant: func(ask string) error { r.warr = append(r.warr, ask); return nil },
Act: func(what, why, cause string) error { r.acts = append(r.acts, what); return nil },
Loaded: func(string) (bool, error) { return r.loaded, nil },
Now: func() time.Time { return r.now },
Logf: func(f string, a ...any) { t.Logf(f, a...) }}
Warrant: func(ask string) error { r.warr = append(r.warr, ask); return nil },
WarrantOf: func(ask string) (string, error) { return ask + "@2026-11-02T10:00:00Z", nil },
Act: func(what, why, cause string) error { r.acts = append(r.acts, what); return nil },
Loaded: func(string) (bool, error) { return r.loaded, nil },
Now: func() time.Time { return r.now },
Logf: func(f string, a ...any) { t.Logf(f, a...) }}
return r
}
@@ -92,13 +92,15 @@ func newHolder() *Holder {
call := Call(stdio.Ask)
rec := handacts.Recorder{Ask: stdio.Ask}
rd := runtimeReader{}
client := asker.New("asked")
return &Holder{
Store: runtimeStore{}, Read: rd, Vault: vaultOver{call: call, read: rd}, Asker: asker.New("asked"),
Store: runtimeStore{}, Read: rd, Vault: vaultOver{call: call, read: rd}, Asker: client,
Say: func(event string, body any, id string) error {
return stdio.SeatPublish(SeatEventSubject(event), body, id)
},
AddNode: addNodeOver(call),
Warrant: func(ask string) error { _, err := rec.RecordWarrant(Module, ask); return err },
AddNode: addNodeOver(call),
WarrantOf: warrantOf(client),
Warrant: func(ask string) error { _, err := rec.RecordWarrant(Module, ask); return err },
Act: func(what, why, cause string) error {
_, err := rec.Record(handacts.Request{What: what, Why: why, Cause: cause})
return err
@@ -110,6 +112,20 @@ func newHolder() *Holder {
}
}
// warrantOf is the warrant id the teller writes, `<ask>@<when>`, read from the router's record of the ask.
func warrantOf(c *asker.Client) func(string) (string, error) {
return func(ask string) (string, error) {
state, w, err := c.Where(ask)
if err != nil {
return "", err
}
if w == nil {
return "", fmt.Errorf("the router records the ask %s as %q, with no warrant", ask, state)
}
return ask + "@" + w.At.UTC().Format(time.RFC3339Nano), nil
}
}
// loadedVia says a bus user is loaded when the grants part's last pass lists it among the composed users and the bus node's
// loaded list holds it (its credentials row says loaded).
func loadedVia(rd Reader) func(string) (bool, error) {
@@ -10,11 +10,6 @@ import (
"git.novox.be/novox/mesh-sdk/go/delivery"
)
// keysOf are the public halves a redeem presented.
func keysOf(a delivery.Redeem) NodeKeys {
return NodeKeys{Public: a.PublicKey, Overlay: a.OverlayKey, Sealing: a.SealingKey, Serving: a.ServingKey, Tunnel: a.Tunnel}
}
// Redeem is the joining node-engine's one call (ADR 0587 §5, §6). It answers only a caller the runtime proves to be
// enrol.<node>, for a key at stage live (or at redeeming with the same keys' digest), checks the proof against the identity key
// presented, and then, each step read back and none repeated that is done: spends the token (the key to redeeming, by
@@ -50,7 +45,7 @@ func (h *Holder) Redeem(caller string, a delivery.Redeem) (delivery.RedeemAnswer
if err != nil {
return delivery.RedeemAnswer{}, fmt.Errorf("the vault did not sign the certificate of %s (call redeem again with the same keys): %w", e.Node, err)
}
if err := h.place(e, keysOf(a)); err != nil {
if err := h.place(e, a); err != nil {
return delivery.RedeemAnswer{}, err
}
if h.Loaded != nil {
@@ -122,7 +117,7 @@ func (h *Holder) spend(node, digest string) (Enrolment, error) {
}
// place writes the node into placement with the issue's warrant, unless placement holds it at the approved address already.
func (h *Holder) place(e Enrolment, keys NodeKeys) error {
func (h *Holder) place(e Enrolment, a delivery.Redeem) error {
placements, err := h.placements()
if err != nil {
return fmt.Errorf("placement cannot be read (call redeem again with the same keys): %w", err)
@@ -130,11 +125,17 @@ func (h *Holder) place(e Enrolment, keys NodeKeys) error {
if p, ok := placements[e.Node]; ok && p.Decided.Address == e.Address {
return nil
}
if h.AddNode == nil {
if h.AddNode == nil || h.WarrantOf == nil {
return errors.New("no route to mesh-placement")
}
if err := h.AddNode(AddNode{Node: e.Node, Address: e.Address, Why: "enrolment of " + e.Node + ": " + e.Why, Keys: keys,
Warrant: Warrant{Ask: e.Ask, Node: e.Node, Address: e.Address}}); err != nil {
warrant, err := h.WarrantOf(e.Ask)
if err != nil {
return fmt.Errorf("the operator's warrant for %s cannot be read from the router (call redeem again with the same keys): %w", e.Ask, err)
}
change := delivery.PlacementProposalChange{Op: delivery.OpAddNode, Node: e.Node, Decided: &delivery.NodeDecided{Address: e.Address},
Keys: &delivery.JoiningKeys{Identity: a.PublicKey, Overlay: a.OverlayKey, Sealing: a.SealingKey, Serving: a.ServingKey}}
if err := h.AddNode(AddNode{Why: "enrolment of " + e.Node + ": " + e.Why, Change: change,
Warrant: delivery.AddNodeWarrant{Node: e.Node, Address: e.Address, Ask: e.Ask, Warrant: warrant}}); err != nil {
return fmt.Errorf("%w (call redeem again with the same keys)", err)
}
return nil
@@ -77,7 +77,8 @@ func TestRedeemRefusesWhatItMustAndSpendsTheTokenOnce(t *testing.T) {
if e.Stage != StageRedeeming || e.KeysDigest != good.KeysDigest() || r.st.ttl[NodeKey("ace")] != 0 || r.st.ttl[AddressKey("10.9.0.2")] != 0 {
t.Errorf("spent %+v, ttl %v/%v", e, r.st.ttl[NodeKey("ace")], r.st.ttl[AddressKey("10.9.0.2")])
}
if len(r.added) != 1 || r.added[0].Warrant != (Warrant{Ask: "ask-ace", Node: "ace", Address: "10.9.0.2"}) || ans.Signature == "" || ans.Account != "account-ace" || ans.Login != "node.ace" || ans.Signer != "vault-signer" || ans.Certificate != "PEM-serving-a" {
if len(r.added) != 1 || r.added[0].Warrant != (delivery.AddNodeWarrant{Ask: "ask-ace", Node: "ace", Address: "10.9.0.2", Warrant: "ask-ace@2026-11-02T10:00:00Z"}) ||
r.added[0].Change.Keys.Digest() != good.KeysDigest() || r.added[0].Change.Decided.Address != "10.9.0.2" || ans.Signature == "" || ans.Account != "account-ace" || ans.Login != "node.ace" || ans.Signer != "vault-signer" || ans.Certificate != "PEM-serving-a" {
t.Errorf("placement %+v, answer %+v", r.added, ans)
}
r.loaded = false
@@ -95,12 +95,9 @@ func (v vaultOver) Signer() (string, error) {
// addNodeOver asks mesh-placement to add the node with the issue's warrant (ADR 0587 §7).
func addNodeOver(call Call) func(AddNode) error {
return func(a AddNode) error {
_, err := call(placementAdd, map[string]any{
"why": a.Why,
"changes": []any{map[string]any{"op": "add-node", "node": a.Node, "address": a.Address, "keys": a.Keys, "warrant": a.Warrant}},
})
_, err := call(placementAdd, map[string]any{"why": a.Why, "changes": []any{a.Change}, "warrant": a.Warrant})
if err != nil {
return fmt.Errorf("mesh-placement did not take the node %s: %w", a.Node, err)
return fmt.Errorf("mesh-placement did not take the node %s: %w", a.Change.Node, err)
}
return nil
}