ucp
This commit is contained in:
@@ -6,20 +6,27 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"log"
|
"log"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"os"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"git.k6n.net/mats/go-cart-actor/pkg/cart"
|
"git.k6n.net/mats/go-cart-actor/pkg/cart"
|
||||||
)
|
)
|
||||||
|
|
||||||
type CartClient struct {
|
type CartClient struct {
|
||||||
httpClient *http.Client
|
httpClient *http.Client
|
||||||
baseUrl string
|
baseUrl string
|
||||||
|
agentProfile string // UCP-Agent profile URI
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewCartClient(baseUrl string) *CartClient {
|
func NewCartClient(baseUrl string) *CartClient {
|
||||||
|
profile := os.Getenv("UCP_AGENT_PROFILE")
|
||||||
|
if profile == "" {
|
||||||
|
profile = "https://checkout.k6n.net/.well-known/ucp"
|
||||||
|
}
|
||||||
return &CartClient{
|
return &CartClient{
|
||||||
httpClient: &http.Client{Timeout: 10 * time.Second},
|
httpClient: &http.Client{Timeout: 10 * time.Second},
|
||||||
baseUrl: baseUrl,
|
baseUrl: baseUrl,
|
||||||
|
agentProfile: profile,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -52,6 +59,7 @@ func (s *CartClient) getCartGrain(ctx context.Context, cartId cart.CartId) (*car
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
req.Header.Set("UCP-Agent", `profile="`+s.agentProfile+`"`)
|
||||||
resp, err := s.httpClient.Do(req)
|
resp, err := s.httpClient.Do(req)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import (
|
|||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"os"
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -16,15 +17,21 @@ import (
|
|||||||
// Currently only CreateOrder is implemented (the from-checkout handoff).
|
// Currently only CreateOrder is implemented (the from-checkout handoff).
|
||||||
// Add GetOrder/ListOrders methods as the backoffice order UI evolves.
|
// Add GetOrder/ListOrders methods as the backoffice order UI evolves.
|
||||||
type OrderClient struct {
|
type OrderClient struct {
|
||||||
httpClient *http.Client
|
httpClient *http.Client
|
||||||
baseURL string // e.g. "http://order-service:8092"
|
baseURL string // e.g. "http://order-service:8092"
|
||||||
|
agentProfile string // UCP-Agent profile URI
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewOrderClient returns a client that talks to the order service at baseURL.
|
// NewOrderClient returns a client that talks to the order service at baseURL.
|
||||||
func NewOrderClient(baseURL string) *OrderClient {
|
func NewOrderClient(baseURL string) *OrderClient {
|
||||||
|
profile := os.Getenv("UCP_AGENT_PROFILE")
|
||||||
|
if profile == "" {
|
||||||
|
profile = "https://cart.k6n.net/.well-known/ucp"
|
||||||
|
}
|
||||||
return &OrderClient{
|
return &OrderClient{
|
||||||
httpClient: &http.Client{Timeout: 30 * time.Second},
|
httpClient: &http.Client{Timeout: 30 * time.Second},
|
||||||
baseURL: baseURL,
|
baseURL: baseURL,
|
||||||
|
agentProfile: profile,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -91,6 +98,7 @@ func (c *OrderClient) CreateOrder(ctx context.Context, req CreateOrderReq, flowN
|
|||||||
return nil, fmt.Errorf("order client: new request: %w", err)
|
return nil, fmt.Errorf("order client: new request: %w", err)
|
||||||
}
|
}
|
||||||
httpReq.Header.Set("Content-Type", "application/json")
|
httpReq.Header.Set("Content-Type", "application/json")
|
||||||
|
httpReq.Header.Set("UCP-Agent", `profile="`+c.agentProfile+`"`)
|
||||||
|
|
||||||
resp, err := c.httpClient.Do(httpReq)
|
resp, err := c.httpClient.Do(httpReq)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -0,0 +1,151 @@
|
|||||||
|
package ucp
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"net/http"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// UCP-Agent header parsing (RFC 8941 Dictionary Structured Field)
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
// ucpAgentProfileKey is the context key for the UCP-Agent profile URI.
|
||||||
|
type ucpAgentProfileKey struct{}
|
||||||
|
|
||||||
|
// UCPAgentProfileFromContext returns the UCP-Agent profile URI stored in the
|
||||||
|
// request context, or empty string if none was advertised.
|
||||||
|
func UCPAgentProfileFromContext(ctx context.Context) string {
|
||||||
|
v, _ := ctx.Value(ucpAgentProfileKey{}).(string)
|
||||||
|
return v
|
||||||
|
}
|
||||||
|
|
||||||
|
// parseUCPAgentProfile parses a single "profile" member value from an RFC 8941
|
||||||
|
// Dictionary Structured Field header. It only handles the string-value form:
|
||||||
|
//
|
||||||
|
// UCP-Agent: profile="https://agent.example/profiles/shopping-agent.json"
|
||||||
|
//
|
||||||
|
// Returns the profile URI (without surrounding quotes) or empty string.
|
||||||
|
func parseUCPAgentProfile(header string) string {
|
||||||
|
if header == "" {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
// Walk the dictionary entries looking for "profile".
|
||||||
|
// RFC 8941 §3.1.2: Dict = ((im-key | key) value) *(SP "," ((im-key | key) value))
|
||||||
|
i := 0
|
||||||
|
bs := []byte(header)
|
||||||
|
n := len(bs)
|
||||||
|
|
||||||
|
for i < n {
|
||||||
|
// Skip whitespace.
|
||||||
|
for i < n && (bs[i] == ' ' || bs[i] == '\t') {
|
||||||
|
i++
|
||||||
|
}
|
||||||
|
if i >= n {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
// Read key (im-key or key). Lowercase letters, digits, "_", "-", "*".
|
||||||
|
start := i
|
||||||
|
for i < n && isKeyChar(bs[i]) {
|
||||||
|
i++
|
||||||
|
}
|
||||||
|
if i == start {
|
||||||
|
// No key found; skip to next comma or end.
|
||||||
|
for i < n && bs[i] != ',' {
|
||||||
|
i++
|
||||||
|
}
|
||||||
|
i++ // skip comma
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
key := string(bs[start:i])
|
||||||
|
|
||||||
|
// Check for "=" separator. RFC 8941 allows bare names (boolean true)
|
||||||
|
// when "=? " follows — but we only care about key=value entries.
|
||||||
|
if i < n && bs[i] == '=' {
|
||||||
|
i++ // skip '='
|
||||||
|
|
||||||
|
// Read the value. For our case it's a string (starts with '"').
|
||||||
|
if i < n && bs[i] == '"' {
|
||||||
|
i++ // skip opening quote
|
||||||
|
valStart := i
|
||||||
|
for i < n && bs[i] != '"' {
|
||||||
|
if bs[i] == '\\' {
|
||||||
|
i++ // skip escape
|
||||||
|
}
|
||||||
|
i++
|
||||||
|
}
|
||||||
|
val := string(bs[valStart:i])
|
||||||
|
if i < n {
|
||||||
|
i++ // skip closing quote
|
||||||
|
}
|
||||||
|
if strings.EqualFold(key, "profile") {
|
||||||
|
return val
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Skip to next comma or end.
|
||||||
|
for i < n && bs[i] != ',' {
|
||||||
|
i++
|
||||||
|
}
|
||||||
|
i++ // skip comma
|
||||||
|
}
|
||||||
|
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
// isKeyChar reports whether b is valid in an RFC 8941 key (lc-ltr, DIGIT, "_", "-", "*").
|
||||||
|
func isKeyChar(b byte) bool {
|
||||||
|
return (b >= 'a' && b <= 'z') || (b >= '0' && b <= '9') || b == '_' || b == '-' || b == '*'
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithUCPAgent is HTTP middleware that reads the UCP-Agent header from incoming
|
||||||
|
// requests, parses it per RFC 8941, and stores the profile URI in the request
|
||||||
|
// context so downstream handlers can inspect the calling platform's identity.
|
||||||
|
//
|
||||||
|
// Usage:
|
||||||
|
//
|
||||||
|
// mux.Handle("/ucp/v1/carts", ucp.WithUCPAgent(ucp.CartHandler(pool)))
|
||||||
|
func WithUCPAgent(next http.Handler) http.Handler {
|
||||||
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if profile := parseUCPAgentProfile(r.Header.Get("UCP-Agent")); profile != "" {
|
||||||
|
ctx := context.WithValue(r.Context(), ucpAgentProfileKey{}, profile)
|
||||||
|
r = r.WithContext(ctx)
|
||||||
|
}
|
||||||
|
next.ServeHTTP(w, r)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// DefaultUCPAgentProfile is the profile URI sent on outgoing requests from
|
||||||
|
// this commerce platform. Override via SetDefaultUCPAgentProfile or the
|
||||||
|
// UCP_AGENT_PROFILE environment variable at init time.
|
||||||
|
var DefaultUCPAgentProfile = "https://cart.k6n.net/.well-known/ucp"
|
||||||
|
|
||||||
|
// SetDefaultUCPAgentProfile overrides the default profile URI used for outgoing
|
||||||
|
// UCP-Agent headers.
|
||||||
|
func SetDefaultUCPAgentProfile(profile string) {
|
||||||
|
if profile != "" {
|
||||||
|
DefaultUCPAgentProfile = profile
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// UCPAgentHeaderValue returns the RFC 8941 Dictionary value for the UCP-Agent
|
||||||
|
// header, encoding the given profile URI.
|
||||||
|
func UCPAgentHeaderValue(profile string) string {
|
||||||
|
if profile == "" {
|
||||||
|
profile = DefaultUCPAgentProfile
|
||||||
|
}
|
||||||
|
return `profile="` + profile + `"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithUCPAgentOnRequest adds the UCP-Agent header to an outgoing HTTP request.
|
||||||
|
// It sets the header to the default profile unless a custom profile is provided.
|
||||||
|
func WithUCPAgentOnRequest(req *http.Request, profile ...string) {
|
||||||
|
p := DefaultUCPAgentProfile
|
||||||
|
if len(profile) > 0 && profile[0] != "" {
|
||||||
|
p = profile[0]
|
||||||
|
}
|
||||||
|
req.Header.Set("UCP-Agent", `profile="`+p+`"`)
|
||||||
|
}
|
||||||
@@ -5,7 +5,9 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"os"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"git.k6n.net/mats/go-cart-actor/pkg/checkout"
|
"git.k6n.net/mats/go-cart-actor/pkg/checkout"
|
||||||
@@ -61,17 +63,52 @@ func (s *CheckoutServer) handleCreateCheckout(w http.ResponseWriter, r *http.Req
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Initialize checkout with cart reference.
|
// Fetch cart state from cart service
|
||||||
|
cartBaseURL := os.Getenv("CART_INTERNAL_URL")
|
||||||
|
if cartBaseURL == "" {
|
||||||
|
cartBaseURL = "http://cart:8080"
|
||||||
|
}
|
||||||
|
url := fmt.Sprintf("%s/cart/byid/%s", cartBaseURL, cartId.String())
|
||||||
|
|
||||||
|
client := &http.Client{Timeout: 10 * time.Second}
|
||||||
|
cartReq, err := http.NewRequestWithContext(r.Context(), "GET", url, nil)
|
||||||
|
if err != nil {
|
||||||
|
writeError(w, http.StatusInternalServerError, "failed to create cart request: "+err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
profile := os.Getenv("UCP_AGENT_PROFILE")
|
||||||
|
if profile == "" {
|
||||||
|
profile = DefaultUCPAgentProfile
|
||||||
|
}
|
||||||
|
cartReq.Header.Set("UCP-Agent", `profile="`+profile+`"`)
|
||||||
|
|
||||||
|
cartResp, err := client.Do(cartReq)
|
||||||
|
if err != nil {
|
||||||
|
writeError(w, http.StatusInternalServerError, "failed to fetch cart: "+err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer cartResp.Body.Close()
|
||||||
|
|
||||||
|
if cartResp.StatusCode != http.StatusOK {
|
||||||
|
writeError(w, http.StatusBadRequest, fmt.Sprintf("cart service returned status: %d", cartResp.StatusCode))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
cartStateBytes, err := io.ReadAll(cartResp.Body)
|
||||||
|
if err != nil {
|
||||||
|
writeError(w, http.StatusInternalServerError, "failed to read cart state: "+err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize checkout with cart reference and state.
|
||||||
initMsg := &checkoutMessages.InitializeCheckout{
|
initMsg := &checkoutMessages.InitializeCheckout{
|
||||||
OrderId: "",
|
OrderId: "",
|
||||||
CartId: uint64(cartId),
|
CartId: uint64(cartId),
|
||||||
}
|
CartState: &anypb.Any{
|
||||||
if req.Country != "" {
|
TypeUrl: "type.googleapis.com/cart.CartGrain",
|
||||||
// Pass country through the cart state snapshot (minimal).
|
Value: cartStateBytes,
|
||||||
initMsg.CartState = &anypb.Any{
|
},
|
||||||
TypeUrl: "request/country",
|
|
||||||
Value: []byte(req.Country),
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if _, err := s.applier.Apply(r.Context(), uint64(checkoutId), initMsg); err != nil {
|
if _, err := s.applier.Apply(r.Context(), uint64(checkoutId), initMsg); err != nil {
|
||||||
@@ -290,14 +327,22 @@ func (s *CheckoutServer) handleCancelCheckout(w http.ResponseWriter, r *http.Req
|
|||||||
type OrderHTTPClient struct {
|
type OrderHTTPClient struct {
|
||||||
baseURL string
|
baseURL string
|
||||||
httpClient *http.Client
|
httpClient *http.Client
|
||||||
|
agentProfile string // UCP-Agent profile URI
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewOrderHTTPClient returns an order applier that creates orders by calling
|
// NewOrderHTTPClient returns an order applier that creates orders by calling
|
||||||
// the order service at baseURL (e.g. "http://order-service:8092").
|
// the order service at baseURL (e.g. "http://order-service:8092"). It reads
|
||||||
|
// the UCP_AGENT_PROFILE env var for the profile URI, falling back to
|
||||||
|
// DefaultUCPAgentProfile from the ucp package.
|
||||||
func NewOrderHTTPClient(baseURL string) *OrderHTTPClient {
|
func NewOrderHTTPClient(baseURL string) *OrderHTTPClient {
|
||||||
|
profile := os.Getenv("UCP_AGENT_PROFILE")
|
||||||
|
if profile == "" {
|
||||||
|
profile = DefaultUCPAgentProfile
|
||||||
|
}
|
||||||
return &OrderHTTPClient{
|
return &OrderHTTPClient{
|
||||||
baseURL: baseURL,
|
baseURL: baseURL,
|
||||||
httpClient: &http.Client{Timeout: 30 * time.Second},
|
httpClient: &http.Client{Timeout: 30 * time.Second},
|
||||||
|
agentProfile: profile,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -381,6 +426,8 @@ func (c *OrderHTTPClient) CreateOrder(ctx context.Context, checkoutID uint64, g
|
|||||||
return "", fmt.Errorf("order: new request: %w", err)
|
return "", fmt.Errorf("order: new request: %w", err)
|
||||||
}
|
}
|
||||||
httpReq.Header.Set("Content-Type", "application/json")
|
httpReq.Header.Set("Content-Type", "application/json")
|
||||||
|
// Advertise this platform's UCP profile on all outgoing requests.
|
||||||
|
WithUCPAgentOnRequest(httpReq, c.agentProfile)
|
||||||
|
|
||||||
resp, err := c.httpClient.Do(httpReq)
|
resp, err := c.httpClient.Do(httpReq)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
+17
-4
@@ -23,7 +23,7 @@ func OrderHandler(applier OrderReadApplier) http.Handler {
|
|||||||
mux.HandleFunc("POST /{id}/fulfillments", s.handleCreateFulfillment)
|
mux.HandleFunc("POST /{id}/fulfillments", s.handleCreateFulfillment)
|
||||||
mux.HandleFunc("POST /{id}/returns", s.handleRequestReturn)
|
mux.HandleFunc("POST /{id}/returns", s.handleRequestReturn)
|
||||||
mux.HandleFunc("POST /{id}/refunds", s.handleIssueRefund)
|
mux.HandleFunc("POST /{id}/refunds", s.handleIssueRefund)
|
||||||
return mux
|
return withUCPAgent(withJSONContentType(mux))
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
@@ -46,7 +46,7 @@ func CartHandler(applier CartApplier) http.Handler {
|
|||||||
mux.HandleFunc("GET /{id}", s.handleGetCart)
|
mux.HandleFunc("GET /{id}", s.handleGetCart)
|
||||||
mux.HandleFunc("PUT /{id}", s.handleUpdateCart)
|
mux.HandleFunc("PUT /{id}", s.handleUpdateCart)
|
||||||
mux.HandleFunc("POST /{id}/cancel", s.handleCancelCart)
|
mux.HandleFunc("POST /{id}/cancel", s.handleCancelCart)
|
||||||
return mux
|
return withUCPAgent(withJSONContentType(mux))
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
@@ -70,7 +70,7 @@ func CheckoutHandler(applier CheckoutApplier, orderSvc ...OrderApplier) http.Han
|
|||||||
mux.HandleFunc("PUT /{id}", s.handleUpdateCheckout)
|
mux.HandleFunc("PUT /{id}", s.handleUpdateCheckout)
|
||||||
mux.HandleFunc("POST /{id}/complete", s.handleCompleteCheckout)
|
mux.HandleFunc("POST /{id}/complete", s.handleCompleteCheckout)
|
||||||
mux.HandleFunc("POST /{id}/cancel", s.handleCancelCheckout)
|
mux.HandleFunc("POST /{id}/cancel", s.handleCancelCheckout)
|
||||||
return mux
|
return withUCPAgent(withJSONContentType(mux))
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
@@ -113,5 +113,18 @@ func Handler(cartApplier CartApplier, opts Options) http.Handler {
|
|||||||
mux.HandleFunc("POST /checkout-sessions/{id}/cancel", checkoutSrv.handleCancelCheckout)
|
mux.HandleFunc("POST /checkout-sessions/{id}/cancel", checkoutSrv.handleCancelCheckout)
|
||||||
}
|
}
|
||||||
|
|
||||||
return mux
|
return withUCPAgent(withJSONContentType(mux))
|
||||||
|
}
|
||||||
|
|
||||||
|
// withUCPAgent wraps a handler with UCP-Agent header parsing middleware.
|
||||||
|
func withUCPAgent(next http.Handler) http.Handler {
|
||||||
|
return WithUCPAgent(next)
|
||||||
|
}
|
||||||
|
|
||||||
|
// withJSONContentType is a middleware that ensures all responses have Content-Type: application/json.
|
||||||
|
func withJSONContentType(next http.Handler) http.Handler {
|
||||||
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
next.ServeHTTP(w, r)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
+43
-1
@@ -4,6 +4,7 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"log"
|
||||||
|
|
||||||
"git.k6n.net/mats/go-cart-actor/pkg/cart"
|
"git.k6n.net/mats/go-cart-actor/pkg/cart"
|
||||||
messages "git.k6n.net/mats/go-cart-actor/proto/cart"
|
messages "git.k6n.net/mats/go-cart-actor/proto/cart"
|
||||||
@@ -26,6 +27,12 @@ func (s *Server) callTool(req *rpcRequest) *rpcResponse {
|
|||||||
if err := json.Unmarshal(req.Params, &p); err != nil {
|
if err := json.Unmarshal(req.Params, &p); err != nil {
|
||||||
return errorResponse(req.ID, codeInvalidParams, "invalid params")
|
return errorResponse(req.ID, codeInvalidParams, "invalid params")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Extract UCP meta from arguments (UCP-Agent profile advertisement).
|
||||||
|
// Per the UCP spec, meta is a sibling key inside arguments:
|
||||||
|
// "arguments": { "meta": { "ucp-agent": { "profile": "..." } }, ... }
|
||||||
|
args := extractUCPAgentMeta(&p.Arguments)
|
||||||
|
|
||||||
var t *tool
|
var t *tool
|
||||||
for i := range s.tools {
|
for i := range s.tools {
|
||||||
if s.tools[i].Name == p.Name {
|
if s.tools[i].Name == p.Name {
|
||||||
@@ -36,13 +43,48 @@ func (s *Server) callTool(req *rpcRequest) *rpcResponse {
|
|||||||
if t == nil {
|
if t == nil {
|
||||||
return errorResponse(req.ID, codeInvalidParams, "unknown tool: "+p.Name)
|
return errorResponse(req.ID, codeInvalidParams, "unknown tool: "+p.Name)
|
||||||
}
|
}
|
||||||
out, err := t.invoke(p.Arguments)
|
out, err := t.invoke(args)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return result(req.ID, toolError(err))
|
return result(req.ID, toolError(err))
|
||||||
}
|
}
|
||||||
return result(req.ID, toolText(out))
|
return result(req.ID, toolText(out))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// extractUCPAgentMeta checks if the JSON arguments contain a "meta" key with
|
||||||
|
// a nested "ucp-agent.profile" field. If found, it logs the profile (for
|
||||||
|
// observability) and strips the "meta" key before returning the cleaned
|
||||||
|
// arguments to the tool handler, so the meta object does not leak into the
|
||||||
|
// tool's argument namespace.
|
||||||
|
func extractUCPAgentMeta(arguments *json.RawMessage) json.RawMessage {
|
||||||
|
if arguments == nil || len(*arguments) == 0 {
|
||||||
|
return *arguments
|
||||||
|
}
|
||||||
|
var obj map[string]json.RawMessage
|
||||||
|
if err := json.Unmarshal(*arguments, &obj); err != nil {
|
||||||
|
return *arguments
|
||||||
|
}
|
||||||
|
metaRaw, hasMeta := obj["meta"]
|
||||||
|
if !hasMeta {
|
||||||
|
return *arguments
|
||||||
|
}
|
||||||
|
// Try to extract the profile for observability.
|
||||||
|
var meta struct {
|
||||||
|
UCPAgent *struct {
|
||||||
|
Profile string `json:"profile"`
|
||||||
|
} `json:"ucp-agent"`
|
||||||
|
}
|
||||||
|
if err := json.Unmarshal(metaRaw, &meta); err == nil && meta.UCPAgent != nil && meta.UCPAgent.Profile != "" {
|
||||||
|
log.Printf("ucp-agent: profile=%s", meta.UCPAgent.Profile)
|
||||||
|
}
|
||||||
|
// Strip meta from arguments before passing to the tool handler.
|
||||||
|
delete(obj, "meta")
|
||||||
|
cleaned, err := json.Marshal(obj)
|
||||||
|
if err != nil {
|
||||||
|
return *arguments
|
||||||
|
}
|
||||||
|
return cleaned
|
||||||
|
}
|
||||||
|
|
||||||
func (s *Server) buildTools() []tool {
|
func (s *Server) buildTools() []tool {
|
||||||
return []tool{
|
return []tool{
|
||||||
{
|
{
|
||||||
|
|||||||
+43
-1
@@ -4,6 +4,7 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"log"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"git.k6n.net/mats/go-cart-actor/pkg/order"
|
"git.k6n.net/mats/go-cart-actor/pkg/order"
|
||||||
@@ -27,6 +28,12 @@ func (s *Server) callTool(req *rpcRequest) *rpcResponse {
|
|||||||
if err := json.Unmarshal(req.Params, &p); err != nil {
|
if err := json.Unmarshal(req.Params, &p); err != nil {
|
||||||
return errorResponse(req.ID, codeInvalidParams, "invalid params")
|
return errorResponse(req.ID, codeInvalidParams, "invalid params")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Extract UCP meta from arguments (UCP-Agent profile advertisement).
|
||||||
|
// Per the UCP spec, meta is a sibling key inside arguments:
|
||||||
|
// "arguments": { "meta": { "ucp-agent": { "profile": "..." } }, ... }
|
||||||
|
args := extractUCPAgentMeta(&p.Arguments)
|
||||||
|
|
||||||
var t *tool
|
var t *tool
|
||||||
for i := range s.tools {
|
for i := range s.tools {
|
||||||
if s.tools[i].Name == p.Name {
|
if s.tools[i].Name == p.Name {
|
||||||
@@ -37,13 +44,48 @@ func (s *Server) callTool(req *rpcRequest) *rpcResponse {
|
|||||||
if t == nil {
|
if t == nil {
|
||||||
return errorResponse(req.ID, codeInvalidParams, "unknown tool: "+p.Name)
|
return errorResponse(req.ID, codeInvalidParams, "unknown tool: "+p.Name)
|
||||||
}
|
}
|
||||||
out, err := t.invoke(p.Arguments)
|
out, err := t.invoke(args)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return result(req.ID, toolError(err))
|
return result(req.ID, toolError(err))
|
||||||
}
|
}
|
||||||
return result(req.ID, toolText(out))
|
return result(req.ID, toolText(out))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// extractUCPAgentMeta checks if the JSON arguments contain a "meta" key with
|
||||||
|
// a nested "ucp-agent.profile" field. If found, it logs the profile (for
|
||||||
|
// observability) and strips the "meta" key before returning the cleaned
|
||||||
|
// arguments to the tool handler, so the meta object does not leak into the
|
||||||
|
// tool's argument namespace.
|
||||||
|
func extractUCPAgentMeta(arguments *json.RawMessage) json.RawMessage {
|
||||||
|
if arguments == nil || len(*arguments) == 0 {
|
||||||
|
return *arguments
|
||||||
|
}
|
||||||
|
var obj map[string]json.RawMessage
|
||||||
|
if err := json.Unmarshal(*arguments, &obj); err != nil {
|
||||||
|
return *arguments
|
||||||
|
}
|
||||||
|
metaRaw, hasMeta := obj["meta"]
|
||||||
|
if !hasMeta {
|
||||||
|
return *arguments
|
||||||
|
}
|
||||||
|
// Try to extract the profile for observability.
|
||||||
|
var meta struct {
|
||||||
|
UCPAgent *struct {
|
||||||
|
Profile string `json:"profile"`
|
||||||
|
} `json:"ucp-agent"`
|
||||||
|
}
|
||||||
|
if err := json.Unmarshal(metaRaw, &meta); err == nil && meta.UCPAgent != nil && meta.UCPAgent.Profile != "" {
|
||||||
|
log.Printf("ucp-agent: profile=%s", meta.UCPAgent.Profile)
|
||||||
|
}
|
||||||
|
// Strip meta from arguments before passing to the tool handler.
|
||||||
|
delete(obj, "meta")
|
||||||
|
cleaned, err := json.Marshal(obj)
|
||||||
|
if err != nil {
|
||||||
|
return *arguments
|
||||||
|
}
|
||||||
|
return cleaned
|
||||||
|
}
|
||||||
|
|
||||||
func (s *Server) buildTools() []tool {
|
func (s *Server) buildTools() []tool {
|
||||||
return []tool{
|
return []tool{
|
||||||
{
|
{
|
||||||
|
|||||||
Reference in New Issue
Block a user