A3d item 1: operatorchannelclient re-exports mesh-sdk's act and its digest, one form
Builds: design 47 G §677, lead answers A-198, A-219 Claude-Session: https://claude.ai/code/session_01NcbgfGKaxw984XyAPfLm4h
This commit is contained in:
@@ -13,12 +13,12 @@ import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"regexp"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
seat "git.novox.be/novox/mesh-contracts/operator-and-conversation/go/seats/operatorchannel"
|
||||
"git.novox.be/novox/mesh-sdk/go/operatorask"
|
||||
)
|
||||
|
||||
// The levels an option is answered at: how much proof its answer needs (novox/hq ADR 0234 §8, the glossary's
|
||||
@@ -57,35 +57,11 @@ const (
|
||||
OutcomeRefused = "refused" // it was never shown: Words says why
|
||||
)
|
||||
|
||||
// An Act is what an option binds: 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.
|
||||
type Act map[string]string
|
||||
// An Act is what an option binds: mesh-sdk's operatorask.Act, re-exported (design 47 G lead answers A-198, A-219).
|
||||
type Act = operatorask.Act
|
||||
|
||||
// ActDigest is the digest an asker puts in an option's Binds: SHA-256 over the act's canonical encoding
|
||||
// (canonical), written "sha256:<hex>". 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")
|
||||
}
|
||||
keys := make([]string, 0, len(act))
|
||||
for k := range act {
|
||||
if k == "" {
|
||||
return "", fmt.Errorf("the act cannot be digested: a field has no name")
|
||||
}
|
||||
keys = append(keys, k)
|
||||
}
|
||||
sort.Strings(keys)
|
||||
var b strings.Builder
|
||||
b.WriteString("novox.act.v1\n")
|
||||
for _, k := range keys {
|
||||
canonical(&b, k)
|
||||
canonical(&b, act[k])
|
||||
}
|
||||
sum := sha256.Sum256([]byte(b.String()))
|
||||
return "sha256:" + hex.EncodeToString(sum[:]), nil
|
||||
}
|
||||
// ActDigest is the digest an asker puts in an option's Binds: mesh-sdk's operatorask.ActDigest, re-exported.
|
||||
func ActDigest(act Act) (string, error) { return operatorask.ActDigest(act) }
|
||||
|
||||
// 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.
|
||||
|
||||
@@ -10,6 +10,7 @@ import (
|
||||
"time"
|
||||
|
||||
seat "git.novox.be/novox/mesh-contracts/operator-and-conversation/go/seats/operatorchannel"
|
||||
"git.novox.be/novox/mesh-sdk/go/operatorask"
|
||||
)
|
||||
|
||||
var now = time.Date(2026, 10, 8, 14, 0, 0, 0, time.UTC)
|
||||
@@ -189,6 +190,11 @@ func TestDigestsAreCanonicalOverNamedFields(t *testing.T) {
|
||||
Digest(gold) != "sha256:997af29a80663c5eb7d9dd38fc029c4a15fd726220b25e3c7cdc54d5a60bbbae" {
|
||||
t.Errorf("the digests changed with the move: %s, %s", Digest(held()), Digest(gold))
|
||||
}
|
||||
// The act is mesh-sdk's, re-exported (A-198, A-219): one form, so the seat server's gate digests as an asker does.
|
||||
var shared operatorask.Act = Act{"verb": "release", "arg.delivery": "hq"}
|
||||
if d, _ := operatorask.ActDigest(shared); d == "" {
|
||||
t.Error("mesh-sdk's digest of the act is empty")
|
||||
}
|
||||
a1, _ := ActDigest(Act{"verb": "release", "arg.delivery": "hq"})
|
||||
b := Act{}
|
||||
b["arg.delivery"] = "hq"
|
||||
|
||||
Reference in New Issue
Block a user