A proposal reads whole on the phone, and Telegram shows every message formatted (hq issue 383)
mesh/delivery-group group fix/383-a-proposal-reads-whole-on-the-phone checking: 0 of 2 member(s) ready
mesh/merge-gate pass: builds claude-code, messenger, telegram → ace, g14, novox, shanks; no bus step; every machine composes with the change as it did with…
mesh/repo-check pass: its merge-check.sh passed, each part in its toolchain: merge-check.sh (go), merge-check-typescript.sh (typescript)
mesh/delivery superseded: a newer head of the same pull request

The operator could not read a settings proposal on the phone: its values were masked ("+ shares:
media=‹path›"), a how-to paragraph followed the fingerprint, and nothing was formatted.

messenger: an ask may carry its explanation whole (asks.Ask.Whole); the router shows it in
place of the explanation only on a kind the controller issued as verified-sender and that was
given the ask's answers, and holds it to the secret rule alone (CheckSecret): a path or an
address is what the operator approves, a secret's shape leaves the mesh nowhere and refuses the
ask as a whole. Everywhere else, and for every condition, the explanation under the content rule
stands. An ask's own message offers Details when the ask says what Details shows
(asks.Ask.Details): the fingerprint and where to read the proposal whole. The expiry line reads
"Expires <when>: <what then>".

telegram: every message is sent in HTML, formatted and never reworded: the title bold, a key
line or a fingerprint in monospace, the chosen line bold, and every '<', '>' and '&' escaped
first. What a tap is compared by stays the plain words, which is what Telegram gives back.

claude-code: an instruction proposal's ask has the same shape — what is asked, the text, who
proposed it and when; the fingerprint and the how-to are the Details answer's; where the sender
is proven, a line with a path or an address is shown, one shaped like a secret is not.

Takes mesh-sdk go at the commit that adds Whole and Details to an ask.
This commit is contained in:
jochen
2026-10-10 15:38:00 +02:00
parent 64feabc6c3
commit 96ba664cb4
20 changed files with 512 additions and 91 deletions
+18 -6
View File
@@ -78,15 +78,16 @@ func needsNothing(r *Record) bool {
}
// choices are the answers a record's next message offers on one kind: the open ask's options offered
// there, and Details for a condition, with a ticket of its own. The tickets of the record's earlier messages
// stand (issue 371): each message keeps a Details of its own, used once, until it is TokenFor old or
// MostDetails newer ones on that kind push it out.
// there, and Details — for a condition, and for an ask's own message while the ask says what Details shows
// (issue 383) — with a ticket of its own. The tickets of the record's earlier messages stand (issue 371):
// each message keeps a Details of its own, used once, until it is TokenFor old or MostDetails newer ones on
// that kind push it out.
func (h *Holder) choices(r *Record, kind string) []Choice {
var out []Choice
if rec := h.openAsk(r); rec != nil {
out = h.offers(rec, kind)
}
if r.Origin != "condition" || len(out) >= MostActions {
if (r.Origin != "condition" && h.detailsOf(r) == "") || len(out) >= MostActions {
return out
}
now := h.Now()
@@ -194,9 +195,20 @@ func (h *Holder) Chosen(kind string, c asks.Choice) (string, error) {
h.mu.Unlock()
headline, _ := wordsOf(r)
// Each line held to the content rule on its own: a line that carries what may not leave is said
// withheld, and the rest is still shown.
// withheld, and the rest is still shown. A condition's details are its summary, key and start; an ask's
// what its asker gave for Details (a fingerprint, where to read it whole), after a condition's own when the
// ask is shown on one (issue 383).
var said []string
if r.Origin == "condition" {
said = []string{r.Summary, "Condition: " + r.Key, "Since: " + r.Raised.In(h.zone()).Format("2 Jan 15:04")}
} else {
said = []string{"Asked: " + r.Raised.In(h.zone()).Format("2 Jan 15:04")}
}
if details := h.detailsOf(r); details != "" {
said = append(said, strings.Split(strings.TrimSpace(details), "\n")...)
}
var lines []string
for _, l := range []string{r.Summary, "Condition: " + r.Key, "Since: " + r.Raised.In(h.zone()).Format("2 Jan 15:04")} {
for _, l := range said {
if refusal, ok := Check(l); !ok {
l = "(a line carrying " + refusal.What + " is withheld; the controller's conditions hold it)"
}
+42
View File
@@ -24,6 +24,7 @@ import (
"encoding/json"
"errors"
"fmt"
"slices"
"sort"
"strings"
"time"
@@ -163,6 +164,40 @@ 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.
func (h *Holder) wholeOf(r *Record, kind string) string {
if r == nil || 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") {
return ""
}
for _, tk := range e.rec.Tickets {
if tk.Kind == kind {
return e.rec.Ask.Whole
}
}
return ""
}
// detailsOf is what the Details answer shows of a record's ask (asks.Ask.Details), while the ask is open.
func (h *Holder) detailsOf(r *Record) string {
if rec := h.openAsk(r); rec != nil {
return rec.Ask.Details
}
return ""
}
// carriers are the kinds that can carry an answer of a level now: ready, offering a choice, and for
// approve verifying their sender with an account of the operator's linked, where the controller says now
// that no agent can become root (rootFree, asked live).
@@ -439,6 +474,13 @@ func (h *Holder) TakeAsk(verb, asker string, body json.RawMessage) error {
h.refuseAsk(asker, a, "its words carry "+refusal.String()+", which may not leave the mesh: move it into a reference or say it in words")
return nil
}
// The whole words are shown only where the sender is proven (wholeOf), so a path or an address may stand
// in them; a secret's shape leaves the mesh nowhere, and an ask carrying one is refused as a whole, never
// shown with its explanation alone (novox/hq issue 383).
if refusal, ok := CheckSecret(a.Whole); !ok {
h.refuseAsk(asker, a, "its whole words carry "+refusal.String()+", which may not leave the mesh on any channel: withhold it")
return nil
}
open, replaced := 0, ""
h.mu.Lock()
for k, e := range h.asks {
+42 -13
View File
@@ -65,6 +65,43 @@ func Check(text string) (Refusal, bool) {
return Refusal{"address", "a hardware address"}, false
case reIPv6.MatchString(text):
return Refusal{"address", "an IPv6 address"}, false
}
if refusal, ok := secretShape(text); !ok {
return refusal, false
}
if reWinPath.MatchString(text) {
return Refusal{"path", "a drive path"}, false
}
if refusal, ok := randomRun(text); !ok {
return refusal, false
}
for _, word := range strings.FieldsFunc(text, func(r rune) bool {
return unicode.IsSpace(r) || strings.ContainsRune("\"'`()[]{}<>,;|", r)
}) {
w := strings.TrimRight(word, ".:!?")
if isPath(w) {
return Refusal{"path", "a file path"}, false
}
if isHostName(w) {
return Refusal{"address", "a host name"}, false
}
}
return Refusal{}, true
}
// 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.
func CheckSecret(text string) (Refusal, bool) {
if refusal, ok := secretShape(text); !ok {
return refusal, false
}
return randomRun(text)
}
// secretShape is the secret shapes a pattern names.
func secretShape(text string) (Refusal, bool) {
switch {
case rePEM.MatchString(text):
return Refusal{"secret", "a key block"}, false
case reJWT.MatchString(text):
@@ -77,25 +114,17 @@ func Check(text string) (Refusal, bool) {
return Refusal{"secret", "a value given to a secret's name"}, false
case reHex.MatchString(text):
return Refusal{"secret", "a long hexadecimal string"}, false
case reWinPath.MatchString(text):
return Refusal{"path", "a drive path"}, false
}
return Refusal{}, true
}
// randomRun is a long unbroken run of characters spread as a random string's are.
func randomRun(text string) (Refusal, bool) {
for _, run := range reRun.FindAllString(text, -1) {
if looksRandom(run) {
return Refusal{"secret", "a long random-looking string"}, false
}
}
for _, word := range strings.FieldsFunc(text, func(r rune) bool {
return unicode.IsSpace(r) || strings.ContainsRune("\"'`()[]{}<>,;|", r)
}) {
w := strings.TrimRight(word, ".:!?")
if isPath(w) {
return Refusal{"path", "a file path"}, false
}
if isHostName(w) {
return Refusal{"address", "a host name"}, false
}
}
return Refusal{}, true
}
+31 -4
View File
@@ -786,8 +786,18 @@ func (h *Holder) sayOn(r *Record, what, kind string) Message {
if what == "cleared" || what == "silenced" || m.Title == "A mesh message was withheld" {
return m
}
// On a kind that proves who answers, an ask's explanation is shown with its values whole (issue 383): the
// path or the address the operator approves is readable there; a secret's shape still is not.
whole := h.wholeOf(r, kind)
if whole != "" {
if wm, ok := h.wholeWords(r, what, whole); ok {
m = wm
} else {
whole = ""
}
}
if r.Answered != "" {
m = h.answeredWords(r, m)
m = h.answeredWords(r, m, whole)
}
if kind == "" {
return m
@@ -827,13 +837,17 @@ func (h *Holder) sayOn(r *Record, what, kind string) Message {
// itself, so its title becomes how it ended — "You chose Approve at 13:30 on Telegram · <headline>" — read at
// a glance on the phone, where the edit rings nothing (issue 372); the rest of what came of it, the
// explanation and when it was asked follow. A condition's message keeps its own title, and says it first.
func (h *Holder) answeredWords(r *Record, m Message) Message {
func (h *Holder) answeredWords(r *Record, m Message, whole string) Message {
if r.Origin == "ask" {
first, rest, _ := strings.Cut(r.Answered, "\n")
headline, explanation := wordsOf(r)
passes := Check
if whole != "" {
explanation, passes = whole, CheckSecret
}
out := Message{Title: strings.TrimSuffix(first, ".") + " · " + headline, Urgent: m.Urgent, Quiet: m.Quiet,
Body: strings.TrimSpace(rest + "\n" + explanation + "\nAsked " + when(r.Raised, h.Now(), h.zone()) + ".")}
if _, ok := Check(out.Text()); ok {
if _, ok := passes(out.Text()); ok {
return out
}
// Its words refused (they are the asker's): the outcome is still said, before what may leave.
@@ -842,6 +856,19 @@ func (h *Holder) answeredWords(r *Record, m Message) Message {
return m
}
// wholeWords is a record's message with the ask's whole words in place of its explanation, when they carry no
// secret's shape once composed; else false, and the message under the whole content rule stands.
func (h *Holder) wholeWords(r *Record, what, whole string) (Message, bool) {
c := *r
c.Explanation = whole
m := compose(&c, what, h.Now(), h.zone(), 0)
if refusal, ok := CheckSecret(m.Text()); !ok {
h.Logf("[messenger] the whole words of %s carry %s; shown as on a channel that proves nothing", r.Key, refusal)
return Message{}, false
}
return m, true
}
// askLines are what an ask's message says of the ask itself: who asks, each option's effect, and its expiry.
func (h *Holder) askLines(rec *AskRecord) []string {
asker := rec.Asker
@@ -855,7 +882,7 @@ func (h *Holder) askLines(rec *AskRecord) []string {
}
}
if !rec.Ask.Expires.IsZero() && rec.Ask.OnExpiry != "" {
lines = append(lines, "If nobody answers by "+by(rec.Ask.Expires, h.Now(), h.zone())+": "+rec.Ask.OnExpiry+".")
lines = append(lines, "Expires "+by(rec.Ask.Expires, h.Now(), h.zone())+": "+rec.Ask.OnExpiry+".")
}
return lines
}
@@ -159,7 +159,7 @@ func TestAnAsksExpiryIsSaidWithItsDay(t *testing.T) {
rec.Ask.Expires = now.Add(7 * 24 * time.Hour)
rec.Ask.OnExpiry = "nothing is done"
lines := strings.Join(h.askLines(rec), "\n")
if !strings.Contains(lines, "If nobody answers by Fri 16 Oct 21:33: nothing is done.") {
if !strings.Contains(lines, "Expires Fri 16 Oct 21:33: nothing is done.") {
t.Fatalf("%s", lines)
}
}
@@ -211,7 +211,7 @@ func TestAnAsksMessageSaysWhoAsksWhatEachAnswerDoesAndWhatExpiryDoes(t *testing.
body := tg.sends[0].Body
for _, want := range []string{"Asked by mesh-delivery.", "Release: the delivery goes on.", "Stop: the delivery ends.",
"Silence for a week: nothing more is said of it for a week.",
"If nobody answers by " + c.now().Add(2*time.Hour-BurstWindow).In(time.UTC).Format("15:04") + ": the delivery stays held."} {
"Expires " + c.now().Add(2*time.Hour-BurstWindow).In(time.UTC).Format("15:04") + ": the delivery stays held."} {
if !strings.Contains(body, want) {
t.Errorf("the message does not say %q:\n%s", want, body)
}
@@ -0,0 +1,134 @@
package main
import (
"strings"
"testing"
"time"
"git.novox.be/novox/mesh-sdk/go/asks"
)
// novox/hq issue 383: a settings proposal's values are what the operator approves, so where the sender is proven
// they are shown whole; where nothing is proven, the explanation under the whole content rule stands.
// proposalAsk is a proposal as the controller composes it: the explanation with its values masked, the whole
// with them whole, and Details with the fingerprint and where to read it.
func proposalAsk(c *clock, id string) asks.Ask {
a := heldAsk(c, id)
a.Headline = "Set mounts on shanks?"
a.Options = a.Options[:2]
a.Explanation = "+ shares: media=‹path›\n+ sources: recalbox=‹address›@‹path›\nProposed by an agent at 15:11."
a.Whole = "+ shares: media=/storage/media\n+ sources: recalbox=smb://nas.lan/recalbox@/mnt/recalbox\nProposed by an agent at 15:11."
a.Details = "Fingerprint 1234 5678 9abc def0 1234 5678.\nRead it whole: settings proposals " + id + " at the controller's terminal."
return a
}
func TestAnAsksWholeWordsAreShownWhereTheSenderIsProvenAndMaskedElsewhere(t *testing.T) {
h, tg, _, c, store, w := newRouter(t)
link(h, store, c, "42", c.now().Add(-2*time.Hour))
submit(t, h, "mesh-controller", proposalAsk(c, "s-1"))
settle(h, c)
if len(tg.sends) != 1 {
t.Fatalf("telegram got %d messages", len(tg.sends))
}
phone := tg.sends[0]
for _, want := range []string{"+ shares: media=/storage/media", "recalbox=smb://nas.lan/recalbox@/mnt/recalbox", "Proposed by an agent at 15:11."} {
if !strings.Contains(phone.Body, want) {
t.Errorf("the phone, which proves who answers, lacks %q:\n%s", want, phone.Body)
}
}
if strings.Contains(phone.Body, "‹") || strings.Contains(phone.Text(), "Fingerprint") || strings.Contains(phone.Text(), "settings proposals") {
t.Errorf("the phone shows a masked value, or the Details' words, in the message:\n%s", phone.Body)
}
if phone.Title != "Set mounts on shanks?" || ticketFor(phone, "Release") == "" || ticketFor(phone, DetailsLabel) == "" {
t.Errorf("the phone's message: %q offering %s", phone.Title, labelsOf(phone))
}
// The desk proves nothing: the same record composed for it shows the explanation, its values masked.
r := h.open["ask.mesh-controller.s-1"]
if r == nil {
t.Fatal("no record of the ask")
}
desk := h.sayOn(r, "raised", "desktop")
if !strings.Contains(desk.Body, "+ shares: media=‹path›") || strings.Contains(desk.Body, "/storage/media") || strings.Contains(desk.Body, "nas.lan") {
t.Errorf("the desk shows a value whole:\n%s", desk.Body)
}
// Details shows the fingerprint and where to read it whole, and answers nothing.
if out, err := h.Chosen("telegram", choiceOf(detailsOf(t, phone), "42", c.now())); err != nil || out != "shown" {
t.Fatalf("Details: %q %v", out, err)
}
if len(tg.sends) != 2 {
t.Fatalf("telegram got %d messages after Details", len(tg.sends))
}
details := tg.sends[1]
if details.Title != "Details: Set mounts on shanks?" || !strings.Contains(details.Body, "Fingerprint 1234 5678 9abc def0 1234 5678.") ||
!strings.Contains(details.Body, "Read it whole: settings proposals s-1 at the controller's terminal.") || !strings.Contains(details.Body, "Asked: ") {
t.Errorf("Details: %q\n%s", details.Title, details.Body)
}
if got := w.of("mesh-controller"); len(got) != 0 || store.asks["mesh-controller.s-1"].State != StateOpen {
t.Errorf("Details answered the ask: %+v / %s", got, store.asks["mesh-controller.s-1"].State)
}
// Approved, the message is edited to say so, and still shows what was approved, whole.
if out, err := h.Chosen("telegram", choiceOf(ticketFor(phone, "Release"), "42", c.now())); err != nil || out != "chosen" {
t.Fatalf("Release: %q %v", out, err)
}
edited, ok := tg.edits["1"]
if !ok {
t.Fatalf("the phone's message was not edited: %+v", tg.edits)
}
if !strings.HasPrefix(edited.Title, "You chose Release at ") || !strings.Contains(edited.Body, "media=/storage/media") ||
strings.Contains(edited.Body, "‹") || len(edited.Actions) != 0 {
t.Errorf("the edited message: %q\n%s\nactions %+v", edited.Title, edited.Body, edited.Actions)
}
}
// The mutation on the switch: the same ask without whole words is shown on the phone as on the desk, from its
// explanation; the whole words alone make the difference, and only where the sender is proven.
func TestAnAskWithoutWholeWordsIsShownFromItsExplanationEverywhere(t *testing.T) {
h, tg, _, c, store, _ := newRouter(t)
link(h, store, c, "42", c.now().Add(-2*time.Hour))
a := proposalAsk(c, "s-2")
a.Whole, a.Details = "", ""
submit(t, h, "mesh-controller", a)
settle(h, c)
if len(tg.sends) != 1 || !strings.Contains(tg.sends[0].Body, "+ shares: media=‹path›") || strings.Contains(tg.sends[0].Body, "/storage/media") {
t.Fatalf("telegram: %+v", tg.sends)
}
if ticketFor(tg.sends[0], DetailsLabel) != "" {
t.Errorf("an ask that says nothing for Details offers it: %s", labelsOf(tg.sends[0]))
}
// A kind that proves nothing never shows the whole words, whatever the ask says.
a = proposalAsk(c, "s-3")
submit(t, h, "mesh-controller", a)
settle(h, c)
if whole := h.wholeOf(h.open["ask.mesh-controller.s-3"], "desktop"); whole != "" {
t.Errorf("the desk is given the whole words: %q", whole)
}
if whole := h.wholeOf(h.open["ask.mesh-controller.s-3"], "telegram"); whole != a.Whole {
t.Errorf("the phone is not given the whole words: %q", whole)
}
}
// 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) {
for _, secret := range []string{
"+ token: ghp_abcdefghijklmnopqrstuvwxyz0123456789",
"+ password=hunter2",
"+ key: 0123456789abcdef0123456789abcdef",
"-----BEGIN PRIVATE KEY-----",
} {
h, tg, _, c, store, w := newRouter(t)
link(h, store, c, "42", c.now().Add(-2*time.Hour))
a := proposalAsk(c, "s-4")
a.Whole = "+ shares: media=/storage/media\n" + secret
submit(t, h, "mesh-controller", a)
settle(h, c)
got := w.of("mesh-controller")
if len(tg.sends) != 0 || len(got) != 1 || got[0].Outcome != asks.OutcomeRefused || !strings.Contains(got[0].Words, "whole words carry secret") {
t.Errorf("%q: sent %d, told %+v", secret, len(tg.sends), got)
}
if _, kept := store.asks["mesh-controller.s-4"]; kept {
t.Errorf("%q: the refused ask was recorded", secret)
}
}
}