373 lines
15 KiB
Go
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)
|
|
}
|