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. // // 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, and every setting a file // asks for unless it says `"trusted": false`. 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"]) != "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. func TrustedMergeable(m Manifest) []string { 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 != "" && 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 }