mesh-issues: record who acted as the caller the runtime proves (hq issue 365)

The tool runner now takes a call's caller from the subject it arrived on and
hands it to the module through the SDK (stdio.CallerOf); the tracker records
that bus user on what each verb writes instead of unproven. Never from the
arguments, which still refuse any caller, and unproven where the runtime proves
none. Needs mesh-sdk's go/caller.go (pinned at its branch head until merged).
This commit is contained in:
2026-10-11 11:22:41 +02:00
parent 9355efa00b
commit 66068febb0
10 changed files with 208 additions and 35 deletions
+14 -10
View File
@@ -45,9 +45,9 @@ issue is taken over, and the reason is noted on it. A worker with no move, note,
issue for `worker-stale-after` (default `24h`) is stale and may be taken over from without a reason, still noted.
The tracker's own writes (the triage, the duplicate check, the delivery notes) keep no worker current. The `moved`
event carries `by` (the worker; who acted, for a move naming none), `since`, `caller` (who acted, as the runtime
proves it: `unproven` today), and `took-over-from`, `take-over` or `released` when they apply. `by` is what the
worker says it is: the runtime does not yet tell a module its caller (hq issue 340), so the worker lets agents and
people see each other; it is not a lock against a false name.
proves it: see *Who acted*), and `took-over-from`, `take-over` or `released` when they apply. `by` is what the
worker says it is, not who called, so the worker lets agents and people see each other; it is not a lock against
a false name.
**An issue with a folder in hq** (`record` at open: the folder's name, without the number) is whole in the
tracker like any other: the folder holds its narrative, the tracker its status. Until the import of
@@ -138,8 +138,8 @@ It is a verb on the seat, callable by any agent, because it can do only this:
A folder numbered at or past the setting `first-number` without an issue in the tracker is refused on its
own: a folder from the tracker's time carries a number the tracker gave.
- **It cannot forge an issue's history.** An issue it writes is `opened-by: import`, its `opened` the date
the report gave, and it carries `imported: {at, by, commit, from}` — when, who ran the import (the bus
credential, `unproven` until the runtime proves it), the commit of hq and the document. Each is an
the report gave, and it carries `imported: {at, by, commit, from}` — when, who ran the import (the caller,
as the runtime proves it), the commit of hq and the document. Each is an
`imported` event. What it writes comes from hq's `main` as the records module holds it, which is reviewed
in pull requests.
- **What it keeps is bounded** as every verb's text is: the title 200 bytes, the report 64 KiB, one issue
@@ -197,7 +197,7 @@ sessions writing a record at once never take one number.
- **A reserved number is spent forever**, whether or not its record ever lands; it is never handed out again.
Each is kept in the state `reservations` under `decision-0292` or `research-046` with who reserved it
(`by`, from the caller as for every verb: `unproven` until the runtime names the caller), when (`at`) and
(`by`, the caller as for every verb), when (`at`) and
`for`, and said as the event `mesh-issues.reserved`. A reservation whose write cannot be confirmed is
refused, and its number stays spent.
- **A refused reserve takes no number**: a kind other than `decision` or `research`, a `for` that is not one
@@ -230,10 +230,14 @@ is kept, and its answer says the event was not said (`announced: false`).
## Who acted
ADR 0275 §5 records the caller from the bus credential, never from an argument. **The runtime does not yet
tell a module who called it**: a tool call reaches a bundle as a name and arguments, and the tool runner is
not told who called it either (hq issue 340). Until it does, every change is recorded as by `unproven`, no
verb takes an argument naming a caller, and none can be given one.
ADR 0275 §5 records the caller from the bus credential, never from an argument. **Who acted is the bus user
that called** (hq issue 365): a call on a tool's caller-named subject carries the caller's bus user as its last
token, the bus lets each user publish there with its own name alone, and the tool runner hands the module that
user from the subject the call arrived on (mesh-sdk `stdio.CallerOf`) — `shanks.node-tools` for an agent on
that machine's console, `person.<name>` for a person's credential. A call on the subject that names no caller
(served beside it for one release, hq issue 464), or through a runtime from before issue 365, is recorded as
by `unproven`. No verb takes an argument naming a caller, and none can be given one. The caller is only as sound
as the credential, which ADR 0266 keeps agents from reading (hq issue 333).
## Settings
@@ -0,0 +1,140 @@
package main
import (
"bufio"
"encoding/json"
"fmt"
"io"
"strings"
"testing"
"time"
stdio "git.novox.be/novox/mesh-sdk/go"
)
// served is the tracker's verbs served as the runtime launches them, over the SDK's stdio loop, with the caller
// read from the runtime (stdio.CallerOf): what reaches the module is what a tools/call carries.
type served struct {
in *io.PipeWriter
lines *bufio.Scanner
next int
}
func serve(t *testing.T, h *harness) *served {
t.Helper()
h.CallerOf = stdio.CallerOf
inR, inW := io.Pipe()
outR, outW := io.Pipe()
go func() { _ = stdio.ServeOn("mesh-issues", Tools(h.Tracker), inR, outW) }()
t.Cleanup(func() { _ = inW.Close() })
return &served{in: inW, lines: bufio.NewScanner(outR)}
}
// call sends one tools/call, with the runtime's `_meta` naming caller when one is given, and answers the
// verb's result or its refusal.
func (s *served) call(t *testing.T, verb, caller string, args map[string]any) (map[string]any, string) {
t.Helper()
s.next++
params := map[string]any{"name": Seat + "." + verb, "arguments": args}
if caller != "" {
params["_meta"] = map[string]any{stdio.MetaCaller: caller}
}
b, _ := json.Marshal(map[string]any{"jsonrpc": "2.0", "id": s.next, "method": "tools/call", "params": params})
_, _ = s.in.Write(append(b, '\n'))
done := make(chan []byte, 1)
go func() {
if s.lines.Scan() {
done <- append([]byte(nil), s.lines.Bytes()...)
}
}()
var line []byte
select {
case line = <-done:
case <-time.After(5 * time.Second):
t.Fatalf("%s was not answered", verb)
}
var m struct {
Result struct {
Content []struct {
Text string `json:"text"`
} `json:"content"`
IsError bool `json:"isError"`
} `json:"result"`
}
_ = json.Unmarshal(line, &m)
if len(m.Result.Content) == 0 {
t.Fatalf("%s was answered %s", verb, line)
}
text := m.Result.Content[0].Text
if m.Result.IsError {
return nil, text
}
var out map[string]any
_ = json.Unmarshal([]byte(text), &out)
return out, ""
}
// **Who acted is the caller the runtime proved** (novox/hq issue 365, ADR 0275 §5): a call whose subject named
// its caller is recorded as that bus user — on the issue it opened, the note, the link, the move and its event,
// the value set, and the number reserved.
func TestWhoActedIsTheCallerTheRuntimeProved(t *testing.T) {
h, _ := reserveHarness(t)
s := serve(t, h)
const who = "shanks.node-tools"
opened, refused := s.call(t, "open", who, map[string]any{"title": "a title", "report": "a report"})
if refused != "" || opened["by"] != who {
t.Fatalf("open answered %v %q, want by %s", opened, refused, who)
}
n := uint64(opened["number"].(float64))
other, _ := s.call(t, "open", who, map[string]any{"title": "another", "report": "another report"})
is, _ := h.Get(n)
if is.OpenedBy != who {
t.Errorf("the issue was opened by %q", is.OpenedBy)
}
if noted, refused := s.call(t, "note", who, map[string]any{"number": n, "text": "seen again"}); refused != "" || noted["by"] != who {
t.Errorf("note answered %v %q", noted, refused)
}
if linked, refused := s.call(t, "link", who, map[string]any{"number": n, "kind": "relates-to", "target": fmt.Sprint(other["number"])}); refused != "" || linked["by"] != who {
t.Errorf("link answered %v %q", linked, refused)
}
if _, refused := s.call(t, "set", who, map[string]any{"number": n, "severity": "minor"}); refused != "" {
t.Errorf("set refused: %s", refused)
}
if _, refused := s.call(t, "move", who, map[string]any{"number": n, "status": "fixing", "by": "fix-1@g14"}); refused != "" {
t.Errorf("move refused: %s", refused)
}
if reserved, refused := s.call(t, "reserve", who, map[string]any{"kind": "decision", "for": "a decision"}); refused != "" || reserved["by"] != who {
t.Errorf("reserve answered %v %q", reserved, refused)
}
is, _ = h.Get(n)
for _, note := range is.Notes {
if note.By != who {
t.Errorf("a note is by %q", note.By)
}
}
for _, e := range h.said() {
if e.Event == EventMoved && e.Body["caller"] != who {
t.Errorf("the move was said as by caller %v", e.Body["caller"])
}
}
}
// A caller named in the arguments is refused, whatever it names — an argument is the caller's to write — and a
// call the runtime proved no caller for is recorded Unproven, as it always was.
func TestACallerInTheArgumentsIsRefusedAndAnUnprovenCallSaysSo(t *testing.T) {
h := newHarness(t)
s := serve(t, h)
for _, args := range []map[string]any{
{"title": "t", "report": "r", "caller": "novox.node-tools"},
{"title": "t", "report": "r", "_meta": map[string]any{stdio.MetaCaller: "novox.node-tools"}},
{"title": "t", "report": "r", "by": "novox.node-tools"},
} {
if out, refused := s.call(t, "open", "shanks.node-tools", args); refused == "" || !strings.Contains(refused, "does not take") {
t.Errorf("an open naming a caller in its arguments %v was answered %v", args, out)
}
}
opened, refused := s.call(t, "open", "", map[string]any{"title": "t", "report": "r"})
if refused != "" || opened["by"] != Unproven {
t.Fatalf("an open the runtime named no caller for answered %v %q, want by %s", opened, refused, Unproven)
}
}
@@ -214,6 +214,11 @@ type candidate struct {
// number: create-if-absent on the key, the counter untouched. See the file's comment for what it never
// does.
func (t *Tracker) Import(a ImportAsk, records Records) (*Imported, error) {
return t.importAs(t.by(), a, records)
}
// importAs is Import as the one who acts.
func (t *Tracker) importAs(by string, a ImportAsk, records Records) (*Imported, error) {
settings, err := t.Settings()
if err != nil {
return nil, fmt.Errorf("the settings cannot be read (%v); nothing was imported", err)
@@ -241,7 +246,6 @@ func (t *Tracker) Import(a ImportAsk, records Records) (*Imported, error) {
}
out := &Imported{DryRun: a.DryRun, Commit: commit, ByStatus: map[string]int{}, Rows: []ImportRow{}}
at := t.now()
by := t.by()
// Every folder's number, and no two folders sharing one.
var cands []*candidate
+7 -6
View File
@@ -55,11 +55,12 @@ func oneOf(field, v string, allowed []string) (string, error) {
return "", fmt.Errorf("%s %q is not one of %s", field, v, strings.Join(allowed, ", "))
}
// Unproven is who acted, as recorded, while the runtime does not tell a module who called it: a tool call
// reaches a bundle as a name and arguments only (mesh-sdk Tool.Run), and the tool runner is not told who
// called it either (hq issue 340). The caller is never taken from an argument, so this is recorded rather
// than a name anybody could type (novox/hq ADR 0275 §5).
const Unproven = "unproven"
// Unproven is who acted, as recorded, where the runtime proves no caller: a call on the subject that names none,
// a runtime from before hq issue 365, or a public method called at the terminal. Otherwise who acted is the bus
// user the runtime took from the subject the call arrived on (stdio.CallerOf). The caller is never taken from an
// argument, so neither is a name anybody could type (novox/hq ADR 0275 §5). Mesh-sdk says the same word
// (stdio.Unproven).
const Unproven = stdio.Unproven
// Issue is one issue as the tracker keeps it.
type Issue struct {
@@ -122,7 +123,7 @@ const ImportedBy = "import"
// Import is how an issue, or its status, came from hq.
type Import struct {
At time.Time `json:"at"`
// By is who ran the import, as the runtime proves it (Unproven until it does).
// By is who ran the import, as the runtime proves it (Unproven where it proves none).
By string `json:"by"`
Commit string `json:"commit"`
// From is the document read, empty for a status filled without one.
+9 -7
View File
@@ -87,6 +87,8 @@ func main() {
Logf: logf,
Sleep: time.Sleep,
Judge: busJudgement(os.Getenv("MESH_NODE")),
// Who called, from the runtime (hq issue 365): never from the arguments.
CallerOf: stdio.CallerOf,
}
t.Notify = busNotifier()
t.Deliveries = func(repository, number string) (Delivery, error) {
@@ -340,7 +342,7 @@ func Tools(t *Tracker) []stdio.Tool {
if ask.Needs, err = a.str("needs"); err != nil {
return nil, err
}
return t.Open(ask)
return t.open(t.callerOf(m), ask)
}},
{Name: Seat + ".get",
Description: "One issue whole, by its number: title, status, severity, what it needs, report, who opened it " +
@@ -463,7 +465,7 @@ func Tools(t *Tracker) []stdio.Tool {
return nil, err
}
}
return t.Move(ask)
return t.move(t.callerOf(m), ask)
}},
{Name: Seat + ".note",
Description: "Add a note to an issue: what was found, tried or decided, in plain words. A note is kept as " +
@@ -486,7 +488,7 @@ func Tools(t *Tracker) []stdio.Tool {
if err != nil {
return nil, err
}
return t.Note(n, s)
return t.note(t.callerOf(m), n, s)
}},
{Name: Seat + ".link",
Description: "Link an issue to something, or with remove, take a link away. Kinds: duplicates or relates-to " +
@@ -518,7 +520,7 @@ func Tools(t *Tracker) []stdio.Tool {
if ask.Remove, err = a.flag("remove"); err != nil {
return nil, err
}
return t.Link(ask)
return t.link(t.callerOf(m), ask)
}},
{Name: Seat + ".import",
Description: "Import hq's issue folders (04-ISSUES/NNN-slug/00-report.md), read through the records module at " +
@@ -548,7 +550,7 @@ func Tools(t *Tracker) []stdio.Tool {
if records == nil {
records = busRecords{}
}
return t.Import(ask, records)
return t.importAs(t.callerOf(m), ask, records)
}},
{Name: Seat + ".groom",
Description: "The triage over the backlog: every issue not resolved and not yet triaged is judged, " +
@@ -606,7 +608,7 @@ func Tools(t *Tracker) []stdio.Tool {
if ask.SuggestedOwner, err = a.str("suggested-owner"); err != nil {
return nil, err
}
return t.Set(ask)
return t.set(t.callerOf(m), ask, false)
}},
{Name: Seat + ".tracking",
Description: "Which issues track which conditions: for every condition key an issue links (link kind condition), " +
@@ -654,7 +656,7 @@ func Tools(t *Tracker) []stdio.Tool {
if ask.For, err = a.str("for"); err != nil {
return nil, err
}
return t.Reserve(ask)
return t.reserve(t.callerOf(m), ask)
}},
}
}
@@ -98,7 +98,10 @@ type Reserved struct {
// Reserve hands out the next number of a sequence and keeps who reserved it, when and for what. A refused
// reserve spends no number; a number handed out is spent forever.
func (t *Tracker) Reserve(a ReserveAsk) (*Reserved, error) {
func (t *Tracker) Reserve(a ReserveAsk) (*Reserved, error) { return t.reserve(t.by(), a) }
// reserve is Reserve as the one who acts.
func (t *Tracker) reserve(by string, a ReserveAsk) (*Reserved, error) {
kind := a.Kind
seq, ok := sequences[kind]
if !ok {
@@ -121,7 +124,7 @@ func (t *Tracker) Reserve(a ReserveAsk) (*Reserved, error) {
if err != nil {
return nil, err
}
r := Reservation{Kind: kind, Number: n, For: purpose, By: t.by(), At: t.now(), ID: newID()}
r := Reservation{Kind: kind, Number: n, For: purpose, By: by, At: t.now(), ID: newID()}
if err := t.keep(&r); err != nil {
return nil, err
}
+22 -5
View File
@@ -34,7 +34,9 @@ type Tracker struct {
Deliveries DeliveryState
// Notify tells the operator's channel of a stuck fix (ADR 0281); nil says nothing there.
Notify Notifier
groom grooming
// CallerOf is who called, for the arguments one call was handed (stdio.CallerOf); nil proves nobody.
CallerOf func(map[string]any) string
groom grooming
}
// The events this module says, one per kind of change.
@@ -57,10 +59,22 @@ const (
createAttempts = 3
)
// by is who acted on this call. The runtime does not hand a module its caller, so it is Unproven; it is
// never read from the call's arguments, so no caller can name another (ADR 0275 §5).
// by is who acted where no call says it — a public method called at the terminal or in a test: Unproven.
func (t *Tracker) by() string { return Unproven }
// callerOf is who acted on one call, as the runtime proves it: the bus user that published the call, which the
// runtime takes from the subject it arrived on (hq issue 365, mesh-sdk stdio.CallerOf), or Unproven where the
// runtime proves none. It is never read from the call's arguments, so no caller can name another (ADR 0275 §5).
func (t *Tracker) callerOf(m map[string]any) string {
if t.CallerOf == nil {
return Unproven
}
if c := t.CallerOf(m); c != "" {
return c
}
return Unproven
}
func (t *Tracker) now() time.Time { return t.Now().UTC().Truncate(time.Second) }
func (t *Tracker) owners() (Owners, error) {
@@ -108,7 +122,10 @@ type Opened struct {
// Open checks an issue, then takes the next number and writes the issue under it (to-be 53 §4). A refused
// issue takes no number. A number taken and then not written — the bus failing between the two — stays
// unused, and the answer says which: numbers may skip, never repeat.
func (t *Tracker) Open(a OpenAsk) (*Opened, error) {
func (t *Tracker) Open(a OpenAsk) (*Opened, error) { return t.open(t.by(), a) }
// open is Open as the one who acts.
func (t *Tracker) open(by string, a OpenAsk) (*Opened, error) {
title, err := singleLine("title", a.Title, MostTitle)
if err != nil {
return nil, err
@@ -143,7 +160,7 @@ func (t *Tracker) Open(a OpenAsk) (*Opened, error) {
}
}
at := t.now()
is := Issue{Title: title, Status: StatusOpen, Report: report, Opened: at, OpenedBy: t.by(), LocatedIn: located, Opening: newID(),
is := Issue{Title: title, Status: StatusOpen, Report: report, Opened: at, OpenedBy: by, LocatedIn: located, Opening: newID(),
Severity: severity, Needs: needs}
n, err := t.next()
@@ -7,9 +7,9 @@ package main
// from a caller for `worker-stale-after` (24 hours) is stale: `list` says so, and it may be taken over without a
// reason.
//
// `by` is what the worker says it is, not who called: the runtime does not yet hand a module its caller (hq
// issue 340), so who acted stays recorded as the runtime proves it (Unproven), and the worker is a way for
// agents and people to see each other, not a lock against someone giving a false name.
// `by` is what the worker says it is, not who called: who acted is recorded beside it as the runtime proves it
// (the caller's bus user, hq issue 365, or Unproven), and the worker is a way for agents and people to see each
// other, not a lock against someone giving a false name.
import (
"fmt"
+1 -1
View File
@@ -3,7 +3,7 @@ module mesh-issues
go 1.25.0
require (
git.novox.be/novox/mesh-sdk/go v0.1.12
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261011031108-d9e24e95a242
github.com/nats-io/nats-server/v2 v2.11.17
github.com/nats-io/nats.go v1.51.0
)
+2
View File
@@ -1,5 +1,7 @@
git.novox.be/novox/mesh-sdk/go v0.1.12 h1:QvmLlT88F+OYt8MOytMGM5ZEOo0fKmNZvNVeJe6gnTk=
git.novox.be/novox/mesh-sdk/go v0.1.12/go.mod h1:GFuZUElBZ9A++mxgIKo97aXXo+kV0uJ/UkbhQPPIbrY=
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261011031108-d9e24e95a242 h1:vpYUOltncgA+GCh/KI8e6azffB1K7768Ka+VuvqXjiE=
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261011031108-d9e24e95a242/go.mod h1:GFuZUElBZ9A++mxgIKo97aXXo+kV0uJ/UkbhQPPIbrY=
github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op h1:Z/MZK75wC/NSrkgqeNIa7jexam9uWzhLmFTSCPI/kn0=
github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op/go.mod h1:FQyySiasQQM8735Ddel3MRojmy4dA1IqCeyJ5jmPMbI=
github.com/google/go-tpm v0.9.8 h1:slArAR9Ft+1ybZu0lBwpSmpwhRXaa85hWtMinMyRAWo=