Merge branch 'feat/47-G-fix11-b4b' into feat/47-G-join11
# Conflicts: # modules/mesh-delivery-assigner/cmd/mesh-delivery-assigner/graphkept.go
This commit is contained in:
@@ -118,23 +118,36 @@ func (p *passer) graphInputs() assigngraph.Inputs {
|
||||
return in
|
||||
}
|
||||
|
||||
// capabilityDigests is the digest of each node's capability reading, the statement its engine made, by node.
|
||||
// capabilityDigests is each node's capability reading, by node: the statement its engine made of what it reads, and the
|
||||
// capabilities its tool runner states with the fingerprint of the runner that announced them, as the node's record carries
|
||||
// them (design 46 approval mark §6, lead answer A-203), the digest over all of it.
|
||||
func (p *passer) capabilityDigests() []meshwants.AssignmentRevisionCapabilitiesItem {
|
||||
if p.reads == nil {
|
||||
return nil
|
||||
}
|
||||
records := p.w.records()
|
||||
known := p.w.knownNodes()
|
||||
slices.Sort(known)
|
||||
var out []meshwants.AssignmentRevisionCapabilitiesItem
|
||||
for _, node := range known {
|
||||
if !p.reads.Known(node) {
|
||||
read := p.reads != nil && p.reads.Known(node)
|
||||
rec, recorded := records[node]
|
||||
if !read && !recorded {
|
||||
continue
|
||||
}
|
||||
reads := slices.Clone(p.reads.Reads([]string{node})[node])
|
||||
slices.Sort(reads)
|
||||
raw, _ := json.Marshal(reads)
|
||||
var reads []string
|
||||
if read {
|
||||
reads = slices.Clone(p.reads.Reads([]string{node})[node])
|
||||
slices.Sort(reads)
|
||||
}
|
||||
item := meshwants.AssignmentRevisionCapabilitiesItem{Node: node, Readings: slices.Clone(rec.RunnerCapabilities),
|
||||
Fingerprint: rec.RunnerFingerprint}
|
||||
slices.Sort(item.Readings)
|
||||
raw, _ := json.Marshal(struct {
|
||||
Reads []string `json:"reads"`
|
||||
Readings []string `json:"readings"`
|
||||
Fingerprint string `json:"fingerprint"`
|
||||
}{reads, item.Readings, item.Fingerprint})
|
||||
sum := sha256.Sum256(raw)
|
||||
out = append(out, meshwants.AssignmentRevisionCapabilitiesItem{Node: node, Digest: "sha256:" + hex.EncodeToString(sum[:])})
|
||||
item.Digest = "sha256:" + hex.EncodeToString(sum[:])
|
||||
out = append(out, item)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
@@ -3,10 +3,12 @@ package main
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"slices"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"git.novox.be/novox/mesh-contracts/go/seats/nodeengine"
|
||||
mcc "git.novox.be/novox/mesh-delivery/modules/mesh-delivery-contracts/go/clients/meshcatalogclient"
|
||||
mwc "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"
|
||||
@@ -38,6 +40,14 @@ func TestTheWantsAreSaidWantedFromTheKeptAssignmentGraphRevision(t *testing.T) {
|
||||
return nil
|
||||
}
|
||||
placeOn(t, p.w, "ace", "web")
|
||||
// Design 46 approval mark §6: a node's reading keeps the fingerprint of the runner that announced it, as its record says.
|
||||
rec, _ := json.Marshal(nodeengine.HaveByNodeNodeValue{V: 1, Record: &nodeengine.NodeRecord{V: 1, Node: "ace",
|
||||
RunnerCapabilities: []string{"bus-state-calls", "approval-mark/1"}, RunnerFingerprint: "sha256:runner"}})
|
||||
p.w.put(buckets.Have, "ace.node", rec)
|
||||
if c := p.capabilityDigests(); len(c) != 1 || c[0].Node != "ace" || c[0].Fingerprint != "sha256:runner" ||
|
||||
!slices.Equal(c[0].Readings, []string{"approval-mark/1", "bus-state-calls"}) || !strings.HasPrefix(c[0].Digest, "sha256:") {
|
||||
t.Errorf("the capability readings are %+v", c)
|
||||
}
|
||||
half := mcc.Half{Revision: meshcatalog.CatalogRevision{Branch: trunkBranch, Kind: "trunk", Revision: 4, Source: 9,
|
||||
Entries: []meshcatalog.EntryRef{{Module: "web", Digest: "sha256:" + strings.Repeat("a", 64)}}},
|
||||
Entries: map[string]meshcatalog.CatalogEntry{"web": {Module: "web", Fp: fpTest}}}
|
||||
|
||||
@@ -241,7 +241,10 @@ func EntryDigest(e meshwants.AssignmentEntry) string {
|
||||
// Same says two revisions list the same entries computed from the same inputs, so the second is no new revision.
|
||||
func Same(a, b meshwants.AssignmentRevision) bool {
|
||||
return slices.Equal(a.Entries, b.Entries) && slices.Equal(a.Catalogs, b.Catalogs) && a.Placements == b.Placements &&
|
||||
slices.Equal(a.Capabilities, b.Capabilities)
|
||||
slices.EqualFunc(a.Capabilities, b.Capabilities, func(x, y meshwants.AssignmentRevisionCapabilitiesItem) bool {
|
||||
// A reading's digest is over all of it: its readings and its runner's fingerprint among it.
|
||||
return x.Node == y.Node && x.Digest == y.Digest
|
||||
})
|
||||
}
|
||||
|
||||
// resolve is the provider requirement r of p points to by §708's order, or the hold that says why none is: none and no
|
||||
|
||||
@@ -483,9 +483,13 @@ type AssignmentEntry struct {
|
||||
|
||||
// AssignmentRevisionCapabilitiesItem is a type of the contract.
|
||||
type AssignmentRevisionCapabilitiesItem struct {
|
||||
// the digest of the node's capability reading
|
||||
// the digest of the node's capability reading: the engine's manifest reads, its tool runner's readings and that runner's fingerprint
|
||||
Digest string `json:"digest"`
|
||||
Node string `json:"node"`
|
||||
// the fingerprint of the tool runner that announced the readings; empty when none is known
|
||||
Fingerprint string `json:"fingerprint,omitempty"`
|
||||
Node string `json:"node"`
|
||||
// the capabilities the node's tool runner states of the calls it answers, as the node's record carries them (design 46 approval mark 6)
|
||||
Readings []string `json:"readings,omitempty"`
|
||||
}
|
||||
|
||||
// AssignmentRevision is an assignment graph revision: the list of its entries' digests and the inputs it was computed from, so it can be computed again and compared.
|
||||
|
||||
@@ -117,7 +117,7 @@ func TestFixtures(t *testing.T) {
|
||||
decode: func(b []byte) (any, error) { var v RevisionInput; err := json.Unmarshal(b, &v); return v, err }, subjectIs: nil},
|
||||
{name: "revision asks a candidate's", subject: "", given: "{\"group\":\"feat/47-G\",\"heads\":\"0123456789ab\"}", wire: "{\"group\":\"feat/47-G\",\"heads\":\"0123456789ab\"}",
|
||||
decode: func(b []byte) (any, error) { var v RevisionInput; err := json.Unmarshal(b, &v); return v, err }, subjectIs: nil},
|
||||
{name: "revision answers one revision naming its inputs", subject: "", given: "{\"revision\":{\"at\":\"2026-11-09T19:00:00Z\",\"capabilities\":[{\"digest\":\"e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855\",\"node\":\"n1\"}],\"catalogs\":[{\"branch\":\"main\",\"revision\":12,\"source\":3589}],\"cause\":\"catalog-revision\",\"entries\":[{\"digest\":\"e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855\",\"module\":\"xclip\",\"node\":\"n1\"}],\"kind\":\"trunk\",\"placements\":\"e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855\",\"revision\":40,\"v\":1}}", wire: "{\"revision\":{\"at\":\"2026-11-09T19:00:00Z\",\"capabilities\":[{\"digest\":\"e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855\",\"node\":\"n1\"}],\"catalogs\":[{\"branch\":\"main\",\"revision\":12,\"source\":3589}],\"cause\":\"catalog-revision\",\"entries\":[{\"digest\":\"e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855\",\"module\":\"xclip\",\"node\":\"n1\"}],\"kind\":\"trunk\",\"placements\":\"e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855\",\"revision\":40,\"v\":1}}",
|
||||
{name: "revision answers one revision naming its inputs", subject: "", given: "{\"revision\":{\"at\":\"2026-11-09T19:00:00Z\",\"capabilities\":[{\"digest\":\"e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855\",\"fingerprint\":\"sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb\",\"node\":\"n1\",\"readings\":[\"approval-mark/1\",\"bus-state-calls\"]}],\"catalogs\":[{\"branch\":\"main\",\"revision\":12,\"source\":3589}],\"cause\":\"catalog-revision\",\"entries\":[{\"digest\":\"e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855\",\"module\":\"xclip\",\"node\":\"n1\"}],\"kind\":\"trunk\",\"placements\":\"e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855\",\"revision\":40,\"v\":1}}", wire: "{\"revision\":{\"at\":\"2026-11-09T19:00:00Z\",\"capabilities\":[{\"digest\":\"e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855\",\"fingerprint\":\"sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb\",\"node\":\"n1\",\"readings\":[\"approval-mark/1\",\"bus-state-calls\"]}],\"catalogs\":[{\"branch\":\"main\",\"revision\":12,\"source\":3589}],\"cause\":\"catalog-revision\",\"entries\":[{\"digest\":\"e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855\",\"module\":\"xclip\",\"node\":\"n1\"}],\"kind\":\"trunk\",\"placements\":\"e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855\",\"revision\":40,\"v\":1}}",
|
||||
decode: func(b []byte) (any, error) { var v RevisionAnswer; err := json.Unmarshal(b, &v); return v, err }, subjectIs: nil},
|
||||
{name: "entry asks by node and module in a revision", subject: "", given: "{\"module\":\"xclip\",\"node\":\"n1\",\"revision\":40}", wire: "{\"module\":\"xclip\",\"node\":\"n1\",\"revision\":40}",
|
||||
decode: func(b []byte) (any, error) { var v EntryInput; err := json.Unmarshal(b, &v); return v, err }, subjectIs: nil},
|
||||
|
||||
@@ -31,7 +31,12 @@
|
||||
"capabilities": [
|
||||
{
|
||||
"node": "n1",
|
||||
"digest": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
|
||||
"digest": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
|
||||
"readings": [
|
||||
"approval-mark/1",
|
||||
"bus-state-calls"
|
||||
],
|
||||
"fingerprint": "sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"
|
||||
}
|
||||
],
|
||||
"cause": "catalog-revision",
|
||||
|
||||
@@ -961,7 +961,18 @@
|
||||
},
|
||||
"digest": {
|
||||
"type": "string",
|
||||
"description": "the digest of the node's capability reading"
|
||||
"description": "the digest of the node's capability reading: the engine's manifest reads, its tool runner's readings and that runner's fingerprint"
|
||||
},
|
||||
"readings": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"description": "the capabilities the node's tool runner states of the calls it answers, as the node's record carries them (design 46 approval mark 6)"
|
||||
},
|
||||
"fingerprint": {
|
||||
"type": "string",
|
||||
"description": "the fingerprint of the tool runner that announced the readings; empty when none is known"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
|
||||
@@ -152,9 +152,13 @@ export interface AssignmentEntry {
|
||||
}
|
||||
|
||||
export interface AssignmentRevisionCapabilitiesItem {
|
||||
/** the digest of the node's capability reading */
|
||||
/** the digest of the node's capability reading: the engine's manifest reads, its tool runner's readings and that runner's fingerprint */
|
||||
"digest": string;
|
||||
/** the fingerprint of the tool runner that announced the readings; empty when none is known */
|
||||
"fingerprint"?: string;
|
||||
"node": string;
|
||||
/** the capabilities the node's tool runner states of the calls it answers, as the node's record carries them (design 46 approval mark 6) */
|
||||
"readings"?: string[];
|
||||
}
|
||||
|
||||
/** an assignment graph revision: the list of its entries' digests and the inputs it was computed from, so it can be computed again and compared */
|
||||
|
||||
@@ -623,3 +623,58 @@ func theToolRunnerHearsEveryEventOnTheBus(t *testing.T) {
|
||||
}
|
||||
hears(ledger, "the restricted stream after its grant value changed")
|
||||
}
|
||||
|
||||
// aRuntimeDeniedTheMarksHearsNoMarkedCall, run by TestTheGrantsPartWhole, defends design 46 approval mark §6 on a real
|
||||
// server (AM-D4): a runtime account composed with the deny, its node's tool runner stating no approval-mark/1, holds
|
||||
// `mesh.mod.<module>.tool.>` and is delivered no call on a marked tool's subject nor on the caller-named subject
|
||||
// process.Call sends on first; a call of an unmarked tool still reaches it.
|
||||
func aRuntimeDeniedTheMarksHearsNoMarkedCall(t *testing.T) {
|
||||
records := buscompose.Records{Nodes: []string{"a"}, Assigned: map[string][]buscompose.Declared{"a": {
|
||||
{Module: "node-tools", ToolRunner: true},
|
||||
{Module: "shell", Serves: []string{"execute", "read"}, Marked: []string{"execute"}},
|
||||
{Module: "asker", Invokes: []string{"shell.execute", "shell.read"}},
|
||||
}}, Readings: map[string]buscompose.RunnerReading{"a": {Capabilities: []string{"bus-state-calls"}, Fingerprint: "sha256:runner"}},
|
||||
Running: map[string]string{"a.node-tools": "sha256:runner"}}
|
||||
bus := startComposedBus(t, records)
|
||||
runner, _ := bus.dial("a.node-tools")
|
||||
heard := make(chan *nats.Msg, 16)
|
||||
for _, s := range []string{"mesh.mod.shell.tool.>", "mesh.mod.shell.call.>"} {
|
||||
if _, err := runner.ChanSubscribe(s, heard); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
if err := runner.Flush(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
asker, askerRefused := bus.dial("a.asker")
|
||||
// The caller-named subject is process.Call's first: mesh.mod.<module>.call.<tool>.<caller's token>.
|
||||
for _, subject := range []string{"mesh.mod.shell.tool.execute", "mesh.mod.shell.call.execute.asker~a", "mesh.mod.shell.tool.read"} {
|
||||
if err := asker.Publish(subject, []byte(`{}`)); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
if err := asker.Flush(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
select {
|
||||
case e := <-askerRefused:
|
||||
t.Fatalf("the asker may not call: %s", e)
|
||||
default:
|
||||
}
|
||||
select {
|
||||
case m := <-heard:
|
||||
if m.Subject != "mesh.mod.shell.tool.read" {
|
||||
t.Errorf("the runtime denied the marks was delivered %s", m.Subject)
|
||||
}
|
||||
case <-time.After(5 * time.Second):
|
||||
t.Fatal("the unmarked call did not reach the runtime")
|
||||
}
|
||||
if err := runner.Flush(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
select {
|
||||
case m := <-heard:
|
||||
t.Errorf("the runtime denied the marks was delivered %s", m.Subject)
|
||||
default:
|
||||
}
|
||||
}
|
||||
|
||||
@@ -628,6 +628,7 @@ func TestTheGrantsPartWhole(t *testing.T) {
|
||||
// And what the part composes holds on a server that loads it (one integration test per module, the test budget).
|
||||
t.Run("the composed grants hold on a server that loads them", theBusNodeFollowsTheSealedListUnderTheComposedGrants)
|
||||
t.Run("the tool runner hears every event on a server that loads its grants", theToolRunnerHearsEveryEventOnTheBus)
|
||||
t.Run("a runtime denied the marks is delivered no call on a marked subject", aRuntimeDeniedTheMarksHearsNoMarkedCall)
|
||||
}
|
||||
|
||||
// lockedLog is the part's log, read by the test while the part writes it.
|
||||
|
||||
@@ -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)
|
||||
@@ -1283,6 +1324,10 @@ func TestGrants(t *testing.T) {
|
||||
for i := range readable {
|
||||
readable[i].PublishDeny = []string{}
|
||||
}
|
||||
// A subscribe the shadow withholds (design 46 approval mark §6) where the loaded list withholds none, as on the first
|
||||
// composition after the release: a narrowing, no right lost. Its reverse, a node gaining approval-mark/1: a right added.
|
||||
withheld := slices.Clone(shadow)
|
||||
withheld[x].SubscribeDeny = []string{"mesh.mod.m.tool.execute", "mesh.mod.m.tool.execute.*"}
|
||||
// The live comparison the operator's yes was refused on, 2026-10-27 21:19 UTC (issue 1945): 41 users differ, every
|
||||
// difference an added right. Read whole; a row's mutate makes one of the cases that must still refuse.
|
||||
var live struct {
|
||||
@@ -1327,6 +1372,8 @@ func TestGrants(t *testing.T) {
|
||||
{"only a right added", fewer, nil, nil, nil, []string{"a.mod-x"}, false, nil, nil, 1},
|
||||
{"a deny added refuses", readable, nil, nil, nil, []string{"a.mod-x"}, false, nil, []string{"a.mod-x"}, 1},
|
||||
{"a deny the loaded list cannot say refuses a user whose grants differ", fewer, nil, nil, nil, []string{"a.mod-x"}, false, nil, []string{"a.mod-x"}, 1},
|
||||
{"a subscribe withheld passes as no right lost", shadow, nil, nil, nil, []string{"a.mod-x"}, false, nil, nil, 0},
|
||||
{"a subscribe no longer withheld is a right added", withheld, nil, nil, nil, []string{"a.mod-x"}, false, nil, nil, 1},
|
||||
{"captured 21:19: only added rights", nil, nil, nil, nil, nil, false, captured(nil), nil, 41},
|
||||
{"captured, a right removed", nil, nil, nil, nil, nil, false, captured(func(c *grants.Comparison) {
|
||||
nodeAce(c).SubscribeDropped = []string{"mesh.mod.mesh-vault.event.secret-changed"}
|
||||
@@ -1351,6 +1398,9 @@ func TestGrants(t *testing.T) {
|
||||
if strings.Contains(row.name, "deny") {
|
||||
from = denied
|
||||
}
|
||||
if strings.Contains(row.name, "withheld passes") {
|
||||
from = withheld
|
||||
}
|
||||
c = grants.Compare(from, row.loaded, grants.Placed(in))
|
||||
var differ []string
|
||||
for _, d := range c.Differ {
|
||||
@@ -5147,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"), " ")
|
||||
@@ -601,6 +602,12 @@ func TestTheCopiedComposerComposesAsTheControllers(t *testing.T) {
|
||||
if err != nil || perr != nil || !strings.Contains(list, " users = [\n"+AccountsEntry(filled[0].Username(), "$2a$11$h", perms)+" ]\n") {
|
||||
t.Errorf("the accounts entry is not ComposeAccounts' own: %v %v\n%s", err, perr, list)
|
||||
}
|
||||
// A subscribe deny (design 46 approval mark §6) is written as the server's deny beside its allow, and outranks it.
|
||||
withheld := Permissions{Publish: []string{"a"}, Subscribe: []string{"mesh.mod.m.tool.>"}, SubscribeDeny: []string{"mesh.mod.m.tool.execute"}}
|
||||
if entry := AccountsEntry("u", "h", withheld); !strings.Contains(entry, `subscribe: { allow: ["mesh.mod.m.tool.>"], deny: ["mesh.mod.m.tool.execute"] }`) ||
|
||||
MaySubscribe(withheld, "mesh.mod.m.tool.execute") || !MaySubscribe(withheld, "mesh.mod.m.tool.read") {
|
||||
t.Errorf("a subscribe deny is written as %s", entry)
|
||||
}
|
||||
c, _ := ConsumerFor(Principal{Kind: KindModule, Node: "a", Module: "m", Consumes: []string{"x.happened"}})
|
||||
cfg := ConsumerConfig(c)
|
||||
if cfg.Durable != "a_m" || cfg.AckPolicy != jetstream.AckExplicitPolicy || cfg.AckWait != 30*time.Second ||
|
||||
@@ -1558,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
|
||||
@@ -392,8 +398,14 @@ func MayPublish(perms Permissions, subject string) bool {
|
||||
return false
|
||||
}
|
||||
|
||||
// MaySubscribe says whether a principal with these permissions may subscribe to (and so answer) a subject.
|
||||
// MaySubscribe says whether a principal with these permissions may subscribe to (and so answer) a subject: an allow
|
||||
// covers it and no deny does.
|
||||
func MaySubscribe(perms Permissions, subject string) bool {
|
||||
for _, d := range perms.SubscribeDeny {
|
||||
if SubjectsOverlap(d, subject) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
for _, a := range perms.Subscribe {
|
||||
if SubjectsOverlap(a, subject) {
|
||||
return true
|
||||
@@ -599,6 +611,10 @@ type Permissions struct {
|
||||
// denied to everybody whose grant is wide enough to reach them. The server's deny outranks its allow.
|
||||
PublishDeny []string
|
||||
Subscribe []string
|
||||
// SubscribeDeny are subjects the principal may not subscribe to although an allow covers them (design 46 approval
|
||||
// mark §6): every marked subject of a module its runtime carries, where the node's tool runner does not honour the mark.
|
||||
// The server's deny outranks its allow of `mesh.mod.<module>.tool.>`.
|
||||
SubscribeDeny []string
|
||||
// AllowResponses lets a principal reply to a request it received, on the reply subject that
|
||||
// request carried, once.
|
||||
//
|
||||
@@ -1302,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
|
||||
@@ -1566,7 +1589,11 @@ func AccountsEntry(user, passwordHash string, perms Permissions) string {
|
||||
} else {
|
||||
fmt.Fprintf(&b, " publish: { allow: [%s] }\n", quoted(perms.Publish))
|
||||
}
|
||||
fmt.Fprintf(&b, " subscribe: { allow: [%s] }\n", quoted(perms.Subscribe))
|
||||
if len(perms.SubscribeDeny) > 0 {
|
||||
fmt.Fprintf(&b, " subscribe: { allow: [%s], deny: [%s] }\n", quoted(perms.Subscribe), quoted(perms.SubscribeDeny))
|
||||
} else {
|
||||
fmt.Fprintf(&b, " subscribe: { allow: [%s] }\n", quoted(perms.Subscribe))
|
||||
}
|
||||
if perms.AllowResponses {
|
||||
fmt.Fprintf(&b, " allow_responses: { max: 1, ttl: \"%dm\" }\n", int(ResponseTTL/time.Minute))
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -75,6 +75,7 @@ func GrantWaits(accounts []string, composed, loaded map[string]User, unknown str
|
||||
if l.PublishDeny != nil {
|
||||
n += len(minus(l.PublishDeny, c.PublishDeny))
|
||||
}
|
||||
n += len(minus(l.SubscribeDeny, c.SubscribeDeny))
|
||||
if n > 0 {
|
||||
adds = append(adds, fmt.Sprintf("%s (%d permissions)", user, n))
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
@@ -60,7 +62,8 @@ func KeepLoaded(composed, loaded []User, inForce map[string]User) map[string]Use
|
||||
// Every deny any list says is kept: a deny the composer drops while it composes part of the mesh would widen the
|
||||
// user (the composer gives every composed user a deny, empty or not, so an intersection would drop the list in
|
||||
// force's).
|
||||
u := User{User: name, Publish: c.Publish, Subscribe: c.Subscribe, AllowResponses: c.AllowResponses, PublishDeny: c.PublishDeny}
|
||||
u := User{User: name, Publish: c.Publish, Subscribe: c.Subscribe, AllowResponses: c.AllowResponses, PublishDeny: c.PublishDeny,
|
||||
SubscribeDeny: c.SubscribeDeny}
|
||||
for _, o := range from {
|
||||
u.Publish = set(append(slices.Clone(u.Publish), o.Publish...))
|
||||
u.Subscribe = set(append(slices.Clone(u.Subscribe), o.Subscribe...))
|
||||
@@ -68,6 +71,9 @@ func KeepLoaded(composed, loaded []User, inForce map[string]User) map[string]Use
|
||||
if o.PublishDeny != nil || u.PublishDeny != nil {
|
||||
u.PublishDeny = set(append(slices.Clone(u.PublishDeny), o.PublishDeny...))
|
||||
}
|
||||
if o.SubscribeDeny != nil || u.SubscribeDeny != nil {
|
||||
u.SubscribeDeny = set(append(slices.Clone(u.SubscribeDeny), o.SubscribeDeny...))
|
||||
}
|
||||
}
|
||||
if u.Publish == nil {
|
||||
u.Publish = []string{}
|
||||
@@ -94,6 +100,11 @@ func Losses(now, had User) []string {
|
||||
if had.AllowResponses && !now.AllowResponses {
|
||||
out = append(out, "responses")
|
||||
}
|
||||
// A subscribe deny now adds is a narrowing and no word of had lost (design 46 approval mark §6); one had held that now
|
||||
// drops widens the user past it.
|
||||
for _, s := range minus(had.SubscribeDeny, now.SubscribeDeny) {
|
||||
out = append(out, "subscribe deny dropped "+s)
|
||||
}
|
||||
if had.PublishDeny != nil {
|
||||
for _, s := range minus(now.PublishDeny, had.PublishDeny) {
|
||||
out = append(out, "deny "+s)
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -39,7 +39,7 @@ func ComposeEach(r buscompose.Records) ([]User, map[string]string, error) {
|
||||
continue
|
||||
}
|
||||
out = append(out, User{User: p.Username(), Publish: set(perms.Publish), Subscribe: set(perms.Subscribe),
|
||||
AllowResponses: perms.AllowResponses, PublishDeny: set(perms.PublishDeny)})
|
||||
AllowResponses: perms.AllowResponses, PublishDeny: set(perms.PublishDeny), SubscribeDeny: set(perms.SubscribeDeny)})
|
||||
}
|
||||
sort.Slice(out, func(i, j int) bool { return out[i].User < out[j].User })
|
||||
return out, refused, nil
|
||||
@@ -81,7 +81,7 @@ func withKept(users []User, kept map[string]User) []User {
|
||||
// (buscompose.AccountsEntry), so the bus reads it as it read the list in force.
|
||||
func keptEntry(u User, hash string) string {
|
||||
return buscompose.AccountsEntry(u.User, hash, buscompose.Permissions{Publish: u.Publish, PublishDeny: u.PublishDeny,
|
||||
Subscribe: u.Subscribe, AllowResponses: u.AllowResponses})
|
||||
Subscribe: u.Subscribe, SubscribeDeny: u.SubscribeDeny, AllowResponses: u.AllowResponses})
|
||||
}
|
||||
|
||||
// withEntries puts entries at the end of the accounts block's users, before it closes.
|
||||
|
||||
@@ -24,6 +24,9 @@ type User struct {
|
||||
// PublishDeny is what the user may not publish though an allow reaches it (buscompose's ControllerOnly). The bus
|
||||
// module's nats_users answers none today, so a loaded user's is nil, read as unknown; empty and not nil once it says.
|
||||
PublishDeny []string `json:"publish_deny,omitempty"`
|
||||
// SubscribeDeny is what the user may not subscribe to though an allow reaches it: the marked subjects withheld from a
|
||||
// runtime whose tool runner does not honour the mark (design 46 approval mark §6). Nil and empty are both none.
|
||||
SubscribeDeny []string `json:"subscribe_deny,omitempty"`
|
||||
// Hash is the bcrypt hash the vault holds for the user (WithHashes): in memory alone, never in a digest, a compare
|
||||
// or the part's state, and empty for a loaded user.
|
||||
Hash string `json:"-"`
|
||||
@@ -227,11 +230,17 @@ type UserDiff struct {
|
||||
// none): it cannot be compared, so it is no added right either (ADR 0562 §2).
|
||||
DenyAdded []string `json:"deny_added,omitempty"`
|
||||
DenyUnread []string `json:"deny_unread,omitempty"`
|
||||
// SubscribeDenyAdded is what the shadow list withholds from the user's subscriptions and the loaded list does not: a
|
||||
// narrowing, which loses no right and passes the guard at once (design 46 approval mark §6). SubscribeDenyDropped is
|
||||
// what the loaded list withholds and the shadow list no longer does: a right added.
|
||||
SubscribeDenyAdded []string `json:"subscribe_deny_added,omitempty"`
|
||||
SubscribeDenyDropped []string `json:"subscribe_deny_dropped,omitempty"`
|
||||
}
|
||||
|
||||
// same says the user's grants agree in both lists.
|
||||
func (d UserDiff) same() bool {
|
||||
return len(d.PublishAdded)+len(d.PublishDropped)+len(d.SubscribeAdded)+len(d.SubscribeDropped)+len(d.DenyAdded) == 0 && d.Responses == ""
|
||||
return len(d.PublishAdded)+len(d.PublishDropped)+len(d.SubscribeAdded)+len(d.SubscribeDropped)+len(d.DenyAdded)+
|
||||
len(d.SubscribeDenyAdded)+len(d.SubscribeDenyDropped) == 0 && d.Responses == ""
|
||||
}
|
||||
|
||||
// Comparison is the shadow list against the loaded list.
|
||||
@@ -320,11 +329,12 @@ func (c Comparison) Removing() []string {
|
||||
func (c Comparison) AddedRights() []UserDiff {
|
||||
var out []UserDiff
|
||||
for _, d := range c.Differ {
|
||||
a := UserDiff{User: d.User, PublishAdded: d.PublishAdded, SubscribeAdded: d.SubscribeAdded}
|
||||
a := UserDiff{User: d.User, PublishAdded: d.PublishAdded, SubscribeAdded: d.SubscribeAdded,
|
||||
SubscribeDenyDropped: d.SubscribeDenyDropped}
|
||||
if !d.responsesLost() {
|
||||
a.Responses = d.Responses
|
||||
}
|
||||
if len(a.PublishAdded)+len(a.SubscribeAdded) > 0 || a.Responses != "" {
|
||||
if len(a.PublishAdded)+len(a.SubscribeAdded)+len(a.SubscribeDenyDropped) > 0 || a.Responses != "" {
|
||||
out = append(out, a)
|
||||
}
|
||||
}
|
||||
@@ -355,6 +365,8 @@ func Compare(shadow, loaded []User, placed func(user string) bool) Comparison {
|
||||
if l.PublishDeny != nil {
|
||||
d.DenyAdded = minus(s.PublishDeny, l.PublishDeny)
|
||||
}
|
||||
// A loaded list that says no subscribe deny withholds none: the bus holds none it was not given.
|
||||
d.SubscribeDenyAdded, d.SubscribeDenyDropped = minus(s.SubscribeDeny, l.SubscribeDeny), minus(l.SubscribeDeny, s.SubscribeDeny)
|
||||
if d.same() {
|
||||
// The same allows compose the same deny: one composer derives it from them.
|
||||
c.Same++
|
||||
|
||||
@@ -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