grants-serve: a process's serves compose its call and tool subscribes and, for a chunked verb, its chunk publish and ack subscribe
Replaces A-60's grant of chunk subjects to the bundle's principal. Builds: design 47 G A-107, A-109, A-110 Claude-Session: https://claude.ai/code/session_01NcbgfGKaxw984XyAPfLm4h
This commit is contained in:
@@ -59,7 +59,7 @@ func TestTheCopiedComposerComposesAsTheControllers(t *testing.T) {
|
||||
t.Run("every node's compositions are read by their named readers alone and written by their node", everyNodesCompositions)
|
||||
t.Run("a module's ledger is written by its ledger account alone", aLedgerIsWrittenByItsLedgerAccountAlone)
|
||||
t.Run("the bus module's process alone keeps every module's bus state and serves the broker's verbs", theBusModulesProcessKeepsBusState)
|
||||
t.Run("chunk subjects go to the principal that answers the verb, and the reverse to an account naming it", chunkSubjectsFollowTheAnswerer)
|
||||
t.Run("a process serving a verb is granted its calls and chunk subjects, and the reverse goes to an account naming it", chunkSubjectsFollowTheAnswerer)
|
||||
// ADR 0567 §3, §9: a node reads every key of the seat's assignments and its candidates (it composes with the whole
|
||||
// proposal laid over), and none of its open proposals; it writes neither.
|
||||
reads := strings.Join(NodeComposeReads("laptop"), " ")
|
||||
@@ -1392,11 +1392,11 @@ func chunkSubjectsFollowTheAnswerer(t *testing.T) {
|
||||
if len(chunked) != 1 || chunked[0] != (ChunkVerb{Seat: meshbroker.Seat, Verb: "snapshot-get"}) {
|
||||
t.Fatalf("the seat definitions' chunked verbs = %v, want the broker's snapshot-get alone", chunked)
|
||||
}
|
||||
chunks, acks := "mesh.seat.mesh-broker.chunk.snapshot-get.>", "mesh.seat.mesh-broker.ack.snapshot-get.>"
|
||||
chunks, acks, servedAcks := "mesh.seat.mesh-broker.chunk.snapshot-get.>", "mesh.seat.mesh-broker.ack.snapshot-get.>", "mesh.seat.mesh-broker.ack.snapshot-get.*.*"
|
||||
r := Records{Nodes: []string{"a"}, Chunked: chunked, Assigned: map[string][]Declared{"a": {
|
||||
{Module: "node-tools", ToolRunner: true},
|
||||
{Module: "nats", SnapshotsTheBus: true, Holds: []Seat{{Name: meshbroker.Seat, Serves: []string{"list", "snapshot-get"}}},
|
||||
Processes: []ProcessPrincipal{{Process: "keeper", Keeps: true}}},
|
||||
Processes: []ProcessPrincipal{{Process: "keeper", Keeps: true, Serves: []string{"seat:mesh-broker.list", "seat:mesh-broker.snapshot-get"}}}},
|
||||
{Module: "recovery", Invokes: []string{"seat:mesh-broker.snapshot-get"}, Processes: []ProcessPrincipal{{Process: "recover", Invokes: []string{"seat:mesh-broker.snapshot-get"}}}},
|
||||
{Module: "admin-ish", Invokes: []string{"*"}},
|
||||
{Module: "mod-x", Streams: []ModuleStream{{Module: "mod-x", Name: "SECRETS", Events: []string{"sealed"}, Restricted: true},
|
||||
@@ -1415,8 +1415,12 @@ func chunkSubjectsFollowTheAnswerer(t *testing.T) {
|
||||
perms[u.Username()] = p
|
||||
}
|
||||
has := func(list []string, s string) bool { return slices.Contains(list, s) }
|
||||
if runner := perms["a.node-tools"]; !has(runner.Publish, chunks) || !has(runner.Subscribe, acks) {
|
||||
t.Errorf("the tool runner, answering the verb for the bus module's tools bundle, may not send chunks and hear acks: %v / %v", runner.Publish, runner.Subscribe)
|
||||
if runner := perms["a.node-tools"]; has(runner.Publish, chunks) || has(runner.Subscribe, servedAcks) {
|
||||
t.Errorf("the tool runner is granted chunk subjects: %v / %v", runner.Publish, runner.Subscribe)
|
||||
}
|
||||
if k := perms["a.nats.keeper"]; !has(k.Publish, chunks) || !has(k.Subscribe, servedAcks) || !has(k.Subscribe, "mesh.seat.mesh-broker.call.snapshot-get.*") ||
|
||||
!has(k.Subscribe, "mesh.seat.mesh-broker.tool.snapshot-get") || !has(k.Subscribe, "mesh.seat.mesh-broker.tool.list") {
|
||||
t.Errorf("the keeper process, serving the verbs, may not answer them and send chunks: %v / %v", k.Publish, k.Subscribe)
|
||||
}
|
||||
if rec := perms["a.recovery"]; !has(rec.Subscribe, chunks) || !has(rec.Publish, acks) || has(rec.Publish, chunks) {
|
||||
t.Errorf("the account naming the verb may not hear chunks and send acks, or may send chunks: %v / %v", rec.Publish, rec.Subscribe)
|
||||
@@ -1424,7 +1428,7 @@ func chunkSubjectsFollowTheAnswerer(t *testing.T) {
|
||||
if rec := perms["a.recovery.recover"]; !has(rec.Subscribe, chunks) || !has(rec.Publish, acks) {
|
||||
t.Errorf("the process naming the verb may not hear chunks and send acks")
|
||||
}
|
||||
for _, u := range []string{"a.admin-ish", "a.nats.keeper"} {
|
||||
for _, u := range []string{"a.admin-ish", "a.nats"} {
|
||||
if p := perms[u]; has(p.Subscribe, chunks) || has(p.Publish, chunks) || has(p.Publish, acks) {
|
||||
t.Errorf("%s, naming no chunked verb, is granted chunk subjects: %v / %v", u, p.Publish, p.Subscribe)
|
||||
}
|
||||
|
||||
@@ -10,7 +10,7 @@ import (
|
||||
|
||||
// **The chunk subjects of a verb that sends a file** (design 47 G §681, A-45, A-60): a verb whose seat definition says `chunks: true`
|
||||
// has its answer's file sent in chunks on `mesh.seat.<seat>.chunk.<verb>.<call>.<n>`, each acknowledged by the caller on
|
||||
// `mesh.seat.<seat>.ack.<verb>.<call>.<n>`. They are granted to the principal that answers the verb (publish the chunks, subscribe to
|
||||
// `mesh.seat.<seat>.ack.<verb>.<call>.<n>`. They are granted to the process that serves the verb (publish the chunks, subscribe to
|
||||
// the acknowledgements) and, reversed, to an account that names the verb by name in `invokes`; a `*` entry grants none.
|
||||
|
||||
// ChunkVerb is one verb of a seat whose definition says it sends chunks.
|
||||
@@ -38,29 +38,20 @@ func ChunkVerbsOf(defs map[string]seatdef.Definition) []ChunkVerb {
|
||||
func chunkSubject(c ChunkVerb) string { return "mesh.seat." + c.Seat + ".chunk." + c.Verb + ".>" }
|
||||
func ackSubject(c ChunkVerb) string { return "mesh.seat." + c.Seat + ".ack." + c.Verb + ".>" }
|
||||
|
||||
// chunkGrants are the chunk subjects p is granted: as the principal that answers a chunked verb, the publish of its chunks and
|
||||
// the subscription to their acknowledgements (a module's own account for a seat it holds, the tool runner's for each seat a
|
||||
// carried module holds; a process serves nothing, ADR 0436 §3), and as an account that names a chunked verb by name in `invokes`,
|
||||
// the subscription to the chunks and the publish of the acknowledgements.
|
||||
// ackServed is what the process that answers a chunked verb subscribes: the acknowledgements of every call and chunk (A-109).
|
||||
func ackServed(c ChunkVerb) string { return "mesh.seat." + c.Seat + ".ack." + c.Verb + ".*.*" }
|
||||
|
||||
// chunkGrants are the chunk subjects p is granted: as a process that serves a chunked verb (`serves`, design 47 G A-109), the
|
||||
// publish of its chunks and the subscription to their acknowledgements, and as an account that names a chunked verb by name in
|
||||
// `invokes`, the subscription to the chunks and the publish of the acknowledgements. The chunks of a snapshot are a transfer on
|
||||
// the core bus: no stream, and any caller of the verb hears them.
|
||||
func chunkGrants(p Principal) (pub, sub []string) {
|
||||
if len(p.Chunked) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
var served []Seat
|
||||
switch p.Kind {
|
||||
case KindModule:
|
||||
served = p.Holds
|
||||
case KindNodeTools:
|
||||
for _, d := range p.Carries {
|
||||
served = append(served, d.Holds...)
|
||||
}
|
||||
}
|
||||
for _, c := range p.Chunked {
|
||||
for _, s := range served {
|
||||
if s.Name == c.Seat && slices.Contains(s.Serves, c.Verb) {
|
||||
pub, sub = append(pub, chunkSubject(c)), append(sub, ackSubject(c))
|
||||
break
|
||||
}
|
||||
if p.Kind == KindProcess && slices.Contains(p.AnswersSeat, "seat:"+c.Seat+"."+c.Verb) {
|
||||
pub, sub = append(pub, chunkSubject(c)), append(sub, ackServed(c))
|
||||
}
|
||||
if slices.Contains(p.Invokes, "seat:"+c.Seat+"."+c.Verb) {
|
||||
sub, pub = append(sub, chunkSubject(c)), append(pub, ackSubject(c))
|
||||
|
||||
@@ -189,6 +189,8 @@ type Principal struct {
|
||||
// What they have is permission to ask. A module that invokes gains exactly the same
|
||||
// permission and nothing beside it.
|
||||
Invokes []string
|
||||
// AnswersSeat are the seat verbs a process answers, each seat:<seat>.<verb> (design 47 G A-109).
|
||||
AnswersSeat []string
|
||||
|
||||
// AsksIssued are, for the vault's holder, the tools it asks in its own name for a credential a provider's software
|
||||
// issued, each `<module>.<tool>` (novox/hq ADR 0305, issue 723, vaultCalls): on the subject that names its caller,
|
||||
|
||||
@@ -1,11 +1,16 @@
|
||||
package buscompose
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// The bus account of one process of a module that declares bus (novox/hq ADR 0436 §1, §2, §3, §5).
|
||||
//
|
||||
// **Never the module's own account's grants, never the tool runner's.** A process's authority is read
|
||||
// straight from its bus declaration — its emits, its queue's consumer, its state, whether it reads the
|
||||
// records of its module's own asks, and the tools and seat verbs it calls — and nothing else: it holds no
|
||||
// seat, serves no tool, hears no module stream, and answers nothing a caller asked (§3). Modelled on
|
||||
// seat, serves no tool and hears no module stream; it answers a seat's verb only where its bus declares `serves` (design 47 G A-109). Modelled on
|
||||
// provisionerGrants, the other kind of process account the mesh mints (ADR 0349 clauses 6 and 11).
|
||||
|
||||
// processGrants is one process's whole authority: to publish, and to subscribe.
|
||||
@@ -52,6 +57,16 @@ func processGrants(p Principal) (pub, sub []string, err error) {
|
||||
}
|
||||
pub = append(pub, invoked...)
|
||||
|
||||
// 5b. The seat verbs it answers (design 47 G A-107, A-109): its subscribe of each verb's call and tool subjects (and
|
||||
// the chunk subjects, chunks.go).
|
||||
for _, v := range p.AnswersSeat {
|
||||
seat, verb, ok := strings.Cut(strings.TrimPrefix(v, "seat:"), ".")
|
||||
if !strings.HasPrefix(v, "seat:") || !ok || seat == "" || verb == "" {
|
||||
return nil, nil, fmt.Errorf("%q does not name a seat's verb: one serves seat:<seat>.<verb>", v)
|
||||
}
|
||||
sub = append(sub, "mesh.seat."+seat+".call."+verb+".*", "mesh.seat."+seat+".tool."+verb)
|
||||
}
|
||||
|
||||
// 6. The bus module's process (busstate.go, ADR 0595 §4, §5): every module's bus state it keeps, by name; its own
|
||||
// consumer of what its module consumes; its module's own stream's events; and its conditions through the seats its
|
||||
// module uses (the operator channel's accept.condition in its module's name).
|
||||
|
||||
@@ -137,6 +137,8 @@ type ProcessPrincipal struct {
|
||||
// Invokes are the tools and seat verbs this process calls, each <module>.<tool> or a role's
|
||||
// seat:<seat>.<verb>, or * for every tool on the mesh.
|
||||
Invokes []string
|
||||
// Serves are the seat verbs this process answers, each seat:<seat>.<verb> (design 47 G A-109).
|
||||
Serves []string
|
||||
// Tools says it is a tools bundle's account, run by the node's tool runtime, not a process the node runs.
|
||||
Tools bool
|
||||
// Keeps says its bus declares `keeps`: the bus module's keeper (novox/hq ADR 0626 §1), composed the bus's create,
|
||||
@@ -265,7 +267,7 @@ func Users(r Records) ([]Principal, error) {
|
||||
out = append(out, Principal{
|
||||
Kind: KindProcess, Node: node, Module: d.Module, Process: pr.Process,
|
||||
Emits: pr.Emits, Queues: pr.Queues, State: pr.State, Uses: d.Uses, Asks: pr.Asks,
|
||||
Invokes: pr.Invokes, Removes: pr.Removes,
|
||||
Invokes: pr.Invokes, AnswersSeat: pr.Serves, Removes: pr.Removes,
|
||||
})
|
||||
// The process of the module holding mesh-broker whose bus declares `keeps` keeps every module's bus state
|
||||
// (busstate.go, ADR 0595 §4, ADR 0626 §1); `keeps` on any other module composes nothing (the judge refuses it,
|
||||
|
||||
@@ -445,7 +445,7 @@ func processesOf(m catalogue.Manifest) []buscompose.ProcessPrincipal {
|
||||
// what it may remove from the bus (ADR 0564 Limits 4, issue 1952 S5).
|
||||
for _, a := range accounts {
|
||||
pr := buscompose.ProcessPrincipal{Process: a.ID, Emits: a.Bus.Emits, State: a.Bus.State, Asks: a.Bus.Asks,
|
||||
Invokes: a.Bus.Invokes, Removes: a.Bus.Removes, Tools: a.Tools, Keeps: a.Bus.Keeps}
|
||||
Invokes: a.Bus.Invokes, Serves: a.Bus.Serves, Removes: a.Bus.Removes, Tools: a.Tools, Keeps: a.Bus.Keeps}
|
||||
for _, q := range queues {
|
||||
if q.Process == a.ID {
|
||||
pr.Queues = append(pr.Queues, q)
|
||||
|
||||
Reference in New Issue
Block a user