|
| 1 | +# ADR-0096: Declarative Connector Instances — Provider-Bound `connectors:` Entries Materialized by Generic Executors |
| 2 | + |
| 3 | +**Status**: Proposed (2026-07-15) |
| 4 | +**Deciders**: ObjectStack Protocol Architects |
| 5 | +**Builds on**: [ADR-0015](./0015-external-datasource-federation.md) (open mechanism / enterprise lifecycle split), [ADR-0018](./0018-unified-node-action-registry.md) (`connector_action` baseline dispatch + `engine.registerConnector()`), [ADR-0022](./0022-connectors-vs-messaging-channels.md) (Connector = transport/integration mechanism), [ADR-0023](./0023-openapi-to-connector-generator.md) (OpenAPI → Connector generator), [ADR-0024](./0024-mcp-connectors.md) (MCP servers as connectors) |
| 6 | +**Tracking**: framework#2977 (supersedes the interim descriptor-only contract from framework#2612) |
| 7 | +**Consumers**: `@objectstack/spec` (`stack.zod.ts`, `integration/connector.zod.ts`), `@objectstack/service-automation` (boot audit → provider binding), `@objectstack/connector-openapi`, `@objectstack/connector-mcp`, `@objectstack/connector-rest`, the Studio flow palette, AI authoring (stack generation) |
| 8 | + |
| 9 | +--- |
| 10 | + |
| 11 | +## TL;DR |
| 12 | + |
| 13 | +Declarative `connectors:` stack entries are **inert**: they register as metadata but never reach the automation engine's connector registry, which only plugins populate via `engine.registerConnector(def, handlers)` (#2612). The interim fix documented the collection as **descriptor-only** (catalog entries; boot audit warns on declared-with-actions-but-unregistered). This ADR specifies the upgrade: a declarative entry may name a **`provider`** — an installed generic executor (`openapi`, `mcp`, `rest`) — which **materializes** the entry into a live, dispatchable connector at boot. Declared-with-provider but no matching executor installed ⇒ **hard boot error** (upgrade of the audit warning). Credentials are **references** (`credentialRef`), never inline secrets. |
| 14 | + |
| 15 | +Why this matters for the platform's mission: ObjectStack builds enterprise core business systems **with AI, out of metadata**. Enterprise core systems are integration-heavy by definition, and AI's native output is metadata, not plugin code. Every comparable platform ships this as table stakes — Salesforce External Services (OpenAPI spec → invocable Flow actions, zero code), Power Platform custom connectors (the connector *is* an OpenAPI document; per-environment "connections" carry the credentials), ServiceNow IntegrationHub spokes. A schema collection the AI can author but the runtime ignores is the worst failure mode a metadata platform can ship: plausible, validated, dead. |
| 16 | + |
| 17 | +The runtime substrate already exists — ADR-0023/0024 deliver generic executors that turn declarative inputs (an OpenAPI document, an MCP endpoint) into `{ def, handlers }` bundles. This ADR adds only the **last mile**: stack metadata → executor factory → `registerConnector`. |
| 18 | + |
| 19 | +--- |
| 20 | + |
| 21 | +## Context |
| 22 | + |
| 23 | +### The two connector worlds (#2612) |
| 24 | + |
| 25 | +1. **Runtime registry** (live): `engine.registerConnector(def, handlers)` requires a handler per action and backs `GET /connectors` + the `connector_action` node. Populated exclusively by plugins. |
| 26 | +2. **Declarative `connectors:`** (inert): registered as metadata kind 'connector'; `ConnectorActionSchema` deliberately carries **no execution binding** (no method/path — ADR-0023 rejected re-inventing OpenAPI inside the stack schema), so a declared action *cannot* be interpreted into behavior. |
| 27 | + |
| 28 | +The interim state (shipped with #2612's resolution): the contract is documented as descriptor-only, `enabled: false` marks deliberate catalog entries, and the automation service warns at boot about declared-with-actions entries lacking a same-name runtime registration. |
| 29 | + |
| 30 | +### Why naive bridging was rejected |
| 31 | + |
| 32 | +Matching declared entries to already-installed plugin connectors **by name** adds nothing (the plugin registers itself) and creates a two-sources-of-truth hazard: the declared def and the plugin def of the same connector can drift. Any real bridge must make the declarative entry the **configuration** and the executor the **behavior** — type/instance separation, exactly the datasource pattern (declared in stack, adapter in code). |
| 33 | + |
| 34 | +--- |
| 35 | + |
| 36 | +## Decision |
| 37 | + |
| 38 | +### 1. `provider` on the stack-level connector entry |
| 39 | + |
| 40 | +A declarative connector entry MAY carry a `provider` plus provider-specific config: |
| 41 | + |
| 42 | +```ts |
| 43 | +connectors: [{ |
| 44 | + name: 'billing', |
| 45 | + label: 'Billing API', |
| 46 | + type: 'api', |
| 47 | + provider: 'openapi', // ← which installed executor materializes this |
| 48 | + providerConfig: { // provider-specific; validated by the provider |
| 49 | + spec: './specs/billing-openapi.json', // openapi: document ref |
| 50 | + baseUrl: 'https://billing.example.com', |
| 51 | + }, |
| 52 | + auth: { type: 'bearer', credentialRef: 'billing_api_token' }, // reference, never a secret |
| 53 | +}] |
| 54 | +``` |
| 55 | + |
| 56 | +- **No `provider`** → the entry is a catalog descriptor, exactly the #2612 interim contract (audit warning applies unless `enabled: false`). |
| 57 | +- **`provider` present** → the entry is an **instance declaration** and MUST be materialized at boot. |
| 58 | + |
| 59 | +### 2. Providers are factories contributed by connector plugins |
| 60 | + |
| 61 | +A connector plugin (e.g. `@objectstack/connector-openapi`) registers a **provider factory** under its provider key at `init()`. At `kernel:ready`, the automation service resolves each instance declaration: |
| 62 | + |
| 63 | +1. Look up the factory for `entry.provider`. |
| 64 | +2. Factory validates `providerConfig`, resolves `credentialRef` via the secrets layer, and returns the same `{ def, handlers }` bundle the generator APIs already produce (ADR-0023 §1). |
| 65 | +3. The service calls `engine.registerConnector(def, handlers)` — the registry and `connector_action` see a finished connector, indistinguishable from a hand-written one. |
| 66 | + |
| 67 | +**Declared `provider` with no installed factory ⇒ boot fails loudly** (validation error naming the entry, the provider, and the plugin that supplies it). This upgrades the #2612 audit from warning to error, but **only** for provider-bound entries — plain descriptors keep the warning-or-`enabled:false` contract. |
| 68 | + |
| 69 | +### 3. Credentials are references |
| 70 | + |
| 71 | +`credentialRef` resolves through the secrets service at materialization time. Inline secrets in stack metadata are rejected at authoring/publish (lint + schema). Per the ADR-0015 line: static credentials resolved from env/config are **open**; managed vaulting, OAuth2 refresh, and per-tenant connection lifecycle are **enterprise**. |
| 72 | + |
| 73 | +### 4. Two-sources-of-truth rule |
| 74 | + |
| 75 | +If a provider-bound instance and a plugin-registered connector share a `name`, boot fails with a naming-conflict error — there is no precedence, because silent precedence is how drift hides. (Plain descriptors sharing a live connector's name stay legal: the descriptor is catalog metadata *about* the live connector.) |
| 76 | + |
| 77 | +### 5. Non-goals |
| 78 | + |
| 79 | +- **No execution bindings inside `ConnectorActionSchema`.** Actions on an instance declaration are *derived by the provider* (from the OpenAPI document / MCP `tools/list`), not authored. Authoring both the instance and its actions would reintroduce drift. |
| 80 | +- **L3 aspirational surfaces stay out of scope**: `syncConfig`, `fieldMappings`, `webhooks`, `triggers` on `ConnectorSchema` remain unimplemented by this ADR. |
| 81 | +- **No messaging semantics** — the ADR-0022 layering stands; this is transport only. |
| 82 | + |
| 83 | +### 6. Acceptance |
| 84 | + |
| 85 | +- An AI-generated app declares a connector instance (e.g. `provider: 'mcp'` pointing at an MCP server) as pure metadata; a flow `connector_action` dispatches one of its actions end-to-end. |
| 86 | +- The showcase demonstrates the declarative path live (upgrading the catalog-descriptor demo shipped with #2612) and `GET /connectors` lists the materialized instance. |
| 87 | +- Boot fails loudly for: unknown provider, invalid providerConfig, unresolvable credentialRef, name conflict with a plugin-registered connector. |
| 88 | + |
| 89 | +--- |
| 90 | + |
| 91 | +## Consequences |
| 92 | + |
| 93 | +**Positive** |
| 94 | +- The `connectors:` collection stops being the platform's one dead metadata surface; AI can wire integrations without leaving metadata. |
| 95 | +- Zero new runtime abstraction: providers reuse the ADR-0023/0024 generator/adapter APIs verbatim. |
| 96 | +- The industry-standard definition/credential split (Salesforce Named Credentials, Power Platform connections) lands with the schema, not as a retrofit. |
| 97 | + |
| 98 | +**Negative / costs** |
| 99 | +- Schema evolution on a shipped collection (`provider`, `providerConfig`, `credentialRef`) — additive, so no migration, but the descriptor-only docs shipped with #2612 must be revised when this lands. |
| 100 | +- The secrets-layer dependency makes credential resolution a boot-path concern; environments without a secrets service need a clear degraded story (env-var fallback, open tier). |
| 101 | +- Provider factories add a registration surface to connector plugins (small; mirrors how they already self-register). |
0 commit comments