From 988e501cccd42f640f8c416ceafdc917264ebe27 Mon Sep 17 00:00:00 2001 From: jochen Date: Fri, 9 Oct 2026 23:41:22 +0200 Subject: [PATCH] Say a secret given in plain words, and log words the keeper refuses The secret-given condition's explanation carried a clock time, which the plain rule refuses, so the operator read the scope's fallback ("needs a look") instead of what changed. Its words now carry no time and say the secret's name as words; a test holds them to the rule through a keeper. With no test hook set, the keeper logs a refused or missing wording once per kind and reason, so a fallback is never silent again (hq issue 359). --- cmd/mesh-controller/desk_secret.go | 24 ++++++++++++---- cmd/mesh-controller/desk_secret_test.go | 38 ++++++++++++++++++++++++- internal/conditions/plain.go | 28 +++++++++++++----- internal/conditions/plain_test.go | 26 +++++++++++++++++ 4 files changed, 103 insertions(+), 13 deletions(-) diff --git a/cmd/mesh-controller/desk_secret.go b/cmd/mesh-controller/desk_secret.go index 1872cd1f..6cc60cf5 100644 --- a/cmd/mesh-controller/desk_secret.go +++ b/cmd/mesh-controller/desk_secret.go @@ -221,20 +221,34 @@ func giveAtDesk(ctx context.Context, node, module, name, desk string) error { const kindSecretGiven = "secret-given" // secretGivenObservation is that condition: which secret, of which module on which machine, how and when. +// The summary, for whoever looks closer, names the secret and the time. The words the operator reads are +// held to the plain rule (conditions.PlainWords): no clock time — the channel says when, in the operator's +// time — and the secret's name said as words. Words that broke the rule were replaced by the keeper with +// "needs a look … a problem it calls secret given" (hq issue 359), which told the operator nothing. func secretGivenObservation(node, module, name, how string, at time.Time) conditions.Observation { key := node + "." + module + "." + name + where := module + " on " + node + headline := "New secret given for " + where + if len(headline) > conditions.HeadlineMax { + headline = "New secret given for " + module + } return conditions.Observation{Scope: conditions.ScopeMachine, ID: key, Token: kindSecretGiven, Kind: kindSecretGiven, Machine: node, Severity: conditions.Urgent, Source: kindSecretGiven, Summary: fmt.Sprintf("%s of %s on %s was given %s at %s", name, module, node, how, at.Local().Format("2006-01-02 15:04")), - Headline: "Secret of " + module + " changed", - Explanation: fmt.Sprintf("The secret %s of %s on %s was given %s at %s. If you did not do this, "+ - "somebody else holds what %s acts with.", name, module, node, how, at.Local().Format("15:04"), module), - Needs: "silence this if you gave it; if you did not, give the secret again yourself and unlink what it serves.", - Resolved: "You saw that " + name + " of " + module + " was changed", + Headline: headline, + Explanation: fmt.Sprintf("The secret %s of %s was given %s. If you gave it, nothing else is needed. If you "+ + "did not, somebody else now holds what %s acts with.", secretNameWords(name), where, how, module), + Needs: "silence this if you just gave it; if you did not, give it again yourself so that only you hold it.", + Resolved: "You saw that " + module + " was given a new secret", Actions: []conditions.Action{conditions.SilenceAction(conditions.Key(conditions.ScopeMachine, key, kindSecretGiven))}} } +// secretNameWords is a secret's name as the operator reads it: "telegram-token" is "telegram token". +func secretNameWords(name string) string { + return strings.Join(strings.FieldsFunc(name, func(r rune) bool { return r == '-' || r == '_' || r == '.' }), " ") +} + // announceSecretGiven raises it on this controller's keeper. func announceSecretGiven(ctx context.Context, node, module, name, how string) error { return withKeeper(ctx, func(k *conditions.Keeper) error { diff --git a/cmd/mesh-controller/desk_secret_test.go b/cmd/mesh-controller/desk_secret_test.go index 7c87b3de..c37ee818 100644 --- a/cmd/mesh-controller/desk_secret_test.go +++ b/cmd/mesh-controller/desk_secret_test.go @@ -178,7 +178,7 @@ func TestAValueGivenAtTheDeskIsAnnounced(t *testing.T) { t.Fatalf("announced %v", said) } o := secretGivenObservation("anchor", "telegram", "telegram-token", "at the desk on laptop", time.Date(2026, 10, 9, 12, 3, 0, 0, time.UTC)) - if o.Severity != conditions.Urgent || !strings.Contains(o.Explanation, "telegram-token") || + if o.Severity != conditions.Urgent || !strings.Contains(o.Explanation, "telegram token") || len(o.Actions) == 0 || o.Key() == "" { t.Errorf("the announcement %+v", o) } @@ -187,6 +187,42 @@ func TestAValueGivenAtTheDeskIsAnnounced(t *testing.T) { } } +// **The secret-given condition says itself in its own plain words** (hq issue 359): the words it carries pass +// the plain rule, from the controller's terminal and from a desk, so the keeper keeps them — they once held a +// clock time, and the operator read "Novox needs a look … a problem it calls secret given" instead. Its +// severity and its answer stay: it is heard on every channel, and silenced by the operator. +func TestTheSecretGivenConditionSaysItselfInPlainWords(t *testing.T) { + at := time.Date(2026, 10, 9, 23, 32, 0, 0, time.Local) + for _, how := range []string{"at the controller's terminal", "at the desk on laptop"} { + o := secretGivenObservation("anchor", "telegram", "telegram-token", how, at) + w := conditions.Words{Headline: o.Headline, Explanation: o.Explanation, Resolved: o.Resolved, Needs: o.Needs, + Actions: o.Actions} + if why, ok := conditions.PlainWords(w, o.Machine); !ok { + t.Fatalf("given %s, the words are not plain: %s", how, why) + } + k, _ := withConditionsInMemory(t) + c, err := k.Observe(t.Context(), o) + if err != nil { + t.Fatal(err) + } + if c.Headline != "New secret given for telegram on anchor" || c.Severity != conditions.Urgent || + !strings.HasPrefix(c.Explanation, conditions.NeedsYou+" silence this") || + !strings.Contains(c.Explanation, "telegram token of telegram on anchor was given "+how) || + len(c.Actions) != 1 || c.Actions[0].Label != "Silence for a week" { + t.Errorf("given %s, the keeper said %q / %q (%s, %v)", how, c.Headline, c.Explanation, c.Severity, c.Actions) + } + if strings.Contains(c.Headline+c.Explanation+c.Resolved+c.Needs, typed) { + t.Error("the words carry the value") + } + } + // A long module name keeps the headline within its bound. + o := secretGivenObservation("anchor", "a-module-with-a-rather-long-name-indeed", "api-key", "at the controller's terminal", at) + if why, ok := conditions.PlainWords(conditions.Words{Headline: o.Headline, Explanation: o.Explanation, + Resolved: o.Resolved, Needs: o.Needs, Actions: o.Actions}, o.Machine); !ok { + t.Errorf("a long module name: %s", why) + } +} + // The bus lets the controller alone ask the desk's prompt (the review of 2026-10-09, M4): the runtime, which // carries every agent's calls, and a person granted every tool are denied it, however wide their grant. func TestOnlyTheControllerMayAskTheDesksPrompt(t *testing.T) { diff --git a/internal/conditions/plain.go b/internal/conditions/plain.go index 53bad38e..ed287d33 100644 --- a/internal/conditions/plain.go +++ b/internal/conditions/plain.go @@ -27,6 +27,7 @@ package conditions import ( "fmt" + "log" "regexp" "strings" "sync" @@ -135,9 +136,26 @@ func Worded(kind string) bool { // Unworded is told of every observation said in borrowed words: its kind has none registered, or the // words it was given break the plain rule. The keeper says it plainly anyway; a test suite sets this to -// fail the producer. +// fail the producer. Unset, as in the serving controller, it is a line in the log (unworded). var Unworded func(o Observation, why string) +// loggedUnworded holds each kind and why already logged: once per process, since a condition observed +// every minute would otherwise say the same line every minute. +var loggedUnworded sync.Map + +// unworded tells Unworded, or else logs once, that an observation is said in borrowed words — so words +// that fell back are never silent (hq issue 359: a secret given reached the operator as "needs a look", +// and nothing said why). +func unworded(o Observation, why string) { + if Unworded != nil { + Unworded(o, why) + return + } + if _, seen := loggedUnworded.LoadOrStore(o.Kind+"\x00"+why, true); !seen { + log.Printf("the condition kind %q is said in the scope's words, not its own: %s", o.Kind, why) + } +} + // The plain rule's shapes. var ( hexID = regexp.MustCompile(`\b[0-9a-f]{7,40}\b`) @@ -263,14 +281,10 @@ func plainly(o Observation) Observation { // An explanation too long is cut, never refused: what it says first is what matters. w.Explanation = cut(w.Explanation, ExplanationMax-len(Verdict(w.Needs, ""))-1) if from == "" { - if Unworded != nil { - Unworded(o, "the kind "+o.Kind+" has no plain words") - } + unworded(o, "the kind "+o.Kind+" has no plain words") w = scopeWords(o) } else if why, ok := PlainWords(w, machines...); !ok { - if Unworded != nil { - Unworded(o, from+" is not plain: "+why) - } + unworded(o, from+" is not plain: "+why) // The scope's words, but never a weaker verdict: what needed the operator still does, and its // answers are kept where they are themselves sound. needed, actions := w.Needs != "", soundActions(w.Actions) diff --git a/internal/conditions/plain_test.go b/internal/conditions/plain_test.go index f7312e5b..53b34bbc 100644 --- a/internal/conditions/plain_test.go +++ b/internal/conditions/plain_test.go @@ -1,7 +1,10 @@ package conditions import ( + "bytes" "encoding/json" + "log" + "os" "strings" "testing" ) @@ -245,3 +248,26 @@ func TestAChangeOfWordsIsSaid(t *testing.T) { t.Fatalf("%+v", said) } } + +// **Words that fall back are never silent** (hq issue 359): with no test listening, the keeper logs which +// kind is said in borrowed words and why — once, however often the condition is observed. +func TestBorrowedWordsAreLogged(t *testing.T) { + k, _, _, _ := keeper(t) + before := Unworded + Unworded = nil + t.Cleanup(func() { Unworded = before }) + var out bytes.Buffer + log.SetOutput(&out) + t.Cleanup(func() { log.SetOutput(os.Stderr) }) + for i := 0; i < 3; i++ { + if _, err := k.Observe(t.Context(), Observation{Scope: ScopeMachine, ID: "ace", Kind: "test-clock", Machine: "ace", + Severity: Warning, Source: "test", Summary: "s", Headline: "ace was given a secret", + Explanation: "It was given at 23:32.", Resolved: "Seen"}); err != nil { + t.Fatal(err) + } + } + if got := out.String(); strings.Count(got, "\n") != 1 || !strings.Contains(got, `"test-clock"`) || + !strings.Contains(got, "a clock time or date") { + t.Errorf("the log said %q", got) + } +}