Files
mesh-controller/internal/catalogue/terminal_keys.go
T
jochen 3275a16beb
mesh/merge-gate head 3275a16bebbb merged into trunk 584ee991961c — pass: builds build-agent, mesh-controller → ace, g14, novox, shanks; no bus step; every …
mesh/repo-check head 3275a16bebbb merged into trunk 584ee991961c — pass: its merge-check.sh passed
mesh/delivery delivered
Let a trusted file that names its keys hold only those keys at the terminal (hq issue 586, ADR 0329 §5)
A module with a trusted mergeable file had its whole layer refused through the
settings verb, because such a file took any key. Once a module's files name the
keys they take (ADR 0326), each file takes only those, so only a trusted file's
named keys are the terminal's: betterbird's tags, in a file nothing trusts, is set
through the verb, while its accounts and identities stay the terminal's or the
operator's warrant. A module whose files name no keys keeps the whole-layer rule.
2026-10-13 05:22:35 +02:00

190 lines
8.2 KiB
Go

package catalogue
import (
"encoding/json"
"fmt"
"sort"
"strings"
)
// Which settings are the controller's terminal's alone (novox/hq issue 339).
//
// A setting is the operator's word on how a module is configured, and the `settings` verb writes it for any
// caller allowed to call verbs — agents among them. Most settings change only the module itself. Some change
// what root or another module trusts, and those are said at the terminal alone, never through a verb:
//
// 1. `places` and `accesses`: where the node-engine creates and, as root, owns a module's directories, and
// which of the machine's paths are mounted into its container.
// 2. **Every key a provider serves**, and every setting a served value asks for. A setting overrides a served
// key (Settle), and what is served is what every consumer of the provision connects to and believes: a
// database's port, an object store's scheme, a registry's port, an identity provider's issuer and token
// path. Through a verb, any caller could point every consumer at a listener of its own and collect the
// credentials they present.
// 3. **Every setting a file asks for, unless the file says `"trusted": false`.** A file root or a consumer trusts —
// a logind drop-in, an env file that says which uid a container runs as, a script run as root — must not
// change through a verb, and the safe reading of a file that says nothing is that it is one of them (fail
// closed). `"trusted": false` is the opt-out, for a file nothing trusts: a person's own notifier settings.
// `module check` lists the files that say nothing, so an author can opt one out where that is true.
// **A mergeable file asks for every key its own content names** (novox/hq issue 340): a setting of that key
// lands in the file as a `${setting:…}` would, without the file ever spelling one. The agent's module keeps the
// managed settings and tool servers every Claude Code session on a node obeys in one, and through a verb an
// agent could have given every person's session a hook of its own.
//
// 4. **A module's whole layer, when it has a mergeable file not marked `"trusted": false`** (novox/hq issue 340,
// TrustedMergeable). A mergeable file takes any key a layer sets, not only those its content names, so no list
// of keys covers it: an empty runtime configuration a provider reads would take a `url` of the caller's.
// **Unless the module's files name the keys they take** (novox/hq ADR 0326, ADR 0329 §5): then a mergeable
// file takes exactly its `takes-settings` and no other key, so those keys of a trusted file are terminal keys
// and the rest of the layer is not. A key no file names is stray and refused where it is set. So betterbird's
// `tags`, in a file nothing trusts, is set through the verb, while its `accounts` and `identities` are not.
// 5. **Every setting a directory's owner asks for** (novox/hq issue 279): root chowns the directory to the number
// at the next push, so through a verb any caller could hand itself — or uid 0 — a module's folder. Whatever
// else asks for the key, a file marked `"trusted": false` included, the owner makes it the terminal's.
//
// Derived from the manifest, never listed by hand, so a provider or a trusted file added tomorrow is covered.
// TrustedField is the key a file resource carries to say whether the settings it asks for are trusted: true, or
// absent, makes each a terminal key; false says, out loud, that none changes what root or a consumer trusts. Said in the
// catalogue, never on the machine: the composer takes it out before the node-engine, which parses strictly.
const TrustedField = "trusted"
// TerminalKeys are the settings keys of a module that are set at the controller's terminal alone: places and
// accesses, every key its provisions serve and every setting a served value asks for, every setting a file
// asks for unless it says `"trusted": false`, and every setting a directory's owner asks for. Places and accesses first, then the rest sorted.
func TerminalKeys(m Manifest) []string {
keys := map[string]bool{}
for _, served := range m.Serves {
for key, value := range served {
keys[key] = true
if s, ok := value.(string); ok {
for _, asked := range settingsUsed(s) {
keys[asked] = true
}
}
}
}
for _, r := range m.Resources {
if fmt.Sprint(r["type"]) == "directory" {
owner, _ := r["owner"].(string)
for _, asked := range settingsUsed(owner) {
keys[asked] = true
}
continue
}
if fmt.Sprint(r["type"]) != "file" {
continue
}
if trusted, said := r[TrustedField].(bool); said && !trusted {
continue
}
for _, asked := range asksFor(r) {
keys[asked] = true
}
}
delete(keys, PlacesSetting)
delete(keys, AccessesSetting)
rest := make([]string, 0, len(keys))
for k := range keys {
rest = append(rest, k)
}
sort.Strings(rest)
return append([]string{PlacesSetting, AccessesSetting}, rest...)
}
// UnsaidTrust is every file of a module that asks for a setting and does not say whether it is trusted, by id:
// each counts as trusted, and is listed so an author can opt out a file nothing trusts.
func UnsaidTrust(m Manifest) []string {
var out []string
for _, r := range m.Resources {
if fmt.Sprint(r["type"]) != "file" {
continue
}
if len(asksFor(r)) == 0 {
continue
}
if _, said := r[TrustedField]; !said {
out = append(out, fmt.Sprint(r["id"]))
}
}
sort.Strings(out)
return out
}
// TrustedMergeable is every mergeable file of a module not marked `"trusted": false`, by id (novox/hq issue 340).
// A mergeable file takes any key a layer sets, not only those its content names: an empty runtime configuration a
// provider reads takes a `url` of a caller's as surely as a declared one. So a module holding one has its whole
// layer set at the controller's terminal; a verb may read it and change nothing. A module whose mergeable files
// name the keys they take (ADR 0326) has none here: each such file takes only its named keys, which TerminalKeys
// holds for a trusted one (ADR 0329 §5).
func TrustedMergeable(m Manifest) []string {
if namesItsKeys(m) {
return nil
}
var out []string
for _, r := range m.Resources {
if fmt.Sprint(r["type"]) != "file" {
continue
}
if how, _ := r["merge"].(string); how == "" {
continue
}
if trusted, said := r[TrustedField].(bool); said && !trusted {
continue
}
out = append(out, fmt.Sprint(r["id"]))
}
sort.Strings(out)
return out
}
// asksFor is every setting a file resource asks for: each `${setting:…}` in its content and, for a mergeable file,
// every key at the top of the content it merges into (novox/hq issue 340). Those are the keys the file declares it
// takes: a layer's value for one of them lands in it as surely as a placeholder would be filled. A key the content
// does not name may land too, but nothing reading the file was written to read it.
func asksFor(r map[string]any) []string {
content, _ := r["content"].(string)
asked := settingsUsed(content)
if how, _ := r["merge"].(string); how != "" {
if taken, names := takenKeys(r); names {
// The file takes exactly the keys it names (ADR 0326), whatever its content holds.
for key := range taken {
asked = append(asked, key)
}
sort.Strings(asked)
return asked
}
}
if how, _ := r["merge"].(string); how != "" && strings.TrimSpace(content) != "" {
var base map[string]any
if json.Unmarshal([]byte(content), &base) == nil {
for key := range base {
asked = append(asked, key)
}
}
}
sort.Strings(asked)
return asked
}
// TrustProblems are the ways a manifest states `trusted` wrongly: anything but true or false, or on anything but
// a file. Refused at registration and by `module check`.
func TrustProblems(m Manifest) []string {
var out []string
for _, r := range m.Resources {
v, said := r[TrustedField]
if !said {
continue
}
if fmt.Sprint(r["type"]) != "file" {
out = append(out, fmt.Sprintf("%s: %v is a %v and says %q; only a file says whether the settings it "+
"asks for are trusted (novox/hq issue 339)", m.Module, r["id"], r["type"], TrustedField))
continue
}
if _, ok := v.(bool); !ok {
out = append(out, fmt.Sprintf("%s: %v says %q as %v; it is true or false (novox/hq issue 339)",
m.Module, r["id"], TrustedField, v))
}
}
return out
}