Files
mesh-controller/internal/catalogue/seats_declared.go
T
jochen f75cd0239d Hear a restricted stream only from a module's own account, never the tool runner's (issue 727)
The tool runner's bus account carried the heard grants, consumer and bind word of every module it runs, and every agent session on that machine reaches the bus as that account, so any session could pull a granted module's consumer on MAIL. Per ADR 0376 §1 the controller now composes a restricted stream's consumer, grants and bind word only for a module that declares runs-as; registration refuses a consumes of a restricted stream without runs-as (§2), and a grant that composes nothing is said in doctor (D-heard) and in the controller's log on every raise and send.
2026-10-15 14:11:37 +02:00

525 lines
22 KiB
Go

package catalogue
import (
"fmt"
"regexp"
"sort"
"strings"
)
// Seats a module declares of its own (novox/hq ADR 0118).
//
// The set of seats a mesh has is **derived**: the mesh's own, in seats.go, plus those declared by
// every module it has registered. Still closed — a seat named nowhere is refused — but computed
// from the catalogue rather than written in the controller, which is what ADR 0110 actually
// needed and a hand-maintained table could not keep. Its own evidence: the enumeration done by
// hand while that record was written reported eleven claims where there were thirteen.
//
// **What can be checked from one manifest and what cannot.** A declaration's shape, its scope,
// and the reserved prefix are facts about the manifest in front of you. Whether a seat anybody
// names actually exists, whether two modules declared the same one, and whether a holder
// satisfies the protocol are facts about the *catalogue* — so they are checked at registration,
// by CatalogueProblems, which is the last moment the mesh can still say no.
// meshSeatPrefix is reserved to the mesh. The prefix *is* the reservation rule: no list of
// reserved names to maintain, no way for the mesh's own namespace to be colonised by a manifest,
// and nothing to keep in step when a mesh seat is added.
const meshSeatPrefix = "mesh-"
// retiredLoginShell is the one name outside the prefix a module may not declare: the login shell's,
// from when a module declared it (novox/hq ADR 0176), before it became the mesh's (ADR 0204).
const retiredLoginShell = "login-shell"
// A SeatDeclaration is a role a module offers on the bus: what may be sent to it, what it says,
// and what it answers. A caller declares that it uses the *seat*, never the module, so the
// implementation can be replaced under it.
type SeatDeclaration struct {
Name string `json:"name"`
Scope string `json:"scope,omitempty"`
// Accepts are the verbs others may submit work on. Each becomes a work-queue subject, and
// the holder is the only consumer — so exactly one worker does the job, by construction
// rather than by how carefully somebody wrote a subscribe call.
Accepts []string `json:"accepts,omitempty"`
// Emits are the verbs the holder publishes: 1:many, nobody obliged to act.
Emits []string `json:"emits,omitempty"`
// Serves are the verbs the holder answers: request and reply, awaited. A bare name, or the
// verb in full with its schema (novox/hq ADR 0132).
Serves []Verb `json:"serves,omitempty"`
// RetainSeconds is how long the inbound backlog survives with no holder, zero for the
// mesh's default. Retention belongs to whoever owns the namespace (design 29 §3) — a seat
// owns its own, which is why a seat is also the answer for a module that needs retention
// its events cannot have.
RetainSeconds int `json:"retain-seconds,omitempty"`
// Kinded makes the seat a kinded bench (novox/hq ADR 0234 §2, ADR 0259 §3): its holders are different
// modules, each claiming one kind, and each verb's subject carries the kind. Only the benches in
// KindedBenches may be kinded; making another is a decision, recorded.
Kinded bool `json:"kinded,omitempty"`
// ByCaller are accepts and emits whose subject's last token names the calling module (ADR 0259 §3): a
// user submits such an accept, and hears such an event, under its own name and no other.
ByCaller []string `json:"by-caller,omitempty"`
// Proofs are verbs carried as core request and reply, never on a stream: what travels on them (a code
// the operator typed) is never kept (ADR 0259 §3). On a kinded bench a holder asks with its own kind and
// the modules watching the seat answer.
Proofs []string `json:"proofs,omitempty"`
// Records are state buckets of the declaring module that each user reads under its own name — the
// keys `<user>.…` and no other (ADR 0259 §3).
Records []string `json:"records,omitempty"`
// Paths are the paths on its machine the seat's holder owns (novox/hq ADR 0287), under the rules the
// mesh's own seats name theirs by: a node seat only, each absolute and literal, and none another seat
// names. A module's seat never steps back.
Paths []string `json:"paths,omitempty"`
}
// KindedBenches are the seats that may be kinded (novox/hq ADR 0234 §2): `channel` sends to the operator,
// `intake` takes what the operator answers. Another is a decision, recorded, as ADR 0223 asks of a bench.
var KindedBenches = map[string]bool{"channel": true, "intake": true}
// NamedByCaller says whether one of the seat's verbs is named by its caller.
func (s SeatDeclaration) NamedByCaller(verb string) bool {
for _, v := range s.ByCaller {
if v == verb {
return true
}
}
return false
}
// At is this declaration's scope, with the default applied. Mesh by default, because a seat
// declared by a module is nearly always "there is one of these in the mesh" — a per-node worker
// is the deliberate case, and says so.
func (s SeatDeclaration) At() string {
if s.Scope == "" {
return ScopeMesh
}
return s.Scope
}
// verbs is everything the protocol names, for the checks that do not care which half.
func (s SeatDeclaration) verbs() []string {
out := append([]string{}, s.Accepts...)
out = append(out, s.Emits...)
return append(out, VerbNames(s.Serves)...)
}
// declaredSeatProblems is what one manifest can be judged on alone.
func declaredSeatProblems(m Manifest) []string {
var problems []string
seen := map[string]bool{}
for _, s := range m.DefinesSeats {
switch {
case s.Name == "":
problems = append(problems, fmt.Sprintf("%s declares a seat with no name", m.Module))
continue
case !name.MatchString(s.Name):
problems = append(problems, fmt.Sprintf(
"%s declares a seat named %q, which is not a usable name", m.Module, s.Name))
continue
case strings.HasPrefix(s.Name, meshSeatPrefix):
// The mesh's own code dereferences its seats by name — the resolver *is* the thing
// that finds the store — so the prefix is not a convention, it is a namespace.
problems = append(problems, fmt.Sprintf(
"%s declares a seat named %q; %q is reserved to the mesh, which defines its own "+
"seats (novox/hq ADR 0118)", m.Module, s.Name, meshSeatPrefix+"*"))
continue
}
if s.Name == retiredLoginShell {
// The name ADR 0176 gave the login shell when the zsh module declared it. The seat is
// the mesh's now, so a module declaring the old name would be a second login shell
// beside it, with a protocol of its own (novox/hq ADR 0204).
problems = append(problems, fmt.Sprintf(
"%s declares a seat named %q; the login shell is the mesh's own seat %s, which a shell "+
"module claims and none declares (novox/hq ADR 0204)", m.Module, s.Name, LoginShellSeat))
continue
}
if seen[s.Name] {
problems = append(problems, fmt.Sprintf(
"%s declares the seat %q twice", m.Module, s.Name))
continue
}
seen[s.Name] = true
if _, isMesh := SeatNamed(s.Name); isMesh {
problems = append(problems, fmt.Sprintf(
"%s declares %q, which is a seat the mesh already defines", m.Module, s.Name))
}
switch s.At() {
case ScopeNode, ScopeSite, ScopeMesh:
default:
problems = append(problems, fmt.Sprintf(
"%s declares seat %s at scope %q; a seat is held per node, per site or per mesh",
m.Module, s.Name, s.Scope))
}
// A seat with an empty protocol is allowed, and is the mesh saying what a machine is:
// which module is this node's packet filter, or its showcase. Design 26 calls it a seat
// that delivers nothing, and that is most of the node-scoped ones. ADR 0126's "a declared
// seat carries a protocol" governs what a holder must satisfy, not that every seat offers
// something — a marker seat's protocol is satisfied by holding it. Nothing can reach this
// state by accident: a mistyped field name is refused by the parser above, so an empty
// protocol was written as one.
for _, v := range s.verbs() {
if !name.MatchString(v) {
problems = append(problems, fmt.Sprintf(
"%s declares %s.%s, which is not a usable verb", m.Module, s.Name, v))
}
}
problems = append(problems, trafficProblems(m, s)...)
problems = append(problems, declaredSeatPathProblems(m, s)...)
}
for _, u := range m.Uses {
if !name.MatchString(u) {
problems = append(problems, fmt.Sprintf("%s uses %q, which is not a usable seat name", m.Module, u))
}
}
return problems
}
// trafficProblems is what one declaration of the rules of ADR 0259 §3 can be judged on alone.
func trafficProblems(m Manifest, s SeatDeclaration) []string {
var problems []string
if s.Kinded && !KindedBenches[s.Name] {
problems = append(problems, fmt.Sprintf(
"%s declares %s as a kinded bench; only channel and intake are kinded, and another is a "+
"decision, recorded (novox/hq ADR 0234)", m.Module, s.Name))
}
if s.Kinded && len(s.ByCaller) > 0 {
problems = append(problems, fmt.Sprintf(
"%s declares %s kinded and names verbs by their caller; a kinded bench's subjects carry the kind",
m.Module, s.Name))
}
for _, v := range s.ByCaller {
inAccepts, inEmits := false, false
for _, a := range s.Accepts {
inAccepts = inAccepts || a == v
}
for _, e := range s.Emits {
inEmits = inEmits || e == v
}
if !inAccepts && !inEmits {
problems = append(problems, fmt.Sprintf(
"%s names %s.%s by its caller, which the seat neither accepts nor emits", m.Module, s.Name, v))
}
}
for _, v := range s.Proofs {
if !name.MatchString(v) || strings.Contains(v, ".") {
problems = append(problems, fmt.Sprintf("%s declares the proof %s.%s, which is not a usable verb",
m.Module, s.Name, v))
}
}
if len(s.Proofs) > 0 && !s.Kinded {
problems = append(problems, fmt.Sprintf(
"%s declares proofs on %s, which is not kinded; a proof is asked by a holder of a kind",
m.Module, s.Name))
}
for _, r := range s.Records {
kept := false
for _, st := range m.State {
kept = kept || st.Name == r
}
if !kept {
problems = append(problems, fmt.Sprintf(
"%s says %s's users read its records %q, which it keeps no state of", m.Module, s.Name, r))
}
}
return problems
}
// A Shelf is every manifest the mesh has registered, by module name.
type Shelf map[string]Manifest
// CatalogueProblems are the rules no single manifest can be judged against.
//
// Run at registration, which is the last moment the mesh can still refuse: after it, a caller is
// bound to a seat and a refusal is an outage rather than a conversation.
// UndeclaredSeat ends the problem of a `uses` or a claim naming a seat no manifest given declares: over the whole
// catalogue (registration, the catalogue's own check) a refusal, and over some manifests alone a seat whose
// declaring module was not given (novox/hq issue 364).
const UndeclaredSeat = "which no module declares and the mesh does not define"
// IsUndeclaredSeat says whether a problem CatalogueProblems gave is a seat no manifest given declares.
func IsUndeclaredSeat(problem string) bool { return strings.HasSuffix(problem, UndeclaredSeat) }
func CatalogueProblems(shelf Shelf) []string {
var problems []string
// A stream name has one owner (novox/hq ADR 0363).
problems = append(problems, StreamNameProblems(shelf)...)
// A module that hears a restricted stream runs as an account of its own (novox/hq ADR 0376 §2).
problems = append(problems, RestrictedHearingProblems(shelf)...)
// Who declares what, and who declared it first.
declaredBy := map[string]string{}
declared := map[string]SeatDeclaration{}
for _, module := range shelfOrder(shelf) {
for _, s := range shelf[module].DefinesSeats {
if s.Name == "" {
continue
}
if first, taken := declaredBy[s.Name]; taken {
// The second loses. A seat name meaning two different protocols is the failure
// nobody could diagnose afterwards — a caller would bind to whichever happened
// to register first, and the symptom would appear in the other module.
problems = append(problems, fmt.Sprintf(
"%s declares the seat %q, which %s already declares; a seat name means one "+
"protocol", module, s.Name, first))
continue
}
declaredBy[s.Name] = module
declared[s.Name] = s
}
}
exists := func(seat string) bool {
if _, isMesh := SeatNamed(seat); isMesh {
return true
}
_, ok := declaredBy[seat]
return ok
}
// Which seats name each path (novox/hq ADR 0287): a module's seat naming one another seat names loses.
named, pathProblems := pathSeats(shelfOrder(shelf), shelf, declaredBy)
problems = append(problems, pathProblems...)
// Who claims each kind of a kinded bench, so a second claim of one kind is refused (ADR 0234 §2).
kindsTaken := map[string]string{}
for _, module := range shelfOrder(shelf) {
m := shelf[module]
for _, c := range m.Claims {
if c.Kind != "" {
if _, isModuleSeat := declared[c.Name]; !isModuleSeat {
problems = append(problems, fmt.Sprintf(
"%s claims %s of kind %q, and only a kinded bench takes a kind", module, c.Name, c.Kind))
}
}
}
// A `uses` naming nothing is where ADR 0110's guarantee lands under a derived set: the
// same refusal, at the same moment, from a set nobody maintains by hand.
for _, u := range m.Uses {
if !exists(u) {
problems = append(problems, fmt.Sprintf(
"%s uses the seat %q, "+UndeclaredSeat,
module, u))
}
}
for _, c := range m.Claims {
if !exists(c.Name) {
problems = append(problems, fmt.Sprintf(
"%s claims the seat %q, "+UndeclaredSeat,
module, c.Name))
continue
}
s, isModuleSeat := declared[c.Name]
if !isModuleSeat {
// **A mesh seat is judged here and nowhere else** (novox/hq ADR 0122): the set is
// the store's, and this is the only place that runs with the store's set loaded.
// The parser cannot do it — it also runs on the build machine, against whatever
// set that binary was compiled with.
seat, _ := SeatNamed(c.Name)
if err := CanHold(m, seat); err != nil {
problems = append(problems, err.Error())
}
continue
}
problems = append(problems, kindProblems(module, c, s, kindsTaken)...)
if c.At() != s.At() {
problems = append(problems, fmt.Sprintf(
"%s claims %s at scope %q, and %s declares it at %s",
module, c.Name, c.At(), declaredBy[c.Name], s.At()))
}
// A holder that does not answer what the seat promises is a caller's timeout, found
// at assignment instead.
if missing := unserved(m, c, s); len(missing) > 0 {
problems = append(problems, fmt.Sprintf(
"%s claims %s but does not serve %s, which that seat's protocol promises",
module, c.Name, strings.Join(missing, ", ")))
}
// And owns what the seat's holder owns, as a holder of the mesh's own seats does (ADR 0287).
if missing := missingPaths(m, asSeatPaths(s.Paths)); len(missing) > 0 {
problems = append(problems, fmt.Sprintf(
"%s claims %s but does not declare %s, which that seat's holder owns (novox/hq ADR 0287)",
module, c.Name, strings.Join(missing, ", ")))
}
}
// Only a claimant declares a seat's path (novox/hq ADR 0287).
problems = append(problems, unclaimedSeatPaths(m, named)...)
}
// **A trusted holder runs as its own account** (novox/hq ADR 0259 §8): a module saying warrants, or
// speaking for a kind that proves its sender, is never carried by a machine's runtime.
for _, module := range shelfOrder(shelf) {
m := shelf[module]
if why := TrustedHolding(m, declared); why != "" && m.RunsAs == "" {
problems = append(problems, fmt.Sprintf(
"%s must run as an account of its own (runs-as): %s, and the machine's runtime runs as the "+
"operator's account, which every agent runs as (novox/hq ADR 0259)", module, why))
}
}
// A read of a module's state that module does not keep (novox/hq ADR 0201) — said only where the
// owner is on the shelf, as a consumer may be installed before its emitter.
var manifests []Manifest
for _, module := range shelfOrder(shelf) {
manifests = append(manifests, shelf[module])
}
problems = append(problems, StateReadsNothingDeclares(manifests)...)
sort.Strings(problems)
return problems
}
// ChannelCapabilities is the fixed vocabulary `channel-capabilities/1` (novox/hq ADR 0234 §2): a word
// outside it is refused. `max-length:<N>` takes a number.
var ChannelCapabilities = map[string]bool{
"deliver": true, "reaches-away": true, "loud": true, "silent": true, "edit": true,
"reaches-when-mesh-down": true, "private": true,
"choice": true, "reply": true, "threads": true, "operator-first": true,
"verified-sender": true, "exact-render": true, "code-factor": true, "key-factor": true,
}
var maxLength = regexp.MustCompile(`^max-length:[1-9][0-9]{0,6}$`)
// capabilityProblems are the words of a claim outside the vocabulary, and capabilities on a claim of a
// seat that is not kinded.
func capabilityProblems(module string, c Claim, kinded bool) []string {
if len(c.Capabilities) == 0 {
return nil
}
if !kinded {
return []string{fmt.Sprintf("%s claims %s with capabilities, and only a kinded bench's claim carries them",
module, c.Name)}
}
var problems []string
for _, w := range c.Capabilities {
if !ChannelCapabilities[w] && !maxLength.MatchString(w) {
problems = append(problems, fmt.Sprintf(
"%s claims %s with the capability %q, which channel-capabilities/1 does not have", module, c.Name, w))
}
}
return problems
}
// kindProblems is a claim judged against a declared seat's kind: a kinded bench takes one claim per kind,
// a usable name; any other seat takes none.
func kindProblems(module string, c Claim, s SeatDeclaration, taken map[string]string) []string {
if problems := capabilityProblems(module, c, s.Kinded); len(problems) > 0 {
return problems
}
switch {
case !s.Kinded && c.Kind != "":
return []string{fmt.Sprintf("%s claims %s of kind %q, and only a kinded bench takes a kind",
module, c.Name, c.Kind)}
case !s.Kinded:
return nil
case c.Kind == "":
return []string{fmt.Sprintf("%s claims the kinded bench %s and names no kind", module, c.Name)}
case !name.MatchString(c.Kind) || strings.Contains(c.Kind, "."):
return []string{fmt.Sprintf("%s claims %s of kind %q, which is not a usable name", module, c.Name, c.Kind)}
}
key := c.Name + "/" + c.Kind
if first, ok := taken[key]; ok && first != module {
return []string{fmt.Sprintf("%s claims %s of kind %q, which %s already claims; a kind has one holder",
module, c.Name, c.Kind, first)}
}
taken[key] = module
return nil
}
// unserved is what a seat's protocol promises and the claimant does not answer. Only the tools
// are checked: `accepts` and `emits` are wired by the runtime from the declaration, while a tool
// is code the module either has or has not written — under the claim's serves, or among its own.
func unserved(m Manifest, c Claim, s SeatDeclaration) []string {
has := map[string]bool{}
for _, t := range c.ServesFor(m) {
has[t] = true
}
var missing []string
for _, t := range s.Serves {
if !has[t.Name] {
missing = append(missing, t.Name)
}
}
return missing
}
// shelfOrder is the catalogue in a stable order, so two runs report the same problems in the same
// sequence — a refusal that reorders itself is a refusal nobody can diff.
func shelfOrder(shelf Shelf) []string {
out := make([]string, 0, len(shelf))
for k := range shelf {
out = append(out, k)
}
sort.Strings(out)
return out
}
var accountName = regexp.MustCompile(`^[a-z_][a-z0-9_-]{0,30}$`)
// RunsAsProblems is what one manifest's `runs-as` is held to (novox/hq ADR 0259 §8): an account of the
// module's own making — a `user` resource of that name — that owns its secrets, with a bus account of its own,
// and that is neither root nor the operator's.
func RunsAsProblems(m Manifest) []string {
if m.RunsAs == "" {
return nil
}
var problems []string
say := func(format string, a ...any) { problems = append(problems, fmt.Sprintf(format, a...)) }
switch {
case !accountName.MatchString(m.RunsAs):
say("%s runs as %q, which is not an account name of the module's own", m.Module, m.RunsAs)
return problems
case m.RunsAs == "root":
say("%s runs as root; a module of its own account runs as an account it makes, never root", m.Module)
}
made := false
for _, r := range m.Resources {
if fmt.Sprint(r["type"]) == "user" && fmt.Sprint(r["name"]) == m.RunsAs {
made = true
}
}
if !made {
say("%s runs as %s, which it does not make: a user resource named %s", m.Module, m.RunsAs, m.RunsAs)
}
if _, has := m.OwnSecrets["broker"]; !has {
say("%s runs as its own account and declares no own secret broker: its runtime reaches the bus on an "+
"account of its own", m.Module)
}
if m.SecretsOwner != m.RunsAs {
say("%s runs as %s, and its secrets belong to %q: they are the account's own", m.Module, m.RunsAs, m.SecretsOwner)
}
return problems
}
// TrustedHolding is why a module must run as its own account (novox/hq ADR 0259 §8), or "": it holds a seat
// whose events it says to one caller each (a warrant), or speaks for a kind of a kinded bench that proves
// its sender, or is private (a link's code is shown there). None may be carried by the machine's runtime, which
// runs as the operator's account.
func TrustedHolding(m Manifest, declared map[string]SeatDeclaration) string {
for _, c := range m.Claims {
s, ok := declared[c.Name]
if !ok {
continue
}
for _, e := range s.Emits {
if s.NamedByCaller(e) {
return fmt.Sprintf("it holds %s, whose %s it says to one caller each", c.Name, e)
}
}
if s.Kinded {
for _, capability := range c.Capabilities {
switch capability {
case "verified-sender":
return fmt.Sprintf("it holds %s of kind %s, which proves its sender", c.Name, c.Kind)
case "private":
// A private kind is shown a link's code, which makes an account the operator's.
return fmt.Sprintf("it holds %s of kind %s, which is private: a link's code is shown there", c.Name, c.Kind)
}
}
}
}
return ""
}