Merge pull request 'mesh-issues: record who acted as the caller the runtime proves (hq issue 365)' (#207) from fix/365-a-tool-call-names-its-caller into main
This commit was merged in pull request #207.
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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.20261011091942-4cf45b5ca0cf
|
||||
github.com/nats-io/nats-server/v2 v2.11.17
|
||||
github.com/nats-io/nats.go v1.51.0
|
||||
)
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
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.20261011091942-4cf45b5ca0cf h1:ED/+RszKwm86U7H/NZRbS4b5LzQ8n7rubqKmeFqFtvM=
|
||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261011091942-4cf45b5ca0cf/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=
|
||||
|
||||
@@ -29,10 +29,11 @@ accepted only on the operator's Approve, given on a channel that proves who answ
|
||||
`ask`, `get`, `holds` and `history` stay within `bound` bytes (4096 unless asked for, at most 65536) and say what they cut
|
||||
and how to read the rest. The module's own tool `knowledge_health` answers its health (below).
|
||||
|
||||
**Who acted** is the caller as the runtime proves it, never an argument: no verb takes one. Today the runtime
|
||||
does not hand a module its caller. The SDK's tool call carries only a name and arguments, so proving the caller
|
||||
needs an SDK change first (hq issue 365). Until then every change is recorded as by `unproven`. In this module
|
||||
the caller is read in one place, `callerOf` in `main.go`, so the SDK change touches that one function.
|
||||
**Who acted** is the caller as the runtime proves it, never an argument: no verb takes one. The tool runner takes
|
||||
the caller's bus user from the caller-named subject a call arrived on and hands it through the SDK
|
||||
(`stdio.CallerOf`, hq issue 365). 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`. In this module the caller is
|
||||
read in one place, `callerOf` in `main.go`.
|
||||
|
||||
The rules about callers:
|
||||
|
||||
@@ -147,7 +148,8 @@ the consolidated schema for a fresh database is `schema.sql`. A test holds the t
|
||||
|
||||
- assignment to the control-node, and a postgres provider for its `postgres-database` provision;
|
||||
- the issue tracker serving `issue-tracker.reserve` (hq issue 424, merged as mesh-catalog#204) on the mesh;
|
||||
- the SDK and the runtime proving the caller (hq issue 365) before any review is taken;
|
||||
- the runtime proving the caller (hq issue 365: mesh-tools#27 and mesh-controller#224 delivered) before any review
|
||||
is taken;
|
||||
- the import run once, then checked against hq record by record (hq issue 426).
|
||||
|
||||
## Tests
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"encoding/json"
|
||||
"io"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
stdio "git.novox.be/novox/mesh-sdk/go"
|
||||
)
|
||||
|
||||
// whoCalled serves one tool that answers callerOf of its arguments, over the SDK's stdio loop as the runtime
|
||||
// launches a bundle, and sends it one call with params.
|
||||
func whoCalled(t *testing.T, params map[string]any) string {
|
||||
t.Helper()
|
||||
inR, inW := io.Pipe()
|
||||
outR, outW := io.Pipe()
|
||||
go func() {
|
||||
_ = stdio.ServeOn("mesh-knowledge", []stdio.Tool{{Name: "who", Run: func(args map[string]any) (any, error) {
|
||||
return callerOf(args), nil
|
||||
}}}, inR, outW)
|
||||
}()
|
||||
defer inW.Close()
|
||||
b, _ := json.Marshal(map[string]any{"jsonrpc": "2.0", "id": 1, "method": "tools/call", "params": params})
|
||||
_, _ = inW.Write(append(b, '\n'))
|
||||
got := make(chan string, 1)
|
||||
go func() {
|
||||
lines := bufio.NewScanner(outR)
|
||||
if lines.Scan() {
|
||||
var m struct {
|
||||
Result struct {
|
||||
Content []struct {
|
||||
Text string `json:"text"`
|
||||
} `json:"content"`
|
||||
} `json:"result"`
|
||||
}
|
||||
_ = json.Unmarshal(lines.Bytes(), &m)
|
||||
var who string
|
||||
if len(m.Result.Content) > 0 {
|
||||
_ = json.Unmarshal([]byte(m.Result.Content[0].Text), &who)
|
||||
}
|
||||
got <- who
|
||||
}
|
||||
}()
|
||||
select {
|
||||
case who := <-got:
|
||||
return who
|
||||
case <-time.After(3 * time.Second):
|
||||
t.Fatal("the call was not answered")
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
// **Who called is the caller the runtime proved** (hq issue 365): read from the call's runtime-written `_meta`,
|
||||
// never from its arguments; Unproven where the runtime named none.
|
||||
func TestTheCallerIsTheOneTheRuntimeProved(t *testing.T) {
|
||||
if got := whoCalled(t, map[string]any{"name": "who", "arguments": map[string]any{"caller": "novox.node-tools"},
|
||||
"_meta": map[string]any{stdio.MetaCaller: "shanks.node-tools"}}); got != "shanks.node-tools" {
|
||||
t.Errorf("callerOf answered %q, want the runtime's caller", got)
|
||||
}
|
||||
if got := whoCalled(t, map[string]any{"name": "who", "arguments": map[string]any{"caller": "novox.node-tools"}}); got != Unproven {
|
||||
t.Errorf("a call the runtime named no caller for answered %q, want %s", got, Unproven)
|
||||
}
|
||||
}
|
||||
@@ -15,11 +15,13 @@ import (
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
stdio "git.novox.be/novox/mesh-sdk/go"
|
||||
)
|
||||
|
||||
// Unproven is who acted, as recorded, while the runtime does not tell a module who called it (hq issue 365). The
|
||||
// caller is never taken from an argument.
|
||||
const Unproven = "unproven"
|
||||
// Unproven is who acted, as recorded, where the runtime proves no caller (hq issue 365): the SDK's word. The caller
|
||||
// is never taken from an argument.
|
||||
const Unproven = stdio.Unproven
|
||||
|
||||
// proven says whether a caller is one the runtime proved.
|
||||
func proven(caller string) bool { return caller != "" && caller != Unproven }
|
||||
|
||||
@@ -103,11 +103,10 @@ func serve(k *KB, status string) {
|
||||
}
|
||||
}
|
||||
|
||||
// callerOf is who called, as the runtime proves it: the one place the caller is read, so the change that brings it
|
||||
// is this one function. The runtime does not hand a module its caller yet (hq issue 365): the SDK's tool call carries
|
||||
// a name and arguments only, so the SDK must first pass the runtime's proven caller to a tool. Until it does, every
|
||||
// caller is Unproven. It is never read from the call's arguments, so no caller can name another.
|
||||
func callerOf(map[string]any) string { return Unproven }
|
||||
// callerOf is who called, as the runtime proves it: the one place the caller is read. The tool runner takes the
|
||||
// caller's bus user from the subject the call arrived on and hands it through the SDK (hq issue 365); Unproven where
|
||||
// it proves none. It is never read from the call's arguments, so no caller can name another.
|
||||
func callerOf(args map[string]any) string { return stdio.CallerOf(args) }
|
||||
|
||||
// hear subscribes to the router's word on this module's asks, asked again until the runtime takes it.
|
||||
func hear(k *KB) {
|
||||
|
||||
@@ -3,7 +3,7 @@ module mesh-knowledge
|
||||
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.20261011091942-4cf45b5ca0cf
|
||||
github.com/jackc/pgx/v5 v5.11.0
|
||||
github.com/nats-io/nats-server/v2 v2.11.17
|
||||
github.com/nats-io/nats.go v1.51.0
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
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.20261011091942-4cf45b5ca0cf h1:ED/+RszKwm86U7H/NZRbS4b5LzQ8n7rubqKmeFqFtvM=
|
||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261011091942-4cf45b5ca0cf/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/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
|
||||
Reference in New Issue
Block a user