A settings proposal carries every key with its whole value in its Details (issue 383)
mesh/delivery-group group fix/383-an-ask-reads-at-a-glance checking: composed check out of date: novox/mesh-controller@52bcef159d95 was judged against trunk 6f…
mesh/merge-gate out of date: judged against trunk 6f1a05366c1d, the trunk is now 8ddf8b700558; judged again on a recheck or when it is merged
mesh/repo-check out of date: judged against trunk 6f1a05366c1d, the trunk is now 8ddf8b700558; judged again on a recheck or when it is merged
mesh/delivery superseded: a newer head of the same pull request

The review of #304: with more than three keys the ask showed only the names of the rest, a long value was
cut without a word, and the Details held no key, so Approve bound values the operator never saw. The
Details now list every key uncut — masked, and whole in WholeDetails for a channel that proves who
answers — before who proposed it and the fingerprint; the glance says what it cut. Vendors mesh-sdk at
f474544 and captures its conformance events there.
This commit is contained in:
jochen
2026-10-13 17:04:48 +02:00
parent 35232c8534
commit 52bcef159d
7 changed files with 114 additions and 27 deletions
+36 -21
View File
@@ -162,29 +162,38 @@ func (p settingsProposal) ask(id string, machines []string) (asks.Ask, map[strin
headline = verb + " settings?"
}
// The message reads at a glance (issues 383 and 639): a summary of what changes where, then at most three
// points, a key each; who proposed it, the fingerprint and how to read it whole are its Details, which the
// router notes after the answers.
shown, shownWhole := p.change(machines, false)
whole, _ := p.change(machines, true)
// points, a key each; every key with its whole value, who proposed it, the fingerprint and how to read it at the
// terminal are its Details, which the router notes after the answers.
shown, shownAll, shownWhole := p.change(machines, false)
whole, wholeAll, _ := p.change(machines, true)
summary := p.summary()
compose := func(points []string) string {
return strings.Join(append([]string{summary}, points...), "\n")
}
var d strings.Builder
fmt.Fprintf(&d, "Proposed by %s at %s. Fingerprint %s.\n", sayable(p.From, machines), p.At.Local().Format("15:04 on 2 Jan"),
fingerprint(p.Digest))
if !shownWhole {
d.WriteString("Where who answers is not proven, its values are shown as ‹address›, ‹path› or ‹withheld›.\n")
// The Details: every setting it sets, one line each, uncut — whole where the sender is proven (WholeDetails),
// masked elsewhere — then who proposed it, the fingerprint and how to read it at the terminal.
details := func(lines []string) string {
var d strings.Builder
d.WriteString(strings.Join(lines, "\n") + "\n")
fmt.Fprintf(&d, "Proposed by %s at %s. Fingerprint %s.\n", sayable(p.From, machines), p.At.Local().Format("15:04 on 2 Jan"),
fingerprint(p.Digest))
if !shownWhole {
d.WriteString("Where who answers is not proven, its values are shown as ‹address›, ‹path› or ‹withheld›.\n")
}
fmt.Fprintf(&d, "Read it whole, with this fingerprint: settings proposals %s at the controller's terminal, or the "+
"controller's settings verb with proposal %s through the mesh MCP server.", id, id)
return d.String()
}
fmt.Fprintf(&d, "Read it whole, with this fingerprint: settings proposals %s at the controller's terminal, or the "+
"controller's settings verb with proposal %s through the mesh MCP server.", id, id)
q := asks.Ask{ID: id, Headline: headline, Explanation: compose(shown), Who: asks.Operator,
Expires: p.At.Add(askApproveFor), OnExpiry: "the proposal is discarded; nothing changes",
About: p.about(), Details: d.String()}
About: p.about(), Details: details(shownAll)}
// Only where a value was masked: an ask whose values all pass the content rule shows the same everywhere.
if w := compose(whole); w != q.Explanation {
// Only where a value was masked: an ask whose values all pass the content rule shows the same everywhere.
q.Whole = w
}
if w := details(wholeAll); w != q.Details {
q.WholeDetails = w
}
options := map[string]int{}
for i, act := range p.actions(id) {
binds, _ := asks.ActDigest(boundAct(act))
@@ -231,12 +240,14 @@ func (p settingsProposal) summary() string {
n, p.Module, p.where(), stay, takes)
}
// change is what changes, as at most asks.MostPoints points, and whether every value was shown whole: "+ key: value"
// change is what changes: as at most asks.MostPoints points for the glance, as every line whole for the note, and
// whether every value was shown exactly: "+ key: value"
// added, "~ key: value (was: old)" changed, "- key (was: old)" removed. Each value is shown under the content rule
// (sayableValue), or — exact, for a channel that proves who answers — as it is, withheld only when shaped like a
// secret (wholeValue). More changes than points: the first two, and the keys of the rest; a point longer than a
// phone shows at a glance is cut. Either is read whole with `settings proposals`.
func (p settingsProposal) change(machines []string, exact bool) ([]string, bool) {
// phone shows at a glance is cut and says so. The note carries every line uncut (the review of #304: Approve sets
// exactly what the operator can read).
func (p settingsProposal) change(machines []string, exact bool) ([]string, []string, bool) {
whole := true
say := func(v any) string {
s, w := sayableValue(v, machines)
@@ -266,6 +277,10 @@ func (p settingsProposal) change(machines []string, exact bool) ([]string, bool)
lines = append(lines, line{k, fmt.Sprintf("- %s (was: %s)", k, say(was[k]))})
}
}
all := make([]string, len(lines))
for i, l := range lines {
all[i] = l.text
}
var out []string
for i, l := range lines {
if len(lines) > asks.MostPoints && i == asks.MostPoints-1 {
@@ -273,19 +288,19 @@ func (p settingsProposal) change(machines []string, exact bool) ([]string, bool)
for _, r := range lines[i:] {
rest = append(rest, r.key)
}
out = append(out, fmt.Sprintf("and %d more: %s", len(rest), strings.Join(rest, ", ")))
whole = false
out = append(out, fmt.Sprintf("and %d more, each whole in the note: %s", len(rest), strings.Join(rest, ", ")))
break
}
out = append(out, l.text)
}
// A point too long for a glance is cut, and says so: its whole value is the note's.
const cut = "… (cut: whole in the note)"
for i, l := range out {
if r := []rune(l); len(r) > asks.PointLength {
out[i] = string(r[:asks.PointLength-1]) + "…"
whole = false
out[i] = string(r[:asks.PointLength-len([]rune(cut))]) + cut
}
}
return out, whole
return out, all, whole
}
func layerKeys(m map[string]any) []string {
+61 -3
View File
@@ -4,6 +4,7 @@ import (
"context"
"encoding/json"
"errors"
"fmt"
"io"
"os"
"slices"
@@ -222,8 +223,8 @@ func TestAProposalShowsItsValuesWholeOnlyWhereTheSenderIsProvenAndNeverASecret(t
t.Fatal(err)
}
q := r.askSent(t)
masked, _ := kept(r).Proposal.change(machines, false)
whole, _ := kept(r).Proposal.change(machines, true)
masked, _, _ := kept(r).Proposal.change(machines, false)
whole, _, _ := kept(r).Proposal.change(machines, true)
if !strings.HasSuffix(q.Explanation, "\n"+strings.Join(masked, "\n")) || !strings.HasSuffix(q.Whole, "\n"+strings.Join(whole, "\n")) {
t.Fatalf("the ask does not carry the change masked and whole:\n%s\n--\n%s", q.Explanation, q.Whole)
}
@@ -272,7 +273,7 @@ func TestAProposalShowsItsValuesWholeOnlyWhereTheSenderIsProvenAndNeverASecret(t
if _, err := r3.pr.propose(context.Background(), proposeInput{module: "notes", node: "laptop", values: five}); err != nil {
t.Fatal(err)
}
if q3 := r3.askSent(t); !strings.HasSuffix(q3.Explanation, "\n+ a: 1\n+ b: 2\nand 3 more: c, d, e") || asks.Glance(q3.Explanation) != nil ||
if q3 := r3.askSent(t); !strings.HasSuffix(q3.Explanation, "\n+ a: 1\n+ b: 2\nand 3 more, each whole in the note: c, d, e") || asks.Glance(q3.Explanation) != nil ||
!strings.Contains(q3.Details, "settings proposals") {
t.Errorf("five keys:\n%s", q3.Explanation)
}
@@ -854,3 +855,60 @@ func captureStdout(t *testing.T, f func()) string {
_ = w.Close()
return <-done
}
// The review of #304: Approve sets exactly what the operator can read. A proposal of five keys, one value 300
// characters long, shows two keys at a glance, says what it cut, and carries every key with its whole value in its
// Details for a channel that proves who answers (WholeDetails), and every key masked for one that does not.
func TestEveryKeyAndItsWholeValueReachTheOperator(t *testing.T) {
var long strings.Builder
long.WriteString("library=/storage")
for i := 0; long.Len() < 300; i++ {
long.WriteString(fmt.Sprintf("/album-%02d", i))
}
value := long.String()[:300]
values := map[string]any{"a": "1", "b": "2", "c": "3", "d": "4", "shares": value}
r := newProposerRig(t)
if _, err := r.pr.propose(context.Background(), proposeInput{module: "mounts", node: "shanks", values: values}); err != nil {
t.Fatal(err)
}
q := r.askSent(t)
if err := q.Check(r.now); err != nil {
t.Fatal(err)
}
// What the operator reads on a proving channel: the whole words, then the note's whole details.
visible := q.Whole + "\n" + q.WholeDetails
if q.Whole == "" {
visible = q.Explanation + "\n" + q.WholeDetails
}
for k, v := range values {
if !strings.Contains(visible, "+ "+k+": "+v.(string)) {
t.Errorf("the operator never reads %s whole:\n%s", k, visible)
}
}
if asks.Glance(q.Explanation) != nil || !strings.HasSuffix(q.Explanation, "\nand 3 more, each whole in the note: c, d, shares") {
t.Errorf("the glance:\n%s", q.Explanation)
}
for k := range values {
if !strings.Contains(q.Details, "+ "+k+": ") {
t.Errorf("the masked details lack %s:\n%s", k, q.Details)
}
}
if strings.Contains(q.Details, "/storage") {
t.Errorf("the masked details show a path:\n%s", q.Details)
}
if refusal, ok := outward.Check(q.Details, "shanks"); !ok {
t.Errorf("the router would withhold the masked details: %s", refusal)
}
if _, ok := outward.Secret(q.WholeDetails, "shanks"); !ok {
t.Error("the router would refuse the whole details")
}
// A value too long for a glance is cut there, and says so.
r2 := newProposerRig(t)
if _, err := r2.pr.propose(context.Background(), proposeInput{module: "mounts", node: "shanks", values: map[string]any{"shares": value}}); err != nil {
t.Fatal(err)
}
q2 := r2.askSent(t)
if !strings.HasSuffix(q2.Whole, "… (cut: whole in the note)") || !strings.Contains(q2.WholeDetails, "+ shares: "+value) || asks.Glance(q2.Whole) != nil {
t.Errorf("the cut is not said, or the note lacks the whole:\n%s\n--\n%s", q2.Whole, q2.WholeDetails)
}
}
+1 -1
View File
@@ -3,7 +3,7 @@ module github.com/novox/mesh-controller
go 1.26.0
require (
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261013141520-9588871d6629
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261013145622-f4745447ba6d
github.com/jackc/pgx/v5 v5.10.0
github.com/nats-io/nats-server/v2 v2.11.17
github.com/nats-io/nats.go v1.54.0
+2
View File
@@ -4,6 +4,8 @@ git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261010191001-33917f91ac3c h1:l5onJwoI
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261010191001-33917f91ac3c/go.mod h1:GFuZUElBZ9A++mxgIKo97aXXo+kV0uJ/UkbhQPPIbrY=
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261013141520-9588871d6629 h1:7TYFV1Q6Mcz+lsLP/sds87tysET4rKCn3rRIQV8gNmA=
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261013141520-9588871d6629/go.mod h1:GFuZUElBZ9A++mxgIKo97aXXo+kV0uJ/UkbhQPPIbrY=
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261013145622-f4745447ba6d h1:HEJyqorKktrkJPeeJdwEo0kdrJjH3fm/YVS9MGftCqY=
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261013145622-f4745447ba6d/go.mod h1:GFuZUElBZ9A++mxgIKo97aXXo+kV0uJ/UkbhQPPIbrY=
github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op h1:Z/MZK75wC/NSrkgqeNIa7jexam9uWzhLmFTSCPI/kn0=
github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op/go.mod h1:FQyySiasQQM8735Ddel3MRojmy4dA1IqCeyJ5jmPMbI=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+1 -1
View File
@@ -9,7 +9,7 @@ a second place to clean besides the catalogue itself.
mesh-catalog e0bc919d4945bb5f13f971f74643a0b714cf45da modules/*/module.json, modules/nats/Dockerfile
mesh-host 7a41e120635b3f7b6cb94cca8b3fc4093e4e6110 examples/foundation-first-node.lock
mesh-sdk 9588871d6629e160bb0667f3d97566a0f8f7a4a6 conformance/events/module-event.json
mesh-sdk f4745447ba6d0c4fd7dc6b1071e26ce012806233 conformance/events/module-event.json
To move them, from this repository's root, with the two repositories checked out beside it:
+12
View File
@@ -174,6 +174,12 @@ func (a Ask) Digest() string {
canonical(&b, a.Whole)
canonical(&b, a.Details)
}
// WholeDetails (novox/hq issue 383, the review of mesh-controller #304) follows only when given, so every
// ask without it digests as it did before it existed.
if a.WholeDetails != "" {
canonical(&b, "whole-details")
canonical(&b, a.WholeDetails)
}
sum := sha256.Sum256([]byte(b.String()))
return "sha256:" + hex.EncodeToString(sum[:])
}
@@ -207,6 +213,12 @@ type Ask struct {
// (Message.Note), after what the ask's options do and when it expires — never in its body (novox/hq
// issue 639).
Details string `json:"details,omitempty"`
// WholeDetails are the Details with their exact values whole, shown in place of Details where Whole is shown
// in place of the explanation (a kind that proves who answers, given the ask's answers): every value an
// approval sets, whole, so the person reads exactly what they approve (novox/hq issue 383). Held, like
// Whole, to the secret rule alone; the router refuses the ask when a secret's shape is left in it. Empty,
// Details are shown everywhere.
WholeDetails string `json:"whole-details,omitempty"`
}
// What an ask's explanation is held to, so it reads at a glance on a phone (novox/hq issue 639): a summary
+1 -1
View File
@@ -1,4 +1,4 @@
# git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261013141520-9588871d6629
# git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261013145622-f4745447ba6d
## explicit; go 1.22
git.novox.be/novox/mesh-sdk/go/asks
# github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op