The gate sits on both handlers the runner serves, a module's tools (its ledger repairs among them) and the seat verbs its modules hold, before the bundle. The same call again is judged on the router's own record of its act's newest ask: held to the warrant (operatorchannelclient.For), the option and the act it bound (Performs), within 30 minutes of the choice, and claimed by compare-and-set (Client.Take) before it runs, so one approval runs one call. A later call is told it still waits, was declined (not asked again for 10 minutes), or asks anew after expired, approved too long ago or already run. A runner's own tool marked is refused unasked; with no state approvals bound a marked call is refused and an unmarked one runs. The gate's reach (state, publish, the router's record) is the runner's own connection, in one place; it waits for the catalog half's first view on the events the runner hears. Builds: design 46 7-approval-mark §13, §14, §15, §17, §20 Claude-Session: https://claude.ai/code/session_01NcbgfGKaxw984XyAPfLm4h
294 lines
8.1 KiB
Go
294 lines
8.1 KiB
Go
package approval
|
||
|
||
import (
|
||
"bytes"
|
||
"encoding/json"
|
||
"fmt"
|
||
"math"
|
||
"sort"
|
||
"strconv"
|
||
"strings"
|
||
"unicode/utf8"
|
||
|
||
"git.novox.be/novox/mesh-contracts/operator-and-conversation/go/clients/operatorchannelclient"
|
||
"git.novox.be/novox/mesh-sdk/go/outward"
|
||
)
|
||
|
||
// Call is one call the runner serves, as the gate judges it: a module's tool, or a seat verb its module serves.
|
||
type Call struct {
|
||
// Address is the address as called: `<module>.<tool>` or `<seat>.<verb>`.
|
||
Address string
|
||
// Module is the module whose bundle serves it; Seat the seat of a verb, empty for a tool; Name the tool or the verb.
|
||
Module, Seat, Name string
|
||
// Fingerprint is the build the bundle serves, from its composed environment (§5).
|
||
Fingerprint string
|
||
// Caller is the caller the bus proved.
|
||
Caller string
|
||
// Args are the call's arguments, a JSON object.
|
||
Args json.RawMessage
|
||
// Description is the tool's or verb's own description, whose first line the ask explains it by.
|
||
Description string
|
||
// Own says it is one of the runner's own tools served under the module's name (its ledger repairs), not its bundle's.
|
||
Own bool
|
||
}
|
||
|
||
// MostArguments is the most a marked call's arguments may hold: more is refused, asking nothing (§9).
|
||
const MostArguments = 16 * 1024
|
||
|
||
// arguments are a call's arguments by name; a body that is not an object is refused.
|
||
func arguments(raw json.RawMessage) (map[string]json.RawMessage, error) {
|
||
args := map[string]json.RawMessage{}
|
||
if len(bytes.TrimSpace(raw)) == 0 || string(bytes.TrimSpace(raw)) == "null" {
|
||
return args, nil
|
||
}
|
||
if err := json.Unmarshal(raw, &args); err != nil {
|
||
return nil, fmt.Errorf("its arguments are not a JSON object: %v", err)
|
||
}
|
||
return args, nil
|
||
}
|
||
|
||
// refusedUnasked says why a marked call is refused at once, asking nothing (§9): its arguments hold a secret's shape, or are
|
||
// more than MostArguments.
|
||
func refusedUnasked(raw json.RawMessage) string {
|
||
if len(raw) > MostArguments {
|
||
return fmt.Sprintf("its arguments are %d bytes, more than the %d a call that needs the operator's approval may carry", len(raw), MostArguments)
|
||
}
|
||
if r, ok := outward.CheckSecret(string(raw)); !ok {
|
||
return fmt.Sprintf("its arguments hold %s, which no ask may carry", r.What)
|
||
}
|
||
return ""
|
||
}
|
||
|
||
// ActOf is the act a marked call binds (§8): `address`, `node`, `module` and `tool` or `seat` and `verb`, `build`, and
|
||
// `arg.<name>` for each argument given, a string as itself and any other value as its JSON in RFC 8785's canonical form.
|
||
func ActOf(c Call, node string) (operatorchannelclient.Act, error) {
|
||
act := operatorchannelclient.Act{"address": c.Address, "node": node, "build": c.Fingerprint}
|
||
if c.Seat == "" {
|
||
act["module"], act["tool"] = c.Module, c.Name
|
||
} else {
|
||
act["seat"], act["verb"] = c.Seat, c.Name
|
||
}
|
||
args, err := arguments(c.Args)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
for name, raw := range args {
|
||
var s string
|
||
if json.Unmarshal(raw, &s) == nil {
|
||
act["arg."+name] = s
|
||
continue
|
||
}
|
||
canon, err := Canonical(raw)
|
||
if err != nil {
|
||
return nil, fmt.Errorf("the argument %s cannot be put in canonical form: %v", name, err)
|
||
}
|
||
act["arg."+name] = canon
|
||
}
|
||
return act, nil
|
||
}
|
||
|
||
// Canonical is a JSON value in RFC 8785's canonical form: no white space, an object's members sorted by their names' UTF-16
|
||
// code units, a number as ECMAScript writes a double, a string with only the escapes the RFC names.
|
||
func Canonical(raw json.RawMessage) (string, error) {
|
||
dec := json.NewDecoder(bytes.NewReader(raw))
|
||
dec.UseNumber()
|
||
var v any
|
||
if err := dec.Decode(&v); err != nil {
|
||
return "", err
|
||
}
|
||
var b strings.Builder
|
||
if err := canonicalValue(&b, v); err != nil {
|
||
return "", err
|
||
}
|
||
return b.String(), nil
|
||
}
|
||
|
||
func canonicalValue(b *strings.Builder, v any) error {
|
||
switch x := v.(type) {
|
||
case nil:
|
||
b.WriteString("null")
|
||
case bool:
|
||
b.WriteString(strconv.FormatBool(x))
|
||
case json.Number:
|
||
f, err := strconv.ParseFloat(string(x), 64)
|
||
if err != nil || math.IsInf(f, 0) || math.IsNaN(f) {
|
||
return fmt.Errorf("the number %s is not a double", x)
|
||
}
|
||
b.WriteString(es6Number(f))
|
||
case string:
|
||
canonicalString(b, x)
|
||
case []any:
|
||
b.WriteByte('[')
|
||
for i, e := range x {
|
||
if i > 0 {
|
||
b.WriteByte(',')
|
||
}
|
||
if err := canonicalValue(b, e); err != nil {
|
||
return err
|
||
}
|
||
}
|
||
b.WriteByte(']')
|
||
case map[string]any:
|
||
names := make([]string, 0, len(x))
|
||
for k := range x {
|
||
names = append(names, k)
|
||
}
|
||
sort.Slice(names, func(i, j int) bool { return utf16Less(names[i], names[j]) })
|
||
b.WriteByte('{')
|
||
for i, k := range names {
|
||
if i > 0 {
|
||
b.WriteByte(',')
|
||
}
|
||
canonicalString(b, k)
|
||
b.WriteByte(':')
|
||
if err := canonicalValue(b, x[k]); err != nil {
|
||
return err
|
||
}
|
||
}
|
||
b.WriteByte('}')
|
||
default:
|
||
return fmt.Errorf("a value of type %T", v)
|
||
}
|
||
return nil
|
||
}
|
||
|
||
// es6Number is a double as ECMAScript's Number.prototype.toString writes it (RFC 8785 §3.2.2.3).
|
||
func es6Number(f float64) string {
|
||
if f == 0 {
|
||
return "0"
|
||
}
|
||
abs := math.Abs(f)
|
||
if abs >= 1e-6 && abs < 1e21 {
|
||
return strconv.FormatFloat(f, 'f', -1, 64)
|
||
}
|
||
s := strconv.FormatFloat(f, 'e', -1, 64) // 1e+21, 1.5e-07
|
||
mant, exp, _ := strings.Cut(s, "e")
|
||
sign, digits := exp[:1], strings.TrimLeft(exp[1:], "0")
|
||
return mant + "e" + sign + digits
|
||
}
|
||
|
||
func canonicalString(b *strings.Builder, s string) {
|
||
b.WriteByte('"')
|
||
for _, r := range s {
|
||
switch {
|
||
case r == '"':
|
||
b.WriteString(`\"`)
|
||
case r == '\\':
|
||
b.WriteString(`\\`)
|
||
case r == '\b':
|
||
b.WriteString(`\b`)
|
||
case r == '\f':
|
||
b.WriteString(`\f`)
|
||
case r == '\n':
|
||
b.WriteString(`\n`)
|
||
case r == '\r':
|
||
b.WriteString(`\r`)
|
||
case r == '\t':
|
||
b.WriteString(`\t`)
|
||
case r < 0x20:
|
||
fmt.Fprintf(b, `\u%04x`, r)
|
||
default:
|
||
b.WriteRune(r)
|
||
}
|
||
}
|
||
b.WriteByte('"')
|
||
}
|
||
|
||
func utf16Less(a, b string) bool {
|
||
ua, ub := utf16Of(a), utf16Of(b)
|
||
for i := 0; i < len(ua) && i < len(ub); i++ {
|
||
if ua[i] != ub[i] {
|
||
return ua[i] < ub[i]
|
||
}
|
||
}
|
||
return len(ua) < len(ub)
|
||
}
|
||
|
||
func utf16Of(s string) []uint16 {
|
||
out := make([]uint16, 0, utf8.RuneCountInString(s))
|
||
for _, r := range s {
|
||
if r >= 0x10000 {
|
||
r -= 0x10000
|
||
out = append(out, uint16(0xD800+(r>>10)), uint16(0xDC00+(r&0x3FF)))
|
||
} else {
|
||
out = append(out, uint16(r))
|
||
}
|
||
}
|
||
return out
|
||
}
|
||
|
||
// Fingerprint is how an act is shown: its digest's first 24 hexadecimal digits in groups of four (§10).
|
||
func Fingerprint(digest string) string {
|
||
hex := strings.TrimPrefix(digest, "sha256:")
|
||
if len(hex) > 24 {
|
||
hex = hex[:24]
|
||
}
|
||
var groups []string
|
||
for len(hex) > 4 {
|
||
groups, hex = append(groups, hex[:4]), hex[4:]
|
||
}
|
||
return strings.Join(append(groups, hex), " ")
|
||
}
|
||
|
||
// The bounds of the arguments shown in an ask (§11).
|
||
const (
|
||
ShownBytes, ShownLines = 2000, 40
|
||
PartBytes, PartLines = 1200, 20
|
||
)
|
||
|
||
// shown is a call's arguments as an ask shows them (§11), one per line as "name: value", and whether all were shown whole:
|
||
// an argument whole where outward.Check passes it, else its shape, ‹path›, ‹address› or ‹withheld›; the whole when it is at
|
||
// most ShownBytes and ShownLines, else its first PartLines lines and PartBytes bytes, marked NOT SHOWN IN FULL.
|
||
func shown(raw json.RawMessage) (string, bool) {
|
||
args, _ := arguments(raw)
|
||
names := make([]string, 0, len(args))
|
||
for n := range args {
|
||
names = append(names, n)
|
||
}
|
||
sort.Strings(names)
|
||
whole := true
|
||
var lines []string
|
||
for _, n := range names {
|
||
var value string
|
||
if json.Unmarshal(args[n], &value) != nil {
|
||
value = string(args[n])
|
||
}
|
||
if r, ok := outward.Check(value); !ok {
|
||
value, whole = shapeOf(r), false
|
||
}
|
||
lines = append(lines, n+": "+value)
|
||
}
|
||
text := strings.Join(lines, "\n")
|
||
if all := strings.Split(text, "\n"); len(text) > ShownBytes || len(all) > ShownLines {
|
||
if len(all) > PartLines {
|
||
all = all[:PartLines]
|
||
}
|
||
text = strings.Join(all, "\n")
|
||
if len(text) > PartBytes {
|
||
text = cutBytes(text, PartBytes)
|
||
}
|
||
text += "\nNOT SHOWN IN FULL"
|
||
whole = false
|
||
}
|
||
return text, whole
|
||
}
|
||
|
||
// shapeOf is what an argument outward.Check refuses is shown as.
|
||
func shapeOf(r outward.Refusal) string {
|
||
switch r.Class {
|
||
case "path":
|
||
return "‹path›"
|
||
case "address":
|
||
return "‹address›"
|
||
}
|
||
return "‹withheld›"
|
||
}
|
||
|
||
// cutBytes is s at most n bytes, cut at a character's end.
|
||
func cutBytes(s string, n int) string {
|
||
for n > 0 && !utf8.RuneStart(s[n]) {
|
||
n--
|
||
}
|
||
return s[:n]
|
||
}
|