Files
mesh-tools/node-tools/internal/approval/run.go
T
jschoubben 65db969f72 A-5 failing closed: a marked call runs nothing when a part of the check is missing, and a stuck gate is said
A marked call is refused, asking nothing, when no module holds operator-channel (read from
the assigner's view of the seats' holders the runner follows, an unread view refusing too);
refused when the served entry cannot be read, the ask cannot be published, the router
refuses it or its record cannot be read; and while the state approvals cannot be read or
written no marked call runs on the machine, the failed read made again at the next marked
call and the next membership change, a bound key raising approvals-unreadable after five
minutes and the read again clearing it. Before the catalog half's first view a call waits
for it within 5 seconds, on the events the runner hears, and is refused naming
mesh_first_views when it is still unread. Each refusal is in the log with the ask's
fingerprint, never the arguments.

Builds: design 46 7-approval-mark §19, §5 (an unread served entry); lead answers A-157, A-154

Claude-Session: https://claude.ai/code/session_01NcbgfGKaxw984XyAPfLm4h
2026-11-12 05:20:46 +01:00

185 lines
6.3 KiB
Go

package approval
import (
"encoding/json"
"fmt"
"strings"
"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"
)
// 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 := operatorchannelclient.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 operatorchannelclient.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, _ := operatorchannelclient.Time(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, operatorchannelclient.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.client(c.Module).Take(*w, func(o seat.Option, _ seat.Warrant) error {
if err := operatorchannelclient.Performs(o, act); err != nil {
ran = err
return err
}
answer, ran = run()
return nil
})
switch {
case h == operatorchannelclient.Done:
return again("was already run")
case h == operatorchannelclient.Refused:
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()
}