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:
@@ -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"),
|
||||
|
||||
@@ -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 })
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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"
|
||||
],
|
||||
|
||||
Reference in New Issue
Block a user