Assigner: the operations of a change and the states of a proposal are the module assigner client's, which the enrolment builds with (B7f finding 4; ADR 0634 §4; issue 1966)

Claude-Session: https://claude.ai/code/session_01NcbgfFSnn24M3GcqttoU7i
This commit is contained in:
2026-11-05 15:44:06 +01:00
parent cd94b19384
commit 891d292dfd
7 changed files with 90 additions and 121 deletions
@@ -1233,7 +1233,7 @@ func TestAProposalIsAnsweredApprovedAndWritten(t *testing.T) {
t.Fatalf("must answer = %v, want both nodes", p.Must)
}
answerAll(p)
p = proposalNow(p.ID, "asked", func(p mma.Proposal) bool { return p.State == plc.ProposalAsked })
p = proposalNow(p.ID, "asked", func(p mma.Proposal) bool { return p.State == mmac.ProposalAsked })
akey, _ := delivery.PlacementAskKey(p.ID)
b.answer(t, akey, assign.OptionYes)
select {
@@ -1277,7 +1277,7 @@ func TestAProposalIsAnsweredApprovedAndWritten(t *testing.T) {
return strings.Contains(p.Words, "laid again") && len(p.Must) > 0
})
answerAll(relaid)
proposalNow(moved.ID, "asked", func(p mma.Proposal) bool { return p.State == plc.ProposalAsked })
proposalNow(moved.ID, "asked", func(p mma.Proposal) bool { return p.State == mmac.ProposalAsked })
akey, _ = delivery.PlacementAskKey(moved.ID)
b.answer(t, akey, assign.OptionNo)
for deadline := time.Now().Add(15 * time.Second); b.hasKey(plc.ProposalsBucket, moved.ID); time.Sleep(50 * time.Millisecond) {
@@ -1297,7 +1297,7 @@ func TestAProposalIsAnsweredApprovedAndWritten(t *testing.T) {
t.Fatal(err)
}
silent := q.(proposed).Proposal
decided := proposalNow(silent.ID, "decided without its silent nodes", func(p mma.Proposal) bool { return p.State == plc.ProposalAsked })
decided := proposalNow(silent.ID, "decided without its silent nodes", func(p mma.Proposal) bool { return p.State == mmac.ProposalAsked })
if decided.Verdict == nil || decided.Verdict.State != delivery.VerdictPass ||
!strings.Contains(strings.Join(decided.Verdict.Notes, "; "), "no answer from") || !strings.Contains(strings.Join(decided.Verdict.Notes, "; "), "laptop (offline)") {
t.Errorf("the silent node's verdict = %+v", decided.Verdict)
@@ -258,13 +258,13 @@ func (p *passer) ask(ctx context.Context, prop mma.Proposal, akey, brief string)
if pk, err := plc.PulseAskKey(prop.ID); err == nil {
p.pulse(ctx, pk, assign.ExpiresAt(placed).Sub(p.clock()))
}
prop.State, prop.Ask = plc.ProposalAsked, placed.ID
prop.State, prop.Ask = mmac.ProposalAsked, placed.ID
return prop, true
}
// stepProposal takes one proposal one step further.
func (p *passer) stepProposal(ctx context.Context, prop mma.Proposal, rev uint64, current map[string]mma.Placement, revs map[string]uint64) {
if !p.saidProposed[prop.ID] && prop.State == plc.ProposalAnswering && prop.Verdict == nil {
if !p.saidProposed[prop.ID] && prop.State == mmac.ProposalAnswering && prop.Verdict == nil {
p.sayEvent(mma.EventPlacementProposed, mma.PlacementEvent{Proposal: prop}, prop.ID)
p.saidProposed[prop.ID] = true
}
@@ -283,7 +283,7 @@ func (p *passer) stepProposal(ctx context.Context, prop mma.Proposal, rev uint64
}
akey, _ := delivery.PlacementAskKey(prop.ID)
again := prop.Again != nil && !prop.Again.IsZero() && (prop.Verdict == nil || prop.Again.After(prop.Verdict.At))
if prop.State == plc.ProposalAsked {
if prop.State == mmac.ProposalAsked {
askPulse, _ := plc.PulseAskKey(prop.ID)
switch {
case p.answeredWith(akey, assign.OptionYes):
@@ -327,11 +327,11 @@ func (p *passer) stepProposal(ctx context.Context, prop mma.Proposal, rev uint64
prop.Verdict = &v
switch v.State {
case delivery.VerdictFail:
prop.State, prop.Ask = plc.ProposalFailed, ""
prop.State, prop.Ask = mmac.ProposalFailed, ""
case plc.VerdictWaits:
prop.State, prop.Ask = plc.ProposalAnswering, ""
prop.State, prop.Ask = mmac.ProposalAnswering, ""
case delivery.VerdictPass:
if prop.State != plc.ProposalAsked || again {
if prop.State != mmac.ProposalAsked || again {
var ok bool
if prop, ok = p.ask(ctx, prop, akey, judge.Brief(prop, v)); !ok {
return
@@ -535,7 +535,7 @@ func (p *passer) addNode(ctx context.Context, prop mma.Proposal, revs map[string
laid, findings := plc.LayChanges(current, prop.Changes)
findings = append(findings, assign.ControllerRefusals(prop.Changes, laid)...)
node := prop.Changes[0].Node
if prop.Changes[0].Op == plc.OpRemoveNode {
if prop.Changes[0].Op == mmac.OpRemoveNode {
p.removeNode(ctx, prop, current, findings)
return
}
@@ -209,7 +209,7 @@ func (s *seatTools) doPropose(args map[string]any, by string) (any, error) {
return nil, err
}
for _, c := range changes {
if c.Op == plc.OpSeed {
if c.Op == mmac.OpSeed {
return nil, errors.New("the seed is gone with adopt: propose takes place, take-off, pin, unpin, hold, unhold, decide and config")
}
}
@@ -42,7 +42,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) (mma.Proposal, map[string]*mma.Placement, []string) {
p := mma.Proposal{ID: in.ID, By: strings.TrimSpace(in.By), Why: strings.TrimSpace(in.Why), At: in.At.UTC(),
Changes: in.Changes, Warrant: in.Warrant, Base: map[string]int64{}, State: plc.ProposalAnswering}
Changes: in.Changes, Warrant: in.Warrant, Base: map[string]int64{}, State: mmac.ProposalAnswering}
var findings []string
if p.By == "" {
findings = append(findings, "the runtime names no caller: a proposal is made by someone")
@@ -54,7 +54,7 @@ func Propose(in ProposeIn) (mma.Proposal, map[string]*mma.Placement, []string) {
findings = append(findings, "a proposal changes something: it names no change")
}
for i, c := range in.Changes {
if c.Op == plc.OpSeed {
if c.Op == mmac.OpSeed {
findings = append(findings, fmt.Sprintf("change %d: the seed is gone with adopt, the controller's export being gone", i+1))
}
}
@@ -99,7 +99,7 @@ func Propose(in ProposeIn) (mma.Proposal, map[string]*mma.Placement, []string) {
// 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 == plc.OpAddNode {
if c.Op == mmac.OpAddNode {
joining[c.Node] = true
}
}
@@ -127,7 +127,7 @@ func ControllerRefusals(changes []mma.ProposalChange, laid map[string]mma.Placem
var out []string
nodes := map[string]bool{}
for i, c := range changes {
places := c.Module == ControllerModule && c.Op != plc.OpTakeOff
places := c.Module == ControllerModule && c.Op != mmac.OpTakeOff
if c.Whole != nil && slices.Contains(mmac.Assigned(*c.Whole), ControllerModule) {
places = true
}
@@ -235,6 +235,6 @@ func Rest(p mma.Proposal, written map[string]*mma.Placement) (mma.Proposal, bool
rest.Base[node] = base
}
}
rest.Must, rest.Verdict, rest.Ask, rest.State = nil, nil, "", plc.ProposalAnswering
rest.Must, rest.Verdict, rest.Ask, rest.State = nil, nil, "", mmac.ProposalAnswering
return rest, len(rest.Changes) > 0
}
@@ -28,9 +28,9 @@ func TestThePlacementSeatDecides(t *testing.T) {
Held: func(m string) bool { return m != "ghost" },
Recorded: func(n string) bool { return n == "anchor" || n == "laptop" }}
}
move := []mma.ProposalChange{{Op: plc.OpTakeOff, Node: "anchor", Module: "web"}, {Op: plc.OpPlace, Node: "laptop", Module: "web"}}
move := []mma.ProposalChange{{Op: mmac.OpTakeOff, Node: "anchor", Module: "web"}, {Op: mmac.OpPlace, Node: "laptop", Module: "web"}}
p, laid, findings := Propose(in(move...))
if len(findings) != 0 || p.State != plc.ProposalAnswering || p.Base["anchor"] != 4 || p.Base["laptop"] != 9 ||
if len(findings) != 0 || p.State != mmac.ProposalAnswering || p.Base["anchor"] != 4 || p.Base["laptop"] != 9 ||
!slices.Equal(mmac.Assigned(*laid["laptop"]), []string{"shell", "web"}) || !slices.Equal(mmac.Assigned(*laid["anchor"]), []string{"store"}) {
t.Fatalf("a move: %+v, %+v, %v", p, laid, findings)
}
@@ -41,18 +41,18 @@ func TestThePlacementSeatDecides(t *testing.T) {
in ProposeIn
want string
}{
"a module not held": {in(mma.ProposalChange{Op: plc.OpPlace, Node: "laptop", Module: "ghost"}), "the indexer holds no module ghost"},
"a node with no record": {in(mma.ProposalChange{Op: plc.OpPlace, Node: "stranger", Module: "web"}), "stranger is a node with no record"},
"nothing changes": {in(mma.ProposalChange{Op: plc.OpPlace, Node: "anchor", Module: "web"}), "leave every placement as it is"},
"a module not held": {in(mma.ProposalChange{Op: mmac.OpPlace, Node: "laptop", Module: "ghost"}), "the indexer holds no module ghost"},
"a node with no record": {in(mma.ProposalChange{Op: mmac.OpPlace, Node: "stranger", Module: "web"}), "stranger is a node with no record"},
"nothing changes": {in(mma.ProposalChange{Op: mmac.OpPlace, Node: "anchor", Module: "web"}), "leave every placement as it is"},
"no why": {func() ProposeIn { i := in(move...); i.Why = " "; return i }(), "says why"},
"no caller": {func() ProposeIn { i := in(move...); i.By = ""; return i }(), "names no caller"},
"a seed by hand": {in(mma.ProposalChange{Op: plc.OpSeed, Node: "laptop", Whole: &laptop}), "gone with adopt"},
"a seed by hand": {in(mma.ProposalChange{Op: mmac.OpSeed, Node: "laptop", Whole: &laptop}), "gone with adopt"},
// 1126: the controller ended by its unassignment; placed again it would be a second writer beside the parts that
// act without its lease.
"the controller placed again": {in(mma.ProposalChange{Op: plc.OpPlace, Node: "laptop", Module: "mesh-controller"}), "ended by its unassignment"},
"the controller seeded whole": {in(mma.ProposalChange{Op: plc.OpSeed, Node: "anchor", Whole: &mma.Placement{V: keys.Version, Node: "anchor",
"the controller placed again": {in(mma.ProposalChange{Op: mmac.OpPlace, Node: "laptop", Module: "mesh-controller"}), "ended by its unassignment"},
"the controller seeded whole": {in(mma.ProposalChange{Op: mmac.OpSeed, Node: "anchor", Whole: &mma.Placement{V: keys.Version, Node: "anchor",
Decided: anchor.Decided, Modules: []mma.PlacedModule{{Module: "mesh-controller"}}}}), "two writers (issue 1126)"},
"a pin of nobody placed": {in(mma.ProposalChange{Op: plc.OpPin, Node: "laptop", Provision: "db", Provider: &mma.Provider{Node: "laptop", Module: "store"}}), "not placed there"},
"a pin of nobody placed": {in(mma.ProposalChange{Op: mmac.OpPin, Node: "laptop", Provision: "db", Provider: &mma.Provider{Node: "laptop", Module: "store"}}), "not placed there"},
} {
if _, _, f := Propose(c.in); !slices.ContainsFunc(f, func(s string) bool { return strings.Contains(s, c.want) }) {
t.Errorf("%s: findings %v, want one naming %q", name, f, c.want)
@@ -61,12 +61,12 @@ func TestThePlacementSeatDecides(t *testing.T) {
// Every kind of change is taken: the placement is the one source, and the controller's verbs limit nothing (ADR 0567 §8).
for _, c := range []mma.ProposalChange{
{Op: plc.OpConfig, Node: "anchor", Module: "web", Values: map[string]any{"puid": 1}},
{Op: plc.OpPlace, Node: "laptop", Module: "web", Ports: map[string]int64{"80": 8080}, Bindings: map[string]mma.Provider{"db": {Node: "anchor", Module: "store"}}},
{Op: plc.OpConfig, Node: "anchor", Module: "web", Layer: map[string]any{"places": []any{}}},
{Op: plc.OpConfig, Node: "anchor", Module: "web", MeshWide: map[string]any{"accesses": []any{}}},
{Op: plc.OpDecide, Node: "laptop", Field: "site", Value: json.RawMessage(`"attic"`)},
{Op: plc.OpHold, Node: "anchor", Module: "store", Claim: "node-shell", Scope: "node"},
{Op: mmac.OpConfig, Node: "anchor", Module: "web", Values: map[string]any{"puid": 1}},
{Op: mmac.OpPlace, Node: "laptop", Module: "web", Ports: map[string]int64{"80": 8080}, Bindings: map[string]mma.Provider{"db": {Node: "anchor", Module: "store"}}},
{Op: mmac.OpConfig, Node: "anchor", Module: "web", Layer: map[string]any{"places": []any{}}},
{Op: mmac.OpConfig, Node: "anchor", Module: "web", MeshWide: map[string]any{"accesses": []any{}}},
{Op: mmac.OpDecide, Node: "laptop", Field: "site", Value: json.RawMessage(`"attic"`)},
{Op: mmac.OpHold, Node: "anchor", Module: "store", Claim: "node-shell", Scope: "node"},
} {
if _, _, f := Propose(in(c)); len(f) != 0 {
t.Errorf("%s is refused: %v", c.Op, f)
@@ -75,7 +75,7 @@ func TestThePlacementSeatDecides(t *testing.T) {
// A key moved while the warrant wrote: what is left to write is the other nodes' changes, to be laid and asked again.
if rest, ok := Rest(p, map[string]*mma.Placement{"anchor": laid["anchor"]}); !ok || len(rest.Changes) != 1 || rest.Changes[0].Node != "laptop" ||
len(rest.Base) != 1 || rest.State != plc.ProposalAnswering || rest.Must != nil {
len(rest.Base) != 1 || rest.State != mmac.ProposalAnswering || rest.Must != nil {
t.Errorf("the rest = %+v, %v", rest, ok)
}
if _, ok := Rest(p, laid); ok {
@@ -100,7 +100,7 @@ func TestThePlacementSeatDecides(t *testing.T) {
t.Fatalf("the ask = %+v, %v", a, err)
}
q := p
q.Changes = append(slices.Clone(p.Changes), mma.ProposalChange{Op: plc.OpUnpin, Node: "laptop", Provision: "x"})
q.Changes = append(slices.Clone(p.Changes), mma.ProposalChange{Op: mmac.OpUnpin, Node: "laptop", Provision: "x"})
if occ.Performs(mustAsk(t, a).Options[0], PlacementAct(q)) == nil {
t.Error("the ask's yes binds changes it was not asked about")
}
@@ -97,10 +97,10 @@ func TestPlacement(t *testing.T) {
t.Run("changes laid over the keys", func(t *testing.T) {
laid, findings := LayChanges(current, []seat.ProposalChange{
{Op: OpPlace, Node: "g14", Module: "new"},
{Op: OpTakeOff, Node: "g14", Module: "zsh"},
{Op: OpUnpin, Node: "g14", Provision: "shell"},
{Op: OpDecide, Node: "novox", Field: "site", Value: "attic"},
{Op: client.OpPlace, Node: "g14", Module: "new"},
{Op: client.OpTakeOff, Node: "g14", Module: "zsh"},
{Op: client.OpUnpin, Node: "g14", Provision: "shell"},
{Op: client.OpDecide, Node: "novox", Field: "site", Value: "attic"},
})
if len(findings) != 0 || len(laid) != 2 || !reflect.DeepEqual(client.Assigned(laid["g14"]), []string{"app", "new"}) ||
len(laid["g14"].Holds) != 0 || laid["g14"].Pins != nil || laid["novox"].Decided.Site != "attic" || !laid["novox"].Decided.Hub {
@@ -109,21 +109,21 @@ func TestPlacement(t *testing.T) {
if !reflect.DeepEqual(client.Assigned(current["g14"]), []string{"zsh", "app"}) {
t.Error("laying changed the current placement")
}
if fresh, f := LayChanges(current, []seat.ProposalChange{{Op: OpPlace, Node: "pi", Module: "zsh"}}); len(f) != 0 || fresh["pi"].Node != "pi" {
if fresh, f := LayChanges(current, []seat.ProposalChange{{Op: client.OpPlace, Node: "pi", Module: "zsh"}}); len(f) != 0 || fresh["pi"].Node != "pi" {
t.Errorf("a node with no key: %+v, %v", fresh, f)
}
for name, c := range map[string]struct {
change seat.ProposalChange
want string
}{
"not placed": {seat.ProposalChange{Op: OpTakeOff, Node: "g14", Module: "nope"}, "not placed"},
"no such field": {seat.ProposalChange{Op: OpDecide, Node: "g14", Field: "colour", Value: "x"}, "no decided field"},
"wrong type": {seat.ProposalChange{Op: OpDecide, Node: "g14", Field: "hub", Value: "x"}, "does not take"},
"not placed": {seat.ProposalChange{Op: client.OpTakeOff, Node: "g14", Module: "nope"}, "not placed"},
"no such field": {seat.ProposalChange{Op: client.OpDecide, Node: "g14", Field: "colour", Value: "x"}, "no decided field"},
"wrong type": {seat.ProposalChange{Op: client.OpDecide, Node: "g14", Field: "hub", Value: "x"}, "does not take"},
"unknown op": {seat.ProposalChange{Op: "move", Node: "g14"}, "no change of placement"},
"pin of nobody": {seat.ProposalChange{Op: OpPin, Node: "g14", Provision: "x"}, "provider"},
"not held": {seat.ProposalChange{Op: OpUnhold, Node: "g14", Module: "app", Claim: "x"}, "holds no"},
"bad port": {seat.ProposalChange{Op: OpPlace, Node: "g14", Module: "x", Ports: map[string]int64{"a": 1}}, "port"},
"seed of another": {seat.ProposalChange{Op: OpSeed, Node: "g14", Whole: &novox}, "seed"},
"pin of nobody": {seat.ProposalChange{Op: client.OpPin, Node: "g14", Provision: "x"}, "provider"},
"not held": {seat.ProposalChange{Op: client.OpUnhold, Node: "g14", Module: "app", Claim: "x"}, "holds no"},
"bad port": {seat.ProposalChange{Op: client.OpPlace, Node: "g14", Module: "x", Ports: map[string]int64{"a": 1}}, "port"},
"seed of another": {seat.ProposalChange{Op: client.OpSeed, Node: "g14", Whole: &novox}, "seed"},
} {
if _, f := LayChanges(current, []seat.ProposalChange{c.change}); len(f) != 1 || !strings.Contains(f[0], c.want) {
t.Errorf("%s: %v, want a finding naming %q", name, f, c.want)
@@ -136,8 +136,8 @@ func TestPlacement(t *testing.T) {
t.Errorf("NewProposalID = %q", got)
}
p := seat.Proposal{ID: id, By: "operator@anchor", Why: "move the app", At: time.Unix(1, 0).UTC(),
Changes: []seat.ProposalChange{{Op: OpTakeOff, Node: "g14", Module: "zsh"}}, Base: map[string]int64{"g14": 4},
State: ProposalAnswering}
Changes: []seat.ProposalChange{{Op: client.OpTakeOff, Node: "g14", Module: "zsh"}}, Base: map[string]int64{"g14": 4},
State: client.ProposalAnswering}
raw, err := EncodeProposal(p)
if err != nil {
t.Fatal(err)
@@ -150,7 +150,7 @@ func TestPlacement(t *testing.T) {
}
seed := p
seed.Seed, seed.ExportDigest = true, ExportDigest(current)
seed.Changes = []seat.ProposalChange{{Op: OpSeed, Node: "g14", Whole: &g14}}
seed.Changes = []seat.ProposalChange{{Op: client.OpSeed, Node: "g14", Whole: &g14}}
if _, err := EncodeProposal(seed); err != nil {
t.Errorf("the seed: %v", err)
}
@@ -161,17 +161,17 @@ func TestPlacement(t *testing.T) {
}
// The digests are what the earlier holder's code made of the same values (issue 1966: a proposal's ask binds its
// changes' digest, and a seed its export's), whatever order the generated types keep their fields in.
rich := seat.Proposal{ID: id, By: "operator@anchor", Why: "w", At: time.Unix(1, 0).UTC(), State: ProposalAnswering,
rich := seat.Proposal{ID: id, By: "operator@anchor", Why: "w", At: time.Unix(1, 0).UTC(), State: client.ProposalAnswering,
Base: map[string]int64{"g14": 4, "lab": 0, "novox": 2},
Changes: []seat.ProposalChange{
{Op: OpTakeOff, Node: "g14", Module: "zsh"},
{Op: OpPlace, Node: "g14", Module: "new", Ports: map[string]int64{"80": 8080}, Bindings: map[string]seat.Provider{"pg": {Node: "novox", Module: "postgres"}}},
{Op: OpPin, Node: "g14", Provision: "shell", Provider: &seat.Provider{Node: "g14", Module: "bash"}},
{Op: OpHold, Node: "g14", Module: "app", Claim: "node-shell", Scope: "node", Kind: "k", Site: "s"},
{Op: OpDecide, Node: "novox", Field: "site", Value: "attic"},
{Op: OpDecide, Node: "novox", Field: "peer-names", Value: map[string]any{"k": "v"}},
{Op: OpConfig, Node: "g14", Module: "app", MeshWide: map[string]any{"a": float64(1)}, Layer: map[string]any{"b": "c"}, MeshWideValues: map[string]any{"d": true}, Values: map[string]any{"e": []any{"f"}}},
{Op: OpAddNode, Node: "lab", Decided: &seat.NodeDecided{Address: "10.0.0.7", Account: "x", AgentAccountHome: "/h"}, Keys: &seat.ProposalChangeKeys{Overlay: "wg", Identity: "id", Sealing: "se"}},
{Op: client.OpTakeOff, Node: "g14", Module: "zsh"},
{Op: client.OpPlace, Node: "g14", Module: "new", Ports: map[string]int64{"80": 8080}, Bindings: map[string]seat.Provider{"pg": {Node: "novox", Module: "postgres"}}},
{Op: client.OpPin, Node: "g14", Provision: "shell", Provider: &seat.Provider{Node: "g14", Module: "bash"}},
{Op: client.OpHold, Node: "g14", Module: "app", Claim: "node-shell", Scope: "node", Kind: "k", Site: "s"},
{Op: client.OpDecide, Node: "novox", Field: "site", Value: "attic"},
{Op: client.OpDecide, Node: "novox", Field: "peer-names", Value: map[string]any{"k": "v"}},
{Op: client.OpConfig, Node: "g14", Module: "app", MeshWide: map[string]any{"a": float64(1)}, Layer: map[string]any{"b": "c"}, MeshWideValues: map[string]any{"d": true}, Values: map[string]any{"e": []any{"f"}}},
{Op: client.OpAddNode, Node: "lab", Decided: &seat.NodeDecided{Address: "10.0.0.7", Account: "x", AgentAccountHome: "/h"}, Keys: &seat.ProposalChangeKeys{Overlay: "wg", Identity: "id", Sealing: "se"}},
}}
for name, c := range map[string]struct{ got, want string }{
"export": {ExportDigest(current), "sha256:742f03d8790d7efa6abff76636f4a69ba2704d646233960f52ce08604b96e128"},
@@ -190,10 +190,10 @@ func TestPlacement(t *testing.T) {
// 774/S42/AC1 (ADR 0587 §7): an add-node carries its enrolment's warrant naming its node and address, as its one
// change, from the enroller alone; a warrant naming another node or address is refused.
// At the operator's yes it carries the tunnel key alone (the operator, 2026-11-02 01:05).
joining := seat.ProposalChange{Op: OpAddNode, Node: "lab", Decided: &seat.NodeDecided{Address: "10.0.0.7"},
joining := seat.ProposalChange{Op: client.OpAddNode, Node: "lab", Decided: &seat.NodeDecided{Address: "10.0.0.7"},
Keys: &seat.ProposalChangeKeys{Overlay: "wg"}}
add := seat.Proposal{ID: id, By: "ace.mesh-enrolment", Why: "lab joins", At: time.Unix(1, 0).UTC(),
Changes: []seat.ProposalChange{joining}, Base: map[string]int64{"lab": 0}, State: ProposalAnswering,
Changes: []seat.ProposalChange{joining}, Base: map[string]int64{"lab": 0}, State: client.ProposalAnswering,
Warrant: &seat.Warrant{Node: "lab", Address: "10.0.0.7", Ask: "a-1", Warrant: "w-1"}}
if raw, err := EncodeProposal(add); err != nil {
t.Errorf("an add-node: %v", err)
@@ -206,7 +206,7 @@ func TestPlacement(t *testing.T) {
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, []seat.ProposalChange{{Op: OpAddNode, Node: "g14", Decided: &seat.NodeDecided{Address: "10.0.0.9"},
if _, f := LayChanges(current, []seat.ProposalChange{{Op: client.OpAddNode, Node: "g14", Decided: &seat.NodeDecided{Address: "10.0.0.9"},
Keys: &seat.ProposalChangeKeys{Overlay: "wg"}}}); len(f) != 1 || !strings.Contains(f[0], "placed already") {
t.Errorf("an add-node of a placed node: %v", f)
}
@@ -229,7 +229,7 @@ func TestPlacement(t *testing.T) {
p.Changes = []seat.ProposalChange{c}
},
"beside another change": func(p *seat.Proposal) {
p.Changes = append(p.Changes, seat.ProposalChange{Op: OpPlace, Node: "lab", Module: "zsh"})
p.Changes = append(p.Changes, seat.ProposalChange{Op: client.OpPlace, Node: "lab", Module: "zsh"})
},
"the hub": func(p *seat.Proposal) {
c := joining
@@ -237,7 +237,7 @@ func TestPlacement(t *testing.T) {
p.Changes = []seat.ProposalChange{c}
},
"a warrant on a move": func(p *seat.Proposal) {
p.Changes, p.Base = []seat.ProposalChange{{Op: OpTakeOff, Node: "g14", Module: "zsh"}}, map[string]int64{"g14": 4}
p.Changes, p.Base = []seat.ProposalChange{{Op: client.OpTakeOff, Node: "g14", Module: "zsh"}}, map[string]int64{"g14": 4}
},
} {
q := add
@@ -256,7 +256,7 @@ func TestPlacement(t *testing.T) {
}
// An enrolment's end takes its never-confirmed node off again: the enroller's alone, of a node running nothing.
remove := add
remove.Changes = []seat.ProposalChange{{Op: OpRemoveNode, Node: "lab"}}
remove.Changes = []seat.ProposalChange{{Op: client.OpRemoveNode, Node: "lab"}}
if _, err := EncodeProposal(remove); err != nil || AddNodeRefusal(remove, "mesh-delivery-assigner", enroller) == "" ||
AddNodeRefusal(remove, enroller, enroller) != "" || !slices.Equal(RemovedNodes(remove), []string{"lab"}) {
t.Errorf("a remove-node: %v, %q", err, AddNodeRefusal(remove, "mesh-delivery-assigner", enroller))
@@ -265,7 +265,7 @@ func TestPlacement(t *testing.T) {
if laid, f := LayChanges(joined, remove.Changes); len(f) != 0 || len(laid) != 0 {
t.Errorf("a joining node removed: %v, %v", laid, f)
}
if _, f := LayChanges(current, []seat.ProposalChange{{Op: OpRemoveNode, Node: "g14"}}); len(f) != 1 ||
if _, f := LayChanges(current, []seat.ProposalChange{{Op: client.OpRemoveNode, Node: "g14"}}); len(f) != 1 ||
!strings.Contains(f[0], "runs modules") {
t.Errorf("a running node removed: %v", f)
}
@@ -280,7 +280,7 @@ func TestPlacement(t *testing.T) {
"no change": func(p *seat.Proposal) { p.Changes = nil },
"no base": func(p *seat.Proposal) { p.Base = map[string]int64{"novox": 1} },
"no state": func(p *seat.Proposal) { p.State = "" },
"asked, no ask": func(p *seat.Proposal) { p.State = ProposalAsked },
"asked, no ask": func(p *seat.Proposal) { p.State = client.ProposalAsked },
"seed mixed": func(p *seat.Proposal) { p.Seed, p.ExportDigest = true, "d" },
"bad change": func(p *seat.Proposal) { p.Changes[0].Op = "swap" },
"seed, no digest": func(p *seat.Proposal) { *p = seed; p.ExportDigest = "" },
@@ -17,37 +17,6 @@ import (
// lays them over the current keys, names the revision of each key it laid them over, and either refuses at once with every
// finding or writes the proposal under its id in its state `proposals` and a candidate for each node it must hear from.
// The operations a proposal's change names (ADR 0567 §2).
const (
OpPlace = "place" // place a module on a node, with its host ports and bindings
OpTakeOff = "take-off" // take a module off a node
OpPin = "pin" // pin the provider of a provision on a node
OpUnpin = "unpin" // remove a pin
OpHold = "hold" // a module placed on the node holds a seat
OpUnhold = "unhold" // it holds it no more
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): the enroller's alone, the proposal's one change.
OpAddNode = "add-node"
// OpRemoveNode takes a joining node off placement again when its enrolment ends unconfirmed, revoked or expired
// (the operator's decision of 2026-11-02 01:05): the enroller's alone, on the same warrant, the proposal's one
// change, of a node with no module and no holding. The holder deletes its key (RemovedNodes).
OpRemoveNode = "remove-node"
)
// The states of an open proposal.
const (
// ProposalAnswering: its candidates are written and the nodes it must hear from have not all answered.
ProposalAnswering = "answering"
// ProposalFailed: its verdict failed; it stays open, for its proposer to read and withdraw, or for a node's later
// answer to judge it again.
ProposalFailed = "failed"
// ProposalAsked: its verdict passed and the operator is asked to approve it.
ProposalAsked = "asked"
)
// VerdictWaits is a proposal's verdict while a node it must hear from has not answered within the bound; it passes or
// fails as a check's verdict does (ADR 0567 §4).
const VerdictWaits = "waits"
@@ -65,7 +34,7 @@ func Nodes(p seat.Proposal) []string { return sortedKeys(p.Base) }
func RemovedNodes(p seat.Proposal) []string {
var out []string
for _, c := range p.Changes {
if c.Op == OpRemoveNode {
if c.Op == client.OpRemoveNode {
out = append(out, c.Node)
}
}
@@ -84,22 +53,22 @@ func CheckChange(c seat.ProposalChange) error {
return nil
}
switch c.Op {
case OpPlace, OpTakeOff, OpConfig:
case client.OpPlace, client.OpTakeOff, client.OpConfig:
return needs("module", keys.CheckName("module", c.Module) == nil)
case OpPin:
case client.OpPin:
return needs("provision or provider", c.Provision != "" && c.Provider != nil && c.Provider.Node != "" && c.Provider.Module != "")
case OpUnpin:
case client.OpUnpin:
return needs("provision", c.Provision != "")
case OpHold, OpUnhold:
case client.OpHold, client.OpUnhold:
return needs("module or claim", keys.CheckName("module", c.Module) == nil && c.Claim != "")
case OpDecide:
case client.OpDecide:
return needs("field or value", c.Field != "" && c.Value != nil)
case OpSeed:
case client.OpSeed:
if c.Whole == nil || c.Whole.Node != c.Node {
return fmt.Errorf("placement: the seed of %s names no placement of that node", c.Node)
}
return client.CheckPlacement(*c.Whole)
case OpAddNode:
case client.OpAddNode:
if c.Decided == nil || c.Decided.Address == "" || c.Keys == nil || c.Keys.Overlay == "" {
return fmt.Errorf("placement: the add-node of %s names no address or no tunnel key", c.Node)
}
@@ -114,7 +83,7 @@ func CheckChange(c seat.ProposalChange) error {
return fmt.Errorf("placement: the add-node of %s places a module; a module is placed by a proposal of its own", c.Node)
}
return nil
case OpRemoveNode:
case client.OpRemoveNode:
if c.Module != "" || c.Decided != nil || c.Keys != nil || c.Whole != nil {
return fmt.Errorf("placement: the remove-node of %s names more than its node", c.Node)
}
@@ -157,16 +126,16 @@ func LayChanges(current map[string]seat.Placement, changes []seat.ProposalChange
findings = append(findings, fmt.Sprintf("change %d: %s is not placed on %s", i+1, c.Module, c.Node))
}
switch c.Op {
case OpSeed:
case client.OpSeed:
p = clonePlacement(*c.Whole)
case OpAddNode:
case client.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 = seat.Placement{V: keys.Version, Node: c.Node, Decided: *c.Decided, Modules: []seat.PlacedModule{}}
case OpRemoveNode:
case client.OpRemoveNode:
// Only a joining node never confirmed goes this way: one with no module and no holding (a node that runs
// anything is taken off module by module, by a proposal the operator approves).
cur, has := current[c.Node]
@@ -176,7 +145,7 @@ func LayChanges(current map[string]seat.Placement, changes []seat.ProposalChange
findings = append(findings, fmt.Sprintf("change %d: %s runs modules or holds seats, so it is no joining node to remove", i+1, c.Node))
}
continue
case OpPlace:
case client.OpPlace:
m := seat.PlacedModule{Module: c.Module, Ports: c.Ports, Bindings: c.Bindings}
if at >= 0 {
o := p.Modules[at]
@@ -186,7 +155,7 @@ func LayChanges(current map[string]seat.Placement, changes []seat.ProposalChange
} else {
p.Modules = append(p.Modules, m)
}
case OpTakeOff:
case client.OpTakeOff:
if at < 0 {
notPlaced()
continue
@@ -199,7 +168,7 @@ func LayChanges(current map[string]seat.Placement, changes []seat.ProposalChange
}
}
p.Holds = holds
case OpConfig:
case client.OpConfig:
if at < 0 {
notPlaced()
continue
@@ -217,23 +186,23 @@ func LayChanges(current map[string]seat.Placement, changes []seat.ProposalChange
if c.Values != nil {
m.Values = c.Values
}
case OpPin:
case client.OpPin:
if p.Pins == nil {
p.Pins = map[string]seat.Provider{}
}
p.Pins[c.Provision] = *c.Provider
case OpUnpin:
case client.OpUnpin:
delete(p.Pins, c.Provision)
if len(p.Pins) == 0 {
p.Pins = nil
}
case OpHold:
case client.OpHold:
if at < 0 {
notPlaced()
continue
}
p.Holds = append(p.Holds, seat.Holding{Claim: c.Claim, Scope: c.Scope, Module: c.Module, Site: c.Site, Kind: c.Kind})
case OpUnhold:
case client.OpUnhold:
kept := p.Holds[:0:0]
for _, h := range p.Holds {
if h.Module != c.Module || h.Claim != c.Claim {
@@ -245,7 +214,7 @@ func LayChanges(current map[string]seat.Placement, changes []seat.ProposalChange
continue
}
p.Holds = kept
case OpDecide:
case client.OpDecide:
d, err := decide(p.Decided, c.Field, c.Value)
if err != nil {
findings = append(findings, fmt.Sprintf("change %d: %v", i+1, err))
@@ -339,7 +308,7 @@ func CheckProposal(p seat.Proposal) error {
if err := CheckChange(c); err != nil {
return fmt.Errorf("placement: the proposal %s, change %d: %w", p.ID, i+1, err)
}
if (c.Op == OpSeed) != p.Seed {
if (c.Op == client.OpSeed) != p.Seed {
return fmt.Errorf("placement: the proposal %s mixes the seed with other changes", p.ID)
}
if _, ok := p.Base[c.Node]; !ok {
@@ -347,8 +316,8 @@ func CheckProposal(p seat.Proposal) error {
}
}
switch p.State {
case ProposalAnswering, ProposalFailed:
case ProposalAsked:
case client.ProposalAnswering, client.ProposalFailed:
case client.ProposalAsked:
if p.Ask == "" || p.Verdict == nil || p.Verdict.State != VerdictPass {
return fmt.Errorf("placement: the proposal %s is asked with no ask or no passing verdict", p.ID)
}
@@ -367,7 +336,7 @@ func CheckProposal(p seat.Proposal) error {
func checkAddNode(p seat.Proposal) error {
adds := 0
for _, c := range p.Changes {
if c.Op == OpAddNode || c.Op == OpRemoveNode {
if c.Op == client.OpAddNode || c.Op == client.OpRemoveNode {
adds++
}
}
@@ -384,7 +353,7 @@ func checkAddNode(p seat.Proposal) error {
if w == nil || w.Ask == "" || w.Warrant == "" {
return fmt.Errorf("placement: the add-node of %s carries no warrant of its enrolment", c.Node)
}
if c.Op == OpRemoveNode {
if c.Op == client.OpRemoveNode {
if w.Node != c.Node {
return fmt.Errorf("placement: the remove-node of %s carries the warrant of %s", c.Node, w.Node)
}