131 lines
5.6 KiB
Go
131 lines
5.6 KiB
Go
package main
|
|
|
|
// A module's act on the operator's warrant, recorded in the hand-act log (novox/hq ADR 0274, ADR 0259 §6).
|
|
//
|
|
// mesh-controller hand-act warrant --asker <module> --ask <id>
|
|
//
|
|
// The verb `warranted` runs it. A module that asks the operator (an asker) acts on the warrant with its own grants;
|
|
// the controller's log is where a person's decisions are read back, so the module asks the controller to record
|
|
// it. **What is recorded is the router's word, never the caller's**: the controller reads the router's own record
|
|
// of that asker's ask — the bus lets only the router write it — and records who chose, through which channel, with
|
|
// which proofs, and which answer. The caller gives nothing but which ask: a word of its own, recorded first under
|
|
// the one id, would stand for every node's (the review of 2026-10-10). Recorded once per
|
|
// ask, under an id the ask decides, however many of the module's instances ask; an ask still open, ended without
|
|
// a choice, or another asker's is refused and nothing is written.
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"errors"
|
|
"flag"
|
|
"fmt"
|
|
"regexp"
|
|
|
|
"github.com/nats-io/nats.go"
|
|
|
|
"git.novox.be/novox/mesh-sdk/go/asks"
|
|
|
|
"github.com/novox/mesh-controller/internal/conditions"
|
|
"github.com/novox/mesh-controller/internal/link"
|
|
)
|
|
|
|
var askerModule = regexp.MustCompile(`^[a-z0-9][a-z0-9-]{0,62}$`)
|
|
|
|
// warrantedID is the one entry an ask's warrant is recorded under.
|
|
func warrantedID(asker, ask string) string { return "warrant-" + asker + "-" + ask }
|
|
|
|
// warrantedAct is the entry for an asker's act on the warrant the router recorded for its ask (state, w), or why
|
|
// none is written.
|
|
func warrantedAct(asker, ask, caller, state string, w *asks.Warrant) (link.HandAct, error) {
|
|
switch {
|
|
case !askerModule.MatchString(asker):
|
|
return link.HandAct{}, fmt.Errorf("%q is not a module's name", asker)
|
|
case asker == askerName:
|
|
return link.HandAct{}, errors.New("the controller records its own acts on a warrant as it performs them")
|
|
case !asks.UsableID(ask):
|
|
return link.HandAct{}, fmt.Errorf("%q is not an ask's id", ask)
|
|
case state == "" || w == nil:
|
|
return link.HandAct{}, fmt.Errorf("the router holds no closed record of %s's ask %s", asker, ask)
|
|
case state == "open":
|
|
return link.HandAct{}, fmt.Errorf("%s's ask %s is still open: nobody has answered it", asker, ask)
|
|
case w.Asker != asker || w.Ask != ask:
|
|
return link.HandAct{}, fmt.Errorf("the router's record is for %s's ask %s", w.Asker, w.Ask)
|
|
case w.Outcome != asks.OutcomeChosen || w.By == nil:
|
|
return link.HandAct{}, fmt.Errorf("%s's ask %s ended %s: no person chose, so there is no warrant to record", asker, ask, w.Outcome)
|
|
case w.AskDigest == "":
|
|
return link.HandAct{}, fmt.Errorf("the router's warrant for %s's ask %s names no ask digest", asker, ask)
|
|
}
|
|
return link.HandAct{ID: warrantedID(asker, ask), Verb: handActWarrant, Args: []string{fmt.Sprintf("the operator chose %s on %s's ask %s", w.Label, asker, ask)},
|
|
Why: fmt.Sprintf("%s (ask %s of %s)", w.Says(), ask, asker), By: byWords(*w), Cause: conditions.CauseOperatorAnswer,
|
|
Via: viaWords(*w), Ask: ask, Proofs: w.Proofs, RequestedBy: asker + ", recorded at the word of " + caller,
|
|
Outcome: "chosen; what " + asker + " did with it is in its own record", At: w.At.UTC()}, nil
|
|
}
|
|
|
|
// readRouterRecord reads the router's record of one asker's ask: its state and warrant, or "" when there is none.
|
|
// The controller's grant reaches the JetStream API whole (`$JS.API.>`), so it reads any asker's record.
|
|
func readRouterRecord(ctx context.Context, conn *nats.Conn, bucket, asker, ask string) (string, *asks.Warrant, error) {
|
|
reply, err := conn.RequestWithContext(ctx, "$JS.API.DIRECT.GET.KV_"+bucket+".$KV."+bucket+"."+asker+"."+ask, nil)
|
|
if err != nil {
|
|
return "", nil, err
|
|
}
|
|
if status := reply.Header.Get("Status"); status != "" {
|
|
if status == "404" {
|
|
return "", nil, nil
|
|
}
|
|
return "", nil, fmt.Errorf("the router's record could not be read: %s %s", status, reply.Header.Get("Description"))
|
|
}
|
|
var rec struct {
|
|
State string `json:"state"`
|
|
Warrant *asks.Warrant `json:"warrant"`
|
|
}
|
|
if err := json.Unmarshal(reply.Data, &rec); err != nil {
|
|
return "", nil, fmt.Errorf("the router's record of %s's ask %s cannot be read: %w", asker, ask, err)
|
|
}
|
|
return rec.State, rec.Warrant, nil
|
|
}
|
|
|
|
func handActWarrantCommand(ctx context.Context, args []string) error {
|
|
set := flag.NewFlagSet("hand-act warrant", flag.ContinueOnError)
|
|
asker := set.String("asker", "", "the module that asked")
|
|
ask := set.String("ask", "", "its ask's id")
|
|
positionals, err := parseAround(set, args)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if *asker == "" || *ask == "" || len(positionals) > 0 {
|
|
return errors.New("hand-act warrant --asker <module> --ask <id>: what is recorded is the router's record, and nothing else")
|
|
}
|
|
open, err := openStores(ctx)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer open.Close()
|
|
bucket, err := asksRecords(ctx, open.inventory)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if bucket == "" {
|
|
return errors.New("no module declares the operator channel's records, so no warrant can be read")
|
|
}
|
|
return onTheBus(func(conn *nats.Conn) error {
|
|
state, w, err := readRouterRecord(ctx, conn, bucket, *asker, *ask)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
act, err := warrantedAct(*asker, *ask, link.Caller(), state, w)
|
|
if err != nil {
|
|
return fmt.Errorf("%w. Nothing was recorded", err)
|
|
}
|
|
written, err := link.RecordHandActOnce(ctx, conn, act)
|
|
if err != nil {
|
|
return fmt.Errorf("the warrant could not be recorded: %w", err)
|
|
}
|
|
if !written {
|
|
fmt.Printf("already recorded as %s: %s\n", act.ID, act.Why)
|
|
return nil
|
|
}
|
|
fmt.Printf("recorded as %s: %s, through %s\n", act.ID, act.Why, act.Via)
|
|
return nil
|
|
})
|
|
}
|