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
173 lines
4.8 KiB
Go
173 lines
4.8 KiB
Go
package approval
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"sort"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
// The mesh MCP server's mesh_approvals reads and steers what the gate asked on this machine (§18).
|
|
|
|
// Listed is one ask of the last day as mesh_approvals lists it: its state and its act's fingerprint.
|
|
type Listed struct {
|
|
Ask string `json:"ask"`
|
|
Address string `json:"address"`
|
|
Module string `json:"module"`
|
|
Asked time.Time `json:"asked"`
|
|
State string `json:"state"`
|
|
Fingerprint string `json:"fingerprint"`
|
|
}
|
|
|
|
// Whole is one ask read whole: its arguments and the act's digest.
|
|
type Whole struct {
|
|
Listed
|
|
Digest string `json:"digest"`
|
|
Act map[string]string `json:"act"`
|
|
Arguments json.RawMessage `json:"arguments"`
|
|
Recorded bool `json:"recorded"`
|
|
}
|
|
|
|
// stateOf is a kept ask's state as the runner knows it.
|
|
func stateOf(acted, recorded bool, ended string) string {
|
|
switch {
|
|
case acted && recorded:
|
|
return "run, recorded"
|
|
case acted:
|
|
return "run, not yet recorded"
|
|
case ended != "":
|
|
return ended
|
|
}
|
|
return "asked"
|
|
}
|
|
|
|
// List is the calls asked on this machine in the last day, newest first (§18).
|
|
func (g *Gate) List() ([]Listed, error) {
|
|
if g.d.State == nil {
|
|
return nil, fmt.Errorf("the tool runner's membership binds no state %s", StateName)
|
|
}
|
|
keys, err := g.d.State.Keys()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
prefix := g.d.Node + ".ask."
|
|
out := []Listed{}
|
|
for _, key := range keys {
|
|
if !strings.HasPrefix(key, prefix) {
|
|
continue
|
|
}
|
|
w, err := g.Read(strings.TrimPrefix(key, prefix))
|
|
if err != nil || g.d.Now().Sub(w.Asked) > 24*time.Hour {
|
|
continue
|
|
}
|
|
out = append(out, w.Listed)
|
|
}
|
|
sort.Slice(out, func(i, j int) bool { return out[i].Asked.After(out[j].Asked) })
|
|
return out, nil
|
|
}
|
|
|
|
// Read is one ask whole: its arguments and the act's digest (§18).
|
|
func (g *Gate) Read(id string) (Whole, error) {
|
|
if g.d.State == nil {
|
|
return Whole{}, fmt.Errorf("the tool runner's membership binds no state %s", StateName)
|
|
}
|
|
k, _, err := g.kept(id)
|
|
if err != nil {
|
|
return Whole{}, err
|
|
}
|
|
if k == nil {
|
|
return Whole{}, fmt.Errorf("no ask %s is kept on %s", id, g.d.Node)
|
|
}
|
|
module := k.Act["module"]
|
|
if module == "" {
|
|
module = moduleOf(k)
|
|
}
|
|
var ask struct {
|
|
About string `json:"about"`
|
|
}
|
|
_ = json.Unmarshal(k.Ask, &ask)
|
|
return Whole{Listed: Listed{Ask: id, Address: k.Act["address"], Module: module, Asked: k.Asked,
|
|
State: stateOf(k.Acted, k.Recorded, k.Ended), Fingerprint: Fingerprint(ask.About)},
|
|
Digest: ask.About, Act: k.Act, Arguments: k.Arguments, Recorded: k.Recorded}, nil
|
|
}
|
|
|
|
// Applied are the marks the gate applies on this machine, each module's marked tools and verbs with their source and the
|
|
// catalog revision read (§18).
|
|
func (g *Gate) Applied() []Marks {
|
|
g.mu.Lock()
|
|
modules := make([]string, 0, len(g.served))
|
|
for m := range g.served {
|
|
modules = append(modules, m)
|
|
}
|
|
g.mu.Unlock()
|
|
sort.Strings(modules)
|
|
var out []Marks
|
|
if g.d.Catalog == nil {
|
|
return out
|
|
}
|
|
for _, m := range modules {
|
|
if marks := Derive(g.d.Catalog, m, g.servedFingerprint(m)); marks.Any() {
|
|
out = append(out, marks)
|
|
}
|
|
}
|
|
return out
|
|
}
|
|
|
|
func (g *Gate) servedFingerprint(module string) string {
|
|
g.mu.Lock()
|
|
defer g.mu.Unlock()
|
|
return g.served[module]
|
|
}
|
|
|
|
// Describe says whether a tool or a seat verb of a module is marked, and its source, for mesh_describe (§18): the served
|
|
// build's marks where this machine serves the module, else the newest entry's and its seat's.
|
|
func (g *Gate) Describe(module, seat, name string) (bool, string) {
|
|
if g.d.Catalog == nil || g.d.Catalog.Ready() != nil {
|
|
return false, ""
|
|
}
|
|
if seat != "" && module == "" {
|
|
for s, d := range g.d.Catalog.Definitions() {
|
|
if s != seat {
|
|
continue
|
|
}
|
|
for _, v := range d.Verbs {
|
|
if v.Name == name && v.NeedsApproval {
|
|
return true, "seat " + seat
|
|
}
|
|
}
|
|
}
|
|
return false, ""
|
|
}
|
|
return Derive(g.d.Catalog, module, g.servedFingerprint(module)).Marked(seat, name)
|
|
}
|
|
|
|
// Again asks `warranted` again for one acted, unrecorded ask, by hand, in the kept-call form of §16 (§18); the caller
|
|
// records the hand act first.
|
|
func (g *Gate) Again(id string) error {
|
|
w, err := g.Read(id)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if !strings.HasPrefix(w.State, "run, not yet") {
|
|
return fmt.Errorf("the ask %s is %s: only a run not yet recorded is asked again", id, w.State)
|
|
}
|
|
g.recordRun(w.Module, id)
|
|
if again, _ := g.Read(id); !again.Recorded {
|
|
return fmt.Errorf("mesh-hand-acts.warranted has not answered for ask %s yet", id)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// Cancel takes an open ask back through operator-channel's client (§18); the caller records the hand act first.
|
|
func (g *Gate) Cancel(id string) error {
|
|
w, err := g.Read(id)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if w.State != "asked" {
|
|
return fmt.Errorf("the ask %s is %s, not open", id, w.State)
|
|
}
|
|
return g.d.Channel.Cancel(w.Module, id)
|
|
}
|