Let a tools bundle declare its own bus account and what it may remove, so the bus tools remove the controller's dead-letter consumer and the node-engine's acts on a declared account with no idle service (issue 1952 S5, ADR 0564 Limits 4)
This commit is contained in:
@@ -13,6 +13,7 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"git.novox.be/novox/mesh-sdk/go/announce"
|
"git.novox.be/novox/mesh-sdk/go/announce"
|
||||||
|
"git.novox.be/novox/mesh-sdk/go/catalogue"
|
||||||
"git.novox.be/novox/mesh-sdk/go/delivery"
|
"git.novox.be/novox/mesh-sdk/go/delivery"
|
||||||
"github.com/nats-io/nats.go/jetstream"
|
"github.com/nats-io/nats.go/jetstream"
|
||||||
)
|
)
|
||||||
@@ -596,8 +597,9 @@ func healthChecks(t *testing.T) {
|
|||||||
{Module: WatchmanModule, State: []Bucket{bucket(WatchmanModule, "findings")}},
|
{Module: WatchmanModule, State: []Bucket{bucket(WatchmanModule, "findings")}},
|
||||||
{Module: "telegram", Holds: []Seat{{Name: "telegram-sender", Accepts: []string{"send"}}},
|
{Module: "telegram", Holds: []Seat{{Name: "telegram-sender", Accepts: []string{"send"}}},
|
||||||
Processes: []ProcessPrincipal{{Process: "w", Queues: []QueueOwner{{Process: "w", Stream: "M_WORK"}, {Process: "w", Stream: "KV_x"}}}}},
|
Processes: []ProcessPrincipal{{Process: "w", Queues: []QueueOwner{{Process: "w", Stream: "M_WORK"}, {Process: "w", Stream: "KV_x"}}}}},
|
||||||
{Module: "nats", SnapshotsTheBus: true, Processes: []ProcessPrincipal{{Process: BusToolsProcess}, {Process: "other"}}},
|
{Module: "nats", SnapshotsTheBus: true, Processes: []ProcessPrincipal{{Process: "nats-tools",
|
||||||
{Module: "shop", Processes: []ProcessPrincipal{{Process: BusToolsProcess}}},
|
Removes: []string{catalogue.RemovesControllerDeadLetterConsumer, catalogue.RemovesEngineActs}}, {Process: "other"}}},
|
||||||
|
{Module: "shop", Processes: []ProcessPrincipal{{Process: "nats-tools", Removes: []string{catalogue.RemovesEngineActs}}}},
|
||||||
}}})
|
}}})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
@@ -627,12 +629,19 @@ func healthChecks(t *testing.T) {
|
|||||||
WatchmanModule: {"$KV.mesh-conditions_conditions.x", "$JS.API.CONSUMER.DELETE.DEAD_LETTER_NOTICES.controller",
|
WatchmanModule: {"$KV.mesh-conditions_conditions.x", "$JS.API.CONSUMER.DELETE.DEAD_LETTER_NOTICES.controller",
|
||||||
"$JS.API.CONSUMER.CREATE.DEAD_LETTER_NOTICES.controller", "$JS.API.CONSUMER.DELETE.DEAD_LETTER_NOTICES." + WatchmanModule},
|
"$JS.API.CONSUMER.CREATE.DEAD_LETTER_NOTICES.controller", "$JS.API.CONSUMER.DELETE.DEAD_LETTER_NOTICES." + WatchmanModule},
|
||||||
// The bus module's tools remove the one consumer and the acts' keys, nothing else (Limits 4); its other accounts
|
// The bus module's tools remove the one consumer and the acts' keys, nothing else (Limits 4); its other accounts
|
||||||
// remove nothing, and a process of that name in another module is no tools account.
|
// remove nothing; an account is granted the removals its bus declares and no other (the catalogue refuses them
|
||||||
|
// outside the bus module).
|
||||||
"a.nats.nats-tools": {"$JS.API.CONSUMER.DELETE.DEAD_LETTER_NOTICES.other", "$JS.API.CONSUMER.DELETE.EVENTS.a_m",
|
"a.nats.nats-tools": {"$JS.API.CONSUMER.DELETE.DEAD_LETTER_NOTICES.other", "$JS.API.CONSUMER.DELETE.EVENTS.a_m",
|
||||||
"$KV.mesh-conditions_conditions.x", "$JS.API.STREAM.DELETE.KV_mesh-host_acts", "$JS.API.STREAM.PURGE.KV_mesh-host_acts"},
|
"$KV.mesh-conditions_conditions.x", "$JS.API.STREAM.DELETE.KV_mesh-host_acts", "$JS.API.STREAM.PURGE.KV_mesh-host_acts"},
|
||||||
"a.nats.other": {"$JS.API.CONSUMER.DELETE.DEAD_LETTER_NOTICES.controller", "$KV.mesh-host_acts.g14.a1"},
|
"a.nats.other": {"$JS.API.CONSUMER.DELETE.DEAD_LETTER_NOTICES.controller", "$KV.mesh-host_acts.g14.a1"},
|
||||||
"a.nats": {"$JS.API.CONSUMER.DELETE.DEAD_LETTER_NOTICES.controller", "$KV.mesh-host_acts.g14.a1"},
|
"a.nats": {"$JS.API.CONSUMER.DELETE.DEAD_LETTER_NOTICES.controller", "$KV.mesh-host_acts.g14.a1"},
|
||||||
"a.shop.nats-tools": {"$JS.API.CONSUMER.DELETE.DEAD_LETTER_NOTICES.controller", "$KV.mesh-host_acts.g14.a1"},
|
"a.shop.nats-tools": {"$JS.API.CONSUMER.DELETE.DEAD_LETTER_NOTICES.controller", "$KV.mesh-conditions_conditions.x"},
|
||||||
|
}
|
||||||
|
// The tools account's secret is the module's own broker-<binary>, which its manifest's own-secrets names and the
|
||||||
|
// vault keeps by that name (AccountSecret).
|
||||||
|
if w := (WantedAccount{User: "a.nats.nats-tools", Kind: KindProcess, Node: "a", Module: "nats", Process: "nats-tools",
|
||||||
|
URL: "nats://a:4222"}); w.Valid() != nil || w.OwnSecret() != "broker-nats-tools" {
|
||||||
|
t.Errorf("the tools account is held in %q (%v)", w.OwnSecret(), w.Valid())
|
||||||
}
|
}
|
||||||
seen := map[string]bool{}
|
seen := map[string]bool{}
|
||||||
for _, u := range users {
|
for _, u := range users {
|
||||||
|
|||||||
@@ -1,9 +1,12 @@
|
|||||||
package buscompose
|
package buscompose
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
|
"slices"
|
||||||
"sort"
|
"sort"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
"git.novox.be/novox/mesh-sdk/go/catalogue"
|
||||||
"git.novox.be/novox/mesh-sdk/go/health"
|
"git.novox.be/novox/mesh-sdk/go/health"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -19,9 +22,9 @@ import (
|
|||||||
// - The watchman makes its own consumer on DEAD_LETTER_NOTICES, by its own name, pulled, and reads whether the
|
// - The watchman makes its own consumer on DEAD_LETTER_NOTICES, by its own name, pulled, and reads whether the
|
||||||
// controller's consumer `controller` still stands; it may neither delete nor change that consumer (clause 5). It
|
// controller's consumer `controller` still stands; it may neither delete nor change that consumer (clause 5). It
|
||||||
// lists and shows DEAD_LETTERS, which is all it does before the lease write.
|
// lists and shows DEAD_LETTERS, which is all it does before the lease write.
|
||||||
// - The bus module's tools run on a process account of their own, BusToolsProcess, which may remove only the
|
// - The bus module's tools run on an account of their own, declared by their bundle's `bus` with `removes`
|
||||||
// consumer `controller` on DEAD_LETTER_NOTICES and keys of the node-engine's acts bucket (Limits 4), never the
|
// (catalogue.BusAccounts), which may remove only the consumer `controller` on DEAD_LETTER_NOTICES and keys of the
|
||||||
// backup's account `broker`, which only reads (ADR 0235).
|
// node-engine's acts bucket (Limits 4), never the backup's account `broker`, which only reads (ADR 0235).
|
||||||
|
|
||||||
// The states the composer names its readers for, beside the lease (NamedReaders).
|
// The states the composer names its readers for, beside the lease (NamedReaders).
|
||||||
var (
|
var (
|
||||||
@@ -34,28 +37,32 @@ var (
|
|||||||
HealersWould = health.HealersModule + "." + health.WouldState
|
HealersWould = health.HealersModule + "." + health.WouldState
|
||||||
)
|
)
|
||||||
|
|
||||||
// BusToolsProcess is the process of the module holding mesh-broker whose account removes from the bus (ADR 0564 Limits
|
// BusRemovalGrants are the whole authority of an account whose bus declares removals (ADR 0564 Limits 4, issue 1952
|
||||||
// 4): its secret is broker-nats-tools, as every process account's is broker-<process id> (ADR 0436 §2).
|
// notes 6 and 7), each removal its grant: the controller's consumer on DEAD_LETTER_NOTICES read and deleted (the lease
|
||||||
const BusToolsProcess = "nats-tools"
|
// write's step, clause 5); the node-engine's acts bucket read (STREAM.INFO with a subject filter lists what a removal
|
||||||
|
// would take, so the tools read before they remove) and its keys removed, which nothing else may empty when it fills. A
|
||||||
// BusRemovalGrants is the whole authority of the bus module's tools account (ADR 0564 Limits 4, issue 1952 notes 6 and
|
// removal outside the set is refused, as the catalogue refuses it (catalogue.BusRemovals).
|
||||||
// 7): read and delete the controller's consumer on DEAD_LETTER_NOTICES (the lease write's step, clause 5), and read and
|
func BusRemovalGrants(removes []string) ([]string, error) {
|
||||||
// remove keys of the node-engine's acts bucket, which nothing else may empty when it fills. STREAM.INFO with a subject
|
var out []string
|
||||||
// filter lists what a removal would take, so the tools read before they remove. A further consumer or bucket is a change
|
for _, r := range removes {
|
||||||
// of this grant.
|
switch r {
|
||||||
func BusRemovalGrants() []string {
|
case catalogue.RemovesControllerDeadLetterConsumer:
|
||||||
return []string{
|
out = append(out, "$JS.API.CONSUMER.INFO."+DeadLetterNoticesStream+"."+ControllerName,
|
||||||
"$JS.API.CONSUMER.INFO." + DeadLetterNoticesStream + "." + ControllerName,
|
"$JS.API.CONSUMER.DELETE."+DeadLetterNoticesStream+"."+ControllerName)
|
||||||
"$JS.API.CONSUMER.DELETE." + DeadLetterNoticesStream + "." + ControllerName,
|
case catalogue.RemovesEngineActs:
|
||||||
"$JS.API.STREAM.INFO.KV_" + EngineActsBucket,
|
out = append(out, "$JS.API.STREAM.INFO.KV_"+EngineActsBucket, "$KV."+EngineActsBucket+".>")
|
||||||
"$KV." + EngineActsBucket + ".>",
|
default:
|
||||||
|
return nil, fmt.Errorf("a bus account removes %q, which is not one of %s (novox/hq ADR 0564 Limits 4)", r,
|
||||||
|
strings.Join(catalogue.BusRemovals, ", "))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
return out, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// removesEngineActs is the bus module's tools account removing keys of the node-engine's acts bucket: the writers
|
// removesEngineActs is an account declared to remove keys of the node-engine's acts bucket: the writers table's one
|
||||||
// table's one exemption from a node's engine being the only writer of its keys, for removal (ADR 0564 Limits 4).
|
// exemption from a node's engine being the only writer of its keys, for removal (ADR 0564 Limits 4).
|
||||||
func removesEngineActs(p Principal, pattern string) bool {
|
func removesEngineActs(p Principal, pattern string) bool {
|
||||||
return p.Kind == KindProcess && p.RemovesFromTheBus && pattern == "$KV."+EngineActsBucket+".>"
|
return p.Kind == KindProcess && slices.Contains(p.Removes, catalogue.RemovesEngineActs) && pattern == "$KV."+EngineActsBucket+".>"
|
||||||
}
|
}
|
||||||
|
|
||||||
// WatchmanDeadLetterConsumer is the watchman's own consumer on DEAD_LETTER_NOTICES, made only once the lease names no
|
// WatchmanDeadLetterConsumer is the watchman's own consumer on DEAD_LETTER_NOTICES, made only once the lease names no
|
||||||
|
|||||||
@@ -196,9 +196,9 @@ type Principal struct {
|
|||||||
// whole authority is BusSnapshotGrants: it copies the streams for the night's backup and nothing
|
// whole authority is BusSnapshotGrants: it copies the streams for the night's backup and nothing
|
||||||
// else — not its declarations, which the node's tool runtime serves for it.
|
// else — not its declarations, which the node's tool runtime serves for it.
|
||||||
SnapshotsTheBus bool
|
SnapshotsTheBus bool
|
||||||
// RemovesFromTheBus is the bus module's tools process (BusToolsProcess, novox/hq ADR 0564 Limits 4): its whole
|
// Removes are, for a process or tools bundle account whose bus declares them, what it may remove from the bus
|
||||||
// authority is BusRemovalGrants.
|
// (novox/hq ADR 0564 Limits 4): its whole authority is then BusRemovalGrants of them.
|
||||||
RemovesFromTheBus bool
|
Removes []string
|
||||||
// DeadLetterSources are, for the watchman, the streams a dead letter it keeps may come from, by name
|
// DeadLetterSources are, for the watchman, the streams a dead letter it keeps may come from, by name
|
||||||
// (deadLetterSourcesOf, novox/hq ADR 0564 clause 5): never a bucket's.
|
// (deadLetterSourcesOf, novox/hq ADR 0564 clause 5): never a bucket's.
|
||||||
DeadLetterSources []string
|
DeadLetterSources []string
|
||||||
@@ -661,8 +661,11 @@ func PermissionsFor(p Principal) (Permissions, error) {
|
|||||||
return Permissions{Publish: pub, Subscribe: []string{p.inbox()}}, nil
|
return Permissions{Publish: pub, Subscribe: []string{p.inbox()}}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
if p.Kind == KindProcess && p.RemovesFromTheBus {
|
if p.Kind == KindProcess && len(p.Removes) > 0 {
|
||||||
pub := BusRemovalGrants()
|
pub, err := BusRemovalGrants(p.Removes)
|
||||||
|
if err != nil {
|
||||||
|
return Permissions{}, err
|
||||||
|
}
|
||||||
sort.Strings(pub)
|
sort.Strings(pub)
|
||||||
if err := CheckWriters(p, pub); err != nil {
|
if err := CheckWriters(p, pub); err != nil {
|
||||||
return Permissions{}, err
|
return Permissions{}, err
|
||||||
|
|||||||
@@ -105,6 +105,9 @@ type ProcessPrincipal struct {
|
|||||||
Emits []string
|
Emits []string
|
||||||
// Queues are the module's own work-queue streams this process is the one consumer of.
|
// Queues are the module's own work-queue streams this process is the one consumer of.
|
||||||
Queues []QueueOwner
|
Queues []QueueOwner
|
||||||
|
// Removes are what its account may remove from the bus, as its bus declares them (catalogue.BusRemovals; novox/hq
|
||||||
|
// ADR 0564 Limits 4): a tools bundle's account, whose whole authority is then BusRemovalGrants.
|
||||||
|
Removes []string
|
||||||
// State is the module's own state, by local name, this process reads and writes.
|
// State is the module's own state, by local name, this process reads and writes.
|
||||||
State []string
|
State []string
|
||||||
// Asks says whether this process reads the records of its module's own asks (novox/hq ADR 0259 §3), on
|
// Asks says whether this process reads the records of its module's own asks (novox/hq ADR 0259 §3), on
|
||||||
@@ -204,7 +207,7 @@ func Users(r Records) ([]Principal, error) {
|
|||||||
out = append(out, Principal{
|
out = append(out, Principal{
|
||||||
Kind: KindProcess, Node: node, Module: d.Module, Process: pr.Process,
|
Kind: KindProcess, Node: node, Module: d.Module, Process: pr.Process,
|
||||||
Emits: pr.Emits, Queues: pr.Queues, State: pr.State, Uses: d.Uses, Asks: pr.Asks,
|
Emits: pr.Emits, Queues: pr.Queues, State: pr.State, Uses: d.Uses, Asks: pr.Asks,
|
||||||
Invokes: pr.Invokes, RemovesFromTheBus: d.SnapshotsTheBus && pr.Process == BusToolsProcess,
|
Invokes: pr.Invokes, Removes: pr.Removes,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
if runtimeHere && d.Module == RuntimeModule {
|
if runtimeHere && d.Module == RuntimeModule {
|
||||||
|
|||||||
@@ -136,7 +136,7 @@ func (m Manifest) Resolve(built []Built) (Manifest, error) {
|
|||||||
Name: a.Name, Source: Recorded(made.Reference), Digest: made.Digest,
|
Name: a.Name, Source: Recorded(made.Reference), Digest: made.Digest,
|
||||||
Language: a.Language, Entrypoints: append([]string(nil), a.Entrypoints...),
|
Language: a.Language, Entrypoints: append([]string(nil), a.Entrypoints...),
|
||||||
Loads: loads, Env: copyWords(a.Env), Launchers: copyWords(made.Launchers),
|
Loads: loads, Env: copyWords(a.Env), Launchers: copyWords(made.Launchers),
|
||||||
Binary: BinaryOf(a),
|
Binary: BinaryOf(a), Bus: a.Bus,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
sort.Slice(out.Bundles, func(i, j int) bool { return out.Bundles[i].Name < out.Bundles[j].Name })
|
sort.Slice(out.Bundles, func(i, j int) bool { return out.Bundles[i].Name < out.Bundles[j].Name })
|
||||||
|
|||||||
@@ -1011,6 +1011,8 @@ type Bundle struct {
|
|||||||
// Binary is the executable a bundle compiled to a binary is, at its root (novox/hq ADR 0193):
|
// Binary is the executable a bundle compiled to a binary is, at its root (novox/hq ADR 0193):
|
||||||
// what runs it, where an interpreted bundle names an interpreter and an entrypoint.
|
// what runs it, where an interpreted bundle names an interpreter and an entrypoint.
|
||||||
Binary string `json:"binary,omitempty"`
|
Binary string `json:"binary,omitempty"`
|
||||||
|
// Bus is the artifact's bus account, kept on the resolved manifest so the grant composition reads it (BusAccounts).
|
||||||
|
Bus *ProcessBus `json:"bus,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// Build says how to produce this module's artifacts from its source.
|
// Build says how to produce this module's artifacts from its source.
|
||||||
@@ -1148,6 +1150,10 @@ type Artifact struct {
|
|||||||
// delivered version. A bundle that carried the package's name was delivered correctly, reported
|
// delivered version. A bundle that carried the package's name was delivered correctly, reported
|
||||||
// success, and was invisible to the launcher (novox/hq 04-ISSUES/142).
|
// success, and was invisible to the launcher (novox/hq 04-ISSUES/142).
|
||||||
Binary string `json:"binary,omitempty"`
|
Binary string `json:"binary,omitempty"`
|
||||||
|
// Bus is a tools bundle's own bus account (novox/hq ADR 0564 Limits 4; the lead, 2026-10-29): read as a process's
|
||||||
|
// (ProcessBus), a principal named after the bundle's binary (BusAccounts), its secret broker-<binary>. A tools bundle
|
||||||
|
// is run by the node's tool runtime per call, so it declares its account here and runs no idle service for it.
|
||||||
|
Bus *ProcessBus `json:"bus,omitempty"`
|
||||||
|
|
||||||
// URL is where an archive's upstream file is fetched from, for an archive that is a release
|
// URL is where an archive's upstream file is fetched from, for an archive that is a release
|
||||||
// somebody else published rather than a directory of this repository (novox/hq ADR 0316 §2, §6).
|
// somebody else published rather than a directory of this repository (novox/hq ADR 0316 §2, §6).
|
||||||
|
|||||||
+105
-1
@@ -4,6 +4,7 @@ import (
|
|||||||
"bytes"
|
"bytes"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"slices"
|
||||||
"strings"
|
"strings"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -29,6 +30,109 @@ type ProcessBus struct {
|
|||||||
// Invokes are the tools and seat verbs this process calls, each <module>.<tool> or a role's
|
// Invokes are the tools and seat verbs this process calls, each <module>.<tool> or a role's
|
||||||
// seat:<seat>.<verb> (as Manifest.Invokes), or * for every tool on the mesh.
|
// seat:<seat>.<verb> (as Manifest.Invokes), or * for every tool on the mesh.
|
||||||
Invokes []string `json:"invokes,omitempty"`
|
Invokes []string `json:"invokes,omitempty"`
|
||||||
|
// Removes are what a tools bundle's account may remove from the bus, from a closed set (BusRemovals; novox/hq ADR
|
||||||
|
// 0564 Limits 4): declared alone, by the module holding mesh-broker, never by a process.
|
||||||
|
Removes []string `json:"removes,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// What a bus account may remove (novox/hq ADR 0564 Limits 4), each its own grant: the controller's consumer on
|
||||||
|
// DEAD_LETTER_NOTICES (the lease write's step, clause 5), and keys of the node-engine's acts bucket. A further consumer
|
||||||
|
// or bucket is a change of this set, through a decision.
|
||||||
|
const (
|
||||||
|
RemovesControllerDeadLetterConsumer = "DEAD_LETTER_NOTICES.controller"
|
||||||
|
RemovesEngineActs = "mesh-host_acts"
|
||||||
|
)
|
||||||
|
|
||||||
|
// BusRemovals is the closed set of what a bus account may remove.
|
||||||
|
var BusRemovals = []string{RemovesControllerDeadLetterConsumer, RemovesEngineActs}
|
||||||
|
|
||||||
|
// BusAccount is one account of a module's own beside its `broker`: a process's (ADR 0436) or a tools bundle's (ADR 0564
|
||||||
|
// Limits 4), by its id (the process resource's, or the bundle's binary), with what its bus says.
|
||||||
|
type BusAccount struct {
|
||||||
|
ID string
|
||||||
|
Bus ProcessBus
|
||||||
|
// Tools says it is a tools bundle's, run by the node's tool runtime, not a process the node runs.
|
||||||
|
Tools bool
|
||||||
|
}
|
||||||
|
|
||||||
|
// BusAccounts is every own bus account of the module, processes first in resource order, then tools bundles in artifact
|
||||||
|
// order: what the grant composition composes a principal for, each delivered in its own secret broker-<id>. A tools
|
||||||
|
// bundle's is read from the repository's build, or from the resolved manifest's bundles.
|
||||||
|
func (m Manifest) BusAccounts() []BusAccount {
|
||||||
|
var out []BusAccount
|
||||||
|
buses := m.ProcessBuses()
|
||||||
|
for _, r := range m.Resources {
|
||||||
|
id := fmt.Sprint(r["id"])
|
||||||
|
if b, ok := buses[id]; ok && fmt.Sprint(r["type"]) == "process" {
|
||||||
|
out = append(out, BusAccount{ID: id, Bus: b})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
seen := map[string]bool{}
|
||||||
|
if m.Build != nil {
|
||||||
|
for _, a := range m.Build.Artifacts {
|
||||||
|
if a.Bus != nil && a.Kind == ArtifactBundle && BinaryOf(a) != "" && !seen[BinaryOf(a)] {
|
||||||
|
seen[BinaryOf(a)] = true
|
||||||
|
out = append(out, BusAccount{ID: BinaryOf(a), Bus: *a.Bus, Tools: true})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for _, b := range m.Bundles {
|
||||||
|
if b.Bus != nil && b.Binary != "" && !seen[b.Binary] {
|
||||||
|
seen[b.Binary] = true
|
||||||
|
out = append(out, BusAccount{ID: b.Binary, Bus: *b.Bus, Tools: true})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
// toolsBusProblems is what is wrong with a tools bundle's bus (ADR 0564 Limits 4): a bus on anything but a bundle
|
||||||
|
// compiled to a binary the runtime loads, a removal outside the closed set, or a removal beside any other grant or by a
|
||||||
|
// module that does not hold mesh-broker. Every other field is judged as a process's.
|
||||||
|
func toolsBusProblems(m Manifest) []string {
|
||||||
|
var problems []string
|
||||||
|
var artifacts []Artifact
|
||||||
|
if m.Build != nil {
|
||||||
|
artifacts = m.Build.Artifacts
|
||||||
|
}
|
||||||
|
claimsBroker := false
|
||||||
|
for _, c := range m.Claims {
|
||||||
|
claimsBroker = claimsBroker || c.Name == "mesh-broker"
|
||||||
|
}
|
||||||
|
for _, a := range artifacts {
|
||||||
|
if a.Bus == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if a.Kind != ArtifactBundle || BinaryOf(a) == "" {
|
||||||
|
problems = append(problems, fmt.Sprintf("%s's artifact %s declares bus, and only a tools bundle compiled to a "+
|
||||||
|
"binary has an account of its own (novox/hq ADR 0564 Limits 4)", m.Module, a.Name))
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
b := *a.Bus
|
||||||
|
if len(b.Queues) > 0 || b.Asks {
|
||||||
|
problems = append(problems, fmt.Sprintf("%s's tools bundle %s's bus consumes a queue or reads asks; a tools "+
|
||||||
|
"bundle runs per call and holds no consumer (novox/hq ADR 0564 Limits 4)", m.Module, a.Name))
|
||||||
|
}
|
||||||
|
for _, r := range b.Removes {
|
||||||
|
if !slices.Contains(BusRemovals, r) {
|
||||||
|
problems = append(problems, fmt.Sprintf("%s's tools bundle %s's bus removes %q, which is not one of %s "+
|
||||||
|
"(novox/hq ADR 0564 Limits 4)", m.Module, a.Name, r, strings.Join(BusRemovals, ", ")))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(b.Removes) > 0 && (!claimsBroker || len(b.Emits)+len(b.State)+len(b.Invokes) > 0) {
|
||||||
|
problems = append(problems, fmt.Sprintf("%s's tools bundle %s's bus removes from the bus; only the bus "+
|
||||||
|
"module's (the holder of mesh-broker) may, and with nothing else (novox/hq ADR 0564 Limits 4)", m.Module, a.Name))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for _, r := range m.Resources {
|
||||||
|
if fmt.Sprint(r["type"]) != "process" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if b, present, err := processBusOf(r); present && err == nil && len(b.Removes) > 0 {
|
||||||
|
problems = append(problems, fmt.Sprintf("%s's process %v's bus removes from the bus; only a tools bundle's "+
|
||||||
|
"may (novox/hq ADR 0564 Limits 4)", m.Module, r["id"]))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return problems
|
||||||
}
|
}
|
||||||
|
|
||||||
// UnmarshalJSON reads a process's bus strictly: an unknown key — serves, a seat to hold or claim, or
|
// UnmarshalJSON reads a process's bus strictly: an unknown key — serves, a seat to hold or claim, or
|
||||||
@@ -87,7 +191,7 @@ func (m Manifest) ProcessBuses() map[string]ProcessBus {
|
|||||||
// §3): the account rule is modelled on RunsAsProblems (seats_declared.go:522-550), and an invoked tool or
|
// §3): the account rule is modelled on RunsAsProblems (seats_declared.go:522-550), and an invoked tool or
|
||||||
// seat verb's shape on invokeProblems.
|
// seat verb's shape on invokeProblems.
|
||||||
func ProcessBusProblems(m Manifest) []string {
|
func ProcessBusProblems(m Manifest) []string {
|
||||||
var problems []string
|
problems := toolsBusProblems(m)
|
||||||
streams := map[string]bool{}
|
streams := map[string]bool{}
|
||||||
for _, s := range m.Streams {
|
for _, s := range m.Streams {
|
||||||
streams[s.Name] = true
|
streams[s.Name] = true
|
||||||
|
|||||||
@@ -47,6 +47,49 @@ func TestAProcessDeclaresItsBusAndIsAccepted(t *testing.T) {
|
|||||||
!b.Asks || !strSliceEqual(b.Invokes, []string{"seat:operator-channel.tell"}) {
|
!b.Asks || !strSliceEqual(b.Invokes, []string{"seat:operator-channel.tell"}) {
|
||||||
t.Fatalf("the bus read as %+v", b)
|
t.Fatalf("the bus read as %+v", b)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 1952 S5 (novox/hq ADR 0564 Limits 4; the lead, 2026-10-29): a tools bundle declares its own bus account, read as a
|
||||||
|
// process's, named after its binary and kept through resolution; a removal is from the closed set, alone, and the
|
||||||
|
// bus module's (mesh-broker) alone.
|
||||||
|
tools := func(claims, bus string) string {
|
||||||
|
return `{"module":"nats","version":"1","claims":` + claims + `,"build":{"artifacts":[{"name":"tools","kind":"bundle",` +
|
||||||
|
`"language":"go","system":"arch","from":"cmd/nats-tools","binary":"nats-tools","loads":["nats-tools"],"bus":` + bus + `}]}}`
|
||||||
|
}
|
||||||
|
broker := `[{"name":"mesh-broker","scope":"mesh"}]`
|
||||||
|
for _, c := range []struct {
|
||||||
|
name, manifest, refused string
|
||||||
|
}{
|
||||||
|
{name: "the bus module's tools removing", manifest: tools(broker, `{"removes":["DEAD_LETTER_NOTICES.controller","mesh-host_acts"]}`)},
|
||||||
|
{name: "a removal outside the set", manifest: tools(broker, `{"removes":["EVENTS.a_m"]}`), refused: "not one of"},
|
||||||
|
{name: "a removal beside a grant", manifest: tools(broker, `{"removes":["mesh-host_acts"],"invokes":["nats.nats_streams"]}`), refused: "with nothing else"},
|
||||||
|
{name: "another module's tools removing", manifest: tools(`[]`, `{"removes":["mesh-host_acts"]}`), refused: "only the bus module"},
|
||||||
|
{name: "a tools bundle consuming a queue", manifest: tools(broker, `{"queues":["x"]}`), refused: "holds no consumer"},
|
||||||
|
{name: "a process removing", manifest: mailSenderProcessManifest(t, `{"removes":["mesh-host_acts"]}`), refused: "only a tools bundle"},
|
||||||
|
} {
|
||||||
|
m, err := ParseManifest([]byte(c.manifest))
|
||||||
|
if c.refused != "" {
|
||||||
|
if err == nil || !strings.Contains(err.Error(), c.refused) {
|
||||||
|
t.Errorf("%s: %v, want refused saying %q", c.name, err, c.refused)
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("%s: %v", c.name, err)
|
||||||
|
}
|
||||||
|
accounts := m.BusAccounts()
|
||||||
|
if len(accounts) != 1 || accounts[0].ID != "nats-tools" || !accounts[0].Tools ||
|
||||||
|
!strSliceEqual(accounts[0].Bus.Removes, []string{RemovesControllerDeadLetterConsumer, RemovesEngineActs}) {
|
||||||
|
t.Errorf("%s: the accounts are %+v", c.name, accounts)
|
||||||
|
}
|
||||||
|
// Resolved, the build goes and its bundles keep the bus (build.go).
|
||||||
|
resolved := Manifest{Module: "nats", Bundles: []Bundle{{Name: "tools", Binary: "nats-tools", Bus: m.Build.Artifacts[0].Bus}}}
|
||||||
|
if got := resolved.BusAccounts(); len(got) != 1 || got[0].ID != "nats-tools" {
|
||||||
|
t.Errorf("%s: resolved, the accounts are %+v", c.name, got)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if got := m.BusAccounts(); len(got) != 1 || got[0].ID != "send" || got[0].Tools {
|
||||||
|
t.Errorf("the process's account is %+v", got)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func strSliceEqual(a, b []string) bool {
|
func strSliceEqual(a, b []string) bool {
|
||||||
|
|||||||
Reference in New Issue
Block a user