SDK-A6: a tool reads its call's id from the runtime, CallIDOf
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
This commit is contained in:
+38
-6
@@ -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)
|
||||
}
|
||||
|
||||
+36
-5
@@ -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.
|
||||
|
||||
+5
-4
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user