Files
mesh-delivery/modules/mesh-delivery-forge-bridge/cmd/mesh-delivery-forge-bridge/call.go
T

77 lines
2.8 KiB
Go

package main
import (
"context"
"encoding/json"
"errors"
"fmt"
"strings"
"time"
"github.com/nats-io/nats.go"
"mesh-delivery-forge-bridge/internal/bridge"
)
// callTimeout bounds a call to a tool (mesh-sdk go/process/call.go CallTimeout).
const callTimeout = 30 * time.Second
// caller calls a module's tool by its manifest key (`<module>.<tool>`) with a JSON body and returns the tool's result.
type caller func(ctx context.Context, key string, body any) (json.RawMessage, error)
// callerOn calls tools as the bridge's own principal. It copies mesh-sdk go/process/call.go (Call, callSubjects,
// request; hand-written here until the SDK's helper is used, issue 1242): the request is the tool's arguments as
// JSON, published on the caller-named subject `mesh.mod.<module>.call.<tool>.<user, dots as ~>`, or on the subject that
// names no caller, `mesh.mod.<module>.tool.<tool>`, when nothing answers the first; the reply is
// {"result", "error", "node"} and a non-empty error is the call's failure (mesh-tools node-tools/internal/bus/bus.go
// answerOf). The grant to publish on these subjects comes from the manifest's invokes alone (mesh-control
// internal/broker/nats.go invokedSubjects).
func callerOn(nc *nats.Conn, user string) caller {
token := strings.ReplaceAll(user, ".", "~")
request := func(ctx context.Context, subject string, data []byte) (json.RawMessage, error) {
ctx, cancel := context.WithTimeout(ctx, callTimeout)
defer cancel()
msg, err := nc.RequestWithContext(ctx, subject, data)
if err != nil {
return nil, err
}
var r struct {
Result json.RawMessage `json:"result"`
Error string `json:"error"`
Node string `json:"node"`
}
if err := json.Unmarshal(msg.Data, &r); err != nil {
return nil, fmt.Errorf("%s answered something that is not a tool's answer: %w", subject, err)
}
if r.Error != "" {
return nil, errors.New(r.Error)
}
return r.Result, nil
}
return func(ctx context.Context, key string, body any) (json.RawMessage, error) {
module, tool, ok := strings.Cut(key, ".")
if !ok || module == "" || tool == "" {
return nil, fmt.Errorf("%q is not <module>.<tool>", key)
}
data, err := json.Marshal(body)
if err != nil {
return nil, err
}
result, err := request(ctx, "mesh.mod."+module+".call."+tool+"."+token, data)
if errors.Is(err, nats.ErrNoResponders) {
result, err = request(ctx, "mesh.mod."+module+".tool."+tool, data)
}
return result, err
}
}
// treesVia asks the forge for a commit's trees through call.
func treesVia(ctx context.Context, call caller) bridge.Trees {
return bridge.TreesVia(ctx, bridge.Caller(call))
}
// filesVia asks the forge for a file through call. A file the forge says is not there is bridge.ErrNotFound.
func filesVia(ctx context.Context, call caller) bridge.Files {
return bridge.FilesVia(ctx, bridge.Caller(call))
}