Files
mesh-sdk/go/catalogue/process_bus.go
T

373 lines
15 KiB
Go

package catalogue
import (
"bytes"
"encoding/json"
"fmt"
"slices"
"strings"
)
// ProcessBusField is the key a process resource declares its bus on. Controller-side only: never composed
// onto the node-engine's process resource (novox/hq ADR 0436 §1) — the wire declaration is mesh-host's own,
// vendored, with a strict decoder that refuses an unknown key.
const ProcessBusField = "bus"
// ProcessBus is what one process of a module does on the bus (novox/hq ADR 0436 §1, §2, §3, §5): its own bus
// account, granted exactly this and nothing of the module's other grants or the tool runner's — never a seat
// to hold or serve, never a tool call or seat verb answered, never a restricted stream heard, never a
// warrant published (§3, §5).
type ProcessBus struct {
// Emits are the events this process emits, named locally, as the module's own Emits names them
// (EmitsAll): a process emits only what its module may emit as a whole.
Emits []string `json:"emits,omitempty"`
// Queues are the module's own work-queue streams (ADR 0436 §6) this process consumes.
Queues []string `json:"queues,omitempty"`
// State is the module's own state, by local name, this process reads and writes.
State []string `json:"state,omitempty"`
// Asks says whether this process reads the router's records of this module's own asks.
Asks bool `json:"asks,omitempty"`
// 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.
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
}
// ToolsBusSecret says name is the own secret a tools bundle's bus account is held in: `broker-<binary>` for a bundle
// whose bus the manifest declares (ADR 0564 Limits 4). Its file belongs to the runtime's account, which runs the bundle.
func ToolsBusSecret(m Manifest, name string) bool {
for _, a := range m.BusAccounts() {
if a.Tools && name == "broker-"+a.ID {
return true
}
}
return false
}
// toolsSecretProblems refuses a tools bundle's bus account secret the runtime's account could not read: it is declared
// as an own secret `broker-<binary>`, at `${dir:<id>}/broker-<binary>` for a directory resource every parent of which the
// runtime's account may pass (passableDir); the file itself is 0600 and the runtime account's.
func toolsSecretProblems(m Manifest) []string {
var problems []string
for _, a := range m.BusAccounts() {
if !a.Tools {
continue
}
name := "broker-" + a.ID
own, declared := m.OwnSecrets.Plain()[name]
if !declared {
problems = append(problems, fmt.Sprintf("%s's tools bundle %s declares bus and no own secret %s to hold its "+
"account in (novox/hq ADR 0564 Limits 4)", m.Module, a.ID, name))
continue
}
dir, _, _ := strings.Cut(strings.TrimPrefix(own.Path, "${dir:"), "}")
if !strings.HasPrefix(own.Path, "${dir:"+dir+"}/") || !passableDir(m, dir, 0) {
problems = append(problems, fmt.Sprintf("%s's own secret %s is at %s, which the node's tool runtime cannot "+
"reach: a tools bundle's account lies in a directory resource the runtime's account may pass, every parent "+
"included, such as {\"id\":\"tools-secret\",\"type\":\"directory\",\"path\":\"%s/%s-tools\",\"mode\":\"0711\"} "+
"with the secret at ${dir:tools-secret}/%s; never under the module's own or mesh place, which are its "+
"own (novox/hq ADR 0564 Limits 4)", m.Module, name, own.Path, meshStateRoot, m.Module, name))
}
}
return problems
}
// meshStateRoot is the mesh's directory of every module's mesh place on a node of the default data root: root's, mode
// 0755, so the runtime's account passes it, while each module's own place beneath it is 0700.
const meshStateRoot = defaultDataRoot + "/" + placeMesh
// passableDir says the runtime's account may pass the directory resource id and every parent the mesh makes for it: its
// own mode lets others pass (the last digit odd), and it is either stated directly under meshStateRoot or beneath a
// placed directory that is passable in turn. A pathless directory resolves under the module's own root or its mesh
// place, which are the module's own, so it never is.
func passableDir(m Manifest, id string, depth int) bool {
if depth > 4 {
return false
}
for _, r := range m.Resources {
if fmt.Sprint(r["type"]) != "directory" || fmt.Sprint(r["id"]) != id {
continue
}
mode, _ := r["mode"].(string)
if mode == "" || !strings.ContainsAny(mode[len(mode)-1:], "1357") {
return false
}
path, _ := r["path"].(string)
switch {
case strings.HasPrefix(path, "${dir:"):
parent, _, _ := strings.Cut(strings.TrimPrefix(path, "${dir:"), "}")
return passableDir(m, parent, depth+1)
case strings.HasPrefix(path, "/"):
return filepathDir(strings.TrimRight(path, "/")) == meshStateRoot
}
return false
}
return false
}
func filepathDir(p string) string {
if i := strings.LastIndex(p, "/"); i > 0 {
return p[:i]
}
return "/"
}
// 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
// anything else that would answer a tool call or a seat's verb — is refused here, by name (novox/hq ADR
// 0436 §3).
func (b *ProcessBus) UnmarshalJSON(raw []byte) error {
type plain ProcessBus
var p plain
dec := json.NewDecoder(bytes.NewReader(raw))
dec.DisallowUnknownFields()
if err := dec.Decode(&p); err != nil {
return fmt.Errorf("a process's bus is {emits, queues, state, asks, invokes}: %w", typedUnknown(err))
}
*b = ProcessBus(p)
return nil
}
// processBusOf is the bus a process resource declares, and whether it declared one at all.
func processBusOf(r map[string]any) (bus ProcessBus, present bool, err error) {
raw, present := r["bus"]
if !present {
return ProcessBus{}, false, nil
}
body, err := json.Marshal(raw)
if err != nil {
return ProcessBus{}, true, err
}
if err := json.Unmarshal(body, &bus); err != nil {
return ProcessBus{}, true, err
}
return bus, true, nil
}
// ProcessBuses is every process of this module that declares bus, by its id (the manifest's own resource
// id, novox/hq ADR 0436, settled by the lead) — what the grant composition reads to grant a process
// account exactly what it emits, consumes and calls (novox/hq ADR 0436 §2).
func (m Manifest) ProcessBuses() map[string]ProcessBus {
var out map[string]ProcessBus
for _, r := range m.Resources {
if fmt.Sprint(r["type"]) != "process" {
continue
}
b, present, err := processBusOf(r)
if !present || err != nil {
continue
}
if out == nil {
out = map[string]ProcessBus{}
}
out[fmt.Sprint(r["id"])] = b
}
return out
}
// ProcessBusProblems is what is wrong with a manifest's processes' bus declarations (novox/hq ADR 0436 §1,
// §3): the account rule is modelled on RunsAsProblems (seats_declared.go:522-550), and an invoked tool or
// seat verb's shape on invokeProblems.
func ProcessBusProblems(m Manifest) []string {
problems := append(toolsBusProblems(m), toolsSecretProblems(m)...)
streams := map[string]bool{}
for _, s := range m.Streams {
streams[s.Name] = true
}
state := map[string]bool{}
for _, s := range m.State {
state[s.Name] = true
}
emits := map[string]bool{}
for _, e := range m.EmitsAll() {
emits[e] = true
}
for _, r := range m.Resources {
if fmt.Sprint(r["type"]) != "process" {
continue
}
id := fmt.Sprint(r["id"])
if _, present := r["bus"]; !present {
continue
}
b, _, err := processBusOf(r)
if err != nil {
problems = append(problems, fmt.Sprintf("%s's process %s declares bus: %v", m.Module, id, err))
continue
}
user, _ := r["user"].(string)
problems = append(problems, processAccountProblems(m, id, user)...)
for _, e := range b.Emits {
if !emits[e] {
problems = append(problems, fmt.Sprintf(
"%s's process %s's bus emits %q, which %s does not declare as its own (novox/hq ADR 0436 §1)",
m.Module, id, e, m.Module))
}
}
for _, q := range b.Queues {
if !streams[q] {
problems = append(problems, fmt.Sprintf(
"%s's process %s's bus queues %q, which %s does not declare as its own stream (novox/hq "+
"ADR 0436 §1)", m.Module, id, q, m.Module))
}
}
for _, s := range b.State {
if !state[s] {
problems = append(problems, fmt.Sprintf(
"%s's process %s's bus reads and writes state %q, which %s does not declare as its own "+
"(novox/hq ADR 0436 §1)", m.Module, id, s, m.Module))
}
}
for _, t := range b.Invokes {
if !invokeNameValid(t) {
problems = append(problems, fmt.Sprintf(
"%s's process %s's bus invokes %q, which does not name a tool: a process invokes "+
"<module>.<tool>, or a role's seat:<seat>.<verb> (novox/hq ADR 0436 §1)", m.Module, id, t))
}
}
}
return problems
}
// processAccountProblems is what is wrong with one process's bus account (novox/hq ADR 0436 §1, §2): an
// account the module makes — a `user` resource of that name — never root and never the operator account,
// which a manifest cannot say safely (a node with no account has nothing to resolve it to, novox/hq ADR
// 0436, settled by the lead).
func processAccountProblems(m Manifest, processID, user string) []string {
switch {
case user == "":
return []string{fmt.Sprintf(
"%s's process %s declares bus and names no account; a process account is a user resource the "+
"module makes, never root and never the operator account (novox/hq ADR 0436 §1, §2)",
m.Module, processID)}
case user == "root":
return []string{fmt.Sprintf(
"%s's process %s runs as root; a process account is the module's own, never root (novox/hq ADR "+
"0436 §1, §2)", m.Module, processID)}
case user == "${machine:account}":
return []string{fmt.Sprintf(
"%s's process %s runs as the operator account; a process account is the module's own, never the "+
"operator's (novox/hq ADR 0436 §1, §2)", m.Module, processID)}
}
for _, r := range m.Resources {
if fmt.Sprint(r["type"]) == "user" && fmt.Sprint(r["name"]) == user {
return nil
}
}
return []string{fmt.Sprintf(
"%s's process %s runs as %s, which it does not make: a user resource named %s (novox/hq ADR 0436 §1, "+
"§2)", m.Module, processID, user, user)}
}
// invokeNameValid says a module or process's invoked entry names a tool: <module>.<tool>, a role's
// seat:<seat>.<verb>, or * for every tool on the mesh (novox/hq ADR 0152, ADR 0436 §1).
func invokeNameValid(t string) bool {
if t == "*" {
return true
}
t = strings.TrimPrefix(t, "seat:")
first, second, named := strings.Cut(t, ".")
return named && name.MatchString(first) && toolName.MatchString(second)
}