AM-D3: a runtime whose tool runner does not honour the mark is denied every marked subject

The grants part follows the assignment half, reads each node's reading and the
runner its node-engine says running, and where the runner does not state
approval-mark/1 or the reading is not the running runner's, composes the
subscribe deny of every marked tool and verb the runtime carries, named in
grants_user_list's withheld and answered by the seat's verb withheld.

Builds: design 46 7 §6 (the deny's subjects, its cases, withheld in grants_user_list and the verb withheld), design 47 G §195, §309, lead answer A-203 (the assignment half followed by revision and assignment-revision; withheld's forms, keyed by node in the user list's state as the lead confirmed)

Claude-Session: https://claude.ai/code/session_01NcbgfGKaxw984XyAPfLm4h
This commit is contained in:
2026-11-12 06:17:59 +01:00
parent 248d09ec14
commit 7bc4836eb0
14 changed files with 394 additions and 13 deletions
@@ -38,7 +38,9 @@ import (
"git.novox.be/novox/mesh-contracts/go/seats/nodeengine"
mcseat "git.novox.be/novox/mesh-contracts/health-and-repair/go/seats/meshconditions"
"git.novox.be/novox/mesh-delivery/modules/mesh-delivery-contracts/go/clients/meshcatalogclient"
"git.novox.be/novox/mesh-delivery/modules/mesh-delivery-contracts/go/clients/meshwantsclient"
"git.novox.be/novox/mesh-delivery/modules/mesh-delivery-contracts/go/seats/meshcatalog"
"git.novox.be/novox/mesh-delivery/modules/mesh-delivery-contracts/go/seats/meshwants"
stdio "git.novox.be/novox/mesh-sdk/go"
"git.novox.be/novox/mesh-sdk/go/announce"
"git.novox.be/novox/mesh-sdk/go/membership"
@@ -168,6 +170,13 @@ func run(ctx context.Context, getenv func(string) string, logOut io.Writer) erro
branches.Changed = func(string, string, json.RawMessage, bool, time.Time) { f.Wake() }
branches.Entries.Changed = func(meshcatalogclient.EntryRow) { f.Wake() }
fv.Catalog = &grants.CatalogOf{Branches: branches, Context: ctx, Wake: f.Wake}
// The assignment half (design 46 approval mark §6, design 47 G §195): its first view by mesh-wants' revision, then its
// assignment-revision, on the same reader; each revision held composes again, since a node's reading may have changed.
assignment, _, err := meshwantsclient.WireAssignmentProcess(reader, conn.Call, func(meshwants.AssignmentRevision) { f.Wake() })
if err != nil {
return err
}
fv.Assignment = assignment
f.Part = &grants.Part{Read: fv, Call: conn, Store: ownStore{conn}, Emit: emitting{conn}, Views: fv, FirstViewContext: ctx,
Create: creating{ctx: ctx, js: js}, Consumers: creating{ctx: ctx, js: js},
Objects: sealedObjects{ctx: ctx, js: js}, Purging: ownPurging{ctx: ctx, js: js, module: account.Module}, Logf: logger.Printf,
@@ -275,6 +275,13 @@ func toolsOf(p *grants.Part) []stdio.Tool {
Run: func(a map[string]any) (any, error) {
return p.GrantsFor(meshgrants.GrantsForInput{Branch: grants.Answer(a, "branch"), Run: grants.Answer(a, "run"), Source: grants.Answer(a, "source")})
}},
{Name: meshgrants.Seat + "." + meshgrants.VerbWithheld,
Description: "The marked subjects withheld from a node's runtime, by module, and why: where its tool runner does not state " +
"approval-mark/1 or its reading is not the running runner's (design 46 approval mark §6). Read-only.",
Input: schema(map[string]string{"node": "the node"}, "node"),
Run: func(a map[string]any) (any, error) {
return p.WithheldOn(grants.Answer(a, "node"))
}},
{Name: grants.ToolRelease, Description: "Release one held granting wait by a person's act with why, recorded as a hand act " +
"(the seat mesh-hand-acts) before it is released (design 47 G §276). grants_waits lists them.",
Input: schema(map[string]string{"module": "the held entry's module", "why": "why, kept in the hand act"}, "module", "why"),
+54 -3
View File
@@ -37,6 +37,7 @@ import (
"git.novox.be/novox/mesh-delivery/modules/mesh-delivery-contracts/go/clients/meshcatalogclient"
"git.novox.be/novox/mesh-delivery/modules/mesh-delivery-contracts/go/clients/meshwantsclient"
"git.novox.be/novox/mesh-delivery/modules/mesh-delivery-contracts/go/seats/meshcatalog"
"git.novox.be/novox/mesh-delivery/modules/mesh-delivery-contracts/go/seats/meshwants"
"git.novox.be/novox/mesh-delivery/modules/mesh-grants/grantsstate"
"git.novox.be/novox/mesh-sdk/go/catalogue"
"git.novox.be/novox/mesh-sdk/go/keys"
@@ -621,9 +622,10 @@ func TestGrants(t *testing.T) {
if err != nil {
t.Fatalf("mesh-sdk cannot decode the manifest: %v", err)
}
// The part claims the seat mesh-grants, which serves `grants-for` (design 47 G §401, §441).
if m.Module != "mesh-grants" || len(m.Claims) != 1 || m.Claims[0].Name != "mesh-grants" || !slices.Equal(m.Claims[0].Serves, []string{"grants-for"}) {
t.Errorf("module %q claims %v: the part claims the seat mesh-grants alone, serving grants-for", m.Module, m.Claims)
// The part claims the seat mesh-grants, which serves `grants-for` (design 47 G §401, §441) and `withheld` (design 46
// approval mark §6).
if m.Module != "mesh-grants" || len(m.Claims) != 1 || m.Claims[0].Name != "mesh-grants" || !slices.Equal(m.Claims[0].Serves, []string{"grants-for", "withheld"}) {
t.Errorf("module %q claims %v: the part claims the seat mesh-grants alone, serving grants-for and withheld", m.Module, m.Claims)
}
for _, row := range []struct {
what string
@@ -1265,6 +1267,45 @@ func TestGrants(t *testing.T) {
}
})
// AM-D3 (design 46 approval mark §6, lead answer A-203): the part reads each node's reading from the assignment half it
// holds, none held keeps the list in force; the subjects withheld are kept by node in the user list's state and the
// seat's verb withheld answers them by module.
t.Run("the withheld subjects are kept by node and answered by the verb withheld", func(t *testing.T) {
if _, held := (&grants.Views{Assignment: heldHalf{}}).Readings(); held {
t.Errorf("an assignment half with no revision is said held")
}
half := heldHalf{rev: &meshwants.AssignmentRevision{Capabilities: []meshwants.AssignmentRevisionCapabilitiesItem{
{Node: "a", Digest: "sha256:x", Readings: []string{"approval-mark/1"}, Fingerprint: "sha256:runner"}}}}
if r, held := (&grants.Views{Assignment: half}).Readings(); !held || r["a"].Fingerprint != "sha256:runner" ||
!slices.Equal(r["a"].Capabilities, []string{"approval-mark/1"}) {
t.Errorf("the readings are %+v (%v)", r, held)
}
shell := buscompose.Declared{Module: "shell", Serves: []string{"execute"}, Marked: []string{"execute"}}
recs := buscompose.Records{Nodes: []string{"a", "b"}, Assigned: map[string][]buscompose.Declared{
"a": {{Module: "node-tools", ToolRunner: true}, shell}, "b": {{Module: "node-tools", ToolRunner: true}, shell}},
Readings: map[string]buscompose.RunnerReading{"b": {Capabilities: []string{"approval-mark/1"}, Fingerprint: "sha256:runner"}},
Running: map[string]string{"a.node-tools": "sha256:runner", "b.node-tools": "sha256:runner"}}
byNode := grants.WithheldByNode(recs)
if len(byNode) != 1 || len(byNode["a"]) != 3 || byNode["a"][0].Why != buscompose.WithheldWhy {
t.Fatalf("withheld by node = %+v", byNode)
}
p := partOn(newFakeBus())
if _, err := p.WithheldOn("a"); err == nil {
t.Errorf("withheld before any composition answered no error")
}
raw, _ := json.Marshal(grants.UserListState{V: 1, Withheld: byNode})
if _, err := p.Store.Put(grants.StateUserList, grants.KeyList, raw); err != nil {
t.Fatal(err)
}
got, err := p.WithheldOn("a")
if err != nil || got.Node != "a" || len(got.Modules) != 1 || got.Modules[0].Module != "shell" || len(got.Modules[0].Subjects) != 3 ||
got.Modules[0].Subjects[0].Subject != "mesh.mod.shell.call.execute.>" {
t.Errorf("withheld on a = %+v (%v)", got, err)
}
if got, err := p.WithheldOn("b"); err != nil || len(got.Modules) != 0 {
t.Errorf("withheld on b = %+v (%v)", got, err)
}
})
t.Run("compare", func(t *testing.T) {
b := fixture()
shadow := shadowOf(t, b)
@@ -5156,3 +5197,13 @@ func TestGrantsFor(t *testing.T) {
t.Error("grants-for with neither a source nor a run answered")
}
}
// heldHalf is an assignment half holding rev, or none.
type heldHalf struct{ rev *meshwants.AssignmentRevision }
func (h heldHalf) Revision() (meshwants.AssignmentRevision, bool) {
if h.rev == nil {
return meshwants.AssignmentRevision{}, false
}
return *h.rev, true
}
@@ -63,6 +63,7 @@ func TestTheCopiedComposerComposesAsTheControllers(t *testing.T) {
t.Run("a process serving a verb is granted its calls and chunk subjects, and the reverse goes to an account naming it", chunkSubjectsFollowTheAnswerer)
t.Run("the tool runner's runtime hears every event subject and nothing wider", theToolRunnerHearsEveryEvent)
t.Run("a per-machine state reaches its machine's keys below it and no other machine's", aPerMachineStateReachesItsMachinesKeys)
t.Run("a runtime whose tool runner does not honour the mark is denied every marked subject", aRuntimeWithholdsTheMarks)
// 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"), " ")
@@ -1564,3 +1565,61 @@ func aPerMachineStateReachesItsMachinesKeys(t *testing.T) {
t.Errorf("a's membership issues %+v", issued)
}
}
// AM-D3 (design 46 approval mark §6): a runtime whose node's reading lacks approval-mark/1, has no reading, or a reading of
// another runner or of none is denied to subscribe every marked subject of what it carries, a marked tool's three forms, a
// marked verb of a seat held per node with its .<node> form and of a seat held once, each named with why; a reading with
// the capability of the running runner composes none.
func aRuntimeWithholdsTheMarks(t *testing.T) {
shell := Declared{Module: "shell", Serves: []string{"execute", "read"}, Marked: []string{"execute"},
Holds: []Seat{{Name: "node-shell", Scope: "node", Serves: []string{"run", "look"}, Marked: []string{"run"}},
{Name: "forge", Scope: "mesh", Serves: []string{"merge"}, Marked: []string{"merge"}}}}
withheld := []string{"mesh.mod.shell.call.execute.>", "mesh.mod.shell.tool.execute", "mesh.mod.shell.tool.execute.*",
"mesh.seat.forge.call.merge.>", "mesh.seat.forge.tool.merge", "mesh.seat.node-shell.call.run.a.>",
"mesh.seat.node-shell.tool.run", "mesh.seat.node-shell.tool.run.a"}
honours := RunnerReading{Capabilities: []string{"bus-state-calls", ApprovalMark}, Fingerprint: "sha256:runner"}
for _, c := range []struct {
name string
readings map[string]RunnerReading
deny bool
}{
{"a reading lacking the capability", map[string]RunnerReading{"a": {Capabilities: []string{"bus-state-calls"}, Fingerprint: "sha256:runner"}}, true},
{"no reading", map[string]RunnerReading{}, true},
{"a reading of another runner", map[string]RunnerReading{"a": {Capabilities: honours.Capabilities, Fingerprint: "sha256:older"}}, true},
{"a reading of no runner", map[string]RunnerReading{"a": {Capabilities: honours.Capabilities}}, true},
{"a reading with the capability of the running runner", map[string]RunnerReading{"a": honours}, false},
} {
users, err := Users(Records{Nodes: []string{"a"}, Assigned: map[string][]Declared{"a": {{Module: "node-tools", ToolRunner: true}, shell}},
Readings: c.readings, Running: map[string]string{"a.node-tools": "sha256:runner"}})
if err != nil {
t.Fatal(err)
}
i := slices.IndexFunc(users, func(p Principal) bool { return p.Kind == KindNodeTools })
if i < 0 {
t.Fatalf("%s: no runtime principal", c.name)
}
perms, err := PermissionsFor(users[i])
if err != nil {
t.Fatal(err)
}
var named []string
for _, w := range MarksWithheld(users[i]) {
if w.Module != "shell" || w.Why != WithheldWhy {
t.Errorf("%s: withheld %+v", c.name, w)
}
named = append(named, w.Subject)
}
want := []string(nil)
if c.deny {
want = withheld
}
if !slices.Equal(perms.SubscribeDeny, want) || !slices.Equal(named, want) {
t.Errorf("%s: denied %v, named %v, want %v", c.name, perms.SubscribeDeny, named, want)
}
// The deny outranks the allow of the module's tools; an unmarked tool stays served.
if MaySubscribe(perms, "mesh.mod.shell.tool.execute") == c.deny || !MaySubscribe(perms, "mesh.mod.shell.tool.read") {
t.Errorf("%s: execute subscribable %v, read %v", c.name, MaySubscribe(perms, "mesh.mod.shell.tool.execute"),
MaySubscribe(perms, "mesh.mod.shell.tool.read"))
}
}
}
@@ -89,6 +89,9 @@ type Seat struct {
// holds.
Kinded bool
Kind string
// Marked are the verbs of the seat that need the operator's approval, as its definition marks them and the holder's
// `needs-approval` adds (design 46 approval mark §2, §6).
Marked []string
// ByCaller are the accepts and emits whose last token names the calling module (ADR 0259 §3).
ByCaller []string
// Proofs are the seat's proof verbs: core request and reply, never on a stream (ADR 0259 §3).
@@ -186,6 +189,9 @@ type Principal struct {
// seat's verbs on this node — derived from the same declarations the modules' own principals
// are, so the runtime can serve nothing a module could not have served for itself.
Carries []Declared
// Withholds says this node's tool runner does not honour the approval mark (design 46 approval mark §6), for a
// KindNodeTools principal: every marked subject of what it carries is denied it (MarksWithheld).
Withholds bool
// Invokes are the tools this principal may call, as `<module>.<tool>`; a single `*` is every
// tool. A person's whole authority (design 25 §7), and a module's only if its manifest says so
@@ -1312,10 +1318,17 @@ func permissionsFor(p Principal) (Permissions, error) {
}
}
}
// And the marked subjects of what a runtime carries, where its tool runner does not honour the mark (design 46 approval
// mark §6): a subscribe deny, which outranks the allow of each module's tools.
var subDeny []string
for _, w := range MarksWithheld(p) {
subDeny = append(subDeny, w.Subject)
}
return Permissions{
Publish: pub,
PublishDeny: deny,
Subscribe: sub,
Publish: pub,
PublishDeny: deny,
Subscribe: sub,
SubscribeDeny: unique(subDeny),
// A module answers what it was asked — a tool call reaches it on its own namespace, so the
// authority is bounded by having been asked. A node is asked one
// thing, a hand-over on its own subject (novox/hq issue 356), and answers that: the node is its
@@ -28,6 +28,9 @@ import (
// Declared is one module on one node, as composing its authority needs it.
type Declared struct {
Module string
// Marked are the tools it serves that need the operator's approval, as its `needs-approval` names them (design 46
// approval mark §1, §6); a marked seat verb is its Holds' Marked.
Marked []string
// ToolRunner says the module is the node's tool runner, as its manifest says (`tool-runner`, ADR 0601 §4).
ToolRunner bool
// Objects are its own object buckets, and ReadsObjects the bus names of the object buckets of others its reads name
@@ -151,6 +154,10 @@ type ProcessPrincipal struct {
// Records is what composing a user list needs to know about the mesh, and nothing more.
type Records struct {
// Readings are each node's capability reading, from the assignment half, by node; Running the fingerprint each
// node-engine last said running, by `<node>.<module>` (design 46 approval mark §6, design 47 G §309).
Readings map[string]RunnerReading
Running map[string]string
// Kept is every seat event kept in a stream of its own (kept.go): the grants part makes each stream, so its principal is
// granted the create of each by name.
Kept []KeptEvent
@@ -354,7 +361,11 @@ func Users(r Records) ([]Principal, error) {
carried = append(carried, d)
}
}
out = append(out, Principal{Kind: KindNodeTools, Node: node, Module: runner, Carries: carried})
// Its node's reading and the runner the node-engine says running decide whether the marks are withheld (design 46
// approval mark §6).
reading, read := r.Readings[node]
out = append(out, Principal{Kind: KindNodeTools, Node: node, Module: runner, Carries: carried,
Withholds: Withholds(reading, read, r.Running[node+"."+runner])})
}
}
// A node enrols through the seat mesh-enrolment alone (ADR 0587 §6, ADR 0590): while no holder serves redeem, an
@@ -0,0 +1,85 @@
package buscompose
import (
"slices"
"sort"
)
// **A machine whose tool runner does not honour the mark serves no marked tool or verb** (design 46 approval mark §6): on
// the account of a node's runtime whose readings lack approval-mark/1, or are not the running runner's, every marked
// subject of every module it carries is denied to subscribe, which outranks its allow of `mesh.mod.<module>.tool.>`, and
// each is named with why.
// ApprovalMark is the capability a tool runner that honours the mark states among its call capabilities (design 46
// approval mark §6).
const ApprovalMark = "approval-mark/1"
// WithheldWhy is why a marked subject is withheld (design 46 approval mark §6).
const WithheldWhy = "its tool runner does not state " + ApprovalMark
// WithheldSubject is one marked subject withheld from a runtime's account: the module it is of, the subject and why
// (lead answer A-203).
type WithheldSubject struct {
Module string `json:"module"`
Subject string `json:"subject"`
Why string `json:"why"`
}
// RunnerReading is a node's capability reading as the assignment half carries it: what its tool runner states of the
// calls it answers, and the fingerprint of the runner that announced it (design 46 approval mark §6).
type RunnerReading struct {
Capabilities []string
Fingerprint string
}
// Withholds says whether a node's runtime withholds the marked subjects: where the node has no reading, the reading
// lacks ApprovalMark, or its fingerprint is absent or differs from the one the node-engine last said running (no time is
// compared, failing closed).
func Withholds(reading RunnerReading, read bool, running string) bool {
return !read || !slices.Contains(reading.Capabilities, ApprovalMark) || reading.Fingerprint == "" ||
reading.Fingerprint != running
}
// MarkedSubjects are the subjects of a module's marked tools and of the marked verbs of the seats it holds, on node: a
// tool's `mesh.mod.<module>.tool.<tool>`, the same followed by `.*`, and the caller-named `mesh.mod.<module>.call.<tool>.>`;
// a verb of a seat held per node `mesh.seat.<seat>.tool.<verb>`, its `.<node>` form and `mesh.seat.<seat>.call.<verb>.<node>.>`;
// a verb of a seat held once for the mesh `mesh.seat.<seat>.tool.<verb>` and `mesh.seat.<seat>.call.<verb>.>`.
func MarkedSubjects(node string, d Declared) []string {
var out []string
for _, t := range d.Marked {
out = append(out, "mesh.mod."+d.Module+".tool."+t, "mesh.mod."+d.Module+".tool."+t+".*",
"mesh.mod."+d.Module+"."+CallKind+"."+t+".>")
}
for _, s := range d.Holds {
for _, v := range s.Marked {
if s.Scope == "node" {
out = append(out, "mesh.seat."+s.Name+".tool."+v, "mesh.seat."+s.Name+".tool."+v+"."+node,
"mesh.seat."+s.Name+"."+CallKind+"."+v+"."+node+".>")
} else {
out = append(out, "mesh.seat."+s.Name+".tool."+v, "mesh.seat."+s.Name+"."+CallKind+"."+v+".>")
}
}
}
return out
}
// MarksWithheld are the subjects withheld from a principal's account, each with its module and why: for a node's runtime that
// withholds, every marked subject of every module it carries; none for any other.
func MarksWithheld(p Principal) []WithheldSubject {
if p.Kind != KindNodeTools || !p.Withholds {
return nil
}
var out []WithheldSubject
for _, d := range p.Carries {
for _, s := range MarkedSubjects(p.Node, d) {
out = append(out, WithheldSubject{Module: d.Module, Subject: s, Why: WithheldWhy})
}
}
sort.Slice(out, func(i, j int) bool {
if out[i].Module != out[j].Module {
return out[i].Module < out[j].Module
}
return out[i].Subject < out[j].Subject
})
return slices.CompactFunc(out, func(a, b WithheldSubject) bool { return a == b })
}
+52 -2
View File
@@ -8,10 +8,14 @@ import (
"sync"
"git.novox.be/novox/mesh-delivery/modules/mesh-delivery-contracts/go/clients/meshcatalogclient"
"git.novox.be/novox/mesh-delivery/modules/mesh-delivery-contracts/go/clients/meshwantsclient"
"git.novox.be/novox/mesh-delivery/modules/mesh-delivery-contracts/go/seats/meshcatalog"
"git.novox.be/novox/mesh-delivery/modules/mesh-delivery-contracts/go/seats/meshdelivery"
"git.novox.be/novox/mesh-delivery/modules/mesh-delivery-contracts/go/seats/meshwants"
"git.novox.be/novox/mesh-sdk/go/catalogue"
"git.novox.be/novox/mesh-sdk/go/views"
"mesh-grants/internal/buscompose"
)
// **The grants part reads the catalog half** (design 47 G §614, §444, §684, A-5, A-52): through meshcatalogclient's branch follower
@@ -24,14 +28,16 @@ import (
// long form with its field `branches`, §558, is not read by mesh-sdk's manifest reader yet.)
func catalogHalfVerbs() []string {
return []string{"seat:" + meshcatalog.Seat + "." + meshcatalog.VerbRevision, "seat:" + meshcatalog.Seat + "." + meshcatalog.VerbRegistrations,
meshcatalogclient.SourceVerb, meshcatalogclient.EntryVerb}
meshcatalogclient.SourceVerb, meshcatalogclient.EntryVerb,
// And the assignment half's revision (design 46 approval mark §6, design 47 G §195).
meshwantsclient.RevisionVerb}
}
// catalogHalfEvents are the events every such part names in its `consumes`, as `<seat>.<event>`: the catalog's revision and
// registration-changed, and the gate's source-moved.
func catalogHalfEvents() []string {
return []string{meshcatalog.Seat + "." + meshcatalog.EventCatalogRevision, meshcatalog.Seat + "." + meshcatalog.EventRegistrationChanged,
meshdelivery.Seat + "." + meshdelivery.EventSourceMoved}
meshdelivery.Seat + "." + meshdelivery.EventSourceMoved, meshwants.Seat + "." + meshwants.EventAssignmentRevision}
}
// CatalogHalf is what the part reads of the catalog half: the entry by its fingerprint, the branches whose first view is not in,
@@ -200,3 +206,47 @@ func catalogWaiting(r Reader) string {
slices.Sort(w)
return fmt.Sprintf("the first views of the catalog half are not in (%v), so each account is composed from its want's entry by fingerprint and the list in force is kept", w)
}
// **The grants part reads the assignment half** (design 46 approval mark §6, design 47 G §195, lead answer A-203): the view of
// meshwantsclient's Assignment, a first view by mesh-wants' `revision`, then its `assignment-revision`, adopted on the part's
// views reader; from it each node's capability reading and the runner that announced it.
// AssignmentHalf is what the part reads of the assignment half: the revision it holds, false while it holds none.
type AssignmentHalf interface {
Revision() (meshwants.AssignmentRevision, bool)
}
// readingsReader is what a Reader that holds the assignment half answers (Views does).
type readingsReader interface {
Readings() (map[string]buscompose.RunnerReading, bool)
}
// Readings are each node's capability reading of the assignment half the part holds, by node; false where it holds none,
// and true with none where the reader follows no assignment half (a tool's reads nothing of it).
func (v *Views) Readings() (map[string]buscompose.RunnerReading, bool) {
if v == nil || v.Assignment == nil {
return nil, true
}
rev, ok := v.Assignment.Revision()
if !ok {
return nil, false
}
out := map[string]buscompose.RunnerReading{}
for _, c := range rev.Capabilities {
out[c.Node] = buscompose.RunnerReading{Capabilities: c.Readings, Fingerprint: c.Fingerprint}
}
return out, true
}
// readingsOf is each node's reading through r, and why the pass composes before the assignment half's first view.
func readingsOf(r Reader) (map[string]buscompose.RunnerReading, string) {
rr, ok := r.(readingsReader)
if !ok {
return nil, ""
}
readings, held := rr.Readings()
if !held {
return nil, "the first view of the assignment half is not in, so no node's capability reading is known and the list in force is kept"
}
return readings, ""
}
@@ -81,6 +81,8 @@ type Views struct {
Watching Watcher
// Catalog is the part's catalog half (catalog.go): nil in a tool, which reads none.
Catalog CatalogHalf
// Assignment is the part's assignment half (catalog.go): nil in a tool, which reads none.
Assignment AssignmentHalf
said *untilSaid
}
+11 -1
View File
@@ -158,7 +158,10 @@ type Inputs struct {
Ran map[string]catalogue.Manifest
// Heard is what each node-engine's `running` said of each `<node>.<module>`: a fingerprint, or stopped (design 47 G §404,
// §459); a key absent was not heard since the part's consumer began. RanByFingerprint is the manifest of each fingerprint heard.
Heard map[string]catalogue.Heard
Heard map[string]catalogue.Heard
// Readings are each node's capability reading as the assignment half the part holds carries it, by node (design 46
// approval mark §6); nil while the part holds no assignment half.
Readings map[string]buscompose.RunnerReading
RanByFingerprint map[string]catalogue.Manifest
// Had is the manifest of the build each machine runs now (its have's fingerprint, the builder's build of it), by
// `<node>.<module>`; the want's Running may already be a newer build. A membership is composed from both and the trunk's.
@@ -212,6 +215,9 @@ type Inputs struct {
// CatalogUnread says why this pass composed before the first views of the catalog half were in (design 47 G §684); empty once they are.
// As WantsUnread, such a pass retires nothing (ReadFailures) and only adds: the list in force is kept.
CatalogUnread string
// AssignmentUnread says why this pass composed before the first view of the assignment half was in (design 46 approval
// mark §6, design 47 G §195): no node's capability reading is known, so the list in force is kept; empty once it is.
AssignmentUnread string
// Held is, by module, the manifest a held module's running build was judged on (its have's fingerprint, the builder's
// build of it): what the seats it defines, the streams others hear from it and its reads are composed from while its
// trunk manifest is refused or waits, so no other principal loses a grant to it (ADR 0601 clause 3).
@@ -366,6 +372,7 @@ func ReadInputs(r Reader, cache *Manifests) Inputs {
Values: map[string]any{}, Gaps: append([]Gap(nil), standingGaps...)}
in.SeatListUnread = gate != nil && !gate.MayAct()
in.CatalogUnread = catalogWaiting(r)
in.Readings, in.AssignmentUnread = readingsOf(r)
in.Defined = definedEventsOf(nil)
// The definitions the reader holds, the bootstrapped ones before the seat list is read (ADR 0636 §2), name the events kept
// on a stream of their own, so a durable made at the first pass already hears them there and never on EVENTS (a held
@@ -764,6 +771,9 @@ func (in Inputs) ReadFailures() []string {
if in.CatalogUnread != "" {
out = append(out, "catalog half: "+in.CatalogUnread)
}
if in.AssignmentUnread != "" {
out = append(out, "assignment half: "+in.AssignmentUnread)
}
for _, g := range in.Refused {
out = append(out, g.Input+": "+g.Why)
}
@@ -25,6 +25,8 @@ func (in Inputs) keepsLoaded() string {
return in.WantsUnread
case in.CatalogUnread != "":
return in.CatalogUnread
case in.AssignmentUnread != "":
return in.AssignmentUnread
case in.SeatListUnread:
return "the seat list is unread, so only the bootstrapped seats compose"
}
+54 -1
View File
@@ -6,6 +6,7 @@ import (
"crypto/sha256"
"encoding/hex"
"encoding/json"
"errors"
"fmt"
"git.novox.be/novox/mesh-contracts/go/seats/meshbroker"
"slices"
@@ -15,6 +16,7 @@ import (
"time"
"git.novox.be/novox/mesh-contracts/go/seats/nodeengine"
"git.novox.be/novox/mesh-contracts/identity-and-access/go/seats/meshgrants"
oc "git.novox.be/novox/mesh-contracts/operator-and-conversation/go/seats/operatorchannel"
"git.novox.be/novox/mesh-delivery/modules/mesh-grants/grantsstate"
"git.novox.be/novox/mesh-sdk/go/membership"
@@ -83,7 +85,10 @@ type UserListState struct {
Credentials Credentials `json:"credentials"`
BusNode string `json:"bus_node,omitempty"`
Gaps []Gap `json:"gaps"`
At time.Time `json:"at"`
// Withheld are, by node, the marked subjects withheld from its runtime's account, each with its module and why (design
// 46 approval mark §6, lead answer A-203): what grants_user_list and the seat's verb withheld say.
Withheld map[string][]buscompose.WithheldSubject `json:"withheld,omitempty"`
At time.Time `json:"at"`
}
func (s UserListState) inputs() string {
@@ -328,6 +333,7 @@ func (p *Part) Compose() Result {
st.LeftOut = []Gap{}
}
users, refused, err := ComposeEach(listRecs)
st.Withheld = WithheldByNode(listRecs)
if err != nil {
res.Errors = append(res.Errors, "the shadow list could not be composed: "+err.Error())
}
@@ -642,3 +648,50 @@ func (p *Part) inputs() Inputs {
p.readRan(&in)
return in
}
// WithheldByNode is, by node, the marked subjects withheld from its runtime's account (design 46 approval mark §6); nil when
// none is. A principal that cannot be derived withholds nothing here: the list is not composed then either. Pure.
func WithheldByNode(r buscompose.Records) map[string][]buscompose.WithheldSubject {
principals, err := buscompose.Users(r)
if err != nil {
return nil
}
var out map[string][]buscompose.WithheldSubject
for _, p := range principals {
if w := buscompose.MarksWithheld(p); len(w) > 0 {
if out == nil {
out = map[string][]buscompose.WithheldSubject{}
}
out[p.Node] = append(out[p.Node], w...)
}
}
return out
}
// WithheldOn answers the seat mesh-grants' verb withheld (design 46 approval mark §6, lead answer A-203): the marked subjects
// withheld from node's runtime, grouped by module, from the user list the part composed last.
func (p *Part) WithheldOn(node string) (meshgrants.Withheld, error) {
if node == "" {
return meshgrants.Withheld{}, errors.New("withheld names a node")
}
out := meshgrants.Withheld{Node: node, Modules: []meshgrants.WithheldModulesItem{}}
raw, _, found, err := p.Store.Get(StateUserList, KeyList)
if err != nil {
return out, fmt.Errorf("its own user list cannot be read: %w", err)
}
if !found {
return out, errors.New("nothing composed yet: grants_compose_now composes it")
}
var st UserListState
if err := json.Unmarshal(raw, &st); err != nil {
return out, fmt.Errorf("its own user list cannot be read: %w", err)
}
for _, w := range st.Withheld[node] {
if n := len(out.Modules); n == 0 || out.Modules[n-1].Module != w.Module {
out.Modules = append(out.Modules, meshgrants.WithheldModulesItem{Module: w.Module, Subjects: []meshgrants.WithheldSubject{}})
}
m := &out.Modules[len(out.Modules)-1]
m.Subjects = append(m.Subjects, meshgrants.WithheldSubject{Subject: w.Subject, Why: w.Why})
}
return out, nil
}
+27 -1
View File
@@ -47,7 +47,14 @@ func records(in Inputs, union bool) (buscompose.Records, []Gap) {
// And each node's logins (ADR 0583 §1): both while it rotates.
NodeLogins: in.NodeLogins, Kept: in.Kept, Chunked: in.Chunked, EngineEvents: in.EventSubjects[buscompose.EngineSeat],
// What the bus module's process may define: every manifest's bus state, and what it said it created (DLV-G2).
BusState: map[string][]catalogue.BusStateEntry{}, Bound: in.Bound}
BusState: map[string][]catalogue.BusStateEntry{}, Bound: in.Bound,
// Each node's capability reading and what each node-engine said running (design 46 approval mark §6).
Readings: in.Readings, Running: map[string]string{}}
for key, h := range in.Heard {
if !h.Stopped {
out.Running[key] = h.Fingerprint
}
}
for module, m := range in.Manifests {
if len(m.BusState) > 0 {
out.BusState[module] = m.BusState
@@ -163,6 +170,12 @@ func declaredFor(m catalogue.Manifest, seats map[string]catalogue.SeatDeclaratio
Ledgers: ledgersOf(m),
}
d.Loads = loadsABundle(m)
// Its marked tools (design 46 approval mark §1, §6): a name its `needs-approval` gives that it serves as a tool.
for _, mark := range m.NeedsApproval {
if slices.Contains(m.Tools, mark) {
d.Marked = append(d.Marked, mark)
}
}
d.CatchUps = catchUpsOf(m)
if _, reads := m.OwnSecrets["broker"]; !reads {
d.NoAccount = true
@@ -176,6 +189,7 @@ func declaredFor(m catalogue.Manifest, seats map[string]catalogue.SeatDeclaratio
held := asSeat(s, declarers[s.Name])
held.Kind = c.Kind
held.Capabilities = c.Capabilities
held.Marked = markedVerbs(m, s)
d.Holds = append(d.Holds, held)
}
}
@@ -396,6 +410,18 @@ func logsOf(m catalogue.Manifest) []buscompose.Log {
return out
}
// markedVerbs are the verbs of a seat the module holds that need the operator's approval: those its definition marks for every
// holder, and those the module's `needs-approval` names as `<seat>.<verb>` (design 46 approval mark §2, §6).
func markedVerbs(m catalogue.Manifest, s catalogue.SeatDeclaration) []string {
var out []string
for _, v := range s.Serves {
if v.NeedsApproval || slices.Contains(m.NeedsApproval, s.Name+"."+v.Name) {
out = append(out, v.Name)
}
}
return out
}
func asSeat(s catalogue.SeatDeclaration, declarer string) buscompose.Seat {
seat := buscompose.Seat{Name: s.Name, Scope: s.Scope, Accepts: s.Accepts, Emits: s.Emits,
Serves: catalogue.VerbNames(s.Serves), Kinded: s.Kinded, ByCaller: s.ByCaller, Proofs: s.Proofs, DeclaredBy: declarer}
+4 -1
View File
@@ -7,7 +7,8 @@
"name": "mesh-grants",
"scope": "mesh",
"serves": [
"grants-for"
"grants-for",
"withheld"
]
}
],
@@ -37,6 +38,7 @@
"mesh-catalog.seat-definition-changed",
"mesh-catalog.seat-definition-removed",
"mesh-delivery.source-moved",
"mesh-wants.assignment-revision",
"node-engine.running.*.*"
],
"uses": [
@@ -57,6 +59,7 @@
"seat:mesh-catalog.revision",
"seat:mesh-catalog.seat-definitions",
"seat:mesh-delivery.source",
"seat:mesh-wants.revision",
"seat:mesh-hand-acts.hand-act",
"seat:mesh-broker.list"
],