mirror of
https://github.com/multica-ai/multica.git
synced 2026-07-08 06:45:55 +02:00
* 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>
79 lines
2.2 KiB
Go
79 lines
2.2 KiB
Go
package composio
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"net/http"
|
|
"net/url"
|
|
"strconv"
|
|
)
|
|
|
|
// Toolkit is the minimal toolkit descriptor used as a nested field inside
|
|
// connected accounts, sessions, and the toolkit list endpoint. Only fields
|
|
// useful for UI / dispatch decisions are typed.
|
|
type Toolkit struct {
|
|
Slug string `json:"slug"`
|
|
Name string `json:"name,omitempty"`
|
|
LogoURL string `json:"logo,omitempty"`
|
|
Description string `json:"description,omitempty"`
|
|
Categories []string `json:"categories,omitempty"`
|
|
AuthSchemes []string `json:"auth_schemes,omitempty"`
|
|
Meta map[string]any `json:"meta,omitempty"`
|
|
}
|
|
|
|
// ListToolkitsRequest carries the optional filters of GET /toolkits.
|
|
type ListToolkitsRequest struct {
|
|
Category string
|
|
Limit int
|
|
Cursor string
|
|
// SortBy is the upstream sort order. Per the v3.1 spec the valid enum
|
|
// values are "usage" and "alphabetically".
|
|
SortBy string
|
|
}
|
|
|
|
// ListToolkitsResponse is the typed paginated response.
|
|
type ListToolkitsResponse struct {
|
|
Items []Toolkit `json:"items"`
|
|
NextCursor string `json:"next_cursor,omitempty"`
|
|
TotalItems int `json:"total_items,omitempty"`
|
|
}
|
|
|
|
// ListToolkits returns toolkits available to the project.
|
|
func (c *Client) ListToolkits(ctx context.Context, req ListToolkitsRequest) (*ListToolkitsResponse, error) {
|
|
q := url.Values{}
|
|
if req.Category != "" {
|
|
q.Set("category", req.Category)
|
|
}
|
|
if req.Limit > 0 {
|
|
q.Set("limit", strconv.Itoa(req.Limit))
|
|
}
|
|
if req.Cursor != "" {
|
|
q.Set("cursor", req.Cursor)
|
|
}
|
|
if req.SortBy != "" {
|
|
q.Set("sort_by", req.SortBy)
|
|
}
|
|
path := "/toolkits"
|
|
if encoded := q.Encode(); encoded != "" {
|
|
path += "?" + encoded
|
|
}
|
|
var out ListToolkitsResponse
|
|
if err := c.do(c.newRequest(ctx), http.MethodGet, path, &out); err != nil {
|
|
return nil, err
|
|
}
|
|
return &out, nil
|
|
}
|
|
|
|
// GetToolkit fetches a single toolkit by its slug (e.g. "notion", "github").
|
|
func (c *Client) GetToolkit(ctx context.Context, slug string) (*Toolkit, error) {
|
|
if slug == "" {
|
|
return nil, errors.New("composio: GetToolkit: slug is required")
|
|
}
|
|
var out Toolkit
|
|
if err := c.do(c.newRequest(ctx),
|
|
http.MethodGet, "/toolkits/"+url.PathEscape(slug), &out); err != nil {
|
|
return nil, err
|
|
}
|
|
return &out, nil
|
|
}
|