Keep places and accesses at the terminal, and refuse a line break in any setting
mesh/merge-gate pass: builds build-agent, mesh-controller, route-proxy → ace, g14, novox, shanks; no bus step; every machine composes with the change as it…
mesh/repo-check pass: its merge-check.sh passed
mesh/delivery superseded: a newer head of the same pull request

Through the settings verb a caller could place a module's directory at /etc
with an owner of its own and have root hand it over at the next send, or mount
any of the machine's paths into a container (hq ADR 0266). Both keys are now
the terminal's and never at the machine's own trees; a plans line that acts is
refused wherever its subcommand stands; and a line break in a setting, which a
file it is written into reads as a directive, is refused where it is kept and
where it is composed.
This commit is contained in:
jochen
2026-10-08 21:54:30 +02:00
parent e003f0e37b
commit 1d5a523a53
9 changed files with 302 additions and 10 deletions
+42
View File
@@ -414,6 +414,9 @@ func settingsCommand(ctx context.Context, args []string) error {
// word (novox/hq issue 304). Adding and changing keys needs nothing; removing one needs this.
replace := set.Bool("replace", false, "for set: remove the keys the new layer does not name")
history := set.Bool("history", false, "for show: the layers this one replaced, the latest first")
// Set by the settings verb on every line it composes (novox/hq ADR 0266): a verb may not change where a
// module's directories are placed or which of the machine's paths it reaches.
throughVerb := set.Bool("through-verb", false, "the line came from the settings verb: places and accesses are refused")
positionals, err := parseAround(set, args[1:])
if err != nil {
return err
@@ -445,6 +448,11 @@ func settingsCommand(ctx context.Context, args []string) error {
if err != nil {
return err
}
if *throughVerb {
if key := terminalSettingChanged(before, values); key != "" {
return terminalSettingRefusal(key, positionals[0], where)
}
}
added, changed, removed := settingsChange(before, values)
if len(removed) > 0 && !*replace {
return fmt.Errorf("%s on %s: this layer would no longer set %s. A layer is replaced whole; "+
@@ -596,6 +604,15 @@ func settingsCommand(ctx context.Context, args []string) error {
if len(positionals) != 1 {
return errors.New("settings clear <module> [--node <node>]")
}
if *throughVerb {
before, _, err := inv.Layer(ctx, *node, positionals[0])
if err != nil {
return err
}
if key := terminalSettingChanged(before, nil); key != "" {
return terminalSettingRefusal(key, positionals[0], where)
}
}
if err := inv.ClearSettings(ctx, *node, positionals[0]); err != nil {
return err
}
@@ -1051,3 +1068,28 @@ func declaresTools(m catalogue.Manifest) bool {
}
return false
}
// terminalSettings are the keys a verb may not change (novox/hq ADR 0266). `places` says where the node-engine
// creates and, as root, owns a module's directories, with an owner the setting names; `accesses` says which of
// the machine's paths are mounted into a module's container. Set through a verb, either would let any caller —
// an agent among them — have root hand it a directory, or mount one of the machine's into a container it
// reaches. They are the operator's, at the controller's terminal.
var terminalSettings = []string{catalogue.PlacesSetting, catalogue.AccessesSetting}
// terminalSettingChanged is the first of those keys a layer change would add, change or remove, or "".
func terminalSettingChanged(before, after map[string]any) string {
for _, key := range terminalSettings {
was, _ := json.Marshal(before[key])
now, _ := json.Marshal(after[key])
if string(was) != string(now) {
return key
}
}
return ""
}
func terminalSettingRefusal(key, module, where string) error {
return terminalRefusal("%s of %s on %s is set at the controller's terminal only, never through a verb: it says "+
"where root creates and owns a module's directories, or which of the machine's paths reach its container, "+
"and whoever may call a verb includes agents (novox/hq ADR 0266). Nothing was changed", key, module, where)
}
+11 -2
View File
@@ -805,13 +805,16 @@ func (a *verbArguments) commandLine() ([]string, error) {
var argv []string
switch {
case on("clear"):
argv = []string{"settings", "clear", str("module")}
argv = []string{"settings", "clear", str("module"), "--through-verb"}
case str("values") != "":
argv = []string{"settings", "set", str("module"), str("values")}
// What a set removes is refused unless meant (novox/hq ADR 0217).
if on("replace") {
argv = append(argv, "--replace")
}
// Through a verb, never places or accesses (novox/hq ADR 0266): the command refuses a change to
// either when told the line came from a verb.
argv = append(argv, "--through-verb")
default:
// Neither values nor clear: the layer as it stands, which is what a caller reads before
// replacing it (novox/hq ADR 0217) — and with history, the layers it replaced.
@@ -1359,7 +1362,10 @@ var commandReadForms = map[string]func(rest []string) bool{
// `plan <node>` previews a node's declaration; it sends nothing.
"plan": func([]string) bool { return true },
// `plans` lists and `plans <id>` shows one; `plans stop|close|go` acts.
"plans": func(r []string) bool { return !subIn(r, "stop", "close", "go") },
// Judged on every word, not the first: a flag before the subcommand (`plans --json go <id>`) still acts.
"plans": func(r []string) bool {
return !slices.ContainsFunc(r, func(w string) bool { return slices.Contains(plansActs, w) })
},
// `doctor` answers the last run, `probes` and `signals` describe; `doctor run` runs.
"doctor": func(r []string) bool { return flagsOnly(r) || subIn(r, "probes", "signals") },
"conditions": func(r []string) bool { return flagsOnly(r) || subIn(r, "list", "show", "history") },
@@ -1380,6 +1386,9 @@ var commandReadForms = map[string]func(rest []string) bool{
},
}
// plansActs are the `plans` subcommands that act on a walk; no other word of a plans line is one of them.
var plansActs = []string{"go", "stop", "close", "retry"}
// heldAtTheTerminal is a refusal of policy (novox/hq ADR 0266): the verb is known and served, and this line is
// the operator's at the controller's terminal. Never read as a verb this binary is behind on.
type heldAtTheTerminal struct{ msg string }
+3 -2
View File
@@ -268,8 +268,9 @@ var accountedFlags = map[string]map[string]string{
"self": "set by the verb from the repository's form: a path on the forge, or a URL",
"dry-run": "withheld: a dry run answers only when the build ends, which a call cannot wait for; `command` reaches it",
},
"builds": {"n": "=limit"},
"plans": {"n": "=limit", "what-if": "=repository"},
"builds": {"n": "=limit"},
"plans": {"n": "=limit", "what-if": "=repository"},
"settings": {"through-verb": "set by the verb on every set and clear: places and accesses are the terminal's (novox/hq ADR 0266)"},
// The machine's tunnel key, named as the verb's other arguments are (novox/hq ADR 0169).
"durations": {
"json": "set by the verb: the answer is data",
+4 -3
View File
@@ -123,11 +123,11 @@ func TestTokenIsRefusedThroughAVerb(t *testing.T) {
// `settings` is `settings set|clear` at a shell, with the values passed inline (novox/hq issue 198).
func TestSettingsSetsOrClearsALayer(t *testing.T) {
argv, err := argvFor("settings", map[string]any{"module": "dnsmasq", "values": `{"a":1}`, "node": "ace"})
if err != nil || strings.Join(argv, " ") != `settings set dnsmasq {"a":1} --node ace` {
if err != nil || strings.Join(argv, " ") != `settings set dnsmasq {"a":1} --through-verb --node ace` {
t.Fatalf("set on a machine: %v %v", argv, err)
}
argv, _ = argvFor("settings", map[string]any{"module": "dnsmasq", "clear": "true"})
if strings.Join(argv, " ") != "settings clear dnsmasq" {
if strings.Join(argv, " ") != "settings clear dnsmasq --through-verb" {
t.Fatalf("clear for the mesh: %v", argv)
}
// Neither values nor clear reads the layer as it stands (novox/hq ADR 0217): what a caller reads
@@ -369,7 +369,8 @@ func TestTheCommandVerbOnlyReads(t *testing.T) {
"secret recover a", "secret export a", "token issue --new x", "identity show", "broker users",
"api key", "licence show", "push anchor --why w", "assign novox m", "settings set m {}",
"settings clear m", "module add f", "module check /etc", "module forget m", "module issue m --node a",
"seat rename a b", "plans close p --why w", "plans go p", "doctor run", "conditions silence c --why w",
"seat rename a b", "plans close p --why w", "plans go p", "plans retry p", "plans stop p",
"plans --json go p", "plans -n 3 close p", "plans --what-if r retry p", "doctor run", "conditions silence c --why w",
"retire approve x", "cleanup delete x", "delivery check", "delivery go x", "bus upgrade",
"mirrors --record x", "mirrors --confirm", "hand-act record x --why y --cause z", "serve", "migrate",
"prepare", "declare x", "overlay x", "facts", "merge-gate", "check-here", "build x", "rebuild x",
@@ -0,0 +1,93 @@
package main
import (
"strings"
"testing"
"github.com/novox/mesh-controller/internal/catalogue"
)
// Where root creates and owns a module's directories, and which of the machine's paths reach its container,
// are the controller's terminal's alone (novox/hq ADR 0266): through the settings verb, a caller who set
// `places` to /etc with an owner of its own would have the next send hand it /etc.
func TestTheSettingsVerbMarksEverySetAndClearAsAVerbs(t *testing.T) {
for _, args := range []map[string]any{
{"module": "plex", "values": `{"places":{"data":"/etc"}}`},
{"module": "plex", "values": `{}`, "replace": "true", "node": "home"},
{"module": "plex", "clear": "true"},
} {
argv, err := argvFor("settings", args)
if err != nil || !strings.Contains(strings.Join(argv, " "), "--through-verb") {
t.Fatalf("%v: %v %v", args, argv, err)
}
}
// The generic verb never reaches settings set or clear at all.
for _, line := range []string{"settings set plex {}", "settings clear plex"} {
if _, err := argvFor("command", map[string]any{"command": line}); err == nil {
t.Fatalf("command ran %q", line)
}
}
}
func TestATerminalSettingIsChangedOnlyAtTheTerminal(t *testing.T) {
cases := []struct {
before, after map[string]any
want string
}{
{nil, map[string]any{"places": map[string]any{"data": "/etc"}}, "places"},
{map[string]any{"accesses": map[string]any{"m": "/storage"}}, map[string]any{}, "accesses"},
{map[string]any{"places": map[string]any{"d": "/srv/d"}}, nil, "places"},
{map[string]any{"places": map[string]any{"d": "/srv/d"}, "a": 1.0},
map[string]any{"places": map[string]any{"d": "/srv/d"}, "a": 2.0}, ""},
}
for _, c := range cases {
if got := terminalSettingChanged(c.before, c.after); got != c.want {
t.Errorf("%v → %v: %q, want %q", c.before, c.after, got, c.want)
}
}
}
// Over the real stores: the verb's line is refused for places, the terminal's is taken, and a clear through
// the verb of a layer that places a directory is refused too.
func TestPlacesAreRefusedThroughTheVerbAndTakenAtTheTerminal(t *testing.T) {
open := aMesh(t)
ctx := t.Context()
register(t, open, catalogue.Manifest{Module: "notes", Version: "1",
Resources: []map[string]any{{"id": "data", "type": "directory", "mode": "0755"},
{"id": "rc", "type": "file", "path": "/etc/notes.conf", "mode": "0644", "content": "x = ${setting:x}\n"}}})
if _, err := assign(ctx, open, "laptop", "notes"); err != nil {
t.Fatal(err)
}
set := func(through bool, values string) error {
args := []string{"set", "notes", values, "--node", "laptop"}
if through {
args = append(args, "--through-verb")
}
return settingsCommand(ctx, args)
}
if err := set(true, `{"places":{"data":{"path":"/srv/notes","owner":"1000:1000"}}}`); err == nil ||
!strings.Contains(err.Error(), "controller's terminal only") {
t.Fatalf("places through the verb: %v", err)
}
if err := set(false, `{"places":{"data":{"path":"/srv/notes","owner":"1000:1000"}},"x":0}`); err != nil {
t.Fatalf("places at the terminal: %v", err)
}
if err := set(true, `{"places":{"data":{"path":"/srv/notes","owner":"1000:1000"}},"x":1}`); err != nil {
t.Fatalf("a verb may change another key and keep places as they are: %v", err)
}
if err := settingsCommand(ctx, []string{"clear", "notes", "--node", "laptop", "--through-verb"}); err == nil ||
!strings.Contains(err.Error(), "controller's terminal only") {
t.Fatalf("a clear through the verb took places away: %v", err)
}
// And never at /etc, from anywhere.
if err := set(false, `{"places":{"data":{"path":"/etc","owner":"1000:1000"}},"x":1}`); err == nil ||
!strings.Contains(err.Error(), "/etc") {
t.Fatalf("a place at /etc: %v", err)
}
// A line break in any setting is refused where it is kept.
if err := set(false, `{"places":{"data":{"path":"/srv/notes","owner":"1000:1000"}},"x":"a\nPATH=/tmp"}`); err == nil ||
!strings.Contains(err.Error(), "line break") {
t.Fatalf("a line break: %v", err)
}
}
+1 -1
View File
@@ -158,7 +158,7 @@ func TestTheVerbsCarryReadReplaceAndMove(t *testing.T) {
}{
{"settings", map[string]any{"module": "plex", "node": "home"}, []string{"settings", "show", "plex", "--node", "home"}},
{"settings", map[string]any{"module": "plex", "history": "true"}, []string{"settings", "show", "plex", "--history"}},
{"settings", map[string]any{"module": "plex", "values": "{}", "replace": "true"}, []string{"settings", "set", "plex", "{}", "--replace"}},
{"settings", map[string]any{"module": "plex", "values": "{}", "replace": "true"}, []string{"settings", "set", "plex", "{}", "--replace", "--through-verb"}},
{"push", map[string]any{"node": "home", "move": "plex", "why": "w"},
[]string{"push", "home", "--wait", "0", "--move", "plex", "--why", "w"}},
{"push", map[string]any{"why": "w"}, []string{"push", "--behind", "--wait", "0", "--why", "w"}},