Merge pull request 'The lease write's contract: the seat mesh-grants, the lease cut, processes follow their membership from state alone (issue 774)' (#156) from feat/l3-removal into main

This commit was merged in pull request #156.
This commit is contained in:
2026-11-02 00:35:20 +00:00
20 changed files with 700 additions and 88 deletions
+4
View File
@@ -219,6 +219,10 @@ type LeaseHolder struct {
// HandedBy is the credential that wrote it (the operator's), and Why its reason. // HandedBy is the credential that wrote it (the operator's), and Why its reason.
HandedBy string `json:"handed-by,omitempty"` HandedBy string `json:"handed-by,omitempty"`
Why string `json:"why,omitempty"` Why string `json:"why,omitempty"`
// Ask is the operator's ask whose warrant handed it (novox/hq ADR 0582 clause 3: "with the why, the ask and the
// time"), and At when it was written. Neither turns the lease: LeaseHandedAway reads HandedTo alone.
Ask string `json:"ask,omitempty"`
At time.Time `json:"at,omitzero"`
} }
// LeaseHandedAway reads the lease key as a getter answered it (its value, whether it was there, the read's error) and // LeaseHandedAway reads the lease key as a getter answered it (its value, whether it was there, the read's error) and
+75 -6
View File
@@ -161,8 +161,11 @@ func grantsAsTheControllers(t *testing.T, got, want string) {
intended := beyondTheControllers(user) intended := beyondTheControllers(user)
for g := range was { for g := range was {
// A runtime's direct get of its membership from the assignments stream went with the stream (research 072 // A runtime's direct get of its membership from the assignments stream went with the stream (research 072
// row 33): it reads the grants part's state memberships instead (ADR 0571 clause 7). // row 33): it reads the grants part's state memberships instead (ADR 0571 clause 7). And it no longer hears
if !now[g] && !strings.HasPrefix(g, "$JS.API.DIRECT.GET."+AssignmentsStream+".mesh.assignment."+strings.SplitN(user, ".", 2)[0]+".") { // the subject, where the grants part stops publishing at the lease write (ADR 0571 clause 10, issue 774).
node := strings.SplitN(user, ".", 2)[0]
if !now[g] && !strings.HasPrefix(g, "$JS.API.DIRECT.GET."+AssignmentsStream+".mesh.assignment."+node+".") &&
!strings.HasPrefix(g, "mesh.assignment."+node+".") {
t.Errorf("%s: the copy does not grant %s, which the controller's does", user, g) t.Errorf("%s: the copy does not grant %s, which the controller's does", user, g)
} }
} }
@@ -561,6 +564,9 @@ func TestTheCopiedComposerComposesAsTheControllers(t *testing.T) {
}{ }{
{"a controller holds it", `{"instance":"novox/1234/2026-10-27T20:00:00Z","epoch":57}`, true, nil, false}, {"a controller holds it", `{"instance":"novox/1234/2026-10-27T20:00:00Z","epoch":57}`, true, nil, false},
{"handed away", `{"handed-to":"the operator's hand-over","handed-by":"person.jochen"}`, true, nil, true}, {"handed away", `{"handed-to":"the operator's hand-over","handed-by":"person.jochen"}`, true, nil, true},
// ADR 0582 clause 3 (774/S3): the write names the ask and the time, which turn nothing.
{"handed to mesh-grants with its ask and time", `{"handed-to":"mesh-grants","handed-by":"person.jochen","why":"w",` +
`"ask":"lease-write-1","at":"2026-11-02T10:00:00Z"}`, true, nil, true},
{"expired or deleted", ``, false, nil, false}, {"expired or deleted", ``, false, nil, false},
{"unreadable", ``, false, errors.New("refused"), false}, {"unreadable", ``, false, errors.New("refused"), false},
{"not a holder", `[1]`, true, nil, false}, {"not a holder", `[1]`, true, nil, false},
@@ -636,6 +642,48 @@ func TestTheCopiedComposerComposesAsTheControllers(t *testing.T) {
t.Errorf("the grants part creates consumers as %v", u.CreatesConsumers) t.Errorf("the grants part creates consumers as %v", u.CreatesConsumers)
} }
} }
// ADR 0582 clause 3 (issue 774, 774/S2): the lease write's cut takes from the controller the lease's key and every
// update, purge and delete of its bucket, and gives the grants part the key holder alone; without it nothing
// changes (the golden lists above are composed without it).
lease := "$KV." + LeaseBucket + ".holder"
for _, cut := range []bool{false, true} {
users, err := Users(Records{Nodes: []string{"a"}, Assigned: map[string][]Declared{"a": {{Module: GrantsModule}}},
LeaseCut: cut})
if err != nil {
t.Fatal(err)
}
for _, c := range []struct {
kind Kind
subject string
may bool
}{
{KindController, lease, !cut},
{KindController, "$JS.API.STREAM.UPDATE.KV_" + LeaseBucket, !cut},
{KindController, "$JS.API.STREAM.PURGE.KV_" + LeaseBucket, !cut},
{KindController, "$JS.API.STREAM.DELETE.KV_" + LeaseBucket, !cut},
{KindController, "$JS.API.STREAM.MSG.DELETE.KV_" + LeaseBucket, !cut},
// It still reads the lease, and keeps its other buckets.
{KindController, "$JS.API.DIRECT.GET.KV_" + LeaseBucket + ".$KV." + LeaseBucket + ".holder", true},
{KindController, "$KV." + CallsBucket + ".x", true},
{KindController, "$JS.API.STREAM.UPDATE.KV_" + CallsBucket, true},
{KindModule, lease, cut},
{KindModule, "$KV." + LeaseBucket + ".other", false},
{KindModule, "$JS.API.STREAM.UPDATE.KV_" + LeaseBucket, false},
} {
for _, u := range users {
if u.Kind != c.kind || (c.kind == KindModule && u.Module != GrantsModule) {
continue
}
perms, err := PermissionsFor(u)
if err != nil {
t.Fatalf("cut %v: %s: %v", cut, u.Username(), err)
}
if MayPublish(perms, c.subject) != c.may {
t.Errorf("cut %v: %s may publish %s: %v, want %v", cut, u.Username(), c.subject, !c.may, c.may)
}
}
}
}
// 1952 S5 (novox/hq ADR 0564 clauses 2, 5, 8; Limits 2, 4, 8): the health holders' credentials. // 1952 S5 (novox/hq ADR 0564 clauses 2, 5, 8; Limits 2, 4, 8): the health holders' credentials.
healthChecks(t) healthChecks(t)
q := ConsumerConfig(QueueConsumer(Principal{Kind: KindProcess, Node: "a", Module: "m", Process: "p"}, q := ConsumerConfig(QueueConsumer(Principal{Kind: KindProcess, Node: "a", Module: "m", Process: "p"},
@@ -743,7 +791,9 @@ func healthChecks(t *testing.T) {
"a.nats.nats-tools": {"$JS.API.CONSUMER.DELETE.DEAD_LETTER_NOTICES.controller", "$JS.API.CONSUMER.INFO.DEAD_LETTER_NOTICES.controller", "a.nats.nats-tools": {"$JS.API.CONSUMER.DELETE.DEAD_LETTER_NOTICES.controller", "$JS.API.CONSUMER.INFO.DEAD_LETTER_NOTICES.controller",
"$KV.mesh-host_acts.g14.a1", "$JS.API.STREAM.INFO.KV_mesh-host_acts", "$KV.mesh-host_acts.g14.a1", "$JS.API.STREAM.INFO.KV_mesh-host_acts",
// Research 072 row 33: the ASSIGNMENTS stream is read and removed at the lease write. // Research 072 row 33: the ASSIGNMENTS stream is read and removed at the lease write.
"$JS.API.STREAM.INFO.ASSIGNMENTS", "$JS.API.STREAM.DELETE.ASSIGNMENTS"}, "$JS.API.STREAM.INFO.ASSIGNMENTS", "$JS.API.STREAM.DELETE.ASSIGNMENTS",
// ADR 0582 clause 3 (issue 774, 774/S2): the lease's bucket's expiry, read and set by its tool.
"$JS.API.STREAM.INFO.KV_mesh-controller_lease", "$JS.API.STREAM.UPDATE.KV_mesh-controller_lease"},
} }
mayNot := map[string][]string{ mayNot := map[string][]string{
// The keeper writes no findings and no attempts; no observer writes the keeper's buckets (clause 2). // The keeper writes no findings and no attempts; no observer writes the keeper's buckets (clause 2).
@@ -786,8 +836,13 @@ func healthChecks(t *testing.T) {
// outside the bus module). // outside the bus module).
"a.nats.nats-tools": {"$JS.API.CONSUMER.DELETE.DEAD_LETTER_NOTICES.other", "$JS.API.CONSUMER.DELETE.EVENTS.a_m", "a.nats.nats-tools": {"$JS.API.CONSUMER.DELETE.DEAD_LETTER_NOTICES.other", "$JS.API.CONSUMER.DELETE.EVENTS.a_m",
"$KV.mesh-conditions_conditions.x", "$JS.API.STREAM.DELETE.KV_mesh-host_acts", "$JS.API.STREAM.PURGE.KV_mesh-host_acts", "$KV.mesh-conditions_conditions.x", "$JS.API.STREAM.DELETE.KV_mesh-host_acts", "$JS.API.STREAM.PURGE.KV_mesh-host_acts",
"$JS.API.STREAM.DELETE.EVENTS", "$JS.API.STREAM.PURGE.ASSIGNMENTS", "$JS.API.STREAM.DELETE.ASSIGNMENTS_X"}, "$JS.API.STREAM.DELETE.EVENTS", "$JS.API.STREAM.PURGE.ASSIGNMENTS", "$JS.API.STREAM.DELETE.ASSIGNMENTS_X",
"a.nats.other": {"$JS.API.CONSUMER.DELETE.DEAD_LETTER_NOTICES.controller", "$KV.mesh-host_acts.g14.a1"}, // It updates the lease's bucket alone, and neither purges nor deletes it nor writes its key (ADR 0582 clause 3).
"$JS.API.STREAM.UPDATE.KV_mesh-host_acts", "$JS.API.STREAM.UPDATE.KV_mesh-controller_calls", "$JS.API.STREAM.UPDATE.EVENTS",
"$JS.API.STREAM.PURGE.KV_mesh-controller_lease", "$JS.API.STREAM.DELETE.KV_mesh-controller_lease",
"$KV.mesh-controller_lease.holder"},
"a.nats.other": {"$JS.API.CONSUMER.DELETE.DEAD_LETTER_NOTICES.controller", "$KV.mesh-host_acts.g14.a1",
"$JS.API.STREAM.UPDATE.KV_mesh-controller_lease"},
"a.nats": {"$JS.API.CONSUMER.DELETE.DEAD_LETTER_NOTICES.controller", "$KV.mesh-host_acts.g14.a1"}, "a.nats": {"$JS.API.CONSUMER.DELETE.DEAD_LETTER_NOTICES.controller", "$KV.mesh-host_acts.g14.a1"},
"a.shop.nats-tools": {"$JS.API.CONSUMER.DELETE.DEAD_LETTER_NOTICES.controller", "$KV.mesh-conditions_conditions.x", "$JS.API.STREAM.DELETE.ASSIGNMENTS"}, "a.shop.nats-tools": {"$JS.API.CONSUMER.DELETE.DEAD_LETTER_NOTICES.controller", "$KV.mesh-conditions_conditions.x", "$JS.API.STREAM.DELETE.ASSIGNMENTS"},
} }
@@ -828,6 +883,17 @@ func healthChecks(t *testing.T) {
if u.Kind == KindModule && MayPublish(perms, "$KV.mesh-delivery-assigner_policies.x") { if u.Kind == KindModule && MayPublish(perms, "$KV.mesh-delivery-assigner_policies.x") {
t.Errorf("%s writes the assigner's policies", u.Username()) t.Errorf("%s writes the assigner's policies", u.Username())
} }
// ADR 0571 clause 10 (issue 774, 774/S5/AC2): nobody hears memberships on mesh.assignment; they are read from
// the grants part's state.
if MaySubscribe(perms, "mesh.assignment.a.restic") {
t.Errorf("%s hears mesh.assignment", u.Username())
}
}
// Nor the runtime carrying the node's modules, which reads each one's membership from the state.
if perms, err := PermissionsFor(Principal{Kind: KindNodeTools, Node: "a", Module: RuntimeModule,
Carries: []Declared{{Module: "restic"}}}); err != nil || MaySubscribe(perms, "mesh.assignment.a.restic") ||
!MayPublish(perms, "$JS.API.DIRECT.GET.KV_mesh-grants_memberships.$KV.mesh-grants_memberships.a.restic") {
t.Errorf("the runtime hears mesh.assignment or reads no membership: %v", err)
} }
// 1953 S-age (ADR 0565 §10): a log that says an age keeps its entries that long and no longer; one that says none, as // 1953 S-age (ADR 0565 §10): a log that says an age keeps its entries that long and no longer; one that says none, as
// long as the log (ADR 0297). // long as the log (ADR 0297).
@@ -885,7 +951,10 @@ func healthChecks(t *testing.T) {
// It hears its watchdogs' words on core subjects (Limits 3, 11). // It hears its watchdogs' words on core subjects (Limits 3, 11).
for _, subject := range []string{"mesh.mod.power.event.sleeping", "mesh.control.g14.tools-alive", for _, subject := range []string{"mesh.mod.power.event.sleeping", "mesh.control.g14.tools-alive",
"mesh.seat.mesh-self-check.event.checked", "mesh.mod.keycloak.event.provisioner.failing", "mesh.seat.mesh-self-check.event.checked", "mesh.mod.keycloak.event.provisioner.failing",
"mesh.mod.nats.event.bus.slow-consumer", "prov.applied.g14.keycloak"} { "mesh.mod.nats.event.bus.slow-consumer", "prov.applied.g14.keycloak",
// ADR 0582 clauses 4, 6 (issue 774, 774/S22): the lease write's events, and ASSIGNMENTS deleted.
"mesh.seat.mesh-grants.event.lease-altered", "mesh.seat.mesh-grants.event.removal-failed",
"mesh.seat.mesh-grants.event.lease-handed", "$JS.EVENT.ADVISORY.STREAM.DELETED.ASSIGNMENTS"} {
if !MaySubscribe(perms, subject) { if !MaySubscribe(perms, subject) {
t.Errorf("the watchman may not hear %s", subject) t.Errorf("the watchman may not hear %s", subject)
} }
+7 -1
View File
@@ -114,7 +114,7 @@ func WatchmanDeadLetterGrants(sources []string) []string {
// WatchmanHears are the core subjects the watchman subscribes, with no durable consumer (ADR 0564 clauses 4, 5 and // WatchmanHears are the core subjects the watchman subscribes, with no durable consumer (ADR 0564 clauses 4, 5 and
// Limits 3, 11): the power seat's words (S1, ADR 0211), the providers' failing and recovered words (S8), each tool // Limits 3, 11): the power seat's words (S1, ADR 0211), the providers' failing and recovered words (S8), each tool
// runner's beat (S11), the self-check's checked (S10), a consumer deleted and the bus module's slow consumers and closed // runner's beat (S11), the self-check's checked (S10), a consumer deleted and the bus module's slow consumers and closed
// connections (S9), the provisioner clients' reports (S21), and the dead letters kept. Never the bus's API audit // connections (S9), the provisioner clients' reports (S21), the dead letters kept, and the lease write's events. Never the bus's API audit
// (`$JS.EVENT.ADVISORY.API`): it carries every API call's request and answer in the account, the vault's hashes among // (`$JS.EVENT.ADVISORY.API`): it carries every API call's request and answer in the account, the vault's hashes among
// them (the lead, 2026-10-29). // them (the lead, 2026-10-29).
func WatchmanHears() []string { func WatchmanHears() []string {
@@ -129,6 +129,12 @@ func WatchmanHears() []string {
"mesh.mod.nats.event.bus.connection-closed", "mesh.mod.nats.event.bus.connection-closed",
ProvAppliedSubjects, ProvAppliedSubjects,
deadLetterPrefix + ">", deadLetterPrefix + ">",
// And the lease write's (ADR 0582 clauses 4, 6; issue 774): the seat mesh-grants' three events, and the stream
// ASSIGNMENTS deleted, which with the controller's consumer deleted clears a removal that failed.
"mesh.seat." + catalogue.GrantsSeat + ".event." + catalogue.GrantsEventLeaseHanded,
"mesh.seat." + catalogue.GrantsSeat + ".event." + catalogue.GrantsEventLeaseAltered,
"mesh.seat." + catalogue.GrantsSeat + ".event." + catalogue.GrantsEventRemovalFailed,
"$JS.EVENT.ADVISORY.STREAM.DELETED." + AssignmentsStream,
} }
} }
+41
View File
@@ -0,0 +1,41 @@
package buscompose
import "slices"
// **The lease write's grants** (novox/hq ADR 0582 clause 3, issue 774). Composed with Records.LeaseCut, the list takes
// from the controller its write of the lease's key and every update, purge and delete of the lease's bucket, and gives
// the grants part the key `holder`, which it creates naming itself and deletes at a hand-back (clause 5). Whatever the
// records, the bus module's tools account may read and update the lease's bucket, for the tool that removes and
// restores its expiry; no other bucket's.
// leaseStream is the lease bucket's stream.
var leaseStream = "KV_" + LeaseBucket
// LeaseHolderWrite is the grants part's write of the lease's one key, from the cut.
func LeaseHolderWrite() string { return "$KV." + LeaseBucket + "." + leaseKey }
// LeaseCutDenied are the controller's subjects of the lease's bucket that its `$JS.API.>` reaches and the cut refuses:
// the bucket's update (its expiry), purge and delete, and a message's delete (the key's last value).
func LeaseCutDenied() []string {
return []string{"$JS.API.STREAM.UPDATE." + leaseStream, "$JS.API.STREAM.PURGE." + leaseStream,
"$JS.API.STREAM.DELETE." + leaseStream, "$JS.API.STREAM.MSG.DELETE." + leaseStream}
}
// LeaseExpiryGrants are the bus module's tools account's read and update of the lease's bucket, for its expiry.
func LeaseExpiryGrants() []string {
return []string{"$JS.API.STREAM.INFO." + leaseStream, "$JS.API.STREAM.UPDATE." + leaseStream}
}
// writesTheLease is the controller holding the lease, or, from the cut, the grants part writing its key holder alone.
func writesTheLease(p Principal, pattern string) bool {
if p.Kind == KindController {
return true
}
return p.Kind == KindModule && p.Module == GrantsModule && p.LeaseCut && pattern == LeaseHolderWrite()
}
// setsLeaseExpiry is the bus module's tools account updating the lease's bucket: the writers table's one exemption from
// stream definitions being the controller's beside ASSIGNMENTS' removal (ADR 0582 clause 3).
func setsLeaseExpiry(p Principal, pattern string) bool {
return p.Kind == KindProcess && len(p.Removes) > 0 && slices.Contains(LeaseExpiryGrants(), pattern)
}
+27 -8
View File
@@ -201,6 +201,9 @@ type Principal struct {
// Removes are, for a process or tools bundle account whose bus declares them, what it may remove from the bus // Removes are, for a process or tools bundle account whose bus declares them, what it may remove from the bus
// (novox/hq ADR 0564 Limits 4): its whole authority is then BusRemovalGrants of them. // (novox/hq ADR 0564 Limits 4): its whole authority is then BusRemovalGrants of them.
Removes []string Removes []string
// LeaseCut is Records.LeaseCut on the controller and the grants part (novox/hq ADR 0582 clause 3): the controller
// loses its write of the lease, and the grants part may write the key `holder`.
LeaseCut bool
// DeadLetterSources are, for the watchman, the streams a dead letter it keeps may come from, by name // DeadLetterSources are, for the watchman, the streams a dead letter it keeps may come from, by name
// (deadLetterSourcesOf, novox/hq ADR 0564 clause 5): never a bucket's. // (deadLetterSourcesOf, novox/hq ADR 0564 clause 5): never a bucket's.
DeadLetterSources []string DeadLetterSources []string
@@ -668,6 +671,9 @@ func PermissionsFor(p Principal) (Permissions, error) {
if err != nil { if err != nil {
return Permissions{}, err return Permissions{}, err
} }
// And the lease's bucket's expiry, read and set, by the bus module's tool the grants part calls at the lease
// write and its hand-back (novox/hq ADR 0582 clauses 3, 5): the one bucket whose stream it may update.
pub = append(pub, LeaseExpiryGrants()...)
sort.Strings(pub) sort.Strings(pub)
if err := CheckWriters(p, pub); err != nil { if err := CheckWriters(p, pub); err != nil {
return Permissions{}, err return Permissions{}, err
@@ -801,6 +807,9 @@ func PermissionsFor(p Principal) (Permissions, error) {
if bucket == delivery.HaveBucket { if bucket == delivery.HaveBucket {
continue // each node's node-engine writes its own keys of it, the controller none (ADR 0429 §1) continue // each node's node-engine writes its own keys of it, the controller none (ADR 0429 §1)
} }
if bucket == LeaseBucket && p.LeaseCut {
continue // the lease write took it (novox/hq ADR 0582 clause 3): a controller started fails on the lease
}
pub = append(pub, "$KV."+bucket+".>") pub = append(pub, "$KV."+bucket+".>")
} }
// **And the object bucket a declaration's large files travel in** (novox/hq ADR 0486 §2, issue 1697): the // **And the object bucket a declaration's large files travel in** (novox/hq ADR 0486 §2, issue 1697): the
@@ -1026,6 +1035,11 @@ func PermissionsFor(p Principal) (Permissions, error) {
pub = append(pub, CreateConsumerGrants(p.CreatesConsumers)...) pub = append(pub, CreateConsumerGrants(p.CreatesConsumers)...)
// And its own object bucket, where it writes the user list sealed to the bus node (ADR 0547 clause 7). // And its own object bucket, where it writes the user list sealed to the bus node (ADR 0547 clause 7).
pub = append(pub, GrantsUserListWrites()...) pub = append(pub, GrantsUserListWrites()...)
// And, from the lease write's cut, the lease's one key, which it creates naming itself and deletes at a
// hand-back (novox/hq ADR 0582 clauses 3, 5): never the bucket's other keys or its stream.
if p.LeaseCut {
pub = append(pub, LeaseHolderWrite())
}
} }
// The view alone reads the build seats' workers for the backlog of asks (issue 1887 A8a). // The view alone reads the build seats' workers for the backlog of asks (issue 1887 A8a).
if p.Module == ViewModule { if p.Module == ViewModule {
@@ -1061,10 +1075,10 @@ func PermissionsFor(p Principal) (Permissions, error) {
} }
} }
sub = append(sub, announcing(announced...)...) sub = append(sub, announcing(announced...)...)
// Its own membership (ADR 0160): the one subject a runtime derives for itself, heard live. Nothing else's. // Its own membership (ADR 0160), from the grants part's state memberships, its own key (novox/hq ADR 0571 clause
sub = append(sub, MembershipSubject(p.Node, p.Module)) // 7): its one read of it, since the assignments stream's direct get went with the stream (research 072 row 33),
// And from the grants part's state memberships, its own key (novox/hq ADR 0571 clause 7): its one read of it, // and no longer heard on its subject, where the grants part stops publishing at the lease write (ADR 0571 clause
// since the assignments stream's direct get went with the stream (research 072 row 33). // 10, issue 774).
pub = append(pub, readGrants(GrantsMembershipsBucket, MembershipKey(p.Node, p.Module))...) pub = append(pub, readGrants(GrantsMembershipsBucket, MembershipKey(p.Node, p.Module))...)
// 1b. The tools it calls, if its manifest says it calls any (novox/hq ADR 0152). The same // 1b. The tools it calls, if its manifest says it calls any (novox/hq ADR 0152). The same
@@ -1226,10 +1240,10 @@ func PermissionsFor(p Principal) (Permissions, error) {
} }
} }
} }
// Every assigned module's membership on this node (ADR 0160): one per module, heard live and read from the // Every assigned module's membership on this node (ADR 0160): one per module, read and watched from the
// grants part's state memberships (ADR 0571 clause 7). This node's and no other's — the one token // grants part's state memberships (ADR 0571 clause 7), and no longer heard on `mesh.assignment`, where the grants
// that varies is the module, so the pattern is the machine's own assignments. // part stops publishing at the lease write (ADR 0571 clause 10, issue 774). This node's and no other's — the
sub = append(sub, "mesh.assignment."+p.Node+".*") // one token that varies is the module, so the pattern is the machine's own assignments.
pub = append(pub, readGrants(GrantsMembershipsBucket, p.Node+".*")...) pub = append(pub, readGrants(GrantsMembershipsBucket, p.Node+".*")...)
// And that it is there (novox/hq to-be 45 §3, S11): its own heartbeat, under its machine's // And that it is there (novox/hq to-be 45 §3, S11): its own heartbeat, under its machine's
// name and no other's, on core NATS like the host's. // name and no other's, on core NATS like the host's.
@@ -1408,6 +1422,11 @@ func PermissionsFor(p Principal) (Permissions, error) {
} }
// What the controller alone may publish is denied to everybody else whose grant reaches it. // What the controller alone may publish is denied to everybody else whose grant reaches it.
var deny []string var deny []string
// And from the lease write's cut, the lease's bucket to the controller, whose `$JS.API.>` reaches it (novox/hq ADR
// 0582 clause 3): a controller started by mistake fails on the lease instead of taking it back.
if p.Kind == KindController && p.LeaseCut {
deny = append(deny, LeaseCutDenied()...)
}
if p.Kind != KindController { if p.Kind != KindController {
for _, only := range ControllerOnly() { for _, only := range ControllerOnly() {
for _, a := range pub { for _, a := range pub {
+7 -1
View File
@@ -148,6 +148,11 @@ type Records struct {
// Announced is each module's last announcement of the states it keeps (novox/hq ADR 0495, issue 1790), as the // Announced is each module's last announcement of the states it keeps (novox/hq ADR 0495, issue 1790), as the
// controller keeps it; a module absent has announced nothing yet. Nil where none was read. // controller keeps it; a module absent has announced nothing yet. Nil where none was read.
Announced map[string]announce.States Announced map[string]announce.States
// LeaseCut composes the list of the lease write (novox/hq ADR 0582 clause 3, issue 774): the controller may neither
// write the lease's key nor update, purge or delete its bucket, and the grants part may write the key `holder`.
// Set by the grants part from the write's second step until a hand-back composes without it (clause 5); the
// controller's composer never sets it.
LeaseCut bool
} }
// Users is every user the composed file should contain, in the order it will be written. // Users is every user the composed file should contain, in the order it will be written.
@@ -157,7 +162,7 @@ type Records struct {
// correct. // correct.
func Users(r Records) ([]Principal, error) { func Users(r Records) ([]Principal, error) {
out := []Principal{{Kind: KindController, Uses: asksSeatOf(r), AsksIssued: issuedOf(r), AsksTheVault: vaultVerbsOf(r), out := []Principal{{Kind: KindController, Uses: asksSeatOf(r), AsksIssued: issuedOf(r), AsksTheVault: vaultVerbsOf(r),
AsksTheConfig: servedOf(r, VerbsTheControllerAsksTheConfig), FilesFor: sortedCopy(r.Nodes)}} AsksTheConfig: servedOf(r, VerbsTheControllerAsksTheConfig), FilesFor: sortedCopy(r.Nodes), LeaseCut: r.LeaseCut}}
served := servedReadsOf(r) served := servedReadsOf(r)
logs := DeclaredLogs(r) logs := DeclaredLogs(r)
@@ -246,6 +251,7 @@ func Users(r Records) ([]Principal, error) {
} }
if d.Module == GrantsModule { if d.Module == GrantsModule {
out[len(out)-1].CreatesStreams = declaredStreamsOf(r) out[len(out)-1].CreatesStreams = declaredStreamsOf(r)
out[len(out)-1].LeaseCut = r.LeaseCut
grants = len(out) - 1 grants = len(out) - 1
} }
// **The vault's holder asks the desks and the issuing providers itself** (issue 723), under the // **The vault's holder asks the desks and the issuing providers itself** (issue 723), under the
+4 -3
View File
@@ -43,7 +43,7 @@ func createsDeclared(p Principal, pattern string) bool {
if isController(p, pattern) { if isController(p, pattern) {
return true return true
} }
if removesAssignmentsStream(p, pattern) { if removesAssignmentsStream(p, pattern) || setsLeaseExpiry(p, pattern) {
return true return true
} }
name, ok := strings.CutPrefix(pattern, "$JS.API.STREAM.CREATE.") name, ok := strings.CutPrefix(pattern, "$JS.API.STREAM.CREATE.")
@@ -158,7 +158,8 @@ var WritersTable = []WriterRow{
// subject is this machine's engine's alone. // subject is this machine's engine's alone.
Subjects: []string{"mesh.control.*.report", "mesh.control.*.health", "mesh.control.*.cli"}, Writes: ownMachine}, Subjects: []string{"mesh.control.*.report", "mesh.control.*.health", "mesh.control.*.cli"}, Writes: ownMachine},
{State: "the controller lease", Writer: "the controller instance holding it", KeptIn: "key-value " + LeaseBucket, {State: "the controller lease", Writer: "the controller instance holding it", KeptIn: "key-value " + LeaseBucket,
Others: "a candidate waits", Subjects: kvOf(LeaseBucket), Writes: isController}, Others: "a candidate waits", Subjects: kvOf(LeaseBucket), Writes: writesTheLease,
Shared: "from the lease write's cut the controller writes it no more and the grants part writes its key holder alone (novox/hq ADR 0582 clause 3)"},
// The merges mesh-delivery announced, for a moved module's registration (ADR 0420 §7), written by the lease holder. // The merges mesh-delivery announced, for a moved module's registration (ADR 0420 §7), written by the lease holder.
{State: "the merges mesh-delivery announced", Writer: "controller (lease holder)", {State: "the merges mesh-delivery announced", Writer: "controller (lease holder)",
KeptIn: "key-value " + DeliveryMergesBucket, Others: "read at a moved module's registration", KeptIn: "key-value " + DeliveryMergesBucket, Others: "read at a moved module's registration",
@@ -188,7 +189,7 @@ var WritersTable = []WriterRow{
Subjects: []string{"$JS.API.STREAM.CREATE.>", "$JS.API.STREAM.UPDATE.>", "$JS.API.STREAM.DELETE.>", Subjects: []string{"$JS.API.STREAM.CREATE.>", "$JS.API.STREAM.UPDATE.>", "$JS.API.STREAM.DELETE.>",
"$JS.API.CONSUMER.DURABLE.CREATE.>"}, "$JS.API.CONSUMER.DURABLE.CREATE.>"},
Writes: createsDeclared, Writes: createsDeclared,
Shared: "the grants part creates a stream a manifest declares, by its name, and never updates or deletes one (novox/hq ADR 0547 §5; the operator, 2026-10-26 08:54); the bus module's tools account deletes the stream ASSIGNMENTS at the lease write, and no other (research 072 row 33)"}, Shared: "the grants part creates a stream a manifest declares, by its name, and never updates or deletes one (novox/hq ADR 0547 §5; the operator, 2026-10-26 08:54); the bus module's tools account deletes the stream ASSIGNMENTS at the lease write, and no other (research 072 row 33), and updates the lease's bucket for its expiry alone (novox/hq ADR 0582 clause 3)"},
{State: "builds and their outcomes", Writer: "the build seat's holder", KeptIn: "its own state", {State: "builds and their outcomes", Writer: "the build seat's holder", KeptIn: "its own state",
Others: "the controller asks", Others: "the controller asks",
Subjects: []string{"mesh.seat.node-build-agent.event.built", "mesh.seat.mesh-build-machine.event.built"}, Subjects: []string{"mesh.seat.node-build-agent.event.built", "mesh.seat.mesh-build-machine.event.built"},
+238
View File
@@ -0,0 +1,238 @@
package catalogue
import (
"fmt"
"slices"
"sort"
"strings"
)
// **What still depends on the controller, and the lease write's check** (novox/hq ADR 0571 §13, ADR 0582 clause 2,
// issue 774): every declaration of a manifest other than the controller's on the controller (a read of one of its
// states, an invoke of one of its verbs or tools, a consume or a wait on one of its events, a use of its seat), judged
// against the named, temporary list of what another accepted record keeps until the lease write. The grants part's
// grants_lease_check runs it over every manifest the indexer holds; ManifestRefusals runs it over a repository's
// manifests, in the test ADR 0571 §13 names in mesh-catalog, mesh-delivery and mesh-tools. A read of the lease key is no
// dependency: it names the lease's holder, whoever that is, and parts read it to learn that the write happened.
// ControllerLease is the controller's lease as a reader names it (`<module>.<state>`): no dependency.
const ControllerLease = ControllerSeatName + ".lease"
// The kinds of a dependency on the controller, as a manifest declares them.
const (
DependsRead = "read"
DependsInvoke = "invoke"
DependsConsume = "consume"
DependsUses = "uses"
DependsWaits = "waits"
)
// The phases of the lease write's check (ADR 0571 §13): before the lease write's own delivery the whole named list is
// allowed; that delivery drops every entry but the three kept to the write itself, and its check runs with those alone.
const (
PhaseBefore = "before-delivery"
PhaseWrite = "write"
)
// Dependency is one declaration of a manifest other than the controller's on the controller: a read of one of its
// states, an invoke of one of its verbs or tools, a consume or a wait on one of its events, or a send to its seat.
type Dependency struct {
Module string `json:"module"`
// By is the account declaring it when it is not the module's own: `process <id>` or `tools <binary>`.
By string `json:"by,omitempty"`
Kind string `json:"kind"`
What string `json:"what"`
// Kept is the record that keeps it until the lease write, from the named list; empty when none does.
Kept string `json:"kept,omitempty"`
// ToTheWrite says it is one of the three kept to the write itself, which the lease write's own delivery keeps.
ToTheWrite bool `json:"to_the_write,omitempty"`
}
// KeptDependency is one entry of the named, temporary list ADR 0571 §13 allows: a read, invoke or consume another
// accepted record keeps until the lease write, with that record. ToTheWrite marks the three kept to the write itself.
type KeptDependency struct {
Module, Kind, What, Record string
ToTheWrite bool
}
// KeepList is the named list. Each entry names the clause that keeps it; a dependency not on it fails the check. It
// holds the three kept to the write itself and nothing else: the lease write's own delivery dropped every other entry
// (ADR 0571 §13; issue 774), so its check runs with these alone in either phase. The third, the bus node agent's
// following of the controller's user list (ADR 0571 §8, ADR 0547 §8), is no manifest's declaration but the
// node-engine's own, so no manifest matches it; it stays named here so the list reads as the record has it.
var KeepList = []KeptDependency{
{Module: "mesh-delivery-builder", Kind: DependsConsume, What: "mesh-controller.artifacts-gone", Record: "ADR 0571 §4, §13", ToTheWrite: true},
{Module: "artifact-store-tools", Kind: DependsInvoke, What: "seat:mesh-controller.collect", Record: "ADR 0571 §4, §13", ToTheWrite: true},
{Module: "node-engine", Kind: DependsRead, What: "the controller's user list of the bus module's composition", Record: "ADR 0571 §8, §13; ADR 0547 §8", ToTheWrite: true},
}
// EndedBy names, for a declaration no entry keeps, the record that already ended it, so a refusal says where it moved.
// The controller's `grants` is one: ADR 0571 §9 has no manifest read it from L7's delivery, which changes ADR 0567 §10
// and ADR 0547 §5's shadow compare, so neither the assigner's nor the grants part's read of it is kept.
var EndedBy = map[string]string{
DependsRead + " mesh-controller.grants": "ADR 0571 §9: no manifest reads the controller's grants; the grants part's key is the one read",
DependsInvoke + " seat:mesh-controller.hand-act": "ADR 0565 §11: every recorder calls mesh-hand-acts",
DependsInvoke + " seat:mesh-controller.warranted": "ADR 0565 §11: every recorder calls mesh-hand-acts",
DependsInvoke + " seat:mesh-controller.root-free": "ADR 0571 §11: each machine's engine answers root-free (seat:node-engine.root-free@machine)",
DependsInvoke + " seat:mesh-controller.held": "ADR 0571 §2: held is the seat mesh-artifact-sweep's",
}
// keptFor is the list's entry for one dependency, or nil.
func keptFor(d Dependency) *KeptDependency {
for i, k := range KeepList {
if k.Module == d.Module && k.Kind == d.Kind && k.What == d.What {
return &KeepList[i]
}
}
return nil
}
// onController says whether a name `<owner>.<rest>` is the controller's.
func onController(name string) bool { return strings.HasPrefix(name, ControllerSeatName+".") }
// invokesController says whether an invoke names a verb of the controller's seat or a tool of its module. `*` names none:
// it is the console's grant to every tool, judged by no record as a dependency.
func invokesController(invoke string) bool {
if seat, _, ok := SeatInvoke(invoke); ok {
return seat == ControllerSeatName
}
return onController(invoke)
}
// Dependencies are every declaration on the controller in the manifests given, by module, each with the list's entry
// that keeps it. The controller's own manifest is not judged, nor a read of its lease key: it names the lease's holder,
// whoever that is, and parts read it to learn that the write happened (ADR 0571 §13).
func Dependencies(manifests map[string]Manifest) []Dependency {
var out []Dependency
add := func(module, by, kind, what string) {
d := Dependency{Module: module, By: by, Kind: kind, What: what}
if k := keptFor(d); k != nil {
d.Kept, d.ToTheWrite = k.Record, k.ToTheWrite
}
if !slices.Contains(out, d) {
out = append(out, d)
}
}
for name, m := range manifests {
if name == ControllerSeatName || m.Module == ControllerSeatName {
continue
}
for _, r := range m.Reads {
if onController(r) && r != ControllerLease {
add(name, "", DependsRead, r)
}
}
for _, i := range m.Invokes {
if invokesController(i) {
add(name, "", DependsInvoke, i)
}
}
for _, c := range m.Consumes {
if onController(c) {
add(name, "", DependsConsume, c)
}
}
for _, w := range m.Waits {
if onController(w) {
add(name, "", DependsWaits, w)
}
}
for _, u := range m.Uses {
if u == ControllerSeatName {
add(name, "", DependsUses, u)
}
}
for _, a := range m.BusAccounts() {
by := "process " + a.ID
if a.Tools {
by = "tools " + a.ID
}
for _, i := range a.Bus.Invokes {
if invokesController(i) {
add(name, by, DependsInvoke, i)
}
}
}
}
sort.Slice(out, func(i, j int) bool {
a, b := out[i], out[j]
if a.Module != b.Module {
return a.Module < b.Module
}
if a.Kind != b.Kind {
return a.Kind < b.Kind
}
if a.What != b.What {
return a.What < b.What
}
return a.By < b.By
})
return out
}
// Verdict is the lease write's check in one phase.
type Verdict struct {
Phase string `json:"phase"`
Passes bool `json:"passes"`
Reasons []string `json:"reasons"`
}
// Coverage is how much of the mesh's manifests the dependencies were derived from: the manifests read, the modules the
// indexer holds (-1 when its keys could not be listed, which Unread then names), and each manifest that could not be
// read.
type Coverage struct {
Read, Indexed int
Unread []Unread
}
// Unread is one manifest, or one listing of them, that could not be read, and why.
type Unread struct {
Input string `json:"input"`
Why string `json:"why"`
}
// Judge runs the lease write's check over the dependencies (ADR 0571 §13) in one phase: before the lease write's own
// delivery, every dependency must be on the named list; at the write, only the three kept to the write itself may stay.
// It passes only over every module the indexer holds: no manifest read, fewer read than the indexer holds, or one that
// could not be read fails it, since what was not read could hide a dependency. A lease already handed away leaves
// nothing to write.
func Judge(deps []Dependency, phase string, cov Coverage, handedAway bool, leaseSays string) Verdict {
v := Verdict{Phase: phase, Reasons: []string{}}
if handedAway {
v.Reasons = append(v.Reasons, "there is nothing to write: "+leaseSays)
}
switch {
case cov.Read == 0:
v.Reasons = append(v.Reasons, fmt.Sprintf("no manifest was read from the indexer (it holds %d modules): with nothing read, nothing on the controller can be judged absent", max(cov.Indexed, 0)))
case cov.Indexed >= 0 && cov.Read < cov.Indexed:
v.Reasons = append(v.Reasons, fmt.Sprintf("only %d of the %d modules the indexer holds had their manifest read, so what the others declare on the controller is not known", cov.Read, cov.Indexed))
}
for _, d := range deps {
who := d.Module
if d.By != "" {
who += " (" + d.By + ")"
}
switch {
case d.Kept == "":
why := fmt.Sprintf("%s declares %s %s, which no record keeps until the lease write (ADR 0571 §13)", who, d.Kind, d.What)
if ended := EndedBy[d.Kind+" "+d.What]; ended != "" {
why += "; ended by " + ended
}
v.Reasons = append(v.Reasons, why)
case phase == PhaseWrite && !d.ToTheWrite:
v.Reasons = append(v.Reasons, fmt.Sprintf("%s declares %s %s, kept by %s only until the lease write's own delivery, which drops it", who, d.Kind, d.What, d.Kept))
}
}
for _, g := range cov.Unread {
v.Reasons = append(v.Reasons, fmt.Sprintf("%s could not be read (%s), so what it declares on the controller is not known", g.Input, g.Why))
}
v.Passes = len(v.Reasons) == 0
return v
}
// ManifestRefusals is the lease write's check over a repository's manifests, by module name, in one phase (PhaseBefore
// or PhaseWrite): each reason the check gives, none when it passes. Every manifest given counts as read: the caller reads
// them from its own tree, and none given fails it.
func ManifestRefusals(manifests map[string]Manifest, phase string) []string {
return Judge(Dependencies(manifests), phase, Coverage{Read: len(manifests), Indexed: -1}, false, "").Reasons
}
+103
View File
@@ -0,0 +1,103 @@
package catalogue
import (
"slices"
"strings"
"testing"
)
// Issue 774 (ADR 0571 §13, ADR 0582 clause 2): the lease write's check judges every manifest's dependency on the
// controller against the named list, at the write (the three kept to it alone) and before its delivery. Moved with the
// judge from mesh-grants' leasecheck_test.go (TestTheLeaseCheckJudgesEveryDependencyOnTheController's manifest rows and
// TestTheKeepListNamesItsRecordsAndThreeToTheWrite), whose manifests were mesh-grants' fixtures; the conditions row is
// mesh-conditions' manifest at mesh-catalog ce3bedd5 (module.json:31-32).
func TestTheLeaseWritesCheckJudgesEveryDependencyOnTheController(t *testing.T) {
toTheWrite := 0
for _, k := range KeepList {
if !strings.HasPrefix(k.Record, "ADR ") || k.Module == "" || k.What == "" {
t.Errorf("entry %+v names no record, module or declaration", k)
}
if k.ToTheWrite {
toTheWrite++
}
}
if toTheWrite != 3 || len(KeepList) != 3 {
t.Errorf("%d entries, %d kept to the write itself: ADR 0571 §13 keeps the three alone from the write's delivery", len(KeepList), toTheWrite)
}
type want struct {
deps []string // "<module> <kind> <what> <kept by>", kept by the record or "unkept"
write string // a reason the check at the write names, or "" when it passes
before string // a reason the check before the delivery names, or "" when it passes
}
for _, row := range []struct {
name string
manifests map[string]string
want want
}{
// 774/S4/AC2.
{"a read of the lease alone is no dependency, and the controller's own manifest is not judged",
map[string]string{"mod-l": `{"module":"mod-l","version":"1","reads":["mesh-controller.lease"]}`,
"mesh-controller": `{"module":"mesh-controller","version":"1","invokes":["seat:mesh-controller.status"]}`},
want{}},
// 774/S4/AC1, 774/S30/AC1: the keeper's read of the controller's conditions put back is refused in either phase,
// since the lease write's delivery dropped its entry (it was kept by ADR 0564 §9 before).
{"the keeper's read of the controller's conditions, put back, is refused",
map[string]string{"mesh-conditions": `{"module":"mesh-conditions","version":"1","reads":["mesh-controller.conditions","mesh-controller.lease"]}`},
want{deps: []string{"mesh-conditions read mesh-controller.conditions unkept"},
write: "mesh-conditions declares read mesh-controller.conditions, which no record keeps until the lease write",
before: "mesh-conditions declares read mesh-controller.conditions, which no record keeps until the lease write"}},
// 774/S30/AC1: the three kept to the write pass in either phase.
{"the artifact sweep's collect is kept to the write itself",
map[string]string{"artifact-store-tools": `{"module":"artifact-store-tools","version":"1","invokes":["seat:mesh-controller.collect"]}`},
want{deps: []string{"artifact-store-tools invoke seat:mesh-controller.collect kept by ADR 0571 §4, §13"}}},
{"the builder's consume of artifacts-gone is kept to the write itself",
map[string]string{"mesh-delivery-builder": `{"module":"mesh-delivery-builder","version":"1","consumes":["mesh-controller.artifacts-gone"]}`},
want{deps: []string{"mesh-delivery-builder consume mesh-controller.artifacts-gone kept by ADR 0571 §4, §13"}}},
{"a read no record keeps fails both phases, named",
map[string]string{"mod-u": `{"module":"mod-u","version":"1","reads":["mesh-controller.declared-packages"]}`},
want{deps: []string{"mod-u read mesh-controller.declared-packages unkept"},
write: "mod-u declares read mesh-controller.declared-packages, which no record keeps until the lease write",
before: "mod-u declares read mesh-controller.declared-packages, which no record keeps until the lease write"}},
{"a seat verb and a module tool of the controller's, a consume and a wait are each one; every tool (*) is none",
map[string]string{"mod-i": `{"module":"mod-i","version":"1","invokes":["seat:mesh-controller.root-free","mesh-controller.status","*","nats.nats_users"],` +
`"consumes":["mesh-controller.checked","mod-x.thing"],"uses":["mesh-controller"]}`,
"node-tools": `{"module":"node-tools","version":"1","waits":["mesh-controller.checked"]}`},
want{deps: []string{"mod-i consume mesh-controller.checked unkept", "mod-i invoke mesh-controller.status unkept",
"mod-i invoke seat:mesh-controller.root-free unkept", "mod-i uses mesh-controller unkept", "node-tools waits mesh-controller.checked unkept"},
write: "mod-i declares invoke seat:mesh-controller.root-free, which no record keeps until the lease write (ADR 0571 §13); ended by ADR 0571 §11",
before: "node-tools declares waits mesh-controller.checked"}},
{"a process's own invoke of the controller is its module's dependency, named by the process",
map[string]string{"mod-p": `{"module":"mod-p","version":"1","resources":[{"id":"account","type":"user","name":"mod-p"},{"id":"worker","type":"process","run":["./w"],"user":"mod-p",` +
`"bus":{"invokes":["seat:mesh-controller.held"]}}]}`},
want{deps: []string{"mod-p invoke seat:mesh-controller.held unkept"},
write: "mod-p (process worker) declares invoke seat:mesh-controller.held", before: "mod-p (process worker)"}},
{"no manifest fails both phases", map[string]string{}, want{write: "no manifest was read", before: "no manifest was read"}},
} {
manifests := map[string]Manifest{}
for name, raw := range row.manifests {
m, err := Decode([]byte(raw))
if err != nil {
t.Fatalf("%s: %s: %v", row.name, name, err)
}
manifests[name] = m
}
var deps []string
for _, d := range Dependencies(manifests) {
kept := "unkept"
if d.Kept != "" {
kept = "kept by " + d.Kept
}
deps = append(deps, d.Module+" "+d.Kind+" "+d.What+" "+kept)
}
if !slices.Equal(deps, row.want.deps) {
t.Errorf("%s: dependencies %q, want %q", row.name, deps, row.want.deps)
}
for phase, reason := range map[string]string{PhaseWrite: row.want.write, PhaseBefore: row.want.before} {
got := ManifestRefusals(manifests, phase)
if (reason == "") != (len(got) == 0) || !slices.ContainsFunc(got, func(r string) bool { return strings.Contains(r, reason) }) && reason != "" {
t.Errorf("%s: at %s refused %q, want %q", row.name, phase, got, reason)
}
}
}
}
+1
View File
@@ -9,6 +9,7 @@ import (
// The delivery seat answers "what did the mesh's checks say of this pull request?" as a verb of its own, // The delivery seat answers "what did the mesh's checks say of this pull request?" as a verb of its own,
// `checks` (novox/hq ADR 0239): read by repository and a pull request's number or a commit. // `checks` (novox/hq ADR 0239): read by repository and a pull request's number or a commit.
func TestTheDeliverySeatPromisesChecks(t *testing.T) { func TestTheDeliverySeatPromisesChecks(t *testing.T) {
t.Run("the seat mesh-grants promises the lease write", grantsSeatPromisesTheLeaseWrite)
seat, ok := SeatNamed(DeliverySeat) seat, ok := SeatNamed(DeliverySeat)
if !ok { if !ok {
t.Fatal("the delivery seat is not in the set") t.Fatal("the delivery seat is not in the set")
+3 -6
View File
@@ -744,12 +744,9 @@ func TestSSHIsOpenFromTheMeshEvenWhenNothingIsAssigned(t *testing.T) {
if strings.Contains(nft, "\t\ttcp dport 22 accept") { if strings.Contains(nft, "\t\ttcp dport 22 accept") {
t.Fatalf("ssh is open to everywhere on a machine that faces inward only:\n%s", nft) t.Fatalf("ssh is open to everywhere on a machine that faces inward only:\n%s", nft)
} }
} // And from outside as well, on a machine that faces outward — because that is the way in when the
// private network is the thing that broke.
// And from outside as well, on a machine that faces outward — because that is the way in when the nft = AsNftables(nil, []string{"198.51.100.2"}, true, nil, nil, "mesh0")
// private network is the thing that broke.
func TestSSHIsOpenFromOutsideOnAMachineThatFacesIt(t *testing.T) {
nft := AsNftables(nil, []string{"198.51.100.2"}, true, nil, nil, "mesh0")
if !strings.Contains(nft, "\t\ttcp dport 22 accept") { if !strings.Contains(nft, "\t\ttcp dport 22 accept") {
t.Fatalf("a machine reachable from outside does not answer ssh there:\n%s", nft) t.Fatalf("a machine reachable from outside does not answer ssh there:\n%s", nft)
} }
+53
View File
@@ -0,0 +1,53 @@
package catalogue
// **The lease write, a verb of the seat `mesh-grants`** (novox/hq ADR 0582 clause 1, issue 774): the grants part
// holds the seat once for the mesh and, on the operator's warrant, hands the controller's lease to itself and back.
// It serves two verbs and says three events, none carrying a secret:
//
// - lease-handed: the lease names mesh-grants, written on the operator's warrant (clause 3);
// - lease-altered: the lease key or its bucket's expiry differs from what mesh-grants wrote (clause 4);
// - removal-failed: the day-later removal of the controller's consumer or stream failed (clause 6).
// GrantsSeat is the seat the grants part (mesh-grants) holds.
const GrantsSeat = "mesh-grants"
// The seat's verbs.
const (
GrantsVerbLeaseWrite = "lease-write"
GrantsVerbLeaseHandBack = "lease-hand-back"
)
// The seat's events.
const (
GrantsEventLeaseHanded = "lease-handed"
GrantsEventLeaseAltered = "lease-altered"
GrantsEventRemovalFailed = "removal-failed"
)
// grantsDecision is the record that made the seat.
const grantsDecision = "novox/hq ADR 0582"
// grantsEvents are the seat's events, in the order the write meets them.
func grantsEvents() []string {
return []string{GrantsEventLeaseHanded, GrantsEventLeaseAltered, GrantsEventRemovalFailed}
}
// grantsVerbs are the seat's verbs (ADR 0582 clauses 1, 2, 3, 5).
func grantsVerbs() []Verb {
return []Verb{
{Name: GrantsVerbLeaseWrite, Description: "Hand the controller's lease to mesh-grants, kept for good. Refused, " +
"naming the check, until the lease check at phase write passes and every condition it does not judge reads " +
"green. Otherwise the operator is asked once; on the warrant the checks are read again, the controller's " +
"service is stopped, the bus's user list is composed without the controller's write on the lease, the lease's " +
"expiry is removed and the key holder names mesh-grants, said as lease-handed and recorded as a hand act. A no " +
"or an expiry of the ask acts on nothing.",
Input: schema(map[string]string{"why": "why the lease is handed now, shown to the operator and kept"},
[]string{"why"})},
{Name: GrantsVerbLeaseHandBack, Description: "Give the lease back to the controller, for a day after the write: " +
"asked and warranted the same way, it restores the lease's fifteen-second expiry, deletes the key holder, " +
"gives the controller its rights back once the list is loaded, and starts the controller. Refused once the " +
"day-later removal ran: the controller would start without its consumer and stream.",
Input: schema(map[string]string{"why": "why the lease goes back, shown to the operator and kept"},
[]string{"why"})},
}
}
+68
View File
@@ -0,0 +1,68 @@
package catalogue
import (
"reflect"
"strings"
"testing"
)
// The seat mesh-grants (novox/hq ADR 0582 clause 1, issue 774, 774/S1/AC2): its holder serves lease-write and
// lease-hand-back, both a condition of holding, and nothing else; the seat says lease-handed, lease-altered and
// removal-failed. A case of TestTheDeliverySeatPromisesChecks, the seat-verb test, within the test budget.
func grantsSeatPromisesTheLeaseWrite(t *testing.T) {
seat, ok := SeatNamed(GrantsSeat)
if !ok {
t.Fatal("the seat mesh-grants is not in the set")
}
if seat.Scope != ScopeMesh || seat.Delivers != "" {
t.Errorf("mesh-grants is at scope %q delivering %q, want mesh and nothing", seat.Scope, seat.Delivers)
}
if got, want := VerbNames(seat.Serves), []string{"lease-write", "lease-hand-back"}; !reflect.DeepEqual(got, want) {
t.Errorf("mesh-grants serves %v, want %v", got, want)
}
if want := []string{"lease-handed", "lease-altered", "removal-failed"}; !reflect.DeepEqual(seat.Emits, want) {
t.Errorf("mesh-grants says %v, want %v", seat.Emits, want)
}
for _, v := range seat.Serves {
if v.Optional {
t.Errorf("%s is optional: the holder ships with the seat, so every verb is a condition of holding", v.Name)
}
props, _ := v.Input["properties"].(map[string]any)
if req, _ := v.Input["required"].([]string); len(props) != 1 || !reflect.DeepEqual(req, []string{"why"}) {
t.Errorf("%s takes %v requiring %v, want only a why", v.Name, props, req)
}
}
claim := func(serves ...string) Manifest {
return Manifest{Module: "mesh-grants", Claims: []Claim{{Name: GrantsSeat, Scope: ScopeMesh, Serves: serves}}}
}
for _, c := range []struct {
name string
m Manifest
refuse string // empty: held
}{
{"both verbs", claim("lease-write", "lease-hand-back"), ""},
{"without the hand-back", claim("lease-write"), "lease-hand-back"},
{"a verb the seat does not promise", claim("lease-write", "lease-hand-back", "lease-take"), "lease-take"},
{"at node scope", Manifest{Module: "mesh-grants", Claims: []Claim{{Name: GrantsSeat, Scope: ScopeNode,
Serves: []string{"lease-write", "lease-hand-back"}}}}, "scope"},
} {
err := CanHold(c.m, seat)
switch {
case c.refuse == "" && err != nil:
t.Errorf("%s: refused: %v", c.name, err)
case c.refuse != "" && (err == nil || !strings.Contains(err.Error(), c.refuse)):
t.Errorf("%s: %v, want a refusal naming %q", c.name, err, c.refuse)
}
}
// No other seat changed: the set is the one before it with mesh-grants added beside mesh-hand-acts.
var names []string
for _, s := range DefaultSeats() {
names = append(names, s.Name)
}
at := strings.Index(strings.Join(names, " "), HandActsSeat+" "+GrantsSeat+" ")
if at < 0 {
t.Errorf("mesh-grants does not follow mesh-hand-acts in the seat set: %v", names)
}
}
+3
View File
@@ -302,6 +302,9 @@ var defaultSeats = append([]Seat{
// policy and the bus step at the assigner, and the hand-act log. // policy and the bus step at the assigner, and the hand-act log.
{Name: WantsSeat, Scope: ScopeMesh, Decision: controlRecordsDecision, Emits: []string{"policy-changed"}, Serves: wantsVerbs()}, {Name: WantsSeat, Scope: ScopeMesh, Decision: controlRecordsDecision, Emits: []string{"policy-changed"}, Serves: wantsVerbs()},
{Name: HandActsSeat, Scope: ScopeMesh, Decision: controlRecordsDecision, Emits: []string{"act-recorded"}, Serves: handActsVerbs()}, {Name: HandActsSeat, Scope: ScopeMesh, Decision: controlRecordsDecision, Emits: []string{"act-recorded"}, Serves: handActsVerbs()},
// The lease write (novox/hq ADR 0582 clause 1, grants_seat.go): the grants part hands the controller's lease to
// itself and back on the operator's warrant.
{Name: GrantsSeat, Scope: ScopeMesh, Decision: grantsDecision, Emits: grantsEvents(), Serves: grantsVerbs()},
// The artifact sweep (novox/hq ADR 0571 clause 2, artifact_sweep_seat.go). // The artifact sweep (novox/hq ADR 0571 clause 2, artifact_sweep_seat.go).
{Name: ArtifactSweepSeat, Scope: ScopeMesh, Decision: "novox/hq ADR 0571", Emits: []string{"artifacts-gone"}, {Name: ArtifactSweepSeat, Scope: ScopeMesh, Decision: "novox/hq ADR 0571", Emits: []string{"artifacts-gone"},
Serves: artifactSweepVerbs()}, Serves: artifactSweepVerbs()},
+14 -1
View File
@@ -242,9 +242,22 @@ func TestRoundTrip(t *testing.T) {
t.Fatal(err) t.Fatal(err)
} }
back, err := DecodeGrantsUserList(raw) back, err := DecodeGrantsUserList(raw)
if err != nil || back.Digest != digest || back.LeftOut == nil || back.Follow != FollowGrants { if err != nil || back.Digest != digest || back.LeftOut == nil || back.Follow != FollowGrants || back.LeaseHanded {
t.Fatalf("decoded %+v, %v", back, err) t.Fatalf("decoded %+v, %v", back, err)
} }
// ADR 0582 clause 3 (issue 774, 774/S3): a key written before the flag existed reads not handed; one written with
// it reads handed.
if old, err := DecodeGrantsUserList([]byte(`{"v":1,"follow":"grants","digest":"","object":"","sealed_to":"",` +
`"node":"novox","revision":3,"users":40,"left_out":[],"at":"2026-10-30T10:00:00Z"}`)); err != nil || old.LeaseHanded {
t.Errorf("a key from before the lease write decodes %+v, %v", old, err)
}
g.LeaseHanded = true
if raw, err := EncodeGrantsUserList(g); err != nil || !bytes.Contains(raw, []byte(`"lease-handed":true`)) {
t.Errorf("the handed flag is written as %s, %v", raw, err)
} else if back, err := DecodeGrantsUserList(raw); err != nil || !back.LeaseHanded {
t.Errorf("the handed flag reads back %+v, %v", back, err)
}
g.LeaseHanded = false
for _, bad := range []GrantsUserList{{Follow: "both", Digest: digest, Object: g.Object}, for _, bad := range []GrantsUserList{{Follow: "both", Digest: digest, Object: g.Object},
{Follow: FollowGrants, Digest: digest, Object: "other"}, {Follow: FollowGrants, Digest: "md5:x", Object: "md5:x"}} { {Follow: FollowGrants, Digest: digest, Object: "other"}, {Follow: FollowGrants, Digest: "md5:x", Object: "md5:x"}} {
if _, err := EncodeGrantsUserList(bad); err == nil { if _, err := EncodeGrantsUserList(bad); err == nil {
-25
View File
@@ -109,31 +109,6 @@ type PlacementProposalChange struct {
Whole *Placement `json:"whole,omitempty"` Whole *Placement `json:"whole,omitempty"`
} }
// BeforeLease is why the change cannot be proposed before the lease write, or "" (ADR 0567 §8): until then the holder
// keeps the controller's assignments in step through its verbs assign, unassign, pin and unpin, so only those are
// proposable — a module placed or taken off with no host port or binding, a pin set or removed, a module's config values
// and the seed. A host port, a binding, a holding, a decided field and a config layer's places or accesses wait for the
// lease write, which the refusal names.
func (c PlacementProposalChange) BeforeLease() string {
const lease = "which the controller's verbs cannot take from a caller, so it waits for the lease write (ADR 0567 §8)"
switch c.Op {
case OpPlace:
if len(c.Ports)+len(c.Bindings) > 0 {
return fmt.Sprintf("the place of %s on %s names host ports or bindings, %s", c.Module, c.Node, lease)
}
case OpTakeOff, OpPin, OpUnpin, OpSeed:
case OpConfig:
// A module's config values are the state's alone (§13); its layers' places and accesses are the controller's
// until the lease write.
if c.MeshWide != nil || c.Layer != nil {
return fmt.Sprintf("the config of %s on %s sets a layer's places or accesses, %s", c.Module, c.Node, lease)
}
default:
return fmt.Sprintf("a change %s of %s is no assignment or pin, %s", c.Op, c.Node, lease)
}
return ""
}
func (c PlacementProposalChange) check() error { func (c PlacementProposalChange) check() error {
if err := checkName("node", c.Node); err != nil { if err := checkName("node", c.Node); err != nil {
return err return err
-13
View File
@@ -651,19 +651,6 @@ func TestPlacement(t *testing.T) {
t.Errorf("%s: %v, want a finding naming %q", name, f, c.want) t.Errorf("%s: %v, want a finding naming %q", name, f, c.want)
} }
} }
for change, waits := range map[*PlacementProposalChange]bool{
{Op: OpPlace, Node: "g14", Module: "x"}: false, {Op: OpTakeOff, Node: "g14", Module: "x"}: false,
{Op: OpPin, Node: "g14"}: false, {Op: OpUnpin, Node: "g14"}: false, {Op: OpSeed, Node: "g14"}: false,
{Op: OpPlace, Node: "g14", Module: "x", Ports: map[string]int{"80": 80}}: true,
{Op: OpPlace, Node: "g14", Module: "x", Bindings: map[string]Provider{"p": {}}}: true,
{Op: OpHold, Node: "g14"}: true, {Op: OpDecide, Node: "g14"}: true,
{Op: OpConfig, Node: "g14", Values: map[string]any{"puid": 1}}: false, {Op: OpConfig, Node: "g14", MeshValues: map[string]any{"a": 1}}: false,
{Op: OpConfig, Node: "g14", Layer: map[string]any{"places": 1}}: true, {Op: OpConfig, Node: "g14", MeshWide: map[string]any{"accesses": 1}}: true,
} {
if got := change.BeforeLease(); (got != "") != waits || (waits && !strings.Contains(got, "lease write")) {
t.Errorf("%+v before the lease write: %q", *change, got)
}
}
const id = "p-20261028t183000-1a2b3c" const id = "p-20261028t183000-1a2b3c"
if got := NewProposalID(time.Date(2026, 10, 28, 18, 30, 0, 0, time.UTC)); !strings.HasPrefix(got, "p-20261028t183000-") || CheckProposalID(got) != nil { if got := NewProposalID(time.Date(2026, 10, 28, 18, 30, 0, 0, time.UTC)); !strings.HasPrefix(got, "p-20261028t183000-") || CheckProposalID(got) != nil {
+4
View File
@@ -65,6 +65,10 @@ type GrantsUserList struct {
// LeftOut are the users the list leaves out for want of a hash in the vault. // LeftOut are the users the list leaves out for want of a hash in the vault.
LeftOut []string `json:"left_out"` LeftOut []string `json:"left_out"`
At time.Time `json:"at"` At time.Time `json:"at"`
// LeaseHanded says the controller's lease is handed to the grants part (novox/hq ADR 0582 clause 3, ADR 0571 clause
// 8): from then on the bus node follows this list alone and never falls back to the controller's. A key written
// before it reads false.
LeaseHanded bool `json:"lease-handed,omitempty"`
} }
// EncodeGrantsUserList is g as JSON at Version; it refuses a value the bus node could not follow. // EncodeGrantsUserList is g as JSON at Version; it refuses a value the bus node could not follow.
+7 -15
View File
@@ -5,7 +5,6 @@ import (
"fmt" "fmt"
"time" "time"
"github.com/nats-io/nats.go"
"github.com/nats-io/nats.go/jetstream" "github.com/nats-io/nats.go/jetstream"
"git.novox.be/novox/mesh-sdk/go/membership" "git.novox.be/novox/mesh-sdk/go/membership"
@@ -17,10 +16,10 @@ import (
// not a membership is handed to onBad (nil to ignore it) and skipped. // not a membership is handed to onBad (nil to ignore it) and skipped.
// //
// It reads the membership from the grants part's state memberships, its own key (novox/hq ADR 0571 clause 7), // It reads the membership from the grants part's state memberships, its own key (novox/hq ADR 0571 clause 7),
// watched so the value there now comes first and each change after it; and it hears the subject the grants part // watched so the value there now comes first and each change after it. Nothing else: the subject the grants part
// publishes on until the lease write. The assignments stream is no longer read: the bus module's tools delete it // published on is no longer heard (ADR 0571 clause 10, issue 774), since it stops publishing at the lease write, and
// at the lease write (research 072 row 33). A state not there yet, or not readable, is told to onBad and the // the assignments stream is no longer read (research 072 row 33). A state not there or not readable is a refusal to
// subject is followed alone. The returned error is a refusal to follow at all. // follow, returned: there is nothing else to follow.
func (c *Conn) FollowMembership(apply func(membership.Membership), onBad func(error)) error { func (c *Conn) FollowMembership(apply func(membership.Membership), onBad func(error)) error {
if c.account.Node == "" || c.account.Module == "" { if c.account.Node == "" || c.account.Module == "" {
return fmt.Errorf("the account names no node or module, so it has no membership") return fmt.Errorf("the account names no node or module, so it has no membership")
@@ -38,27 +37,20 @@ func (c *Conn) FollowMembership(apply func(membership.Membership), onBad func(er
} }
apply(issued) apply(issued)
} }
subject := membership.Subject(c.account.Node, c.account.Module)
if _, err := c.nc.Subscribe(subject, func(m *nats.Msg) { take(m.Data) }); err != nil {
return fmt.Errorf("cannot follow %s: %w", subject, err)
}
key := membership.GrantsKey(c.account.Node, c.account.Module) key := membership.GrantsKey(c.account.Node, c.account.Module)
js, err := jetstream.New(c.nc) js, err := jetstream.New(c.nc)
if err != nil { if err != nil {
bad(fmt.Errorf("cannot read %s of %s (%v); following %s alone", key, membership.GrantsBucket, err, subject)) return fmt.Errorf("cannot read %s of %s: %w", key, membership.GrantsBucket, err)
return nil
} }
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel() defer cancel()
kv, err := js.KeyValue(ctx, membership.GrantsBucket) kv, err := js.KeyValue(ctx, membership.GrantsBucket)
if err != nil { if err != nil {
bad(fmt.Errorf("cannot read %s of %s yet (%v); following %s alone", key, membership.GrantsBucket, err, subject)) return fmt.Errorf("cannot read %s of %s: %w", key, membership.GrantsBucket, err)
return nil
} }
w, err := kv.Watch(context.Background(), key) w, err := kv.Watch(context.Background(), key)
if err != nil { if err != nil {
bad(fmt.Errorf("cannot watch %s of %s (%v); following %s alone", key, membership.GrantsBucket, err, subject)) return fmt.Errorf("cannot watch %s of %s: %w", key, membership.GrantsBucket, err)
return nil
} }
go func() { go func() {
for e := range w.Updates() { for e := range w.Updates() {
+41 -9
View File
@@ -3,14 +3,20 @@
package process package process
import ( import (
"context"
"strings"
"testing" "testing"
"time" "time"
"github.com/nats-io/nats.go/jetstream"
"git.novox.be/novox/mesh-sdk/go/membership" "git.novox.be/novox/mesh-sdk/go/membership"
) )
// A process connected pinned to a test bus hears the membership published for its node and module, and // A process connected pinned to a test bus follows the membership the grants part writes for its node and module in
// ignores what is not one (novox/hq issue 1633, AC2). // its state memberships, and reports what is not one (novox/hq issue 1633, AC2); it no longer hears mesh.assignment,
// where the grants part stops publishing at the lease write (ADR 0571 clause 10; issue 774, 774/S5/AC1). Without the
// state it refuses to follow, naming it.
func TestFollowMembershipHearsWhatIsIssuedToTheAccount(t *testing.T) { func TestFollowMembershipHearsWhatIsIssuedToTheAccount(t *testing.T) {
url, fingerprint := selfSignedServer(t) url, fingerprint := selfSignedServer(t)
account := mailSenderAccount(t, url, fingerprint) account := mailSenderAccount(t, url, fingerprint)
@@ -20,26 +26,52 @@ func TestFollowMembershipHearsWhatIsIssuedToTheAccount(t *testing.T) {
} }
defer conn.Close() defer conn.Close()
got := make(chan membership.Membership, 1) got := make(chan membership.Membership, 4)
bad := make(chan error, 4) bad := make(chan error, 4)
if err := conn.FollowMembership(func(m membership.Membership) { got <- m }, func(e error) { bad <- e }); err != nil { follow := func() error {
return conn.FollowMembership(func(m membership.Membership) { got <- m }, func(e error) { bad <- e })
}
if err := follow(); err == nil || !strings.Contains(err.Error(), membership.GrantsBucket) {
t.Fatalf("followed with no state memberships: %v", err)
}
js, err := jetstream.New(conn.nc)
if err != nil {
t.Fatal(err) t.Fatal(err)
} }
if err := conn.nc.Publish(membership.Subject("novox", "mesh-mail"), []byte("not json")); err != nil { ctx := context.Background()
kv, err := js.CreateKeyValue(ctx, jetstream.KeyValueConfig{Bucket: membership.GrantsBucket})
if err != nil {
t.Fatal(err) t.Fatal(err)
} }
if err := conn.nc.Publish(membership.Subject("novox", "mesh-mail"), []byte(`{"node":"novox","module":"mesh-mail","mesh":["10.0.0.1"]}`)); err != nil { key := membership.GrantsKey("novox", "mesh-mail")
if _, err := kv.Put(ctx, key, []byte("not json")); err != nil {
t.Fatal(err)
}
if err := follow(); err != nil {
t.Fatal(err)
}
// Published on the subject, as the grants part did until the write: not heard.
if err := conn.nc.Publish(membership.Subject("novox", "mesh-mail"), []byte(`{"node":"novox","module":"mesh-mail","mesh":["10.0.0.9"]}`)); err != nil {
t.Fatal(err)
}
if _, err := kv.Put(ctx, key, membership.EncodeGrantsMembership(membership.Membership{Node: "novox", Module: "mesh-mail",
Mesh: []string{"10.0.0.1"}})); err != nil {
t.Fatal(err) t.Fatal(err)
} }
select { select {
case m := <-got: case m := <-got:
if m.Module != "mesh-mail" || len(m.Mesh) != 1 { if m.Module != "mesh-mail" || len(m.Mesh) != 1 || m.Mesh[0] != "10.0.0.1" {
t.Fatalf("membership: %+v", m) t.Fatalf("membership: %+v", m)
} }
case <-time.After(5 * time.Second): case <-time.After(5 * time.Second):
t.Fatal("no membership heard") t.Fatal("no membership followed")
}
select {
case m := <-got:
t.Errorf("a second membership was applied, from the subject: %+v", m)
case <-time.After(300 * time.Millisecond):
} }
if len(bad) == 0 { if len(bad) == 0 {
t.Error("the body that is not a membership was not reported") t.Error("the value that is not a membership was not reported")
} }
} }