A settings proposal carries every key with its whole value in its Details (issue 383)
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:
@@ -164,29 +164,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))
|
||||
@@ -233,12 +242,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)
|
||||
@@ -268,6 +279,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 {
|
||||
@@ -275,19 +290,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 {
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user