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.AsyncClientwith a connection pool reused across all calls. - Resolves the encrypted
agent_credentialby id, enforces the tenant boundary, and rejects cross-tenant access at the resolver (not the route). - Validates that the credential
kindmatches 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-honoredRetry-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
| Adapter | Actions | Credential kind(s) | Wire protocol |
|---|---|---|---|
| Slack | slack_dm · slack_post_channel | slack_bot · slack_user | Web API; OAuth bot/user token; conversations.open for DM-by-email. |
| Notion | notion_query · notion_create_page | notion | REST; Notion-Version: 2022-06-28 pinned; optional block fetch on query. |
| HubSpot | hubspot_upsert_contact | hubspot | CRM v3 batch upsert by email — idempotent single round trip. |
| Linear | linear_create_issue | linear | GraphQL; issueCreate mutation; priority remap (low/medium/high/urgent → 4/3/2/1). |
| X / Twitter | social_post:x · social_post:twitter | x_twitter | v2 POST /tweets; 280-char check; media via v1.1 upload first. |
social_post:linkedin | linkedin | UGC posts API; author URN from credential metadata. | |
| SMTP | email_send | gmail_oauth · outlook_oauth · generic_api_key | Gmail/Outlook XOAUTH2; generic SMTP via metadata.smtp_host/port/ssl. |
| IMAP | email_search · email_watch_inbox | gmail_oauth · outlook_oauth · generic_api_key | XOAUTH2 + generic IMAP; small filter DSL (is:unread, from:, newer_than:24h). |
| Google Calendar | calendar_create_event · calendar_list_events | gmail_oauth | v3 events.insert + list; Meet link via conferenceDataVersion=1. |
| HTTP (generic) | http_post_json | None (optional bearer) | Generic JSON webhook; SSRF guard blocks private/loopback/link-local; 1 MiB body cap. |
| RSS | rss_subscribe | None | stdlib 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:
linkedin—metadata.urn(e.g.urn:li:person:abc123) — required because LinkedIn UGC needs the author URN.generic_api_keywhen used for SMTP —metadata.smtp_host,metadata.smtp_port,metadata.smtp_ssl,metadata.username,metadata.from_address.generic_api_keywhen used for IMAP —metadata.imap_host,metadata.imap_port,metadata.imap_ssl,metadata.username.gmail_oauthfor Calendar —metadata.calendar_id(defaultprimary).linear—metadata.auth_formset tobearerwhen 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-Afterheader / 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_secondsin payload.