Merge pull request 'Say a secret given in plain words, and log words the keeper refuses (hq issue 359)' (#189) from fix/the-secret-given-words-pass-plain into main

This commit was merged in pull request #189.
This commit is contained in:
2026-10-09 22:00:15 +00:00
4 changed files with 128 additions and 13 deletions
+26 -5
View File
@@ -221,20 +221,41 @@ 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
// Within the bounds whatever the names' length: the module and machine, else the module, else the machine.
headline := "New secret given for " + where
for _, h := range []string{"New secret given for " + module, "New secret given on " + node} {
if len(headline) > conditions.HeadlineMax {
headline = h
}
}
resolved := "You saw that " + module + " was given a new secret"
if len(resolved) > conditions.HeadlineMax+20 {
resolved = "You saw that a new secret was given on " + node
}
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: resolved,
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 {
+40 -1
View File
@@ -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,45 @@ 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 and the resolved line within their bounds.
for _, module := range []string{"a-module-with-a-rather-long-name-indeed",
"a-module-with-a-name-so-long-that-no-headline-could-ever-hold-it"} {
o := secretGivenObservation("anchor", module, "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("the module %s: %s", module, 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) {
+27 -7
View File
@@ -27,6 +27,7 @@ package conditions
import (
"fmt"
"log"
"regexp"
"strings"
"sync"
@@ -135,9 +136,32 @@ 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
}
// The reason without what it quotes of the words: a quoted fragment may be a hash-shaped secret, and a
// fragment that changes (a clock time) would log a new line every time.
reason := quotedFragment.ReplaceAllString(why, "")
if _, seen := loggedUnworded.LoadOrStore(o.Kind+"\x00"+reason, true); !seen {
log.Printf("the condition kind %q is said in the scope's words, not its own: %s", o.Kind, reason)
}
}
// quotedFragment is what a reason of the plain rule quotes of the words it refused, at its end.
var quotedFragment = regexp.MustCompile(` \([^()]*\)$`)
// The plain rule's shapes.
var (
hexID = regexp.MustCompile(`\b[0-9a-f]{7,40}\b`)
@@ -263,14 +287,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)
+35
View File
@@ -1,7 +1,9 @@
package conditions
import (
"bytes"
"encoding/json"
"log"
"strings"
"testing"
)
@@ -245,3 +247,36 @@ 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 })
loggedUnworded.Clear()
t.Cleanup(loggedUnworded.Clear)
var out bytes.Buffer
writer := log.Writer()
log.SetOutput(&out)
t.Cleanup(func() { log.SetOutput(writer) })
// A time that changes each observation, and a hash-shaped word: one line, quoting neither.
for _, at := range []string{"23:32", "23:33", "23:34"} {
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 " + at + ".", Resolved: "Seen"}); err != nil {
t.Fatal(err)
}
}
if _, err := k.Observe(t.Context(), Observation{Scope: ScopeMachine, ID: "ace", Kind: "test-hash", Machine: "ace",
Severity: Warning, Source: "test", Summary: "s", Headline: "ace was given 0123abcd4567ef89",
Explanation: "It was given.", Resolved: "Seen"}); err != nil {
t.Fatal(err)
}
if got := out.String(); strings.Count(got, "\n") != 2 || !strings.Contains(got, `"test-clock"`) ||
!strings.Contains(got, "a clock time or date") || strings.Contains(got, "23:3") ||
!strings.Contains(got, `"test-hash"`) || strings.Contains(got, "0123abcd4567ef89") {
t.Errorf("the log said %q", got)
}
}