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
+4 -2
View File
@@ -148,8 +148,10 @@ said in the render's answer. What was registered before is still rendered and ma
hand-over, the context a later session needs — or with `remove` for a section approved before. Nothing becomes
active: the proposal is kept in the module's `proposals` state and the operator is asked through the operator
channel (`operator-channel`, hq ADR 0259) with Approve and Decline, both at the level approve, so only a channel that
proves who answered (Telegram) carries either. The phone shows the whole text when it is short and every line may
leave the mesh, else its first lines and a fingerprint; the whole text is read at the controller's terminal with
proves who answered (Telegram) carries either. The phone shows what is asked, the text and who proposed it when
(hq issue 383): the whole text when it is short, else its first lines; a line that might not leave the mesh is kept
back where the sender is not proven, and only one shaped like a secret where it is. The Details answer shows the
fingerprint and where to read all of it, at the controller's terminal with
`mesh-cli ask claude-code claude_code_proposals '{"id": "<id>"}'`. Decline, or no answer within a day, discards it.
**Every node proves the approval itself**: it reads the router's own record of the ask under this module's name,
judges the record as this module would have made it, composes the ask again from the proposal and holds the warrant to it (the same ask digest, the option's bound act,
@@ -165,10 +165,14 @@ func (p Proposal) Act(option string) asks.Act {
}
// Ask is the ask for a proposal, composed from it alone: every node composes the same one, and holds the
// router's warrant to it.
// router's warrant to it. Its shape is a settings proposal's (novox/hq issue 383): what is asked, the text, who
// proposed it and when; the fingerprint and where to read all of it are the Details answer's. The explanation
// keeps back every line that might not leave the mesh; the whole (asks.Ask.Whole), shown only where the sender
// is proven, keeps back only a line shaped like a secret or a long unbroken word.
func (p Proposal) Ask() asks.Ask {
verb := map[string]string{ActionAdd: "Add", ActionReplace: "Replace", ActionRemove: "Remove"}[p.Action]
shown, whole := excerpt(p.Text)
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")
part := ""
if p.Action != ActionRemove && !whole {
part = " Part shown"
@@ -177,37 +181,46 @@ func (p Proposal) Ask() asks.Ask {
if len([]rune(headline)) > asks.HeadlineLength {
headline = verb + " Claude instructions?" + part
}
var b strings.Builder
switch p.Action {
case ActionAdd:
fmt.Fprintf(&b, "Add instructions '%s' to %s?\n\n", p.Name, p.where())
case ActionReplace:
fmt.Fprintf(&b, "Replace the instructions '%s' in %s?\n\n", p.Name, p.where())
case ActionRemove:
fmt.Fprintf(&b, "Remove the instructions '%s' from %s?\n\n", p.Name, p.where())
}
fmt.Fprintf(&b, "An agent proposed this through the claude-code module on %s, at %s UTC.", p.From,
p.At.UTC().Format("15:04 on 2 Jan"))
if p.Action == ActionRemove {
fmt.Fprintf(&b, " The approved text it removes has the fingerprint %s.", p.Fingerprint())
} else {
fmt.Fprintf(&b, " The text has %d lines, %d bytes, fingerprint %s. Once approved, every new session there "+
"reads it as the mesh's own word until it is removed the same way.\n\n", p.Lines(), len(p.Text), p.Fingerprint())
if whole {
b.WriteString("The whole text:\n\n" + shown)
} else {
fmt.Fprintf(&b, "NOT SHOWN IN FULL here. Its first lines:\n\n%s\n\nRead all of it before you answer, at the controller's terminal: "+
"mesh-cli ask claude-code claude_code_proposals '{\"id\": \"%s\"}'. The fingerprint there must be %s.",
shown, p.ID, p.Fingerprint())
compose := func(text string) string {
var b strings.Builder
switch p.Action {
case ActionAdd:
fmt.Fprintf(&b, "Add instructions '%s' to %s?\n\n", p.Name, p.where())
case ActionReplace:
fmt.Fprintf(&b, "Replace the instructions '%s' in %s?\n\n", p.Name, p.where())
case ActionRemove:
fmt.Fprintf(&b, "Remove the instructions '%s' from %s?\n\n", p.Name, p.where())
}
if p.Action != ActionRemove {
if !whole {
b.WriteString("NOT SHOWN IN FULL here; its first lines:\n\n")
}
b.WriteString(text + "\n\n")
}
fmt.Fprintf(&b, "Proposed by an agent through the claude-code module on %s, at %s UTC.", p.From,
p.At.UTC().Format("15:04 on 2 Jan"))
return b.String()
}
var d strings.Builder
if p.Action == ActionRemove {
fmt.Fprintf(&d, "The approved text it removes has the fingerprint %s.\n", p.Fingerprint())
} else {
fmt.Fprintf(&d, "The text has %d lines, %d bytes, fingerprint %s.\n", p.Lines(), len(p.Text), p.Fingerprint())
d.WriteString("Once approved, every new session there reads it as the mesh's own word until it is removed the same way.\n")
}
fmt.Fprintf(&d, "Read all of it at the controller's terminal: mesh-cli ask claude-code claude_code_proposals "+
"'{\"id\": \"%s\"}'. The fingerprint there must be %s.", p.ID, p.Fingerprint())
does := "the text becomes part of those sessions' instructions"
if p.Action == ActionRemove {
does = "the instructions are removed from those sessions"
}
a := asks.Ask{ID: p.ID, Headline: headline, Explanation: strings.TrimRight(b.String(), "\n"), Who: asks.Operator,
a := asks.Ask{ID: p.ID, Headline: headline, Explanation: compose(shown), 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}
About: "instructions." + p.Scope + "." + strings.Join(p.Nodes, "+") + "." + p.Name,
Details: d.String()}
if p.Action != ActionRemove && wholeShown != shown {
a.Whole = compose(wholeShown)
}
if len(p.Nodes) == 0 {
a.About = "instructions." + p.Scope + "." + p.Name
}
@@ -221,10 +234,10 @@ func (p Proposal) Ask() asks.Ask {
return a
}
// excerpt is what of a text the operator's phone is shown, and whether it is the whole text. A line that might
// carry what may not leave the mesh (a path, an address, a long unbroken word) is not shown, and a marker says
// so; the router refuses an ask whose words carry one, and the whole text is read at the terminal.
func excerpt(text string) (string, bool) {
// excerpt is what of a text the operator's phone is shown, and whether it is the whole text: every line that
// may be shown (shownLine for the explanation, wholeLine for the whole), up to the bounds; a line kept back is
// counted in a marker that says why (hidden), and the whole text is read at the terminal.
func excerpt(text string, may func(string) bool, hidden string) (string, bool) {
lines := strings.Split(strings.TrimRight(text, "\n"), "\n")
whole := len(text) <= WholeTextBytes && len(lines) <= WholeTextLines
limitBytes, limitLines := FirstBytes, FirstLines
@@ -232,11 +245,11 @@ func excerpt(text string) (string, bool) {
limitBytes, limitLines = WholeTextBytes, WholeTextLines
}
var out []string
size, hidden := 0, 0
size, kept := 0, 0
flush := func() {
if hidden > 0 {
out = append(out, fmt.Sprintf("[%d line(s) not shown here: a path, an address or a long word]", hidden))
hidden = 0
if kept > 0 {
out = append(out, fmt.Sprintf("[%d line(s) not shown here: %s]", kept, hidden))
kept = 0
}
}
for i, line := range lines {
@@ -244,9 +257,9 @@ func excerpt(text string) (string, bool) {
whole = false
break
}
if !shownLine(line) {
if !may(line) {
whole = false
hidden++
kept++
continue
}
flush()
@@ -265,6 +278,19 @@ var (
assigned = regexp.MustCompile(`(?i)(password|passwd|passphrase|secret|token|api[_-]?key|credential|private[_-]?key)\s*[=:]`)
)
// wholeLine says whether a line may be shown where the sender is proven (asks.Ask.Whole): a path, an address or
// a host-like name may, as what the operator approves; a long unbroken word, a hexadecimal run or a value given
// to a secret's name may not — the router refuses the whole ask for a secret's shape, so nothing that might be
// one is risked.
func wholeLine(line string) bool {
for _, re := range []*regexp.Regexp{unbroken, hexRun, assigned} {
if re.MatchString(line) {
return false
}
}
return true
}
// shownLine says whether a line may be shown on the phone: no path, address, host-like name, long unbroken word
// or value given to a secret's name. Deliberately narrower than the router's own rule, so an ask is rarely
// refused for its words; a line kept back is read at the terminal.
@@ -153,8 +153,18 @@ func TestAProposalAsksTheOperatorAtTheLevelApproveAndChangesNothing(t *testing.T
if a.Headline != "Add Claude instructions 'handoff-from-g14'?" {
t.Errorf("headline: %q", a.Headline)
}
if !strings.Contains(a.Explanation, "The whole text:\n\n"+strings.TrimRight(handover, "\n")) {
t.Errorf("a short text is shown whole: %q", a.Explanation)
if !strings.Contains(a.Explanation, "?\n\n"+strings.TrimRight(handover, "\n")+"\n\nProposed by an agent through the claude-code module on g14, at ") {
t.Errorf("a short text is shown whole, then who proposed it: %q", a.Explanation)
}
// The shape of a settings proposal (issue 383): no fingerprint and no how-to in the message; both in Details.
if strings.Contains(a.Explanation, "ingerprint") || strings.Contains(a.Explanation, "mesh-cli") || strings.Contains(a.Explanation, "Once approved") {
t.Errorf("the message carries what Details shows: %q", a.Explanation)
}
if !strings.Contains(a.Details, "fingerprint "+fingerprintOf(textDigest(handover))) || !strings.Contains(a.Details, "mesh-cli ask claude-code claude_code_proposals '{\"id\": \""+id+"\"}'") {
t.Errorf("Details: %q", a.Details)
}
if a.Whole != "" {
t.Errorf("a text every line of which is shown has whole words of its own: %q", a.Whole)
}
if len(a.Options) != 2 {
t.Fatalf("options: %+v", a.Options)
@@ -420,9 +430,30 @@ func TestALongTextIsShownInPartAndReadWholeAtTheTerminal(t *testing.T) {
if strings.Contains(a.Explanation, "/etc/claude-code") || !strings.Contains(a.Explanation, "[1 line(s) not shown here") {
t.Errorf("a line with a path was shown: %q", a.Explanation)
}
if !strings.Contains(a.Explanation, "mesh-cli ask claude-code claude_code_proposals") ||
!strings.Contains(a.Explanation, "fingerprint there must be "+out["fingerprint"].(string)) {
t.Errorf("the phone does not say where to read all of it: %q", a.Explanation)
if !strings.Contains(a.Explanation, "NOT SHOWN IN FULL here") || strings.Contains(a.Explanation, "mesh-cli ask claude-code claude_code_proposals") {
t.Errorf("the message does not say it is cut, or says where to read all of it (Details does): %q", a.Explanation)
}
if !strings.Contains(a.Details, "mesh-cli ask claude-code claude_code_proposals") ||
!strings.Contains(a.Details, "fingerprint there must be "+out["fingerprint"].(string)) {
t.Errorf("Details do not say where to read all of it: %q", a.Details)
}
// Where the sender is proven, the line with the path is shown; the cut stands.
if !strings.Contains(a.Whole, "See /etc/claude-code for the managed files.") || !strings.Contains(a.Whole, "NOT SHOWN IN FULL here") ||
strings.Contains(a.Whole, "not shown here") {
t.Errorf("the whole words: %q", a.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{
"See /etc/claude-code for the managed files.": true,
"Ask on nas.lan, or mail ops@example.com.": true,
"token: 0123456789abcdef0123456789abcdef": false,
"api_key=whatever": false,
"abcdefghijklmnopqrstuvwxyz0123456789": false,
} {
if wholeLine(line) != may {
t.Errorf("wholeLine(%q) = %v", line, !may)
}
}
if len(a.Explanation) > 3000 {
t.Errorf("the explanation is %d bytes", len(a.Explanation))
+1 -1
View File
@@ -2,6 +2,6 @@ module claude-code
go 1.25.0
require git.novox.be/novox/mesh-sdk/go v0.1.11-0.20261009143344-f047d0a4a970
require git.novox.be/novox/mesh-sdk/go v0.1.13-0.20261010132341-4f3efc98e0c0
require gopkg.in/yaml.v3 v3.0.1
+2 -4
View File
@@ -1,7 +1,5 @@
git.novox.be/novox/mesh-sdk/go v0.1.7 h1:C0sTQmtTiyYH7bnqZb7PusXnqA37gKuT7Nqjn9gG47w=
git.novox.be/novox/mesh-sdk/go v0.1.7/go.mod h1:GFuZUElBZ9A++mxgIKo97aXXo+kV0uJ/UkbhQPPIbrY=
git.novox.be/novox/mesh-sdk/go v0.1.11-0.20261009143344-f047d0a4a970 h1:9tFDQsgmI+4X7/BpZGXIr+HemPKE7YddYGqWV0lINAI=
git.novox.be/novox/mesh-sdk/go v0.1.11-0.20261009143344-f047d0a4a970/go.mod h1:GFuZUElBZ9A++mxgIKo97aXXo+kV0uJ/UkbhQPPIbrY=
git.novox.be/novox/mesh-sdk/go v0.1.13-0.20261010132341-4f3efc98e0c0 h1:qi0slgoZVexIr/gxfLhN8kJxy2LVvGTHiWuuGBPkfDo=
git.novox.be/novox/mesh-sdk/go v0.1.13-0.20261010132341-4f3efc98e0c0/go.mod h1:GFuZUElBZ9A++mxgIKo97aXXo+kV0uJ/UkbhQPPIbrY=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=