Files
mesh-tools/node-tools/internal/approval/ask.go
T
jschoubben 7b37e7e1d6 A3c item 2: the gate's whole test on the real bus; the act's key escapes its digest
Design 46 7-approval-mark §22's integration test: the runner's gate wired as main wires it, on a
real bus with stand-ins of the operator channel's router and the hand-act seat: a marked call
asked, approved, run once, recorded, listed; a restart between the run and warranted's answer
recorded once; a Decline runs nothing; no state bound or no holder of operator-channel runs
nothing. It failed first on the act's key: `<node>.act.sha256:<hex>` is a key the bus refuses,
so every marked call was refused on the live bus. The digest is now escaped with
keys.EscapeName (lead answer A-209). The runner's two start tests are one, so the module gains
no test (design 47 G §419, §626).

Builds: design 46 7-approval-mark §13, §22; design 47 G §419, §626; lead answer A-209

Claude-Session: https://claude.ai/code/session_01NcbgfGKaxw984XyAPfLm4h
2026-11-12 06:02:17 +01:00

243 lines
9.6 KiB
Go

package approval
import (
"crypto/rand"
"encoding/hex"
"encoding/json"
"fmt"
"strings"
"time"
"git.novox.be/novox/mesh-contracts/operator-and-conversation/go/clients/operatorchannelclient"
seat "git.novox.be/novox/mesh-contracts/operator-and-conversation/go/seats/operatorchannel"
stdio "git.novox.be/novox/mesh-sdk/go"
"git.novox.be/novox/mesh-sdk/go/keys"
)
// StateName is the runner's own state its asks and claims are kept in, per machine (§13).
const StateName = "approvals"
// Store is the runner's state `approvals` (§13): a key's value, a create that refuses a key that exists and an update by
// compare-and-set, each failing with stdio.ErrChanged when another write came first; a put, and its keys.
type Store interface {
Get(key string) (*stdio.StateEntry, error)
Create(key string, value any) (uint64, error)
Update(key string, value any, revision uint64) (uint64, error)
Put(key string, value any) (uint64, error)
Keys() ([]string, error)
}
// The bounds of an ask (§10, §12, §13, §14, §17).
const (
AskLasts = 60 * time.Minute
RecordWithin = 5 * time.Second
RunWithin = 30 * time.Minute
DeclinedFor = 10 * time.Minute
MostKeptValue = 256 * 1024
OnExpiry = "nothing runs; call again to ask anew"
)
// The options every ask offers (§10), both at level approve, both binding the act's digest.
const (
OptionApprove = "approve"
OptionDecline = "decline"
)
// client is operator-channel's client asking under the marked module's name, its asks kept in the runner's state under
// this machine's keys, its publish and the router's record on the runner's own account (§7, §10).
func (g *Gate) client(module string) *operatorchannelclient.Client {
return &operatorchannelclient.Client{Module: module, State: g.d.State, KeyPrefix: g.d.Node + ".", Now: g.d.Now,
Publish: func(subject string, body any, id string) error { return g.d.Publish(module, subject, body, id) },
Record: func(id string) (*stdio.StateEntry, error) { return g.d.Record(module, id) }}
}
// actKey is the key naming the newest ask of an act (§13), its digest escaped as a key's name is, since the bus holds no `:` in a key (lead answer A-209).
func (g *Gate) actKey(digest string) string { return g.d.Node + ".act." + keys.EscapeName(digest) }
// askKey is the key the client keeps an ask under (§13).
func (g *Gate) askKey(id string) string { return g.d.Node + ".ask." + id }
// newID is an ask's id: 128 random bits.
func newID() string {
var b [16]byte
_, _ = rand.Read(b[:])
return hex.EncodeToString(b[:])
}
// askFor is the ask a marked call raises (§10, §11).
func (g *Gate) askFor(c Call, digest, id string) seat.Ask {
args, whole := shown(c.Args)
name := c.Name
if c.Seat != "" {
name = c.Seat + "." + c.Name
}
headline := headlineOf("Run", name, g.d.Node, whole)
first, _, _ := strings.Cut(strings.TrimSpace(c.Description), "\n")
if first == "" {
first = name
}
lines := []string{first, "On " + g.d.Node + ", called through the mesh's tools, which cannot tell who called."}
if args != "" {
lines = append(lines, args)
}
if !whole {
lines = append(lines, "The whole is read with mesh_approvals on "+g.d.Node+", whose fingerprint must be the one on the phone.")
}
lines = append(lines, "Fingerprint "+Fingerprint(digest))
return seat.Ask{ID: id, About: digest, Who: operatorchannelclient.Operator, Headline: headline,
Explanation: strings.Join(lines, "\n"), Expires: operatorchannelclient.FormatTime(g.d.Now().Add(AskLasts)), OnExpiry: OnExpiry,
Options: []seat.Option{
{ID: OptionApprove, Label: "Approve", Does: "the same call, made again within 30 minutes, runs once",
Level: operatorchannelclient.Approve, Binds: digest},
{ID: OptionDecline, Label: "Decline", Does: "nothing runs; the same call is not asked again for 10 minutes",
Level: operatorchannelclient.Approve, Binds: digest}}}
}
// headlineOf is "<verb> <names> on <node>?", "<verb> <names>?" when that is longer than the router allows, and the names cut
// with "…" when that too is longer; ending "Part shown" whenever an argument is not shown whole (§10, Q-HL). verb is "Run"
// for a call and "Wait on" for a wait (design 34 every-event §35).
func headlineOf(verb, name, node string, whole bool) string {
suffix := ""
if !whole {
suffix = " Part shown"
}
max := operatorchannelclient.HeadlineLength
fits := func(s string) bool { return len([]rune(s)) <= max }
if h := verb + " " + name + " on " + node + "?" + suffix; fits(h) {
return h
}
if h := verb + " " + name + "?" + suffix; fits(h) {
return h
}
room := max - len([]rune(verb+" …?"+suffix))
r := []rune(name)
if room < 1 {
room = 1
}
if len(r) > room {
r = r[:room]
}
return verb + " " + string(r) + "…?" + suffix
}
// firstCall is a marked call whose act has no ask open: refused at once when its arguments may not be asked (§9), else
// asked (§10, §12).
func (g *Gate) firstCall(c Call) (any, error) {
if why := refusedUnasked(c.Args); why != "" {
return nil, g.refused(c, "", why)
}
act, err := ActOf(c, g.d.Node)
if err != nil {
return nil, g.refused(c, "", err.Error())
}
digest, err := operatorchannelclient.ActDigest(act)
if err != nil {
return nil, g.refused(c, "", err.Error())
}
return nil, g.raise(c, act, digest, "")
}
// raise raises the ask of a marked call whose act has none open (§10): kept in the runner's state with the act and the whole
// arguments, published under the module's name, its act's key naming it; then the 5 seconds for the router's record and
// the first answer (§12), always an error, never a result.
func (g *Gate) raise(c Call, act operatorchannelclient.Act, digest, markedSince string) error {
id := newID()
if err := g.keepAsk(c, act, digest, g.askFor(c, digest, id)); err != nil {
return err
}
return g.firstAnswer(c, id, digest, markedSince)
}
// keepAsk raises an ask and keeps it in the runner's state with the act and the whole arguments, its act's key naming it
// (§10, §13); refused, as an error, when it cannot be.
func (g *Gate) keepAsk(c Call, act operatorchannelclient.Act, digest string, a seat.Ask) error {
cl := g.client(c.Module)
if size := keptSize(a, act, c.Args); size > MostKeptValue {
return g.refused(c, digest, fmt.Sprintf("its ask would keep %d bytes, more than the %d a value of the state %s holds", size, MostKeptValue, StateName))
}
if err := cl.Ask(a); err != nil {
return g.refused(c, digest, "the ask could not be raised: "+err.Error())
}
if err := g.keepCall(a.ID, act, c.Args); err != nil {
g.stateFailed(err)
return g.refused(c, digest, "the ask was raised and its call could not be kept: "+err.Error())
}
if _, err := g.d.State.Put(g.actKey(digest), a.ID); err != nil {
g.stateFailed(err)
return g.refused(c, digest, "the ask was raised and its act could not be kept: "+err.Error())
}
return nil
}
// keptSize is the bytes the kept ask would take.
func keptSize(a seat.Ask, act operatorchannelclient.Act, args json.RawMessage) int {
raw, _ := json.Marshal(operatorchannelclient.Kept{Ask: a, Act: act, Arguments: args})
return len(raw)
}
// keepCall writes the act and the whole arguments beside the ask the client kept (§13).
func (g *Gate) keepCall(id string, act operatorchannelclient.Act, args json.RawMessage) error {
e, err := g.d.State.Get(g.askKey(id))
if err != nil || e == nil {
return fmt.Errorf("the kept ask %s cannot be read: %v", id, err)
}
var k operatorchannelclient.Kept
if err := json.Unmarshal(e.Value, &k); err != nil {
return err
}
k.Act, k.Arguments = act, args
_, err = g.d.State.Update(g.askKey(id), k, e.Revision)
return err
}
// firstAnswer waits up to RecordWithin for the router's record of the ask and says the call was not run (§12).
func (g *Gate) firstAnswer(c Call, id, digest, markedSince string) error {
rec, _ := g.awaitRecord(c.Module, id)
since := ""
if markedSince != "" {
since = "; marked since revision " + markedSince
}
if rec == nil {
return fmt.Errorf("Not run, asked; not yet taken by the router (ask %s, fingerprint %s); make the same call again later%s",
id, Fingerprint(digest), since)
}
return fmt.Errorf("Not run: this needs the operator's approval. Asked on %s at %s (ask %s, fingerprint %s); make the same call "+
"again within 30 minutes of the approval; the ask expires at %s%s", orNone(strings.Join(rec.OfferedOn, ", ")), rec.Opened,
id, Fingerprint(digest), rec.Expires, since)
}
// record is the router's record of an ask under the module's name, nil when it holds none.
func (g *Gate) record(module, id string) (*seat.AsksByAskerIDValue, error) {
e, err := g.d.Record(module, id)
if err != nil || e == nil {
return nil, err
}
var rec seat.AsksByAskerIDValue
if err := json.Unmarshal(e.Value, &rec); err != nil {
return nil, fmt.Errorf("the router's record of %s cannot be read: %w", id, err)
}
return &rec, nil
}
// awaitRecord reads the router's record of a new ask until it is there, within RecordWithin: the seat answers a key's
// value and says nothing when the router takes an ask, so it is read again after a growing pause (§12).
func (g *Gate) awaitRecord(module, id string) (*seat.AsksByAskerIDValue, error) {
until := g.d.Now().Add(RecordWithin)
pause := 50 * time.Millisecond
for {
rec, err := g.record(module, id)
if rec != nil || g.d.Now().Add(pause).After(until) {
return rec, err
}
<-g.d.After(pause)
pause *= 2
}
}
// refused is a marked call refused, run nothing, written in the runner's log with the ask's fingerprint and never its
// arguments (§19).
func (g *Gate) refused(c Call, digest, why string) error {
g.d.Logf("[mesh-tools] %s on %s is refused (fingerprint %s): %s", c.Address, g.d.Node, orNone(Fingerprint(digest)), why)
return fmt.Errorf("Not run: %s needs the operator's approval and %s", c.Address, why)
}