Merge pull request 'go/asks: an ask says what a channel that proves who answers shows whole, and what Details shows (issue 383)' (#17) from fix/383-an-ask-says-what-a-proving-channel-shows-whole into main
This commit was merged in pull request #17.
This commit is contained in:
@@ -96,7 +96,8 @@ an event under the module's own name or kind, `stdio.SeatTake` takes a held seat
|
||||
and `stdio.SeatAnswer` carry a proof (a code typed by a person, by request and reply, never kept),
|
||||
`stdio.SeatRecord` reads a record kept for the module, and `State(...).Create` and `.Update` write by
|
||||
compare-and-set. The runtime does each only as far as the module's membership lists. `go/asks` is the
|
||||
contract on top: the `Ask` an asker publishes and the checks it is held to, the `Warrant` it acts on
|
||||
contract on top: the `Ask` an asker publishes and the checks it is held to (its `Whole` is what a channel that proves
|
||||
who answers shows in place of its `Explanation`, and its `Details` what the Details answer shows, hq issue 383), the `Warrant` it acts on
|
||||
(`For`, `Says`), and the `Message`, `Choice`, `LinkAsked` and `Code` a channel takes and says, with the
|
||||
subjects each travels on.
|
||||
`go/asker` is an asking module's client (ADR 0259 §10): `Ask` keeps an ask in the module's own state and then
|
||||
|
||||
+16
-1
@@ -154,7 +154,9 @@ func canonical(b *strings.Builder, v string) {
|
||||
//
|
||||
// It is over the ask's named fields in a fixed order, each written canonically, and the expiry as UTC
|
||||
// RFC 3339 to the nanosecond — never over a language's encoding of the struct, so a field added to Ask
|
||||
// later changes no digest until it is added here, on purpose.
|
||||
// later changes no digest until it is added here, on purpose. Whole and Details (novox/hq issue 383) follow
|
||||
// the options only when the ask gives either: an ask without them digests as it did before they existed, so
|
||||
// a router and an asker of different builds still agree on every such ask.
|
||||
func (a Ask) Digest() string {
|
||||
var b strings.Builder
|
||||
b.WriteString("novox.ask.v1\n")
|
||||
@@ -168,6 +170,10 @@ func (a Ask) Digest() string {
|
||||
canonical(&b, v)
|
||||
}
|
||||
}
|
||||
if a.Whole != "" || a.Details != "" {
|
||||
canonical(&b, a.Whole)
|
||||
canonical(&b, a.Details)
|
||||
}
|
||||
sum := sha256.Sum256([]byte(b.String()))
|
||||
return "sha256:" + hex.EncodeToString(sum[:])
|
||||
}
|
||||
@@ -190,6 +196,15 @@ type Ask struct {
|
||||
// About is what the ask is about (a condition's key): a newer ask about it replaces the older.
|
||||
About string `json:"about,omitempty"`
|
||||
Urgent bool `json:"urgent,omitempty"`
|
||||
// Whole is the explanation with its exact values whole, shown in place of Explanation on a channel kind
|
||||
// that proves who answers (verified-sender) and carries the ask's answers (novox/hq issue 383): what the
|
||||
// person approves — a mount point, a share, a private address — must be readable where they approve it.
|
||||
// The asker withholds a value shaped like a secret in it, and the router refuses the ask when one is left;
|
||||
// Explanation stays under the whole content rule everywhere else. Empty, Explanation is shown everywhere.
|
||||
Whole string `json:"whole,omitempty"`
|
||||
// Details is what the Details answer on the ask's message shows, line by line under the content rule: a
|
||||
// fingerprint, how to read the proposal whole at the terminal. Empty, an ask's own message offers no Details.
|
||||
Details string `json:"details,omitempty"`
|
||||
}
|
||||
|
||||
// The bounds of an ask (novox/hq ADR 0234 §8, ADR 0259 §4).
|
||||
|
||||
+24
-2
@@ -230,9 +230,31 @@ func TestDigestsAreCanonicalOverNamedFields(t *testing.T) {
|
||||
t.Errorf("changing %s left the digest the same", name)
|
||||
}
|
||||
}
|
||||
// Whole and Details (issue 383) are in the digest once given, and change it; an ask without either digests
|
||||
// exactly as before they existed — the encoding by hand, so no build can drift from it unseen.
|
||||
plain := Ask{ID: "p", Headline: "h", Explanation: "e", Who: Operator, Expires: now, OnExpiry: "o", About: "a",
|
||||
Options: []Option{{ID: "ok", Label: "OK", Does: "d", Level: Acknowledge}}}
|
||||
byHand := sha256.Sum256([]byte("novox.ask.v1\n1:p\n1:h\n1:e\n8:operator\n20:2026-10-08T14:00:00Z\n1:o\n1:a\n5:false\n1:1\n" +
|
||||
"2:ok\n2:OK\n1:d\n11:acknowledge\n0:\n"))
|
||||
if plain.Digest() != "sha256:"+hex.EncodeToString(byHand[:]) {
|
||||
t.Errorf("an ask without Whole and Details does not digest as it did before them: %s", plain.Digest())
|
||||
}
|
||||
withWhole, withDetails := plain, plain
|
||||
withWhole.Whole = "e, whole"
|
||||
withDetails.Details = "fingerprint 1234"
|
||||
if withWhole.Digest() == plain.Digest() || withDetails.Digest() == plain.Digest() || withWhole.Digest() == withDetails.Digest() {
|
||||
t.Error("Whole or Details left the digest the same")
|
||||
}
|
||||
both := withWhole
|
||||
both.Details = "fingerprint 1234"
|
||||
changed := both
|
||||
changed.Whole += "!"
|
||||
if changed.Digest() == both.Digest() {
|
||||
t.Error("changing Whole beside Details left the digest the same")
|
||||
}
|
||||
// A field added to Ask or Option without being added to Digest fails here.
|
||||
if n := reflect.TypeOf(Ask{}).NumField(); n != 9 {
|
||||
t.Errorf("Ask has %d fields, and Digest covers 9: add the new one to Digest and to this test", n)
|
||||
if n := reflect.TypeOf(Ask{}).NumField(); n != 11 {
|
||||
t.Errorf("Ask has %d fields, and Digest covers 11: add the new one to Digest and to this test", n)
|
||||
}
|
||||
if n := reflect.TypeOf(Option{}).NumField(); n != 5 {
|
||||
t.Errorf("Option has %d fields, and Digest covers 5: add the new one to Digest and to this test", n)
|
||||
|
||||
Reference in New Issue
Block a user