diff --git a/cmd/mesh-controller/check.go b/cmd/mesh-controller/check.go index d53d3a82..cb4c6acd 100644 --- a/cmd/mesh-controller/check.go +++ b/cmd/mesh-controller/check.go @@ -137,23 +137,11 @@ 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. + // **A file that asks for a setting and does not say whether it is trusted counts as trusted** (novox/hq issue + // 339): listed and counted, never refused, so an author can opt out a file nothing trusts. 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 - } - } + unsaid += len(catalogue.UnsaidTrust(shelf[name])) } for _, name := range names { @@ -198,8 +186,9 @@ func moduleCheckFor(paths []string, longestMachine int, out io.Writer) error { 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")) + fmt.Fprintf(out, "; WARNING: %s ask(s) for a setting and do(es) not say whether it is trusted, so it counts as "+ + "trusted: set at the terminal alone; say %q false where nothing trusts it (novox/hq issue 339)", + strings.Join(missing, ", "), catalogue.TrustedField) } 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, "+ @@ -225,7 +214,7 @@ func moduleCheckFor(paths []string, longestMachine int, out io.Writer) error { 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). +// whether it is trusted, and so count as 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. diff --git a/cmd/mesh-controller/health_check_test.go b/cmd/mesh-controller/health_check_test.go index 4b835017..5ac0e1cc 100644 --- a/cmd/mesh-controller/health_check_test.go +++ b/cmd/mesh-controller/health_check_test.go @@ -46,9 +46,9 @@ func TestModuleCheckCountsTheUndeclaredAndRefusesThemFromTheDate(t *testing.T) { } } -// A file that asks for a setting says whether it is trusted (novox/hq issue 339): `module check` warns and counts -// it before the date, and refuses it from the date; a file that says so passes either way. -func TestModuleCheckCountsUnsaidTrustAndRefusesItFromTheDate(t *testing.T) { +// A file that asks for a setting without saying whether it is trusted counts as trusted (novox/hq issue 339): +// `module check` lists and counts it, and never refuses it — there is nothing unsafe to refuse. +func TestModuleCheckListsUnmarkedFilesAndNeverRefusesThem(t *testing.T) { dir := t.TempDir() path := filepath.Join(dir, "module.json") os.WriteFile(path, []byte(`{"module":"power","resources":[ @@ -56,20 +56,17 @@ func TestModuleCheckCountsUnsaidTrustAndRefusesItFromTheDate(t *testing.T) { "content":"HandleLidSwitch=${setting:lid}\n"}, {"id":"note","type":"file","path":"/var/lib/power/note","mode":"0644","content":"${setting:greeting}\n"}]}`), 0o600) defer func() { checkNow = time.Now }() - - checkNow = func() time.Time { return catalogue.TrustRequiredFrom.Add(-time.Hour) } - var out bytes.Buffer - if err := moduleCheck([]string{path}, &out); err != nil { - t.Fatalf("refused before the date: %v\n%s", err, out.String()) - } - for _, want := range []string{"WARNING: note ask(s) for a setting", UnsaidTrustLine + " 1"} { - if !strings.Contains(out.String(), want) { - t.Errorf("the check does not say %q:\n%s", want, out.String()) + for _, at := range []time.Time{time.Date(2026, 10, 1, 0, 0, 0, 0, time.UTC), time.Date(2036, 1, 1, 0, 0, 0, 0, time.UTC)} { + checkNow = func() time.Time { return at } + var out bytes.Buffer + if err := moduleCheck([]string{path}, &out); err != nil { + t.Fatalf("refused at %v: %v\n%s", at, err, out.String()) + } + for _, want := range []string{"note ask(s) for a setting and do(es) not say whether it is trusted, so it counts as trusted", + UnsaidTrustLine + " 1"} { + if !strings.Contains(out.String(), want) { + t.Errorf("the check does not say %q:\n%s", want, out.String()) + } } } - checkNow = func() time.Time { return catalogue.TrustRequiredFrom } - out.Reset() - if err := moduleCheck([]string{path}, &out); err == nil || !strings.Contains(out.String(), "power: the file note asks for a setting") { - t.Fatalf("an unsaid file passed after the date: %v\n%s", err, out.String()) - } } diff --git a/cmd/mesh-controller/terminal_settings_test.go b/cmd/mesh-controller/terminal_settings_test.go index 849979a6..9d9803e4 100644 --- a/cmd/mesh-controller/terminal_settings_test.go +++ b/cmd/mesh-controller/terminal_settings_test.go @@ -49,7 +49,10 @@ func TestPlacesAndAccessesAreRefusedThroughEveryVerb(t *testing.T) { register(t, open, catalogue.Manifest{Module: "notes", Version: "1", Accesses: []catalogue.Access{{ID: "media", Path: "/storage/media", Mode: "read"}}, 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"}}}) + // Nothing trusts this file: said, so a verb may change what it asks for (an unmarked one counts as + // trusted, and only the terminal could). + {"id": "rc", "type": "file", "path": "/etc/notes.conf", "mode": "0644", "trusted": false, + "content": "x = ${setting:x}\n"}}}) if _, err := assign(ctx, open, "laptop", "notes"); err != nil { t.Fatal(err) } diff --git a/internal/catalogue/terminal_keys.go b/internal/catalogue/terminal_keys.go index a08dfa26..342b33ac 100644 --- a/internal/catalogue/terminal_keys.go +++ b/internal/catalogue/terminal_keys.go @@ -3,7 +3,6 @@ package catalogue import ( "fmt" "sort" - "time" ) // Which settings are the controller's terminal's alone (novox/hq issue 339). @@ -19,25 +18,22 @@ import ( // 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 marked `trusted` asks for**: 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. The manifest says so on the -// file (TrustedField), and `module check` names a file that asks for a setting without saying. +// 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. // // 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 -// makes each a terminal key; false says, out loud, that none changes what root or a consumer trusts. Said in the +// 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" -// TrustRequiredFrom is when `module check` refuses a file that asks for a setting and does not say whether it is -// trusted. Until then it is warned and counted: the catalogue's files get the field in their own change, which -// can only land once a controller that takes the field out of the declaration runs. -var TrustRequiredFrom = time.Date(2026, 10, 10, 0, 0, 0, 0, time.UTC) - // 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 -// marked trusted asks for. Places and accesses first, then the rest sorted. +// 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 { @@ -51,7 +47,10 @@ func TerminalKeys(m Manifest) []string { } } for _, r := range m.Resources { - if trusted, _ := r[TrustedField].(bool); !trusted || fmt.Sprint(r["type"]) != "file" { + if fmt.Sprint(r["type"]) != "file" { + continue + } + if trusted, said := r[TrustedField].(bool); said && !trusted { continue } if content, ok := r["content"].(string); ok { @@ -70,7 +69,8 @@ func TerminalKeys(m Manifest) []string { 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. +// 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 { diff --git a/internal/catalogue/terminal_settings_test.go b/internal/catalogue/terminal_settings_test.go index 79950ff7..c9c3bac6 100644 --- a/internal/catalogue/terminal_settings_test.go +++ b/internal/catalogue/terminal_settings_test.go @@ -138,14 +138,16 @@ func TestTerminalKeysAreDerived(t *testing.T) { power := Manifest{Module: "power", Resources: []map[string]any{ {"id": "logind", "type": "file", "path": "/etc/systemd/logind.conf.d/power.conf", "trusted": true, "content": "HandleLidSwitch=${setting:handle-lid-switch}\n"}, - {"id": "note", "type": "file", "path": "/var/lib/power/note", "trusted": false, "content": "${setting:greeting}\n"}}} + {"id": "note", "type": "file", "path": "/var/lib/power/note", "trusted": false, "content": "${setting:greeting}\n"}, + // Unmarked counts as trusted (fail closed): only `"trusted": false` lets a verb change what a file asks for. + {"id": "unmarked", "type": "file", "path": "/etc/power/unmarked", "content": "${setting:unmarked}\n"}}} for _, c := range []struct { m Manifest want string }{ {postgres, "places,accesses,port"}, {keycloak, "places,accesses,issuer,token-path"}, - {power, "places,accesses,handle-lid-switch"}, + {power, "places,accesses,handle-lid-switch,unmarked"}, {Manifest{Module: "plain"}, "places,accesses"}, } { if got := strings.Join(TerminalKeys(c.m), ","); got != c.want {