Skip to main content

SaaS integrations — reference

Midcore ships eleven first-class SaaS adapters under services/autonomy/tools/integrations/. They share one dispatcher, one credential resolver, one retry policy, one redaction policy. This page is the authoritative reference: every adapter's wire protocol, accepted credential kinds, supported actions, and failure-mode semantics.

IntegrationDispatcher

One IntegrationDispatcher per process. The dispatcher:

  • Holds a shared httpx.AsyncClient with a connection pool reused across all calls.
  • Resolves the encrypted agent_credential by id, enforces the tenant boundary, and rejects cross-tenant access at the resolver (not the route).
  • Validates that the credential kind matches the action — a HubSpot key cannot be passed to a Slack action; the dispatcher refuses before the network call.
  • Wraps the adapter call in call_with_retry — exponential backoff with provider-honored Retry-After, capped at 3 attempts. Auth and validation errors never retry.
  • Emits a structured log line per call containing the credential UUID and elapsed_ms — never the plaintext token.

Errors are split into four classes so the retry layer behaves correctly:AdapterAuthError (refresh required, no retry), AdapterRateLimitedError (retry with backoff), AdapterTransientError (5xx / network, retry), AdapterValidationError (bad input, no retry).

Adapter matrix

AdapterActionsCredential kind(s)Wire protocol
Slackslack_dm · slack_post_channelslack_bot · slack_userWeb API; OAuth bot/user token; conversations.open for DM-by-email.
Notionnotion_query · notion_create_pagenotionREST; Notion-Version: 2022-06-28 pinned; optional block fetch on query.
HubSpothubspot_upsert_contacthubspotCRM v3 batch upsert by email — idempotent single round trip.
Linearlinear_create_issuelinearGraphQL; issueCreate mutation; priority remap (low/medium/high/urgent → 4/3/2/1).
X / Twittersocial_post:x · social_post:twitterx_twitterv2 POST /tweets; 280-char check; media via v1.1 upload first.
LinkedInsocial_post:linkedinlinkedinUGC posts API; author URN from credential metadata.
SMTPemail_sendgmail_oauth · outlook_oauth · generic_api_keyGmail/Outlook XOAUTH2; generic SMTP via metadata.smtp_host/port/ssl.
IMAPemail_search · email_watch_inboxgmail_oauth · outlook_oauth · generic_api_keyXOAUTH2 + generic IMAP; small filter DSL (is:unread, from:, newer_than:24h).
Google Calendarcalendar_create_event · calendar_list_eventsgmail_oauthv3 events.insert + list; Meet link via conferenceDataVersion=1.
HTTP (generic)http_post_jsonNone (optional bearer)Generic JSON webhook; SSRF guard blocks private/loopback/link-local; 1 MiB body cap.
RSSrss_subscribeNonestdlib XML parser; RSS 2.0 + Atom; keyword + since-filter.

Credential schema

One row per (tenant, kind, label) in automation.agent_credentials:

{
  "id": "<UUID>",
  "tenant_id": "<UUID>",
  "kind": "slack_bot" | "gmail_oauth" | "hubspot" | ...,
  "label": "primary slack workspace",
  "token_cipher": "<Fernet ciphertext>",
  "refresh_token_cipher": "<optional Fernet ciphertext>",
  "scope": "channels:read chat:write",
  "metadata": { ...kind-specific... },
  "expires_at": "<ISO 8601 | null>",
  "revoked_at": "<ISO 8601 | null>"
}

Kind-specific metadata examples:

  • linkedinmetadata.urn (e.g. urn:li:person:abc123) — required because LinkedIn UGC needs the author URN.
  • generic_api_key when used for SMTP — metadata.smtp_host, metadata.smtp_port, metadata.smtp_ssl, metadata.username, metadata.from_address.
  • generic_api_key when used for IMAP — metadata.imap_host, metadata.imap_port, metadata.imap_ssl, metadata.username.
  • gmail_oauth for Calendar — metadata.calendar_id (default primary).
  • linearmetadata.auth_form set to bearer when using OAuth tokens, omitted for personal API keys.

LLM-callable tool surface

Each adapter exposes its actions as LLM tools via services/autonomy/tool_executor.py:TOOL_SCHEMAS. The chat agent calls them like any other function. Example tool call:

{
  "name": "slack_dm",
  "arguments": {
    "credential_id": "<UUID>",
    "to": "U0123ABC",     // user id, channel id, or email
    "text": "Heads-up: the PR is ready for review"
  }
}

When the dispatcher returns ok=false (auth, validation, rate limit, transient), the tool result is an is_error=true ToolResult carrying the structured refusal_reason and error_code. The chat UI surfaces those verbatim — agents are not allowed to paper over them.

Retry policy & rate limits

One consistent policy across all adapters — adapters do NOT retry themselves; retries live in the dispatcher's call_with_retry:

  • 429 / rate-limited: honor Retry-After header / payload; up to 3 attempts.
  • 5xx / network: exponential backoff (0.6s → 1.2s → 2.4s) with 25% jitter, capped at 8s.
  • 401 / 403: no retry. Surface as AdapterAuthError — caller must reconnect.
  • 4xx (other): no retry. Surface as AdapterValidationError.
  • Hard timeout: 30s per HTTP call by default; callers can override via timeout_seconds in payload.

See also