From 0e3cc811746c0722a4e515eda70e023a7aeaa852 Mon Sep 17 00:00:00 2001 From: jochen Date: Thu, 12 Nov 2026 06:09:18 +0100 Subject: [PATCH] A3d item 1: the act and its digest are mesh-sdk's, so the seat server's gate digests as every asker MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Builds: design 47 G §677, lead answers A-198, A-219 Claude-Session: https://claude.ai/code/session_01NcbgfGKaxw984XyAPfLm4h --- go/operatorask/act.go | 51 ++++++++++++++++++++++++++++++++++++++ go/operatorask/act_test.go | 34 +++++++++++++++++++++++++ 2 files changed, 85 insertions(+) create mode 100644 go/operatorask/act.go create mode 100644 go/operatorask/act_test.go diff --git a/go/operatorask/act.go b/go/operatorask/act.go new file mode 100644 index 00000000..0c3b949f --- /dev/null +++ b/go/operatorask/act.go @@ -0,0 +1,51 @@ +package operatorask + +import ( + "crypto/sha256" + "encoding/hex" + "fmt" + "sort" + "strconv" + "strings" +) + +// An Act is what an option of an ask binds (novox/hq ADR 0286 §10, §11): named fields, each a string — the verb, the +// machine, the level, and each argument under a name of its own ("arg.delivery"). Flat on purpose: its digest is over +// these names and values alone, never over how a language happens to encode a struct. It is mesh-sdk's (design 47 G +// lead answers A-198, A-219), so the seat server's gate in go/process digests an act as every asker does; +// operatorchannelclient re-exports it. +type Act map[string]string + +// ActDigest is the digest an asker puts in an option's binds: SHA-256 over the act's canonical encoding, written +// "sha256:". The same names and values give the same digest in any language, whatever order they were set in; a +// field renamed, added or emptied gives another. +func ActDigest(act Act) (string, error) { + if len(act) == 0 { + return "", fmt.Errorf("the act cannot be digested: it names nothing") + } + names := make([]string, 0, len(act)) + for k := range act { + if k == "" { + return "", fmt.Errorf("the act cannot be digested: a field has no name") + } + names = append(names, k) + } + sort.Strings(names) + var b strings.Builder + b.WriteString("novox.act.v1\n") + for _, k := range names { + canonical(&b, k) + canonical(&b, act[k]) + } + sum := sha256.Sum256([]byte(b.String())) + return "sha256:" + hex.EncodeToString(sum[:]), nil +} + +// canonical writes one value as its length in bytes, a colon, the bytes and a newline: no value can be read as +// another's end or start, so two different sequences of values never encode the same. +func canonical(b *strings.Builder, v string) { + b.WriteString(strconv.Itoa(len(v))) + b.WriteByte(':') + b.WriteString(v) + b.WriteByte('\n') +} diff --git a/go/operatorask/act_test.go b/go/operatorask/act_test.go new file mode 100644 index 00000000..f5a6debf --- /dev/null +++ b/go/operatorask/act_test.go @@ -0,0 +1,34 @@ +package operatorask + +import ( + "crypto/sha256" + "encoding/hex" + "testing" +) + +// Design 47 G A-198: the act's form and its digest are mesh-sdk's, so a seat server in go/process digests an act as +// every asker does, with no import of the contracts; the digest is canonical over the act's named fields. +func TestTheActsDigestIsCanonicalOverItsNamedFields(t *testing.T) { + a1, _ := ActDigest(Act{"verb": "release", "arg.delivery": "hq"}) + b := Act{} + b["arg.delivery"] = "hq" + b["verb"] = "release" + if a2, _ := ActDigest(b); a1 != a2 { + t.Error("the order an act was built in changed its digest") + } + x, _ := ActDigest(Act{"a": "b\n1:c"}) + y, _ := ActDigest(Act{"a": "b", "c": ""}) + if x == y { + t.Error("two different acts encode the same") + } + if _, err := ActDigest(Act{}); err == nil { + t.Error("an empty act was digested") + } + if _, err := ActDigest(Act{"": "x"}); err == nil { + t.Error("an act with a nameless field was digested") + } + sum := sha256.Sum256([]byte("novox.act.v1\n4:verb\n7:release\n")) + if got, _ := ActDigest(Act{"verb": "release"}); got != "sha256:"+hex.EncodeToString(sum[:]) { + t.Errorf("the act's encoding is not the one written down: %s", got) + } +}