From 3622c129f860e806d55244905390bec438ad8753 Mon Sep 17 00:00:00 2001 From: jochen Date: Wed, 4 Nov 2026 12:44:16 +0100 Subject: [PATCH] SDK-A6: a tool reads its call's id from the runtime, CallIDOf MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A6's request: the ledger's apply names the call it serves (ADR 0597 §7) so the runtime stamps the caller it proved as the operation's author; the id is read from the call's _meta under mesh/call, never from the arguments. Claude-Session: https://claude.ai/code/session_01JG73KJNrH5EgFXMKKYSrig --- go/caller.go | 44 ++++++++++++++++++++++++++++++++++++++------ go/caller_test.go | 41 ++++++++++++++++++++++++++++++++++++----- go/stdio.go | 9 +++++---- 3 files changed, 79 insertions(+), 15 deletions(-) diff --git a/go/caller.go b/go/caller.go index f9a108a..8045d40 100644 --- a/go/caller.go +++ b/go/caller.go @@ -22,8 +22,19 @@ const MetaCaller = "mesh/caller" // parts are safe in a subject. Anything else the runtime named is not a caller and reads as Unproven. var busUser = regexp.MustCompile(`^[A-Za-z0-9_-]+(\.[A-Za-z0-9_-]+)*$`) -// calls holds the caller of every call while its tool runs, by the arguments map that call was handed. -var calls sync.Map // unsafe.Pointer → string +// MetaCall is where in a `tools/call`'s `_meta` the runtime names the id of the call in flight, the id a ledger's +// apply names so the runtime stamps the caller it proved as the operation's author (novox/hq ADR 0597 §7): +// buscalls.MetaCall, written here too so this package imports nothing of buscalls. +const MetaCall = "mesh/call" + +// callID is the shape of a call's id: one subject-safe token, as the runtime makes it. Anything else reads as none. +var callID = regexp.MustCompile(`^[A-Za-z0-9_-]{1,128}$`) + +// inFlight is what the runtime said of one call while its tool runs. +type inFlight struct{ caller, call string } + +// calls holds the caller and the id of every call while its tool runs, by the arguments map that call was handed. +var calls sync.Map // unsafe.Pointer → inFlight func argsKey(args map[string]any) unsafe.Pointer { return reflect.ValueOf(args).UnsafePointer() @@ -37,11 +48,32 @@ func CallerOf(args map[string]any) string { return Unproven } if c, ok := calls.Load(argsKey(args)); ok { - return c.(string) + return c.(inFlight).caller } return Unproven } +// CallIDOf is the id of the call that was handed args, as the runtime named it under MetaCall: what a ledger's apply +// names (buscalls.ApplyRequest.Call) so the operation's author is the caller the runtime proved. Empty for arguments +// no call was handed, or a call the runtime named no id for; read from `_meta`, never from the arguments. +func CallIDOf(args map[string]any) string { + if args == nil { + return "" + } + if c, ok := calls.Load(argsKey(args)); ok { + return c.(inFlight).call + } + return "" +} + +// callIn is the call id a `tools/call`'s `_meta` names, or empty. +func callIn(meta map[string]any) string { + if s, ok := meta[MetaCall].(string); ok && callID.MatchString(s) { + return s + } + return "" +} + // callerIn is the caller a `tools/call`'s `_meta` names, or Unproven. func callerIn(meta map[string]any) string { if s, ok := meta[MetaCaller].(string); ok && busUser.MatchString(s) { @@ -50,10 +82,10 @@ func callerIn(meta map[string]any) string { return Unproven } -// runAs runs a tool with the caller held for its arguments for as long as it runs. -func runAs(t Tool, caller string, args map[string]any) (any, error) { +// runAs runs a tool with the caller and the call's id held for its arguments for as long as it runs. +func runAs(t Tool, caller, call string, args map[string]any) (any, error) { key := argsKey(args) - calls.Store(key, caller) + calls.Store(key, inFlight{caller, call}) defer calls.Delete(key) return t.Run(args) } diff --git a/go/caller_test.go b/go/caller_test.go index ee0d5b8..bac2a36 100644 --- a/go/caller_test.go +++ b/go/caller_test.go @@ -6,25 +6,33 @@ import ( "io" "testing" "time" + + "git.novox.be/novox/mesh-sdk/go/buscalls" ) // callWith serves one tool that answers the caller CallerOf reads from its arguments, sends it one tools/call // with the params given, and returns the caller it answered. func callWith(t *testing.T, params map[string]any) string { + caller, _ := callAnswering(t, params) + return caller +} + +// callAnswering is callWith answering the call's id CallIDOf read too. +func callAnswering(t *testing.T, params map[string]any) (string, string) { servesAlone(t) t.Helper() inR, inW := io.Pipe() outR, outW := io.Pipe() go func() { _ = ServeOn("tracker", []Tool{{Name: "who", Run: func(args map[string]any) (any, error) { - return map[string]any{"caller": CallerOf(args)}, nil + return map[string]any{"caller": CallerOf(args), "call": CallIDOf(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')) lines := bufio.NewScanner(outR) - done := make(chan string, 1) + done := make(chan [2]string, 1) go func() { if lines.Scan() { var m struct { @@ -37,19 +45,20 @@ func callWith(t *testing.T, params map[string]any) string { _ = json.Unmarshal(lines.Bytes(), &m) var answer struct { Caller string `json:"caller"` + Call string `json:"call"` } if len(m.Result.Content) > 0 { _ = json.Unmarshal([]byte(m.Result.Content[0].Text), &answer) } - done <- answer.Caller + done <- [2]string{answer.Caller, answer.Call} } }() select { case got := <-done: - return got + return got[0], got[1] case <-time.After(3 * time.Second): t.Fatal("the bundle did not answer the call") - return "" + return "", "" } } @@ -62,6 +71,28 @@ func TestAToolReadsTheCallerTheRuntimeNamed(t *testing.T) { if got != "shanks.node-tools" { t.Fatalf("the tool read caller %q, want the one the runtime named", got) } + // The call's id is the runtime's too (ADR 0597 §7): read from `_meta`, never from the arguments, and none when it + // is not one token. + for _, c := range []struct { + args, meta map[string]any + want string + }{ + {map[string]any{}, map[string]any{MetaCaller: "shanks.node-tools", MetaCall: "c-7Hq2"}, "c-7Hq2"}, + {map[string]any{}, map[string]any{MetaCaller: "shanks.node-tools"}, ""}, + {map[string]any{MetaCall: "c-forged"}, map[string]any{}, ""}, + {map[string]any{}, map[string]any{MetaCall: "a b"}, ""}, + {map[string]any{}, map[string]any{MetaCall: 7}, ""}, + } { + if _, call := callAnswering(t, map[string]any{"name": "who", "arguments": c.args, "_meta": c.meta}); call != c.want { + t.Errorf("args %v, meta %v: the tool read call %q, want %q", c.args, c.meta, call, c.want) + } + } + if CallIDOf(map[string]any{"x": 1}) != "" || CallIDOf(nil) != "" { + t.Error("arguments no call was handed have a call id") + } + if MetaCall != buscalls.MetaCall { + t.Errorf("the call id's place is %q here and %q in buscalls", MetaCall, buscalls.MetaCall) + } } // A caller named in the arguments is not the caller: an argument is the caller's to write. diff --git a/go/stdio.go b/go/stdio.go index 62d0053..85f4eae 100644 --- a/go/stdio.go +++ b/go/stdio.go @@ -248,7 +248,8 @@ func ServeOn(name string, tools []Tool, in io.Reader, out io.Writer) error { var p struct { Name string `json:"name"` Arguments map[string]any `json:"arguments"` - // Meta is the runtime's, never the caller's: who called, taken from the subject (CallerOf). + // Meta is the runtime's, never the caller's: who called, taken from the subject (CallerOf), and + // the call's id (CallIDOf). Meta map[string]any `json:"_meta"` } _ = json.Unmarshal(m.Params, &p) @@ -259,18 +260,18 @@ func ServeOn(name string, tools []Tool, in io.Reader, out io.Writer) error { } // Beside the loop, never in it: a tool that emits waits for the runtime's answer, which // arrives on the line this loop reads next — and calls do not queue behind a slow one. - go func(id any, t Tool, args map[string]any, caller string) { + go func(id any, t Tool, args map[string]any, caller, call string) { if args == nil { args = map[string]any{} } - result, err := runAs(t, caller, args) + result, err := runAs(t, caller, call, args) if err != nil { c.say(map[string]any{"id": id, "result": map[string]any{"content": []map[string]any{{"type": "text", "text": err.Error()}}, "isError": true}}) return } text, _ := json.Marshal(result) c.say(map[string]any{"id": id, "result": map[string]any{"content": []map[string]any{{"type": "text", "text": string(text)}}}}) - }(id, t, p.Arguments, callerIn(p.Meta)) + }(id, t, p.Arguments, callerIn(p.Meta), callIn(p.Meta)) case "mesh/event": // One event the runtime read from this module's consumer (ADR 0198), answered once every // handler it is for has taken it; an error is answered as one, so the runtime does not