Derive the terminal's settings from what a module serves and which files it trusts; a found directory is its own condition
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

The third review of #170 (hq issue 339): a setting overrides any key a
provider serves, so any caller of the settings verb could move a database's
port, a registry's port or an issuer to a listener of its own and collect
what consumers present. TerminalKeys now derives from the manifest: places,
accesses, every served key and every setting a served value asks for, and
every setting a file marked `trusted` asks for. `trusted` is the catalogue's
word, taken out before the declaration; `module check` warns of a file that
asks for a setting without saying, and refuses it from 2026-10-30. The hand
list is gone. A directory used as found is now its own condition kind, the
operator's, never urgent, and the gate exempts it where it exempts a relogin.
This commit is contained in:
jochen
2026-10-09 01:23:44 +02:00
parent ec7b8bcd58
commit b9fc09c375
12 changed files with 368 additions and 58 deletions
+35
View File
@@ -65,6 +65,13 @@ func moduleCheckFor(paths []string, longestMachine int, out io.Writer) error {
}
// A definition names no installation (novox/hq ADR 0112, ADR 0155): judged here, in the
// catalogue-wide test, and at registration, which refuses in the same words.
if wrong := catalogue.TrustProblems(m); len(wrong) > 0 {
for _, p := range wrong {
fmt.Fprintf(out, "%s: %s\n", path, p)
}
failed += len(wrong)
faulted[m.Module] = true
}
if named := catalogue.InstallationProblems(m); len(named) > 0 {
for _, p := range named {
fmt.Fprintf(out, "%s: %s\n", path, p)
@@ -130,6 +137,25 @@ func moduleCheckFor(paths []string, longestMachine int, out io.Writer) error {
}
}
// **Every file that asks for a setting says whether it is trusted** (novox/hq issue 339): warned until the
// date, refused from it, and counted for the catalogue's merge check like the resources without health.
unsaid := 0
trustRequired := !checkNow().Before(catalogue.TrustRequiredFrom)
for _, name := range names {
missing := catalogue.UnsaidTrust(shelf[name])
unsaid += len(missing)
if trustRequired {
for _, id := range missing {
fmt.Fprintf(out, "%s: the file %s asks for a setting and does not say whether it is trusted: say "+
"%q true or false (novox/hq issue 339)\n", name, id, catalogue.TrustedField)
}
if len(missing) > 0 {
failed += len(missing)
faulted[name] = true
}
}
}
for _, name := range names {
m := shelf[name]
if faulted[name] {
@@ -171,6 +197,10 @@ func moduleCheckFor(paths []string, longestMachine int, out io.Writer) error {
if len(checks) > 0 {
fmt.Fprintf(out, ", ready: %s", strings.Join(checks, "; "))
}
if missing := catalogue.UnsaidTrust(m); len(missing) > 0 {
fmt.Fprintf(out, "; WARNING: %s ask(s) for a setting and say(s) not whether it is trusted, refused from "+
"%s (novox/hq issue 339)", strings.Join(missing, ", "), catalogue.TrustRequiredFrom.Format("2006-01-02"))
}
if missing := catalogue.Undeclared(m); len(missing) > 0 {
fmt.Fprintf(out, "; WARNING: %s stay(s) up and say(s) not how it is ready — judged by liveness alone, "+
"refused from %s (ADR 0240 rule 8)", strings.Join(missing, ", "), catalogue.HealthRequiredFrom.Format("2006-01-02"))
@@ -179,6 +209,7 @@ func moduleCheckFor(paths []string, longestMachine int, out io.Writer) error {
}
// The count the catalogue keeps (ADR 0240 rule 8), in a line its merge check reads.
fmt.Fprintf(out, "%s %d\n", UndeclaredHealthLine, undeclared)
fmt.Fprintf(out, "%s %d\n", UnsaidTrustLine, unsaid)
if failed > 0 {
return fmt.Errorf("%d problem(s) in %d manifest(s)", failed, len(paths))
}
@@ -193,6 +224,10 @@ func moduleCheckFor(paths []string, longestMachine int, out io.Writer) error {
// `health` in, over the manifests given: the catalogue's merge check compares it with the number it keeps.
const UndeclaredHealthLine = "long-running resources without health:"
// UnsaidTrustLine starts the line `module check` says the count of files that ask for a setting and do not say
// whether it is trusted (novox/hq issue 339).
const UnsaidTrustLine = "files asking for a setting without saying whether it is trusted:"
// checkNow is the clock `module check` judges the date by; a test sets it.
var checkNow = time.Now
+52
View File
@@ -6,6 +6,7 @@ import (
"testing"
"time"
"github.com/novox/mesh-controller/internal/conditions"
"github.com/novox/mesh-controller/internal/inventory"
"github.com/novox/mesh-controller/internal/link"
)
@@ -65,6 +66,12 @@ func TestAFixGoesThroughPastADirectoryFoundBefore(t *testing.T) {
backlogFacts := gatherGateFacts
gatherGateFacts = func(ctx context.Context, open *stores, component string) (gateFacts, error) {
f, err := backlogFacts(ctx, open, component)
// The used-as-found condition, raised after the send (its second statement): its own kind, never a
// fault the gate reads as the build's.
f.judged, f.openErr = true, nil
f.open = append(f.open, conditions.Condition{Key: usedAsFoundKey("app", "anchor"), Kind: kindUsedAsFound,
Subject: conditions.Subject{Scope: conditions.ScopeModule, ID: "app.anchor", Machine: "anchor"},
Raised: time.Now()})
f.health = map[string]inventory.NodeHealth{}
for _, n := range []string{"anchor", "laptop"} {
f.health[n] = inventory.NodeHealth{Node: n, HeardAt: time.Now(), Resources: []inventory.ResourceHealth{
@@ -102,3 +109,48 @@ func TestAFixGoesThroughPastADirectoryFoundBefore(t *testing.T) {
})
}
}
// The condition says the wait in its own kind: the operator's, never urgent, and cleared once handed over.
func TestADirectoryUsedAsFoundIsItsOwnCondition(t *testing.T) {
k, _ := withConditionsInMemory(t)
ctx := t.Context()
rs := map[string][]inventory.ResourceHealth{"notes": {foundDirectory("notes", time.Now().Add(-time.Hour))}}
for i := 0; i < 2; i++ {
if err := judgeModuleHealth(ctx, nil, k, "laptop", rs, map[string]int{"notes": i + 1}, time.Now()); err != nil {
t.Fatal(err)
}
}
open, _ := k.Open(ctx)
var got *conditions.Condition
for i, c := range open {
if c.Key == usedAsFoundKey("notes", "laptop") {
got = &open[i]
}
if c.Kind == kindModuleUnhealthy {
t.Fatalf("raised as a fault: %+v", c)
}
}
if got == nil || got.Resolver != conditions.ResolverOperator || got.Severity == conditions.Urgent ||
!strings.Contains(got.Summary, "notes.data") {
t.Fatalf("the condition: %+v", got)
}
// Long open is still not urgent: only a person can hand it over, and nothing is broken by the wait.
if err := judgeModuleHealth(ctx, nil, k, "laptop", rs, map[string]int{"notes": 3}, time.Now().Add(48*time.Hour)); err != nil {
t.Fatal(err)
}
open, _ = k.Open(ctx)
for _, c := range open {
if c.Key == usedAsFoundKey("notes", "laptop") && c.Severity == conditions.Urgent {
t.Fatal("a directory used as found became urgent")
}
}
if err := judgeModuleHealth(ctx, nil, k, "laptop", map[string][]inventory.ResourceHealth{}, nil, time.Now()); err != nil {
t.Fatal(err)
}
open, _ = k.Open(ctx)
for _, c := range open {
if c.Key == usedAsFoundKey("notes", "laptop") {
t.Fatal("not cleared once handed over")
}
}
}
+6 -4
View File
@@ -216,9 +216,10 @@ func judgeHealth(module, component string, m catalogue.Manifest, machine string,
firstLine(f.openErr.Error())
}
for _, c := range f.open {
// A wait for a person's new login is the module's reading, not a fault raised since the send: the
// gate reads it from the statement below (ADR 0254).
if c.Source == gateProbe || c.Raised.Before(since) || c.Kind == kindReloginNeeded {
// A wait for a person's new login, or for a directory used as found to be handed over, is the module's
// reading, not a fault raised since the send: the gate reads it from the statement below (ADR 0254,
// novox/hq issue 339).
if c.Source == gateProbe || c.Raised.Before(since) || c.Kind == kindReloginNeeded || c.Kind == kindUsedAsFound {
continue
}
onIt := c.Subject.Machine == machine || slices.Contains(c.Subject.Also, machine) ||
@@ -329,7 +330,8 @@ func aboutTheMachine(machine string, moved []string, since time.Time, f gateFact
for _, c := range f.open {
aboutIt := c.Subject.Scope == conditions.ScopeMachine && (c.Subject.ID == machine || c.Subject.Machine == machine ||
slices.Contains(c.Subject.Also, machine))
if !aboutIt || c.Source == gateProbe || c.Raised.Before(since) {
// A directory used as found waits for a person, whatever the send did (novox/hq issue 339).
if !aboutIt || c.Source == gateProbe || c.Raised.Before(since) || c.Kind == kindUsedAsFound {
kept = append(kept, c)
continue
}
+66 -1
View File
@@ -135,7 +135,8 @@ func judgeModuleHealth(ctx context.Context, inv *inventory.Inventory, k *conditi
}
standing := map[string]conditions.Condition{}
for _, c := range open {
if (c.Kind == kindModuleUnhealthy || c.Kind == kindReloginNeeded) && c.Subject.Machine == node {
if (c.Kind == kindModuleUnhealthy || c.Kind == kindReloginNeeded || c.Kind == kindUsedAsFound) &&
c.Subject.Machine == node {
standing[c.Key] = c
}
}
@@ -158,6 +159,21 @@ func judgeModuleHealth(ctx context.Context, inv *inventory.Inventory, k *conditi
heldOn := map[string]string{}
providers := map[catalogue.Chosen]bool{}
for _, m := range modules {
// **A directory used as found is said as that** (novox/hq issue 339): the operator's to hand over at the
// machine, never urgent — nothing is broken by the wait that a person was not told of — and its own kind,
// so the gate never reads it as a fault of the build that happened to be sent beside it.
if said, waits := foundWait(m, node, unhealthy[m]); waits {
o := usedAsFoundObservation(m, node, said, unhealthy[m])
seen[o.Key()] = true
became[m] = kindUsedAsFound
if _, isOpen := standing[o.Key()]; streaks[m] < moduleUnhealthyAfter && !isOpen {
continue
}
if _, err := k.Observe(ctx, o); err != nil {
problems = append(problems, err.Error())
}
continue
}
// **A wait for a person's new login is said as that** (novox/hq ADR 0254): one plain sentence to the
// operator, never urgent, cleared on the first statement that no longer says it.
if said, waits := personWait(m, node, unhealthy[m]); waits {
@@ -209,6 +225,9 @@ func judgeModuleHealth(ctx context.Context, inv *inventory.Inventory, k *conditi
if c.Kind == kindReloginNeeded {
why = fmt.Sprintf("%s says %s no longer waits for a new login", node, module)
}
if c.Kind == kindUsedAsFound {
why = fmt.Sprintf("%s says no directory of %s is used as found any more", node, module)
}
if on, held := heldOn[key]; held {
why = fmt.Sprintf("what %s finds on %s waits on %s, which is unhealthy: held under its condition", module, node, on)
}
@@ -617,3 +636,49 @@ func addsAccountGroups(from catalogue.Manifest, hadFrom bool, to catalogue.Manif
}
return false
}
// kindUsedAsFound is a module's condition while the node-engine uses one of its directories as found (novox/hq
// issue 339): its own kind, the operator's, never urgent, and never read by the gate as a fault of a build.
const kindUsedAsFound = "directory-used-as-found"
// usedAsFoundKey is a module's used-as-found condition on a machine.
func usedAsFoundKey(module, node string) string {
return conditions.Key(conditions.ScopeModule, module+"."+node, kindUsedAsFound)
}
// foundWait is whether everything unhealthy of a module on a machine is a directory used as found, and that in
// one sentence. Anything else unhealthy beside it is judged as a fault, with the directory among its resources.
func foundWait(module, node string, rs []inventory.ResourceHealth) (string, bool) {
var ids, why []string
for _, r := range rs {
if r.Module != module || r.State != link.StateUnhealthy {
continue
}
if !usedAsFound(r) {
return "", false
}
ids = append(ids, r.Resource)
why = append(why, strings.TrimSpace(strings.TrimPrefix(r.Reason, link.ReasonUsedAsFound)))
}
if len(ids) == 0 {
return "", false
}
return fmt.Sprintf("%s on %s uses %s as found: %s", module, node, strings.Join(ids, ", "),
strings.Join(why, "; ")), true
}
// usedAsFoundObservation is a module whose directory the node-engine uses as found, in words: the operator's, a
// warning however long it stays, its summary naming the directories and their owners; the paths are evidence.
func usedAsFoundObservation(module, node, said string, rs []inventory.ResourceHealth) conditions.Observation {
o := moduleUnhealthyObservation(module, node, rs)
o.Token, o.Kind, o.Resolver, o.Severity, o.Summary = kindUsedAsFound, kindUsedAsFound, conditions.ResolverOperator,
conditions.Warning, said
o.Headline = fmt.Sprintf("%s waits for a directory on %s", module, node)
o.Explanation = fmt.Sprintf("A directory of %s was already on %s, with another owner or mode than %s declares. "+
"The mesh left it as it was rather than hand it to an account, so %s may not be able to use it.",
module, node, module, module)
o.Needs = fmt.Sprintf("on %s, run mesh-host hand-over with the directory's path as root.", node)
o.Resolved = fmt.Sprintf("%s's directory on %s is the mesh's", module, node)
o.Actions = nil
return o
}
+14 -4
View File
@@ -445,7 +445,7 @@ func settingsCommand(ctx context.Context, args []string) error {
if err != nil {
return err
}
if err := refuseTerminalSettingsThroughAVerb(before, values, positionals[0], where); err != nil {
if err := refuseTerminalSettingsThroughAVerb(ctx, inv, before, values, positionals[0], where); err != nil {
return err
}
added, changed, removed := settingsChange(before, values)
@@ -603,7 +603,7 @@ func settingsCommand(ctx context.Context, args []string) error {
if err != nil {
return err
}
if err := refuseTerminalSettingsThroughAVerb(before, nil, positionals[0], where); err != nil {
if err := refuseTerminalSettingsThroughAVerb(ctx, inv, before, nil, positionals[0], where); err != nil {
return err
}
if err := inv.ClearSettings(ctx, *node, positionals[0]); err != nil {
@@ -998,6 +998,9 @@ func whereItComesFrom(repository, ref, commit, path string, self bool) (inventor
// definition that got past the check — written elsewhere, or checked by nobody — is refused here
// in the same words. A name meant on purpose is declared with its reason and passes.
func namesNoInstallation(m catalogue.Manifest) error {
if problems := catalogue.TrustProblems(m); len(problems) > 0 {
return fmt.Errorf("%s", strings.Join(problems, "; "))
}
named := catalogue.InstallationProblems(m)
if len(named) == 0 {
return nil
@@ -1080,12 +1083,19 @@ func throughAVerb() (string, bool) {
// refuseTerminalSettingsThroughAVerb refuses a layer change through a verb that would add, change or remove
// places or accesses; a change that leaves both as they were is not refused.
func refuseTerminalSettingsThroughAVerb(before, after map[string]any, module, where string) error {
func refuseTerminalSettingsThroughAVerb(ctx context.Context, inv *inventory.Inventory, before, after map[string]any,
module, where string) error {
verb, through := throughAVerb()
if !through {
return nil
}
for _, key := range catalogue.TerminalKeys(module) {
// Judged against the module's definition as the catalogue holds it: what it serves and which of its files
// are trusted. A catalogue that cannot be read refuses rather than judging against nothing.
shelf, err := inv.Catalogue(ctx)
if err != nil {
return fmt.Errorf("which settings of %s are the terminal's cannot be read, so nothing was changed: %w", module, err)
}
for _, key := range catalogue.TerminalKeys(shelf[module]) {
was, _ := json.Marshal(before[key])
now, _ := json.Marshal(after[key])
if string(was) == string(now) {
+8
View File
@@ -204,6 +204,14 @@ var plainWordings = map[string]func(conditions.Observation) words{
}
return reloginWords(orModule(module), machineOr(o, "a machine"), false)
}),
kindUsedAsFound: worded(func(o conditions.Observation) words {
module := ""
if o.Scope == conditions.ScopeModule && o.Machine != "" {
module = strings.TrimSuffix(o.ID, "."+o.Machine)
}
w := usedAsFoundObservation(orModule(module), machineOr(o, "a machine"), o.Summary, nil)
return words{Headline: w.Headline, Explanation: w.Explanation, Needs: w.Needs, Resolved: w.Resolved}
}),
kindProviderFailing: worded(func(o conditions.Observation) words {
thing, consumer := conditions.ThingWords(o), idPart(o, 2)
if consumer == "" {
+32 -18
View File
@@ -151,22 +151,33 @@ func TestPlacesAndAccessesAreRefusedThroughEveryVerb(t *testing.T) {
}
}
// The mesh's trust anchors are set at the terminal alone (novox/hq issue 339): through the settings verb, a caller
// could replace the internal authority's root every consumer trusts, or the issuer every login is checked against.
func TestATrustAnchorIsRefusedThroughAVerb(t *testing.T) {
// What a provider serves is set at the terminal alone (novox/hq issue 339): through the settings verb, a caller
// could move a database's port to a listener of its own and collect every consumer's credentials, or point every
// login at an issuer of its own.
func TestAServedKeyIsRefusedThroughAVerb(t *testing.T) {
open := aMesh(t)
ctx := t.Context()
register(t, open, catalogue.Manifest{Module: "step-ca", Version: "1",
Provides: catalogue.FromAnywhere("acme-ca"),
Serves: map[string]map[string]any{"acme-ca": {"root": "", "path": "/acme/acme/directory"}},
Resources: []map[string]any{{"id": "rc", "type": "file", "path": "/etc/step.conf", "mode": "0644",
"content": "x = ${setting:x}\n"}}})
register(t, open, catalogue.Manifest{Module: "store", Version: "1",
Provides: catalogue.FromAnywhere("database"),
Serves: map[string]map[string]any{"database": {"port": 5432.0}},
Resources: []map[string]any{{"id": "rc", "type": "file", "path": "/etc/store.conf", "mode": "0644",
"trusted": false, "content": "x = ${setting:x}\n"}}})
register(t, open, catalogue.Manifest{Module: "keycloak", Version: "1",
Provides: catalogue.FromAnywhere("oidc-client"),
Serves: map[string]map[string]any{"oidc-client": {"issuer": "${setting:issuer}"}},
Resources: []map[string]any{{"id": "rc", "type": "file", "path": "/etc/kc.conf", "mode": "0644",
"content": "issuer = ${setting:issuer}\nx = ${setting:x}\n"}}})
for _, m := range []string{"step-ca", "keycloak"} {
"trusted": false, "content": "x = ${setting:x}\n"}}})
register(t, open, catalogue.Manifest{Module: "power", Version: "1",
Resources: []map[string]any{{"id": "logind", "type": "file", "path": "/etc/systemd/logind.conf.d/power.conf",
"mode": "0644", "trusted": true, "content": "HandleLidSwitch=${setting:lid}\nx=${setting:x}\n"}}})
if err := atTheTerminal(t, "settings", "set", "keycloak", `{"issuer":"https://id.example/realms/mesh","x":0}`,
"--node", "anchor"); err != nil {
t.Fatalf("the issuer at the terminal: %v", err)
}
if err := atTheTerminal(t, "settings", "set", "power", `{"lid":"suspend","x":0}`, "--node", "anchor"); err != nil {
t.Fatal(err)
}
for _, m := range []string{"store", "keycloak", "power"} {
if _, err := assign(ctx, open, "anchor", m); err != nil {
t.Fatal(err)
}
@@ -177,20 +188,23 @@ func TestATrustAnchorIsRefusedThroughAVerb(t *testing.T) {
t.Fatalf("%s: %v", what, err)
}
}
if err := atTheTerminal(t, "settings", "set", "keycloak", `{"issuer":"https://id.example/realms/mesh","x":0}`,
"--node", "anchor"); err != nil {
t.Fatalf("the issuer at the terminal: %v", err)
}
refused("a served port through the verb", throughVerb(t, "settings", map[string]any{"module": "store",
"node": "anchor", "values": `{"port":6543,"x":0}`}))
refused("a served port, mesh-wide, through the verb", throughVerb(t, "settings",
map[string]any{"module": "store", "values": `{"port":6543}`}))
refused("the issuer through the verb", throughVerb(t, "settings", map[string]any{"module": "keycloak",
"node": "anchor", "values": `{"issuer":"https://evil.example/realms/mesh","x":0}`}))
refused("the authority's root path through the verb", throughVerb(t, "settings", map[string]any{"module": "step-ca",
"node": "anchor", "values": `{"path":"/evil","x":0}`}))
refused("the authority's root path, mesh-wide, through the verb", throughVerb(t, "settings",
map[string]any{"module": "step-ca", "values": `{"path":"/evil"}`}))
refused("clearing the issuer through the verb", throughVerb(t, "settings", map[string]any{"module": "keycloak",
"node": "anchor", "clear": "true"}))
if err := throughVerb(t, "settings", map[string]any{"module": "keycloak", "node": "anchor",
"values": `{"issuer":"https://id.example/realms/mesh","x":1}`}); err != nil {
t.Fatalf("another key through the verb, the issuer kept: %v", err)
}
refused("a setting a trusted file asks for, through the verb", throughVerb(t, "settings", map[string]any{
"module": "power", "node": "anchor", "values": `{"lid":"ignore","x":0}`}))
// And `trusted` is the catalogue's word: it never reaches the machine, whose engine parses strictly.
plan := printed(t, func() error { return atTheTerminal(t, "plan", "anchor", "--json") })
if strings.Contains(plan, `"trusted"`) {
t.Fatal("the declaration carries the catalogue's `trusted`")
}
}