Summary
Add a connection pack — a new pack kind (pack.json with kind: "connection") whose payload is a portable, signed, registry-listable definition of a provider: the authorize / token / revoke endpoints, the read/write OAuth scope catalog, and how the integration is reached (an MCP server, an OpenAPI surface, or a core integration node). It is the missing sibling in the pack family (node / agent / prompt / artifact-type / chat-card / workflow-chain packs each already have a dedicated -pack-manifest.schema.json). A connection pack carries no secret — only public provider metadata; it makes RFC 0047's auth: { type: 'oauth2', provider } magic string resolve against an installed artifact* instead of host-locked code, so any conformant host gains Google / Slack / GitHub / Jira / Stripe support by installing a pack rather than shipping a bespoke provider registry.
Motivation
The connector stack is already three-quarters built and Accepted:
- RFC 0045 (
connectorblock) lets a pack declare a named integration's typed actions —auth: { type: 'oauth2', provider: 'salesforce', scopes: [...] }. That is what you do. - RFC 0047 (
host.oauth) has the host perform the OAuth 2.0 authorization-code + refresh dance for that declaration and persist the result as a credential. That is how the token is obtained. - RFC 0046 (
host.credentials) defines credential resolution, lifecycle, rotation, and redaction. That is the credential at rest.
The seam they all lean on is provider — a bare string. RFC 0045 says provider: 'salesforce'; RFC 0047 performs the dance "for that provider"; but nothing in the spec defines what 'salesforce' resolves to — its authorize_url, token_url, the scope strings that mean "read" vs "write", or whether it is reached via an MCP server or a REST surface. Today every host hard-codes that catalog (the openwop reference app keeps it in providerRegistry.ts; other hosts re-implement it). The consequences:
- No portability. "A Google connection" is not an installable thing; it is code each host re-writes, so a workflow that works on host A may have no provider definition on host B even though both advertise
host.oauth. - No marketplace data. RFC 0045 §Discovery promises a connector marketplace on
packs.openwop.dev, but the provider catalog that marketplace would render has no wire representation. - No community providers. Adding the 100th SaaS provider requires shipping host code, not publishing a manifest — the exact friction the pack model exists to remove.
This RFC closes that seam: the provider definition becomes a connection pack, distributed through the same signed pipeline as every other pack, and provider: 'salesforce' resolves against the installed connection pack whose provider.id is salesforce. The spec is the right place because provider is already a normative wire identifier (RFC 0045/0047) — only its resolution source was left implementation-defined.
Proposal
§A — New schema schemas/connection-pack-manifest.schema.json (additive)
A connection pack is pack.json at the pack root with kind: "connection", peer to and disjoint from the other pack manifests via the kind discriminator (the established pattern from prompt-pack-manifest.schema.json / workflow-chain-pack-manifest.schema.json). It distributes through the same signed-tarball + Ed25519 + SRI pipeline.
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://openwop.dev/spec/v1/connection-pack-manifest.schema.json",
"title": "ConnectionPackManifest",
"type": "object",
"required": ["name", "version", "kind", "engines", "provider"],
"additionalProperties": false,
"properties": {
"name": { "type": "string", "pattern": "^(core|vendor|community|private)\\.[a-z][a-z0-9_-]*(\\.[a-z][a-zA-Z0-9_-]*)+$" },
"version": { "type": "string", "pattern": "^\\d+\\.\\d+\\.\\d+(?:-[0-9A-Za-z.-]+)?(?:\\+[0-9A-Za-z.-]+)?$" },
"kind": { "const": "connection" },
"engines": { "type": "object", "required": ["openwop"], "properties": { "openwop": { "type": "string" } } },
"provider": {
"type": "object",
"required": ["id", "displayName", "category", "auth", "reach"],
"additionalProperties": false,
"properties": {
"id": { "type": "string", "pattern": "^[a-z][a-z0-9-]*$", "description": "Stable provider id RFC 0045/0047 `provider` resolves to, e.g. `github`." },
"displayName": { "type": "string", "minLength": 1 },
"category": { "type": "string", "enum": ["communication","docs","crm","dev","storage","email-calendar","ticketing","data-warehouse","marketing","finance","hr","esignature","support","project-management","payments","other"] },
"auth": {
"type": "object",
"required": ["kind"],
"additionalProperties": false,
"properties": {
"kind": { "enum": ["oauth2","api_key","bearer","basic"] },
"authFlow": { "enum": ["pkce","client_credentials","manual","none"] },
"scopeModel": { "enum": ["groups","coarse","capabilities"], "default": "groups",
"description": "`groups` = read/write scope groups (Google/Jira/Graph). `coarse` = a single read-vs-write toggle (Stripe Connect read_only/read_write). `capabilities` = consent is a static developer-portal capability set, not per-request scopes (Notion)." },
"endpoints": {
"type": "object", "additionalProperties": false,
"properties": {
"authorize": { "type": "string", "format": "uri", "pattern": "^https://" },
"token": { "type": "string", "format": "uri", "pattern": "^https://" },
"revoke": { "type": "string", "format": "uri", "pattern": "^https://" }
}
},
"scopes": {
"type": "object", "additionalProperties": false,
"properties": {
"read": { "type": "array", "items": { "$ref": "#/$defs/ScopeGroup" } },
"write": { "type": "array", "items": { "$ref": "#/$defs/ScopeGroup" } }
}
},
"instanceUrlTemplate": { "type": "string",
"description": "For per-account-host providers (Snowflake/NetSuite/ServiceNow): a template the host fills per connection, e.g. `https://{account}.snowflakecomputing.com`. Its presence signals the provider cannot use a single global OAuth app." }
}
},
"reach": {
"type": "object", "minProperties": 1, "maxProperties": 1, "additionalProperties": false,
"description": "Exactly ONE of mcp | openapi | integration — which core node injects the resolved credential.",
"properties": {
"mcp": { "type": "object", "required": ["server"], "additionalProperties": false,
"properties": { "server": { "type": "object", "required": ["url","transport"], "properties": { "url": { "type": "string", "format": "uri", "pattern": "^https://" }, "transport": { "enum": ["http","sse"] } } } } },
"openapi": { "type": "object", "required": ["ref"], "additionalProperties": false,
"properties": { "ref": { "type": "string", "description": "URL or in-pack path of the OpenAPI document for core.openwop.http.openapi-call." } } },
"integration": { "type": "object", "required": ["node"], "additionalProperties": false,
"properties": { "node": { "type": "string", "description": "A core.openwop.integration.* node typeId." } } }
}
},
"consumerNodes": { "type": "array", "items": { "type": "string" },
"description": "The core node typeIds that consume this provider's credential (e.g. core.openwop.mcp.invoke-tool)." },
"docsUrl": { "type": "string", "format": "uri" }
}
}
},
"$defs": {
"ScopeGroup": {
"type": "object", "required": ["key","label","scopes"], "additionalProperties": false,
"properties": {
"key": { "type": "string", "pattern": "^[a-z][a-z0-9._-]*$" },
"label": { "type": "string", "minLength": 1 },
"scopes": { "type": "array", "items": { "type": "string" }, "description": "The provider's raw scope strings, e.g. `https://www.googleapis.com/auth/drive.readonly`." }
}
}
}
}
§B — Normative prose (new spec/v1/connection-packs.md)
2026-06-12 amendment (pre-floor, from the two host implementations). The prose lands as a standalone
spec/v1/connection-packs.mdper the pack-family precedent (artifact-type-packs.md,chat-card-packs.md,workflow-chain-packs.mdare each standalone docs), not as anode-packs.mdsection. §B.2 gains the normative blocklist, the full-pathtokenexemption, and the scan-before-validation ordering; §B.6 gains the SemVer §11 prerelease-precedence sentence; new §B.8 (rejection isolation + publish-path idempotency ordering) and §B.9 (validator robustness) capture failure-mode conduct both reference implementations converged on during cross-review.
- A connection pack manifest MUST carry
kind: "connection"and validate againstconnection-pack-manifest.schema.json. It MUST declare exactly oneprovider. - A connection pack MUST NOT contain credential material — no client secret, no access/refresh token, no API key, no password — in any field. A host MUST reject a connection-pack manifest that carries a property whose name indicates a secret with
connection_pack_credential_material; the normative minimum blocklist (matched case-insensitively, at any depth) isclientSecret,client_secret,apiKey,api_key,token,accessToken,refreshToken,password,privateKey,secret— with the single exemption of the property namedtokenat exactly the pathprovider.auth.endpoints.token(the OAuth token-endpoint URL). The scan MUST run before generic schema validation so the specific code wins over a generic shape error. Hosts MAY extend the blocklist and SHOULD also reject values matching well-known credential formats. The OAuth client credential is a host concern (RFC 0047; supplied out of band by the operator), never shipped in the pack. provider.auth.endpoints.{authorize,token,revoke}, when present, MUST be absolutehttps://URLs. A host MUST treat them as fixed, manifest-declared values and MUST NOT derive them from runtime user input — they are not an SSRF surface (the same posture RFC 0047 already requires for token endpoints).- When
provider.auth.kindisoauth2andscopeModelisgroups,auth.scopes.readSHOULD be present, andauth.scopes.write(when present) MUST be requested as a separate consent step — a host MUST NOT bundle write scopes into the initial read authorization (composes with the write-re-consent pattern already used by RFC 0047 hosts). provider.reachMUST specify exactly one ofmcp/openapi/integration, declaring which core node family injects the resolved credential.- Resolution (the core contract). When an RFC 0045 connector declares
auth: { type: 'oauth2', provider: P }(or RFC 0047host.oauthis invoked for providerP), a host advertisingcapabilities.connections.packsSupported: trueMUST resolvePagainst the installed connection pack whoseprovider.id === Pto obtain the authorize/token endpoints and scope catalog. If no installed connection pack matchesPand the host has no built-in definition forP, the host MUST refuse to register the dependent connector/pack withconnection_provider_unresolved. A host MAY retain built-in provider definitions; an installed connection pack for the sameidMUST take precedence over a built-in of the sameidonly when itsversionis greater-or-equal (else the host MUST surface aconnection_provider_conflictdiagnostic rather than silently choosing). Version comparison MUST follow SemVer §11 — a prerelease (1.0.0-alpha.1) is lower than its release (1.0.0), and the prerelease component begins at the first hyphen after the patch digits. - Connection packs MUST be distributed through the same signed-tarball + Ed25519 + SRI pipeline as node/prompt/agent packs (
node-packs.md§Signing), and MUST surface in theregistry/index and onpacks.openwop.devas a distinctconnectionartifact facet (data-only — no new endpoint, mirroring RFC 0045 §Discovery). - Rejection isolation. A rejected connection pack means not installed — nothing more. On loader-path hosts (packs loaded at process start), one malformed pack MUST NOT abort host startup or block the registration of other packs; the host MUST warn-and-continue per pack with the rejection reason observable (e.g. an
errors[]projection) carrying the specific error code. On publish-path hosts (validation behind an idempotent publish endpoint), §B.6 resolution MUST run after the idempotency short-circuit — a byte-identical re-publish MUST succeed even when resolution inputs changed since the original publish. - Validator robustness. Failure to load or compile
connection-pack-manifest.schema.jsonitself MUST NOT abort host startup and MUST NOT surface as an unstructured5xx: publish-path hosts return a structured error; loader-path hosts disable connection-pack loading for the process while continuing to boot.
§C — capabilities.schema.json (additive)
Add capabilities.connections.packsSupported: boolean (default absent ⇒ unsupported), mirroring capabilities.prompts.packsSupported (a packsSupported flag nested under a per-feature block — the established convention). A host advertising it MUST implement §B.6 resolution. Connection packs are only useful on a host that also advertises capabilities.oauth.supported (RFC 0047) or capabilities.credentials.supported (RFC 0046); a host SHOULD NOT advertise connections.packsSupported without at least one of those.
Positive example (core.openwop.connections.github)
{ "name": "core.openwop.connections.github", "version": "1.0.0", "kind": "connection",
"engines": { "openwop": ">=1.0.0" },
"provider": {
"id": "github", "displayName": "GitHub", "category": "dev",
"auth": { "kind": "oauth2", "authFlow": "pkce", "scopeModel": "groups",
"endpoints": { "authorize": "https://github.com/login/oauth/authorize", "token": "https://github.com/login/oauth/access_token" },
"scopes": { "read": [ { "key": "repo.read", "label": "Read repositories", "scopes": ["repo:status","public_repo"] } ],
"write": [ { "key": "repo.write", "label": "Write repositories", "scopes": ["repo"] } ] } },
"reach": { "mcp": { "server": { "url": "https://api.githubcopilot.com/mcp/", "transport": "http" } } },
"consumerNodes": ["core.openwop.mcp.invoke-tool","core.openwop.mcp.read-resource"] } }
Negative examples (MUST fail validation / be rejected)
// 1. Carries credential material — rejected with connection_pack_credential_material (§B.2)
{ "kind": "connection", "provider": { "id": "github", "auth": { "kind": "oauth2", "clientSecret": "ghs_xxx" } } }
// 2. Two reach modes — violates reach maxProperties:1 (§B.5)
{ "provider": { "reach": { "mcp": { "server": {} }, "openapi": { "ref": "..." } } } }
// 3. Non-https endpoint — violates the https pattern (§B.3)
{ "provider": { "auth": { "endpoints": { "token": "http://example.com/token" } } } }
Compatibility
Additive per COMPATIBILITY.md §2.2:
- No existing field changes optionality or type — this adds a new schema (
kind: "connection"), disjoint from all others via thekinddiscriminator. - No event-type shape changes. (The
connector.authorized/connector.auth_expiredevents from RFC 0047 are unchanged.) - No endpoint contract changes — the registry pack index gains a
connectionfacet (data-only), no new or altered route. - No
MUSTrelaxed, no error-code meaning changed — three new error codes are introduced (connection_provider_unresolved,connection_provider_conflict,connection_pack_credential_material). - New optional capability flag
connections.packsSupported— a host that does not advertise it behaves exactly as today (provider resolution stays implementation-defined / host-built-in). Existing hosts ignore connection packs; existing clients never depend on one being installed.
Forward-compat clauses: a connection pack a host hasn't installed is simply absent — a connector referencing an unknown provider fails the same way it does today (connection_provider_unresolved instead of a host-specific error), so no client regresses.
Conformance
Existing coverage: RFC 0045 connector-manifest scenarios; RFC 0047 host.oauth scenarios; RFC 0046 credential + redaction.test.ts scenarios; the pack-manifest validity scenarios (spec-corpus-validity.test.ts).
New scenarios (gated on capabilities.connections.packsSupported; server-free where possible, <1s):
connection-pack-manifest-valid— a well-formed manifest validates;kinddiscriminator routes it to this schema and away from node/prompt/agent. Citesnode-packs.md §Connection packs.connection-pack-no-credential-material— a manifest carryingclientSecret/apiKey/tokenis rejected withconnection_pack_credential_material. Enforces the new SECURITY invariant.connection-pack-reach-exclusive— a manifest with tworeachmodes fails validation.connection-provider-resolution— with agithubconnection pack installed, an RFC 0045 connector declaringprovider: 'github'registers; with it absent and no built-in, registration fails withconnection_provider_unresolved.connection-pack-write-reconsent— a host requestingwritescopes does so as a separate consent step, not bundled into the read authorization (composes with RFC 0047).
Fixtures: fixtures/connection-pack-github.json (positive) + fixtures/connection-pack-credential-leak.json (negative) → catalog rows in fixtures.md.
INTEROP-MATRIX: add a connection-packs column; reference hosts that implement §B.6 mark it.
Alternatives considered
- Do nothing — keep the provider catalog host-locked. Every host re-implements Google/Slack/GitHub; no marketplace data; community providers need host code. Rejected: this is precisely the friction the pack model exists to remove, and it leaves RFC 0045's marketplace promise unfulfillable.
- Bake providers into RFC 0047 as a fixed enum. A spec-blessed
providerenum with hard-coded endpoints. Rejected: not extensible (an RFC per provider), no community/private providers, and it puts a fast-moving catalog (MCP endpoints change monthly) inside the normative spec where it can't be versioned independently. - Put the provider definition inside the RFC 0045
connectorblock (each connector self-describes its provider). Rejected: couples the provider definition to each connector, so N connectors for one provider (a Salesforce-read pack + a Salesforce-write pack) duplicate and can disagree on the provider's endpoints; and it breaks credential sharing (RFC 0046 keys a credential by provider, not by connector). The provider is a separate, reusable artifact — hence a distinct pack kind. - A
providerblock on the node-pack manifest (like RFC 0045'sconnectorblock). Rejected for the same reason prompt/artifact-type/chat-card packs got their ownkindrather than node-pack sub-blocks: a provider definition ships and versions independently of any node code, and a pure provider pack carries nonodes[].
Unresolved questions
- MCP endpoint freshness. The MCP-server ecosystem moves monthly and the old
modelcontextprotocol/serversmonorepo is no longer the provider catalog. Shouldreach.mcp.server.urlbe treated as re-verifiable metadata the registry periodically re-checks (and the host SHOULD re-validate at install), rather than a frozen value? (Proposed: yes — add a non-normative "MCP endpoints are volatile" note + a registry health-check facet.) scopeModelcoverage. Isgroups | coarse | capabilitiessufficient, or do Stripe Connect (coarseread_only/read_write) and Notion (capabilities, not scopes) need richer modeling so a host renders the consent UI correctly?- Per-account-host providers. Is
auth.instanceUrlTemplatethe right shape for Snowflake/NetSuite/ServiceNow per-connection hosts, and does the resolved{account}belong in the connection pack or in the per-connection record (RFC 0046)? - Provider-id namespacing & precedence. §B.6 gives a version-based precedence rule for a built-in vs an installed pack of the same
id. Is that sufficient, or do we need scoped ids (core.githubvscommunity.x.github) to avoid two community packs both claiminggithub? - Catalog seeding. Should the Tier-1 providers (GitHub, Jira, Confluence, Notion, Linear, Microsoft 365, Stripe) ship as
core.openwop.connections.packs in this RFC*, or as a follow-up "core connection catalog" RFC once the manifest is Accepted?
Implementation notes (non-normative)
- Sequencing: spec text + schema →
capabilitiesflag → conformance scenarios + fixtures → reference-host resolution (§B.6) → seed the Tier-1core.connection packs. The reference openwop app (ADR 0024) already has an in-code provider registry with these exact fields; the host work is to load installed connection packs into that registry* and keep the operator-supplied OAuth client secret host-side (ADR 0024 §7) — so the impl is "make the existing registry installable," not new infrastructure. - Cross-cut: additive — the spec PR can merge independently of any host; a
CC-Nentry is not required (no breaking impl assumption). Hosts adopt §B.6 when they flipconnectionsSupported. - Effort: ~1 schema + 1 spec section + 5 conformance scenarios + 1 capability flag + reference-host loader; the deep-research top-20 supplies the initial catalog content.
Acceptance criteria
- [ ] Spec text merged (
spec/v1/connection-packs.mdstandalone doc +node-packs.mdpack-kinds cross-reference;host-capabilities.md§host.oauth note on theproviderresolution source) - [ ]
connection-pack-manifest.schema.jsonadded;capabilities.schema.jsonconnections.packsSupportedadded; both passnpm run openwop:check - [ ]
SECURITY/invariants.yamlgainsconnection-pack-no-credential-materialwith a public conformance test - [ ] At least the five conformance scenarios above land, capability-gated, with fixtures cataloged
- [ ] CHANGELOG entry under
[Unreleased] - [ ] A reference host implements §B.6 and passes the new scenarios, OR the RFC explicitly defers reference-host implementation to the catalog-seeding follow-up
References
- RFC 0045 (connector pack manifest & action model) — the action layer this provider layer completes
- RFC 0046 (
host.credentials) — credential resolution/lifecycle/redaction the resolved token lands in - RFC 0047 (
host.oauth) — the OAuth dance whoseproviderstring this RFC makes portable - RFC 0043 (registry and extension policy);
spec/v1/node-packs.md§Connectors, §Signing, §Naming prompt-pack-manifest.schema.json/workflow-chain-pack-manifest.schema.json— thekind-discriminated pack-family precedent- openwop-app ADR 0024 (Connections credential broker) + §7 (host-managed OAuth client config) — the reference-host implementation whose provider registry this RFC makes portable
- Prior art: n8n/Make/Zapier connector catalogs; Composio/Paragon/Workato unified-integration catalogs; the MCP server ecosystem (Cloudflare MCP Demo Day, Anthropic Integrations,
github.com/microsoft/mcp, the GitHub MCP Registry) — corroborating the initial provider catalog - Companion:
0095-connection-packs-portable-provider-definitions.gaps.md,…risks.md