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
177 lines
5.9 KiB
Go
177 lines
5.9 KiB
Go
package approval
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"strings"
|
|
|
|
"git.novox.be/novox/mesh-sdk/go/operatorask"
|
|
)
|
|
|
|
// Run is a call through the gate (§20): an unmarked call runs; a marked one runs only on the operator's approval of that
|
|
// one call, judged on the router's record and claimed once (§14, §15), and is otherwise answered as an error saying why
|
|
// (§12, §17, §19). run is called at most once.
|
|
func (g *Gate) Run(c Call, run func() (any, error)) (any, error) {
|
|
m, err := g.marksOf(c)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if missing := g.unservedOf(c.Module); len(missing) > 0 {
|
|
return nil, fmt.Errorf("Not run: %s's marks name %s, which its bundle on %s does not register, so none of its tools is "+
|
|
"served here (design 46 7-approval-mark §4)", c.Module, strings.Join(missing, ", "), g.d.Node)
|
|
}
|
|
marked, _ := m.Marked(c.Seat, c.Name)
|
|
since := g.markedSince(c, marked, m.Revision)
|
|
if !marked {
|
|
return run()
|
|
}
|
|
if c.Own {
|
|
return nil, g.refused(c, "", "it is one of the tool runner's own tools, which no ask covers")
|
|
}
|
|
if m.Unread != "" {
|
|
return nil, g.refused(c, "", m.Unread)
|
|
}
|
|
if b, ok := g.d.State.(Binding); g.d.State == nil || ok && !b.Bound() {
|
|
return nil, g.refused(c, "", "the tool runner's membership binds no state "+StateName+" to keep its asks in")
|
|
}
|
|
if why := refusedUnasked(c.Args); why != "" {
|
|
return nil, g.refused(c, "", why)
|
|
}
|
|
if why := g.held(); 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())
|
|
}
|
|
named, err := g.d.State.Get(g.actKey(digest))
|
|
if err != nil {
|
|
g.stateFailed(err)
|
|
return nil, g.refused(c, digest, "its state "+StateName+" cannot be read: "+err.Error())
|
|
}
|
|
g.stateRead()
|
|
if named == nil {
|
|
return nil, g.raise(c, act, digest, since)
|
|
}
|
|
var id string
|
|
if err := json.Unmarshal(named.Value, &id); err != nil || id == "" {
|
|
return nil, g.refused(c, digest, fmt.Sprintf("its act's key in %s names no ask", StateName))
|
|
}
|
|
return g.judge(c, act, digest, id, since, run)
|
|
}
|
|
|
|
// Binding is a Store that says whether the runner's membership binds the state now (§13).
|
|
type Binding interface{ Bound() bool }
|
|
|
|
// marksOf is the module's marks as the catalog half has them now; before its first view, the call waits for it within
|
|
// RecordWithin and is then judged, refused naming mesh_first_views when it is still unread (§19).
|
|
func (g *Gate) marksOf(c Call) (Marks, error) {
|
|
if g.d.Catalog == nil {
|
|
return Marks{}, g.refused(c, "", "the tool runner reads no catalog half, so its marks are not read")
|
|
}
|
|
if err := g.d.Catalog.Ready(); err != nil {
|
|
if g.d.Wait != nil {
|
|
err = g.d.Wait(RecordWithin)
|
|
}
|
|
if err != nil {
|
|
return Marks{}, fmt.Errorf("Not run: the marks of %s are not read yet (%v); mesh_first_views says what the tool runner "+
|
|
"waits for", c.Module, err)
|
|
}
|
|
}
|
|
m := Derive(g.d.Catalog, c.Module, c.Fingerprint)
|
|
g.checkServed(m)
|
|
return m, nil
|
|
}
|
|
|
|
func (g *Gate) unservedOf(module string) []string {
|
|
g.mu.Lock()
|
|
defer g.mu.Unlock()
|
|
return g.unserved[module]
|
|
}
|
|
|
|
// markedSince is the revision a call was marked at when the same caller last called it unmarked (§12), empty otherwise.
|
|
func (g *Gate) markedSince(c Call, marked bool, revision string) string {
|
|
key := c.Caller + " " + c.Address
|
|
g.mu.Lock()
|
|
defer g.mu.Unlock()
|
|
if g.unmarkedAt == nil {
|
|
g.unmarkedAt = map[string]bool{}
|
|
}
|
|
was := g.unmarkedAt[key]
|
|
if marked {
|
|
delete(g.unmarkedAt, key)
|
|
if was {
|
|
return revision
|
|
}
|
|
return ""
|
|
}
|
|
g.unmarkedAt[key] = true
|
|
return ""
|
|
}
|
|
|
|
// judge is the same call made again, judged on the router's own record of its act's newest ask (§14 to §17).
|
|
func (g *Gate) judge(c Call, act operatorask.Act, digest, id, since string, run func() (any, error)) (any, error) {
|
|
rec, err := g.record(c.Module, id)
|
|
if err != nil {
|
|
return nil, g.refused(c, digest, "the router's record of its ask "+id+" cannot be read: "+err.Error())
|
|
}
|
|
if rec == nil {
|
|
return nil, g.firstAnswer(c, id, digest, since) // §17: not yet taken by the router
|
|
}
|
|
again := func(why string) (any, error) {
|
|
err := g.raise(c, act, digest, since)
|
|
return nil, fmt.Errorf("The ask %s %s; asked again. %w", id, why, err)
|
|
}
|
|
switch rec.State {
|
|
case "open", "held-back":
|
|
return nil, fmt.Errorf("Not run: still waiting for the operator's answer to ask %s (fingerprint %s); it expires at %s",
|
|
id, Fingerprint(digest), rec.Expires)
|
|
case "refused":
|
|
return nil, g.refused(c, digest, "the router refused its ask "+id+": "+orNone(rec.Answer))
|
|
case "chosen":
|
|
default: // expired, cancelled, replaced: each asks again (§15, §17)
|
|
return again("ended " + rec.State)
|
|
}
|
|
w := rec.Warrant
|
|
if w == nil {
|
|
return nil, g.refused(c, digest, "the router's record of its ask "+id+" is chosen and carries no warrant")
|
|
}
|
|
at, _ := parseTime(w.At)
|
|
if w.Option == OptionDecline {
|
|
if until := at.Add(DeclinedFor); g.d.Now().Before(until) {
|
|
return nil, fmt.Errorf("Not run: the operator declined ask %s; the same call is not asked again until %s",
|
|
id, formatTime(until))
|
|
}
|
|
return again("was declined more than 10 minutes ago")
|
|
}
|
|
if g.d.Now().After(at.Add(RunWithin)) {
|
|
return again("was approved too long ago (more than 30 minutes)")
|
|
}
|
|
var answer any
|
|
var ran error
|
|
h, err := g.d.Channel.Claim(c.Module, *w, act, func() { answer, ran = run() })
|
|
switch {
|
|
case h == ClaimDone:
|
|
return again("was already run")
|
|
case h == ClaimRefused:
|
|
return nil, g.refused(c, digest, "its warrant does not hold: "+errText(err))
|
|
case h == "" && err != nil:
|
|
g.stateFailed(err)
|
|
return nil, g.refused(c, digest, "its state "+StateName+" cannot be written: "+err.Error())
|
|
case err != nil: // the act about to run is not the one the option bound: nothing runs, the warrant stays used
|
|
return nil, g.refused(c, digest, err.Error())
|
|
}
|
|
return g.ran(c, id, rec, answer, ran)
|
|
}
|
|
|
|
func errText(err error) string {
|
|
if err == nil {
|
|
return "no reason given"
|
|
}
|
|
return err.Error()
|
|
}
|