From 545e4319f98f6e00b3f8408c2686571848c1ae6f Mon Sep 17 00:00:00 2001 From: jochen Date: Sat, 10 Oct 2026 15:46:54 +0200 Subject: [PATCH] Review: a path is read as a path in the whole words, which an ask's own message alone shows (issue 383) The review of 2026-10-10: a mixed-case path with a digit ("/mnt/Photos_2024/Jochen") read as a random string and was withheld from the whole words, the symptom again; the whole was judged on the kind's issued capabilities instead of the controller's fresh word (Promises); a condition's record could have taken an ask's whole words in place of its own; and a short instruction text with a path line was said to be cut where it was shown whole. --- .../claude-code/cmd/claude-code/proposals.go | 10 ++++--- .../cmd/claude-code/proposals_test.go | 11 ++++++++ modules/messenger/cmd/messenger/asks.go | 20 +++++++------- modules/messenger/cmd/messenger/content.go | 20 ++++++++++++-- modules/messenger/cmd/messenger/whole_test.go | 27 +++++++++++++++++++ 5 files changed, 72 insertions(+), 16 deletions(-) diff --git a/modules/claude-code/cmd/claude-code/proposals.go b/modules/claude-code/cmd/claude-code/proposals.go index db5fc7e7..28cf61ee 100644 --- a/modules/claude-code/cmd/claude-code/proposals.go +++ b/modules/claude-code/cmd/claude-code/proposals.go @@ -172,7 +172,7 @@ func (p Proposal) Act(option string) asks.Act { func (p Proposal) Ask() asks.Ask { verb := map[string]string{ActionAdd: "Add", ActionReplace: "Replace", ActionRemove: "Remove"}[p.Action] shown, whole := excerpt(p.Text, shownLine, "a path, an address or a long word") - wholeShown, _ := excerpt(p.Text, wholeLine, "a long word or a secret's shape") + wholeShown, wholeIsAll := excerpt(p.Text, wholeLine, "a long word or a secret's shape") part := "" if p.Action != ActionRemove && !whole { part = " Part shown" @@ -181,7 +181,7 @@ func (p Proposal) Ask() asks.Ask { if len([]rune(headline)) > asks.HeadlineLength { headline = verb + " Claude instructions?" + part } - compose := func(text string) string { + compose := func(text string, whole bool) string { var b strings.Builder switch p.Action { case ActionAdd: @@ -214,12 +214,14 @@ func (p Proposal) Ask() asks.Ask { if p.Action == ActionRemove { does = "the instructions are removed from those sessions" } - a := asks.Ask{ID: p.ID, Headline: headline, Explanation: compose(shown), Who: asks.Operator, + a := asks.Ask{ID: p.ID, Headline: headline, Explanation: compose(shown, whole), Who: asks.Operator, Expires: p.At.Add(ProposalLasts), OnExpiry: "the proposal is discarded; nothing changes", About: "instructions." + p.Scope + "." + strings.Join(p.Nodes, "+") + "." + p.Name, Details: d.String()} if p.Action != ActionRemove && wholeShown != shown { - a.Whole = compose(wholeShown) + // Cut only when it is: a short text with a path line is whole here (the review of 2026-10-10). The + // headline's "Part shown" is one for every channel and follows the explanation. + a.Whole = compose(wholeShown, wholeIsAll) } if len(p.Nodes) == 0 { a.About = "instructions." + p.Scope + "." + p.Name diff --git a/modules/claude-code/cmd/claude-code/proposals_test.go b/modules/claude-code/cmd/claude-code/proposals_test.go index 19362107..b6746fb0 100644 --- a/modules/claude-code/cmd/claude-code/proposals_test.go +++ b/modules/claude-code/cmd/claude-code/proposals_test.go @@ -442,6 +442,17 @@ func TestALongTextIsShownInPartAndReadWholeAtTheTerminal(t *testing.T) { strings.Contains(a.Whole, "not shown here") { t.Errorf("the whole words: %q", a.Whole) } + // A short text with one path line: shown whole where the sender is proven, and said to be whole there. + short := "See /etc/claude-code for the managed files.\nA plain line of the hand-over.\n" + out, err = Propose(p, ProposeInput{Scope: ScopeMesh, Name: "short", Text: short}, store, r, t0.Add(time.Minute)) + if err != nil { + t.Fatal(err) + } + sa := r.published[out["proposed"].(string)] + if !strings.Contains(sa.Explanation, "NOT SHOWN IN FULL") || !strings.Contains(sa.Explanation, "[1 line(s) not shown here") || + strings.Contains(sa.Whole, "NOT SHOWN IN FULL") || !strings.Contains(sa.Whole, "?\n\n"+strings.TrimRight(short, "\n")+"\n\nProposed by") { + t.Errorf("the short text:\n%s\n--\n%s", sa.Explanation, sa.Whole) + } // The whole words keep back what might be a secret's shape (Propose refuses such a text before it is asked; // this holds the ask's own composition to it), and nothing else. for line, may := range map[string]bool{ diff --git a/modules/messenger/cmd/messenger/asks.go b/modules/messenger/cmd/messenger/asks.go index 6ca823f4..fd6dcd8b 100644 --- a/modules/messenger/cmd/messenger/asks.go +++ b/modules/messenger/cmd/messenger/asks.go @@ -24,7 +24,6 @@ import ( "encoding/json" "errors" "fmt" - "slices" "sort" "strings" "time" @@ -164,22 +163,23 @@ func (h *Holder) openAsk(r *Record) *AskRecord { return &rec } -// wholeOf is an ask's explanation with its values whole (asks.Ask.Whole), where a record's message on one -// kind shows it in place of the explanation (novox/hq issue 383): on a kind that proves who answers, as the -// controller issued it, and that the router gave the ask's answers (a ticket there, so the kind was linked and -// root-free when the ask was taken). Empty where the kind proves nothing, the ask has no whole words, or the -// record has no ask — then the explanation, held to the whole content rule, is shown. The ask's state does -// not matter: a message edited to say how the ask ended still shows what was approved. +// wholeOf is an ask's explanation with its values whole (asks.Ask.Whole), where an ask's own message on one +// kind shows it in place of the explanation (novox/hq issue 383): on a kind that proves who answers by the +// controller's fresh word (Promises, as every other verified-sender judgement here), and that the router gave +// the ask's answers (a ticket there, so the kind was linked and root-free when the ask was taken). Empty where +// the kind proves nothing, the ask has no whole words, or the record is a condition's — a condition's words +// stay under the whole content rule, whatever an ask about it carries — then the explanation, held to that +// rule, is shown. The ask's state does not matter: a message edited to say how the ask ended still shows what +// was approved. func (h *Holder) wholeOf(r *Record, kind string) string { - if r == nil || r.Ask == "" || kind == "" || h.Bench == nil { + if r == nil || r.Origin != "ask" || r.Ask == "" || kind == "" || h.Bench == nil { return "" } e := h.askOf(r.Ask) if e == nil || e.rec.Ask.Whole == "" { return "" } - info, held := h.Bench.Kind(kind) - if !held || !slices.Contains(info.Capabilities, "verified-sender") { + if !h.Bench.Promises(kind, "verified-sender") { return "" } for _, tk := range e.rec.Tickets { diff --git a/modules/messenger/cmd/messenger/content.go b/modules/messenger/cmd/messenger/content.go index 5829929d..2c0b5552 100644 --- a/modules/messenger/cmd/messenger/content.go +++ b/modules/messenger/cmd/messenger/content.go @@ -91,12 +91,28 @@ func Check(text string) (Refusal, bool) { // CheckSecret says whether a text carries a secret's shape — the one class that leaves the mesh nowhere. It is // what an ask's whole words (asks.Ask.Whole, novox/hq issue 383) are held to: on a kind that proves who answers, -// a path or an address is what the operator approves and may be shown, a secret never. +// a path or an address is what the operator approves and may be shown, a secret never. A path is read as one: +// a run with a slash in it is judged piece by piece between the slashes, so "/mnt/Photos_2024/Jochen" is a +// path and not a random string (the review of 2026-10-10); the named shapes — a key block, a signed token, a +// bot token, a known token shape, a value given to a secret's name, a long hexadecimal string — hold whatever +// the run holds. func CheckSecret(text string) (Refusal, bool) { if refusal, ok := secretShape(text); !ok { return refusal, false } - return randomRun(text) + return randomRunOutsidePaths(text) +} + +// randomRunOutsidePaths is randomRun with each run that holds a slash judged by its pieces between the slashes. +func randomRunOutsidePaths(text string) (Refusal, bool) { + for _, run := range reRun.FindAllString(text, -1) { + for _, piece := range strings.Split(run, "/") { + if len(piece) >= 20 && looksRandom(piece) { + return Refusal{"secret", "a long random-looking string"}, false + } + } + } + return Refusal{}, true } // secretShape is the secret shapes a pattern names. diff --git a/modules/messenger/cmd/messenger/whole_test.go b/modules/messenger/cmd/messenger/whole_test.go index 52cb9e8b..e82768ae 100644 --- a/modules/messenger/cmd/messenger/whole_test.go +++ b/modules/messenger/cmd/messenger/whole_test.go @@ -111,6 +111,33 @@ func TestAnAskWithoutWholeWordsIsShownFromItsExplanationEverywhere(t *testing.T) // A secret's shape leaves the mesh nowhere: an ask whose whole words carry one is refused as a whole, so the // phone never sees it, with or without the proven sender. func TestAnAskWhoseWholeWordsCarryASecretIsRefused(t *testing.T) { + // A path is read as a path, whatever its case and digits; a random run outside one is still a secret. + for text, may := range map[string]bool{ + "+ shares: photos=/mnt/Photos_2024/Jochen": true, + "+ sources: games=smb://nas.lan/Recalbox_Games2024": true, + "+ library: /srv/media/Series_Archive/Season01": true, + "+ key: xK9mQ2vL8pR4tW7yB3nF6hJ1dG5sA0zC": false, + } { + if _, ok := CheckSecret(text); ok != may { + t.Errorf("CheckSecret(%q) lets it leave: %v", text, ok) + } + } + // A condition's record never shows an ask's whole words, whichever kind: its words are its own. + { + h, _, _, c, store, _ := newRouter(t) + link(h, store, c, "42", c.now().Add(-2*time.Hour)) + k := cond("delivery.hq.held", Warning, "the delivery of hq is held") + k.Raised = c.now() + h.Condition(EventRaised, k) + settle(h, c) + a := proposalAsk(c, "s-5") + a.About = "delivery.hq.held" + submit(t, h, "mesh-controller", a) + settle(h, c) + if r := h.open["delivery.hq.held"]; r == nil || r.Ask != "mesh-controller.s-5" || h.wholeOf(r, "telegram") != "" { + t.Errorf("a condition's record is given an ask's whole words") + } + } for _, secret := range []string{ "+ token: ghp_abcdefghijklmnopqrstuvwxyz0123456789", "+ password=hunter2",