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).
This commit is contained in:
@@ -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 {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user