Skip to content

RFC 0049 — Plugin External Connections

Summary

Add a platform pattern for plugins that connect to external services on behalf of a user or instance. The platform provides connection metadata, OAuth callback routing, token/credential storage through the secret vault, reconnect and disconnect semantics, and operator visibility. Plugins still own provider adapters and domain-specific behavior.

This fills the gap between deployment-time plugin env vars and runtime secrets: a plugin can let a user connect an email account, model provider, payment provider, social account, or other external API without each plugin inventing its own credential lifecycle.

Motivation

Several plugin categories need external accounts:

  • syncing messages from email, chat, or social providers;
  • sending through communication providers;
  • connecting model providers or self-hosted AI endpoints;
  • connecting payment or invoice delivery providers;
  • importing data from third-party tools.

RFC 0043 defines where secrets live, but it does not define the provider connection lifecycle around those secrets. Without a shared pattern, plugins will duplicate OAuth state validation, callback routing, token refresh, disconnect behavior, audit logs, and Account/Console visibility.

Current state

  • RFC 0018 supports plugin-scoped env vars for deployment-time secrets.
  • RFC 0043 proposes sdk.secrets for runtime secrets.
  • RFC 0047 proposes tool contracts for explicit external effects.
  • Public API delegation exists, and public page routes are planned in RFC 0042.
  • There is no standard sdk.connections surface, OAuth callback route model, or external-provider inventory.

Proposed design

Connection records

Add platform-owned metadata records:

text
plugin_connections
  id
  tenant_id
  plugin_id
  user_id nullable
  scope                 # user | plugin | instance
  provider              # email.imap, google.oauth, openrouter, stripe, custom
  label
  status                # connected | needs_reauth | paused | disconnected | error
  secret_ref nullable
  metadata
  last_checked_at nullable
  last_used_at nullable
  last_error nullable
  created_at
  updated_at
  disconnected_at nullable

The connection record stores metadata only. Secret values remain in the vault.

SDK surface

Add experimental sdk.connections:

ts
sdk.connections.create(input: {
  scope: 'user' | 'plugin' | 'instance';
  provider: string;
  label: string;
  secretRef?: string;
  metadata?: unknown;
}): Promise<ConnectionRef>;

sdk.connections.list(filter?: { provider?: string; scope?: string }): Promise<ConnectionRef[]>;
sdk.connections.get(id: string): Promise<ConnectionRef | null>;
sdk.connections.update(id: string, input: { label?: string; status?: string; metadata?: unknown }): Promise<ConnectionRef>;
sdk.connections.disconnect(id: string): Promise<void>;
sdk.connections.markUsed(id: string): Promise<void>;
sdk.connections.markError(id: string, error: SanitizedConnectionError): Promise<void>;

The runtime injects plugin ID, tenant ID, and user ID. Plugins cannot list or read another plugin's connections.

OAuth/connect callback flow

Plugins may register connection callback routes through manifest declarations:

jsonc
{
  "connections": {
    "providers": [
      {
        "id": "email.google",
        "title": "Google Mail",
        "callbackPath": "/connections/google/callback",
        "scopes": ["user"],
      },
    ],
  },
}

Callback rules:

  • callback paths resolve under the plugin route prefix;
  • the platform provides a signed, short-lived state helper;
  • plugins validate provider response and exchange codes server-side;
  • token material is stored through sdk.secrets;
  • connection metadata is stored through sdk.connections;
  • failed callbacks never log raw tokens or authorization codes.

External effects

Connecting an account does not grant arbitrary external side effects. Sending, publishing, charging, or mutating remote state remains plugin-owned and should use tool contracts when another caller initiates the action.

Security requirements

  • Secret material is never stored in connection metadata.
  • OAuth state values are signed, single-use, and expiry-bound.
  • OAuth callbacks validate plugin ID, provider ID, actor user, and expected redirect path.
  • Disconnect deletes or revokes associated secrets where possible.
  • Token refresh errors move the connection to needs_reauth or error.
  • Account and Console can show connection metadata without revealing secrets.
  • External provider errors are sanitized before activity logs or UI display.

Alternatives considered

Let every plugin manage its own connection table

Rejected. Plugins still need domain tables, but the security-sensitive lifecycle around credentials and account visibility should be consistent.

Treat connections as secrets only

Rejected. Secrets lack user-facing labels, status, reconnect state, provider metadata, and audit visibility.

Require only deployment-time env vars

Rejected. User-authorized accounts and BYO API keys are runtime decisions.

Open questions

  1. Should OAuth callback routing be implemented as public plugin API routes or a dedicated runtime callback dispatcher?
  2. Should provider manifests declare OAuth scopes as display-only metadata, or should the platform validate scope changes?
  3. Should connection records support health-check handlers?
  4. How much provider metadata should be exported during user data export?

Adoption path

  1. Add connection metadata tables.
  2. Add sdk.connections and manifest provider declarations.
  3. Add signed OAuth state helper and callback validation helpers.
  4. Add Account/Console connection metadata views.
  5. Document provider adapter, reconnect, and disconnect patterns.

Changelog

VersionDateChange
0.1June 2026Initial draft

Open source under AGPL-3.0. Each Sovereign instance is independently operated.