Files
multica/server/pkg/composio/tools.go
Multica Eve 8d0ea04fb0 feat(composio): add standalone Go SDK client (MVP) (#4603)
* feat(composio): add standalone Go SDK client (MVP)

Adds server/pkg/composio — a self-contained Go SDK for the Composio v3.1
REST API. Built on go-resty/resty v2; zero coupling to other Multica
packages so it can be vendored or extracted later without surgery.

MVP surface (just the endpoints Stage 2 needs):

- POST /connected_accounts/link        Client.CreateLink
- POST /tool_router/session            Client.CreateSession
- GET  /connected_accounts             Client.ListConnectedAccounts
- POST /connected_accounts/{id}/revoke Client.RevokeConnection
- DELETE /connected_accounts/{id}      Client.DeleteConnectedAccount
                                       (404 -> nil, idempotent)
- GET  /toolkits                       Client.ListToolkits
- GET  /toolkits/{slug}                Client.GetToolkit
- POST /tools/execute/{slug}           Client.ExecuteTool
- Webhook HMAC-SHA256 verification     composio.VerifyWebhook /
                                       VerifyHTTPRequest + ParseEvent

Other notes:

- Auth via x-api-key header (Composio v3.1 contract).
- Typed *APIError envelope with IsNotFound / IsUnauthorized /
  IsRateLimited helpers; falls back to raw body when upstream returns
  non-JSON.
- Webhook signature accepts the official "v1,<sig>" format and any
  comma-separated multi-version list; 300s replay tolerance by default,
  honors an injectable clock for tests; RFC3339 timestamps tolerated.
- README.md documents all public APIs and design choices.

Tests:

- All exercise httptest.NewServer - no real Composio calls.
- 36 tests covering happy paths, validation, 404 idempotence, error
  decoding, signature verify (good / tampered / stale / multi-version /
  bare / RFC3339 / missing headers / empty secret).
- go test ./pkg/composio/... -cover -> 82.2%, exceeds the >=80% bar.

Follow-ups (separate PRs):

- server/internal/integrations/composio - DB schema, REST handlers,
  registration_service (CSRF), dispatch hook (MUL-3720 remainder).
- Pagination iterators, retry middleware, proxy execute, triggers.

Refs: MUL-3720, MUL-3715
Co-authored-by: multica-agent <github@multica.ai>

* fix(composio): align SDK with v3.1 wire contract (PR #4603 review)

Addresses GPT-Boy's review on PR #4603:

Must-fix
- ListConnectedAccountsRequest: switch UserID/AuthConfigID singular fields
  to plural slices (UserIDs, AuthConfigIDs, ToolkitSlugs, ConnectedAccountIDs,
  Statuses). The Composio v3.1 spec ships these as `*_ids` array params;
  our singular form silently dropped the user-filter on the wire. Also
  surfaces order_by / order_direction / account_type from the same spec.
- ExecuteToolRequest: rename ToolkitVersions -> Version with json tag
  `version` (the actual v3.1 body field). Marks AllowTracing as
  deprecated per the spec.

Nits
- ListToolkitsRequest.SortBy comment: `popular | alphabetical` -> the
  real enum `usage | alphabetically`.
- Client constructor: when Options.HTTPClient is provided, use
  resty.NewWithClient(hc) so the caller's Transport, Jar, CheckRedirect,
  and Timeout all carry through — the prior code only forwarded
  Transport + Timeout despite the field comment promising the full
  *http.Client.

Tests
- TestListConnectedAccounts_QueryString now asserts plural query keys
  (user_ids, auth_config_ids, connected_account_ids, statuses) and
  explicitly guards that the legacy singular keys do not leak.
- TestExecuteTool_Success decodes the body as a raw map and asserts the
  wire key is `version` (not `toolkit_versions`).
- New TestExecuteToolRequest_VersionSerialization locks the json tag.
- New TestNewClient_HonorsInjectedHTTPClient drives a request through a
  recordingTransport and asserts the inbound *http.Client actually
  handled it.

Verification
- go test ./pkg/composio/... -cover -> 85.1% (38 tests; was 82.2% / 36).
- go vet, go build, gofmt -l all clean.

Refs: PR #4603 review, MUL-3720
Co-authored-by: multica-agent <github@multica.ai>

---------

Co-authored-by: Eve <eve@multica-ai.local>
Co-authored-by: multica-agent <github@multica.ai>
2026-06-26 15:07:47 +08:00

75 lines
2.9 KiB
Go

package composio
import (
"context"
"errors"
"net/http"
"net/url"
)
// ExecuteToolRequest is the body for POST /tools/execute/{tool_slug}.
//
// Spec: https://docs.composio.dev/reference/api-reference/tools/postToolsExecuteByToolSlug
//
// Either ConnectedAccountID or (UserID + the tool's toolkit) is required so
// Composio knows which credential set to use. The SDK does not enforce that
// invariant up front; the upstream returns a 422 with a clear message when
// missing.
type ExecuteToolRequest struct {
// Arguments is the structured input to the tool. Shape varies per tool.
Arguments map[string]any `json:"arguments,omitempty"`
// ConnectedAccountID pins execution to a specific connected account.
ConnectedAccountID string `json:"connected_account_id,omitempty"`
// UserID lets Composio resolve the connected account by user when
// the caller does not have the explicit `ca_` id handy.
UserID string `json:"user_id,omitempty"`
// Version pins the tool definition version. Pass "latest" or a dated
// version like "20251027_00"; defaults to "00000000_00" upstream.
//
// The Composio docs note that manual tool execution requires an explicit
// version; setting this avoids unintended drift when Composio promotes
// a new latest.
Version string `json:"version,omitempty"`
// AllowTracing is the upstream-deprecated debug-tracing flag.
//
// Deprecated: marked deprecated on the Composio side (v3.1) — kept here
// only for backward compatibility with existing callers. Will be removed
// once Composio drops the field.
AllowTracing bool `json:"allow_tracing,omitempty"`
}
// ExecuteToolResponse is the typed result. The upstream wire shape varies by
// tool, so [Data] is intentionally generic; callers cast to whatever the
// tool's documented output schema looks like.
type ExecuteToolResponse struct {
Successful bool `json:"successful"`
Data map[string]any `json:"data,omitempty"`
Error string `json:"error,omitempty"`
LogID string `json:"log_id,omitempty"`
SessionInfo map[string]any `json:"session_info,omitempty"`
}
// ExecuteTool calls a Composio tool by its slug
// (SCREAMING_SNAKE_CASE, e.g. GITHUB_CREATE_ISSUE).
//
// This is the deterministic backend path — it skips MCP/session orchestration
// and is the right call for fixed flows like autopilots or built-in skills.
func (c *Client) ExecuteTool(ctx context.Context, toolSlug string, req ExecuteToolRequest) (*ExecuteToolResponse, error) {
if toolSlug == "" {
return nil, errors.New("composio: ExecuteTool: toolSlug is required")
}
if req.ConnectedAccountID == "" && req.UserID == "" {
return nil, errors.New("composio: ExecuteTool: either ConnectedAccountID or UserID must be set")
}
var out ExecuteToolResponse
if err := c.do(c.newRequest(ctx).SetBody(req),
http.MethodPost, "/tools/execute/"+url.PathEscape(toolSlug), &out); err != nil {
return nil, err
}
return &out, nil
}