Main reverted G's half merge (#189, reverted by8afc3ebin #191) and then took the reader release's S1-S5, which copied G's code. G's form holds where they differ (process_bus.go, seats.go, state.go and their tests); the release's kept names stay so main's callers keep building: SeatViewPrefix beside SeatReadPrefix, and QueueInvoke's empty strings on not-ok (with its test TestQueueInvoke). The tree differs from G's headf30dee54only by those and the release's two tests (objects cap option, seat states and stream kinds). Builds: design 47 G (the milestone's sdk part, as atf30dee54) Claude-Session: https://claude.ai/code/session_01NcbgfGKaxw984XyAPfLm4h
240 lines
8.8 KiB
Go
240 lines
8.8 KiB
Go
package approval
|
|
|
|
import (
|
|
"crypto/rand"
|
|
"encoding/hex"
|
|
"encoding/json"
|
|
"fmt"
|
|
"strings"
|
|
"time"
|
|
|
|
stdio "git.novox.be/novox/mesh-sdk/go"
|
|
"git.novox.be/novox/mesh-sdk/go/keys"
|
|
"git.novox.be/novox/mesh-sdk/go/operatorask"
|
|
)
|
|
|
|
// 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"
|
|
)
|
|
|
|
// 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.prefix() + "act." + keys.EscapeName(digest) }
|
|
|
|
// prefix is what the gate's keys begin with: the tool runner's machine, `<node>.`, since its state is shared by every
|
|
// machine's runner (§13); none for a module's process, whose state approvals is its module's own (design 47 G §677).
|
|
func (g *Gate) prefix() string {
|
|
if g.d.Holding {
|
|
return ""
|
|
}
|
|
return g.d.Node + "."
|
|
}
|
|
|
|
// askKey is the key the client keeps an ask under (§13).
|
|
func (g *Gate) askKey(id string) string { return g.prefix() + "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) 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
|
|
}
|
|
how := "called through the mesh's tools, which cannot tell who called."
|
|
if g.d.Holding {
|
|
how = "called by " + c.Caller + "."
|
|
}
|
|
lines := []string{first, "On " + g.d.Node + ", " + how}
|
|
if args != "" {
|
|
lines = append(lines, args)
|
|
}
|
|
if !whole {
|
|
read := "mesh_approvals on " + g.d.Node
|
|
if g.d.Holding {
|
|
read = c.Seat + ".approvals"
|
|
}
|
|
lines = append(lines, "The whole is read with "+read+", whose fingerprint must be the one on the phone.")
|
|
}
|
|
lines = append(lines, "Fingerprint "+Fingerprint(digest))
|
|
return Ask{ID: id, About: digest, Headline: headline,
|
|
Explanation: strings.Join(lines, "\n"), Expires: formatTime(g.d.Now().Add(AskLasts)), OnExpiry: OnExpiry,
|
|
Options: []Option{
|
|
{ID: OptionApprove, Label: "Approve", Does: "the same call, made again within 30 minutes, runs once", Binds: digest},
|
|
{ID: OptionDecline, Label: "Decline", Does: "nothing runs; the same call is not asked again for 10 minutes", 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 := 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 := operatorask.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 operatorask.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 operatorask.Act, digest string, a Ask) error {
|
|
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 := g.d.Channel.Ask(c.Module, 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 Ask, act operatorask.Act, args json.RawMessage) int {
|
|
raw, _ := json.Marshal(Kept[Ask, Warrant]{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 operatorask.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 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) (*Record, error) {
|
|
return g.d.Channel.Record(module, id)
|
|
}
|
|
|
|
// 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) (*Record, 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("[approval] %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)
|
|
}
|