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 . or a role's // seat:. (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-. 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-` 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-`, at `${dir:}/broker-` 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 "+ "., or a role's seat:. (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: ., a role's // seat:., 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) }