Files
mesh-controller/internal/catalogue/process_bus.go
T

190 lines
7.1 KiB
Go

package catalogue
import (
"bytes"
"encoding/json"
"fmt"
"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"`
}
// 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 {
var problems []string
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)
}