Compose the lease write's cut, so a controller started after the write fails on the lease
ADR 0582 clause 3 composes the bus's user list without the controller's write on the lease key and update of the lease's bucket. Records.LeaseCut, set by the grants part from the write's second step, drops the controller's $KV grant on the lease, denies it the bucket's update, purge, delete and message delete (its $JS.API.> reaches them), and lets the grants part write the key holder alone. The bus module's tools account may read and update the lease's bucket, for the expiry tool the write and the hand-back call. Without the cut the lists equal the controller's golden. Tests: rows added to TestTheCopiedComposerComposesAsTheControllers' tables (the cut on and off; the tools account's lease grants); no new test function. (issue 774, 774/S2) Claude-Session: https://claude.ai/code/session_01NcbgfJ9Gum47zkFKWRaovL
This commit is contained in:
@@ -636,6 +636,48 @@ func TestTheCopiedComposerComposesAsTheControllers(t *testing.T) {
|
||||
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.
|
||||
healthChecks(t)
|
||||
q := ConsumerConfig(QueueConsumer(Principal{Kind: KindProcess, Node: "a", Module: "m", Process: "p"},
|
||||
@@ -743,7 +785,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",
|
||||
"$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.
|
||||
"$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{
|
||||
// The keeper writes no findings and no attempts; no observer writes the keeper's buckets (clause 2).
|
||||
@@ -786,8 +830,13 @@ func healthChecks(t *testing.T) {
|
||||
// outside the bus module).
|
||||
"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",
|
||||
"$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"},
|
||||
"$JS.API.STREAM.DELETE.EVENTS", "$JS.API.STREAM.PURGE.ASSIGNMENTS", "$JS.API.STREAM.DELETE.ASSIGNMENTS_X",
|
||||
// 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.shop.nats-tools": {"$JS.API.CONSUMER.DELETE.DEAD_LETTER_NOTICES.controller", "$KV.mesh-conditions_conditions.x", "$JS.API.STREAM.DELETE.ASSIGNMENTS"},
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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
|
||||
// (novox/hq ADR 0564 Limits 4): its whole authority is then BusRemovalGrants of them.
|
||||
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
|
||||
// (deadLetterSourcesOf, novox/hq ADR 0564 clause 5): never a bucket's.
|
||||
DeadLetterSources []string
|
||||
@@ -668,6 +671,9 @@ func PermissionsFor(p Principal) (Permissions, error) {
|
||||
if err != nil {
|
||||
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)
|
||||
if err := CheckWriters(p, pub); err != nil {
|
||||
return Permissions{}, err
|
||||
@@ -801,6 +807,9 @@ func PermissionsFor(p Principal) (Permissions, error) {
|
||||
if bucket == delivery.HaveBucket {
|
||||
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+".>")
|
||||
}
|
||||
// **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)...)
|
||||
// And its own object bucket, where it writes the user list sealed to the bus node (ADR 0547 clause 7).
|
||||
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).
|
||||
if p.Module == ViewModule {
|
||||
@@ -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.
|
||||
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 {
|
||||
for _, only := range ControllerOnly() {
|
||||
for _, a := range pub {
|
||||
|
||||
@@ -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
|
||||
// controller keeps it; a module absent has announced nothing yet. Nil where none was read.
|
||||
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.
|
||||
@@ -157,7 +162,7 @@ type Records struct {
|
||||
// correct.
|
||||
func Users(r Records) ([]Principal, error) {
|
||||
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)
|
||||
logs := DeclaredLogs(r)
|
||||
@@ -246,6 +251,7 @@ func Users(r Records) ([]Principal, error) {
|
||||
}
|
||||
if d.Module == GrantsModule {
|
||||
out[len(out)-1].CreatesStreams = declaredStreamsOf(r)
|
||||
out[len(out)-1].LeaseCut = r.LeaseCut
|
||||
grants = len(out) - 1
|
||||
}
|
||||
// **The vault's holder asks the desks and the issuing providers itself** (issue 723), under the
|
||||
|
||||
@@ -43,7 +43,7 @@ func createsDeclared(p Principal, pattern string) bool {
|
||||
if isController(p, pattern) {
|
||||
return true
|
||||
}
|
||||
if removesAssignmentsStream(p, pattern) {
|
||||
if removesAssignmentsStream(p, pattern) || setsLeaseExpiry(p, pattern) {
|
||||
return true
|
||||
}
|
||||
name, ok := strings.CutPrefix(pattern, "$JS.API.STREAM.CREATE.")
|
||||
@@ -158,7 +158,8 @@ var WritersTable = []WriterRow{
|
||||
// subject is this machine's engine's alone.
|
||||
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,
|
||||
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.
|
||||
{State: "the merges mesh-delivery announced", Writer: "controller (lease holder)",
|
||||
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.>",
|
||||
"$JS.API.CONSUMER.DURABLE.CREATE.>"},
|
||||
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",
|
||||
Others: "the controller asks",
|
||||
Subjects: []string{"mesh.seat.node-build-agent.event.built", "mesh.seat.mesh-build-machine.event.built"},
|
||||
|
||||
Reference in New Issue
Block a user