From 0e5aed125320281af7655fbba19e04675b4a6d28 Mon Sep 17 00:00:00 2001 From: jochen Date: Sat, 10 Oct 2026 03:40:49 +0200 Subject: [PATCH] Record a module's act on the operator's warrant from the router's own record (hq ADR 0274) A module that asks the operator acts with its own grants, and the hand-act log is where a person's decisions are read back. The new verb warranted records who chose, how and with which proofs from the router's record, never the caller's word, once per ask however many instances ask. --- cmd/mesh-controller/handacts.go | 3 + cmd/mesh-controller/seatverbs.go | 7 ++ cmd/mesh-controller/warranted.go | 133 ++++++++++++++++++++++++++ cmd/mesh-controller/warranted_test.go | 79 +++++++++++++++ internal/catalogue/verbs.go | 9 ++ internal/link/handacts.go | 27 ++++++ internal/link/handacts_test.go | 27 ++++++ module.json | 1 + 8 files changed, 286 insertions(+) create mode 100644 cmd/mesh-controller/warranted.go create mode 100644 cmd/mesh-controller/warranted_test.go diff --git a/cmd/mesh-controller/handacts.go b/cmd/mesh-controller/handacts.go index ae1916bf..fdfa987e 100644 --- a/cmd/mesh-controller/handacts.go +++ b/cmd/mesh-controller/handacts.go @@ -251,6 +251,9 @@ func handActCommand(ctx context.Context, args []string) error { if len(args) > 0 && args[0] == "drill" { return handActDrill(ctx, args[1:]) } + if len(args) > 0 && args[0] == "warrant" { + return handActWarrantCommand(ctx, args[1:]) + } if len(args) > 0 && args[0] != "list" && !strings.HasPrefix(args[0], "-") { return errors.New("hand-act record --why --cause | hand-act drill --why " + "| hand-acts [--days N] [--json]") diff --git a/cmd/mesh-controller/seatverbs.go b/cmd/mesh-controller/seatverbs.go index 48b17938..db546626 100644 --- a/cmd/mesh-controller/seatverbs.go +++ b/cmd/mesh-controller/seatverbs.go @@ -536,6 +536,11 @@ func (a *verbArguments) commandLine() ([]string, error) { argv = append(argv, "--condition", c) } return argv, nil + case "warranted": + if err := need("asker", "ask", "what"); err != nil { + return nil, err + } + return []string{"hand-act", "warrant", "--asker", str("asker"), "--ask", str("ask"), str("what")}, nil case "hand-acts": argv := []string{"hand-acts", "--json"} if d := str("days"); d != "" { @@ -935,6 +940,8 @@ func repairingCommand(argv []string) string { return "plans " + argv[1] case argv[0] == "broker" && len(argv) > 1 && argv[1] == "consumer-reset": return "broker consumer-reset" + case argv[0] == "hand-act" && len(argv) > 1 && argv[1] == "warrant": + return "" // the router's record of a person's answer, never a repair (novox/hq ADR 0274) case argv[0] == "hand-act" && len(argv) > 1 && argv[1] == "drill": return "hand-act drill" case argv[0] == "hand-act": diff --git a/cmd/mesh-controller/warranted.go b/cmd/mesh-controller/warranted.go new file mode 100644 index 00000000..90f1df9f --- /dev/null +++ b/cmd/mesh-controller/warranted.go @@ -0,0 +1,133 @@ +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 --ask +// +// 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 only what it did, said as its own words. 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" + "strings" + + "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, what, 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 strings.TrimSpace(what) == "": + return link.HandAct{}, errors.New("say what was done on the warrant") + 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{strings.TrimSpace(what)}, + 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: "done by " + asker, 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 + } + what := strings.TrimSpace(strings.Join(positionals, " ")) + if *asker == "" || *ask == "" || what == "" { + return errors.New("hand-act warrant --asker --ask ") + } + 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, what, 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; %s\n", act.ID, act.Why, act.Via, what) + return nil + }) +} diff --git a/cmd/mesh-controller/warranted_test.go b/cmd/mesh-controller/warranted_test.go new file mode 100644 index 00000000..7de5d4a2 --- /dev/null +++ b/cmd/mesh-controller/warranted_test.go @@ -0,0 +1,79 @@ +package main + +import ( + "slices" + "strings" + "testing" + "time" + + "git.novox.be/novox/mesh-sdk/go/asks" + + "github.com/novox/mesh-controller/internal/conditions" +) + +func chosenWarrant() *asks.Warrant { + return &asks.Warrant{Ask: "instr-1", Asker: "claude-code", Outcome: asks.OutcomeChosen, Option: "approve", + Label: "Approve", Level: asks.Approve, Channel: "telegram", Proofs: []string{"P1"}, + By: &asks.Person{Who: asks.Operator, Kind: "telegram", Identity: "42", Verified: "user id verified"}, + At: time.Date(2026, 10, 10, 4, 0, 0, 0, time.UTC), AskDigest: "sha256:ab"} +} + +// What is recorded of a module's act on a warrant is the router's word (novox/hq ADR 0274): who chose, how and +// with which proofs; the caller gives only what it did. Nothing is recorded without a person's choice. +func TestAWarrantIsRecordedFromTheRoutersRecordAlone(t *testing.T) { + act, err := warrantedAct("claude-code", "instr-1", "claude-code proposal instr-1 approved on shanks", + "node-tools.shanks", "chosen", chosenWarrant()) + if err != nil { + t.Fatal(err) + } + if act.ID != "warrant-claude-code-instr-1" || act.Verb != handActWarrant || act.Cause != conditions.CauseOperatorAnswer || + act.By != "the operator, as telegram identity 42" || act.Ask != "instr-1" || !slices.Equal(act.Proofs, []string{"P1"}) || + !strings.Contains(act.Why, "the operator, via telegram (user id verified), chose Approve") || + !strings.Contains(act.RequestedBy, "node-tools.shanks") { + t.Fatalf("recorded as %+v", act) + } + for name, c := range map[string]struct { + asker, ask, state string + w func() *asks.Warrant + }{ + "no record": {"claude-code", "instr-1", "", func() *asks.Warrant { return nil }}, + "still open": {"claude-code", "instr-1", "open", chosenWarrant}, + "another asker's": {"messenger", "instr-1", "chosen", chosenWarrant}, + "another ask's": {"claude-code", "instr-2", "chosen", chosenWarrant}, + "the controller's": {"mesh-controller", "instr-1", "chosen", chosenWarrant}, + "not a module": {"Claude Code", "instr-1", "chosen", chosenWarrant}, + "expired": {"claude-code", "instr-1", "expired", func() *asks.Warrant { + w := chosenWarrant() + w.Outcome, w.By = asks.OutcomeExpired, nil + return w + }}, + "no digest": {"claude-code", "instr-1", "chosen", func() *asks.Warrant { + w := chosenWarrant() + w.AskDigest = "" + return w + }}, + } { + if _, err := warrantedAct(c.asker, c.ask, "did it", "x", c.state, c.w()); err == nil { + t.Errorf("%s: recorded", name) + } + } +} + +func TestTheWarrantedVerbRunsTheWarrantLineWithoutAWhy(t *testing.T) { + argv, err := argvFor("warranted", map[string]any{"asker": "claude-code", "ask": "instr-1", "what": "approved on shanks"}) + if err != nil { + t.Fatal(err) + } + if !slices.Equal(argv, []string{"hand-act", "warrant", "--asker", "claude-code", "--ask", "instr-1", "approved on shanks"}) { + t.Fatalf("%v", argv) + } + if repairingCommand(argv) != "" { + t.Error("recording a person's answer is taken for a repair") + } + if terminalOnly(argv) != nil { + t.Error("the verb is kept for the terminal") + } + if _, err := argvFor("warranted", map[string]any{"asker": "claude-code"}); err == nil { + t.Error("a call naming no ask was taken") + } +} diff --git a/internal/catalogue/verbs.go b/internal/catalogue/verbs.go index a052ab98..adbccaea 100644 --- a/internal/catalogue/verbs.go +++ b/internal/catalogue/verbs.go @@ -338,6 +338,15 @@ var ControllerVerbs = []Verb{ "why": "what the drill tests", "condition": "the key of the condition the drill is meant to raise, if any (optional)", }, []string{"what", "why"})}, + {Name: "warranted", Description: "Record in the hand-act log what a module did on the operator's warrant (novox/hq " + + "ADR 0274, ADR 0259): who chose, through which channel, with which proofs and which answer are read from the " + + "router's own record of that module's ask, never from the caller; recorded once per ask however often it is " + + "asked. Refused for an ask still open, ended without a choice, or not that module's.", + Input: schema(map[string]string{ + "asker": "the module that asked, e.g. claude-code", + "ask": "its ask's id", + "what": "what it did on the warrant, in a line", + }, []string{"asker", "ask", "what"})}, {Name: "hand-acts", Description: "What was done by hand lately — pushes, plans ended, consumers re-made, acts " + "recorded — who, why and the cause of each, and which causes repeat: each repeat is a healer the mesh lacks.", Input: schema(map[string]string{"days": "how many days back (default 14)"}, nil)}, diff --git a/internal/link/handacts.go b/internal/link/handacts.go index dd42f6e0..92de55c7 100644 --- a/internal/link/handacts.go +++ b/internal/link/handacts.go @@ -122,6 +122,33 @@ func RecordHandAct(ctx context.Context, conn *nats.Conn, act HandAct) (HandAct, return act, err } +// RecordHandActOnce writes one entry under the id it carries, only where none is: an act recorded once however +// often it is asked, such as a module's act on a warrant, asked by each of its instances (novox/hq ADR 0274). It +// answers false, with no error, when the entry was already there. +func RecordHandActOnce(ctx context.Context, conn *nats.Conn, act HandAct) (bool, error) { + if act.ID == "" || strings.TrimSpace(act.Why) == "" { + return false, errors.New("an act recorded once carries its id and why") + } + if act.At.IsZero() { + act.At = time.Now().UTC() + } + kv, err := handActs(ctx, conn) + if err != nil { + return false, err + } + body, err := json.Marshal(act) + if err != nil { + return false, err + } + if _, err := kv.Create(ctx, act.ID, body); err != nil { + if errors.Is(err, jetstream.ErrKeyExists) { + return false, nil + } + return false, err + } + return true, nil +} + // HandActs is every entry since a moment, oldest first. func HandActs(ctx context.Context, conn *nats.Conn, since time.Time) ([]HandAct, error) { kv, err := handActs(ctx, conn) diff --git a/internal/link/handacts_test.go b/internal/link/handacts_test.go index 059e5527..e670fe7c 100644 --- a/internal/link/handacts_test.go +++ b/internal/link/handacts_test.go @@ -57,3 +57,30 @@ func TestNatsAnActByHandIsKeptWithWhyAndARepeatIsFound(t *testing.T) { t.Fatalf("%q", acts[2].ID) } } + +// An act on a warrant asked by each of a module's instances is recorded once (novox/hq ADR 0274). +func TestNatsAnActRecordedOnceIsWrittenOnce(t *testing.T) { + js := aBus(t) + api, err := jetstream.New(js.Conn()) + if err != nil { + t.Fatal(err) + } + _ = api.DeleteKeyValue(t.Context(), broker.HandActsBucket) + if err := js.EnsureControllerBuckets(); err != nil { + t.Fatal(err) + } + act := HandAct{ID: "warrant-claude-code-instr-1", Verb: "warrant", Why: "the operator chose Approve", By: "the operator"} + if _, err := RecordHandActOnce(t.Context(), js.Conn(), HandAct{Verb: "warrant", Why: "x"}); err == nil { + t.Fatal("an act without its id was written") + } + for i, want := range []bool{true, false, false} { + written, err := RecordHandActOnce(t.Context(), js.Conn(), act) + if err != nil || written != want { + t.Fatalf("ask %d: written %v, %v", i, written, err) + } + } + acts, err := HandActs(t.Context(), js.Conn(), time.Now().Add(-time.Hour)) + if err != nil || len(acts) != 1 || acts[0].ID != act.ID { + t.Fatalf("%v %+v", err, acts) + } +} diff --git a/module.json b/module.json index e0af4cbb..b3cef223 100644 --- a/module.json +++ b/module.json @@ -63,6 +63,7 @@ "resume", "hand-act", "drill", + "warranted", "hand-acts", "durations", "conditions",