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
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:
@@ -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))
|
||||
|
||||
Reference in New Issue
Block a user