fix9-serves-2: the grants part reads a process's serves beside its bus; a bus-less process's serves are the module's own account's
Builds: design 47 G A-115, A-127 Claude-Session: https://claude.ai/code/session_01NcbgfGKaxw984XyAPfLm4h
This commit is contained in:
@@ -1399,6 +1399,7 @@ func chunkSubjectsFollowTheAnswerer(t *testing.T) {
|
||||
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: "plain-serve", ServesOwn: []string{"seat:mesh-broker.snapshot-get"}},
|
||||
{Module: "mod-x", Streams: []ModuleStream{{Module: "mod-x", Name: "SECRETS", Events: []string{"sealed"}, Restricted: true},
|
||||
{Module: "mod-x", Name: "PLAIN", Events: []string{"plain"}}}},
|
||||
}}}
|
||||
@@ -1422,6 +1423,11 @@ func chunkSubjectsFollowTheAnswerer(t *testing.T) {
|
||||
!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)
|
||||
}
|
||||
// A process declaring no bus runs on its module's own account, which answers its serves (design 47 G A-115, A-127).
|
||||
if m := perms["a.plain-serve"]; !has(m.Subscribe, "mesh.seat.mesh-broker.tool.snapshot-get") || !has(m.Subscribe, "mesh.seat.mesh-broker.call.snapshot-get.*") ||
|
||||
!has(m.Publish, chunks) || !has(m.Subscribe, servedAcks) {
|
||||
t.Errorf("the module account of a bus-less serving process may not answer the verb and send chunks: %v / %v", m.Publish, m.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)
|
||||
}
|
||||
|
||||
@@ -50,7 +50,7 @@ func chunkGrants(p Principal) (pub, sub []string) {
|
||||
return nil, nil
|
||||
}
|
||||
for _, c := range p.Chunked {
|
||||
if p.Kind == KindProcess && slices.Contains(p.AnswersSeat, "seat:"+c.Seat+"."+c.Verb) {
|
||||
if (p.Kind == KindProcess || p.Kind == KindModule) && 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) {
|
||||
|
||||
@@ -192,7 +192,7 @@ 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 are the seat verbs a process answers, each seat:<seat>.<verb> (design 47 G A-109), of a process with a bus on its own account, else of the module's own (A-115).
|
||||
AnswersSeat []string
|
||||
|
||||
// AsksIssued are, for the vault's holder, the tools it asks in its own name for a credential a provider's software
|
||||
@@ -649,6 +649,14 @@ func PermissionsFor(p Principal) (Permissions, error) {
|
||||
// The chunk subjects of a verb that sends a file (design 47 G §681, A-60), to the principal that answers it and to an account
|
||||
// naming it in `invokes`.
|
||||
pub, sub := chunkGrants(p)
|
||||
// A module's own account answers the verbs of its processes that declare no bus (design 47 G A-115, A-127).
|
||||
if p.Kind == KindModule && len(p.AnswersSeat) > 0 {
|
||||
answered, err := answeredSubjects(p.AnswersSeat)
|
||||
if err != nil {
|
||||
return Permissions{}, err
|
||||
}
|
||||
sub = append(sub, answered...)
|
||||
}
|
||||
perms.Publish, perms.Subscribe = append(perms.Publish, pub...), append(perms.Subscribe, sub...)
|
||||
return perms, nil
|
||||
}
|
||||
|
||||
@@ -10,9 +10,23 @@ import (
|
||||
// **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 and hears no module stream; it answers a seat's verb only where its bus declares `serves` (design 47 G A-109). Modelled on
|
||||
// seat, serves no tool and hears no module stream; it answers a seat's verb only where its resource declares `serves` beside its bus (design 47 G A-109). Modelled on
|
||||
// provisionerGrants, the other kind of process account the mesh mints (ADR 0349 clauses 6 and 11).
|
||||
|
||||
// answeredSubjects are the subscribes of the seat verbs an account answers, each seat:<seat>.<verb>: the call and tool subjects
|
||||
// of each (design 47 G A-107, A-109, A-115).
|
||||
func answeredSubjects(verbs []string) ([]string, error) {
|
||||
var sub []string
|
||||
for _, v := range verbs {
|
||||
seat, verb, ok := strings.Cut(strings.TrimPrefix(v, "seat:"), ".")
|
||||
if !strings.HasPrefix(v, "seat:") || !ok || seat == "" || verb == "" {
|
||||
return 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)
|
||||
}
|
||||
return sub, nil
|
||||
}
|
||||
|
||||
// processGrants is one process's whole authority: to publish, and to subscribe.
|
||||
func processGrants(p Principal) (pub, sub []string, err error) {
|
||||
// 1. Its own events, under its module's own namespace — the same subjects the module's own principal
|
||||
@@ -59,13 +73,11 @@ func processGrants(p Principal) (pub, sub []string, err error) {
|
||||
|
||||
// 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)
|
||||
answered, err := answeredSubjects(p.AnswersSeat)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
sub = append(sub, answered...)
|
||||
|
||||
// 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
|
||||
|
||||
@@ -110,6 +110,9 @@ type Declared struct {
|
||||
// composed whether or not the module itself has an account (NoAccount), because a process account is
|
||||
// delivered in its own secret, broker-<process id>, never the module's `broker`.
|
||||
Processes []ProcessPrincipal
|
||||
// ServesOwn are the seat verbs of this module's processes that declare no bus, each seat:<seat>.<verb>: such a process
|
||||
// runs on the module's own account, which is granted their subscribes (design 47 G A-115, A-127).
|
||||
ServesOwn []string
|
||||
// Ledgers are the local names of the ledgers it asks for in `requires` (catalogue.StorageLedger); each assignment of a
|
||||
// module with any gets a ledger principal of its own (issue 1966 D5, ADR 0597 §9).
|
||||
Ledgers []string
|
||||
@@ -326,7 +329,7 @@ func Users(r Records) ([]Principal, error) {
|
||||
State: stateNames(d.State), Reads: d.Reads, ReadLogs: d.ReadLogs, SnapshotsTheBus: d.SnapshotsTheBus,
|
||||
PerMachine: perMachineNames(d.State), ReadOnly: readOnlyNames(d.State), KeyedReads: d.KeyedReads,
|
||||
Logs: logNames(d.Logs), Streams: d.Streams, Hears: ComposedHears(d), Queues: d.Queues,
|
||||
Objects: objectNames(d.Objects), ReadsObjects: d.ReadsObjects, Bound: d.Bindings,
|
||||
Objects: objectNames(d.Objects), ReadsObjects: d.ReadsObjects, Bound: d.Bindings, AnswersSeat: d.ServesOwn,
|
||||
})
|
||||
// **The grants part creates what a manifest declares** (novox/hq ADR 0547 §5; the operator, 2026-10-26
|
||||
// 08:54): every placed module's buckets, logs and streams, each by its stream's name.
|
||||
|
||||
@@ -146,7 +146,7 @@ func declaredFor(m catalogue.Manifest, seats map[string]catalogue.SeatDeclaratio
|
||||
d := buscompose.Declared{
|
||||
Module: m.Module, ToolRunner: catalogue.IsToolRunner(m), Emits: m.EmitsAll(), Consumes: fromModules, Watches: watches, Serves: m.Tools,
|
||||
Invokes: m.Invokes, Waits: waitsOf(m, seats), State: bucketsOf(m), Reads: m.Reads, Logs: logsOf(m),
|
||||
Streams: streamsOf(m), Queues: queueOwnersOf(m), Processes: processesOf(m), Checks: catalogue.HealthChecks(m),
|
||||
Streams: streamsOf(m), Queues: queueOwnersOf(m), Processes: processesOf(m), ServesOwn: ownServesOf(m), Checks: catalogue.HealthChecks(m),
|
||||
RunsAs: m.RunsAs, Issued: issuedTools(m),
|
||||
// The keys its served values fill, which every node may read where it runs (issue 1126).
|
||||
Served: catalogue.ServedSettings(m),
|
||||
@@ -435,6 +435,21 @@ func queueOwnersOf(m catalogue.Manifest) []buscompose.QueueOwner {
|
||||
return out
|
||||
}
|
||||
|
||||
// ownServesOf is the seat verbs of the module's processes that declare no bus, which run on the module's own account (design
|
||||
// 47 G A-115, A-127); a process with a bus carries its own serves (processesOf).
|
||||
func ownServesOf(m catalogue.Manifest) []string {
|
||||
buses := m.ProcessBuses()
|
||||
var out []string
|
||||
for _, r := range m.Resources {
|
||||
id := fmt.Sprint(r["id"])
|
||||
if _, bus := buses[id]; bus {
|
||||
continue
|
||||
}
|
||||
out = append(out, m.ProcessServes()[id]...)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func processesOf(m catalogue.Manifest) []buscompose.ProcessPrincipal {
|
||||
accounts := m.BusAccounts()
|
||||
if len(accounts) == 0 {
|
||||
@@ -446,7 +461,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, Serves: a.Bus.Serves, Removes: a.Bus.Removes, Tools: a.Tools, Keeps: a.Bus.Keeps}
|
||||
Invokes: a.Bus.Invokes, Serves: a.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)
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
package grants
|
||||
|
||||
import (
|
||||
"slices"
|
||||
"testing"
|
||||
|
||||
"git.novox.be/novox/mesh-sdk/go/catalogue"
|
||||
)
|
||||
|
||||
// Design 47 G A-115, A-127: a process's `serves`, beside its bus, is its account's (a process with a bus) or the module's own
|
||||
// (a process with none); a `serves` inside the bus is no grant.
|
||||
func TestAProcessesServesAreReadBesideItsBus(t *testing.T) {
|
||||
m := catalogue.Manifest{Module: "nats", Resources: []map[string]any{
|
||||
{"id": "keeper", "type": "process", "serves": []any{"seat:mesh-broker.list"}, "bus": map[string]any{"keeps": true}},
|
||||
{"id": "plain", "type": "process", "serves": []any{"seat:mesh-broker.snapshot-get"}},
|
||||
{"id": "inside", "type": "process", "bus": map[string]any{"serves": []any{"seat:mesh-broker.remove"}}},
|
||||
}}
|
||||
procs := processesOf(m)
|
||||
if len(procs) != 1 || procs[0].Process != "keeper" || !slices.Equal(procs[0].Serves, []string{"seat:mesh-broker.list"}) {
|
||||
t.Errorf("the process principals = %+v; want the keeper serving seat:mesh-broker.list", procs)
|
||||
}
|
||||
if got := ownServesOf(m); !slices.Equal(got, []string{"seat:mesh-broker.snapshot-get"}) {
|
||||
t.Errorf("the module's own serves = %v; want the bus-less process's alone", got)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user