diff --git a/.changeset/connector-authoring-guide.md b/.changeset/connector-authoring-guide.md
new file mode 100644
index 0000000000..5b684a8655
--- /dev/null
+++ b/.changeset/connector-authoring-guide.md
@@ -0,0 +1,12 @@
+---
+---
+
+docs-only: hand-written connector authoring guide at
+`content/docs/automation/connectors.mdx` (#4289) — the declarative
+`connectors:` path (ADR-0097) finally has a page: the three entry shapes,
+per-provider `providerConfig` contracts (`rest` / `openapi` / `mcp`),
+`credentialRef`-based auth with the enterprise-tier `oauth2` absence stated
+explicitly, boot/reload failure modes, and the showcase pointer. The two
+connector-auth entries in `packages/spec/variant-docs.json` flip from
+`exempt` to governed, so future auth variants must update the guide.
+Releases nothing.
diff --git a/content/docs/ai/connect-mcp.mdx b/content/docs/ai/connect-mcp.mdx
index c6cf8acecb..a16e615763 100644
--- a/content/docs/ai/connect-mcp.mdx
+++ b/content/docs/ai/connect-mcp.mdx
@@ -204,3 +204,4 @@ skill and a guided `/objectstack:connect` command.
- [Actions](/docs/ui/actions) — defining the actions you expose
- [Your app as an MCP server](/docs/api#your-app-as-an-mcp-server) — the API-level view
- [AI Agents](/docs/ai/agents) — building agents *inside* your app (the other direction)
+- [Connectors](/docs/automation/connectors) — the outbound counterpart: *consuming* an external MCP server as a flow-dispatchable connector (`provider: 'mcp'`)
diff --git a/content/docs/automation/connectors.mdx b/content/docs/automation/connectors.mdx
new file mode 100644
index 0000000000..c9148dc81a
--- /dev/null
+++ b/content/docs/automation/connectors.mdx
@@ -0,0 +1,363 @@
+---
+title: Connectors
+description: Call external systems from flows — plugin-registered connectors, and declarative provider-bound instances (rest / openapi / mcp) authored as pure metadata with reference-based credentials.
+---
+
+# Connectors
+
+> **Status:** Shipped · **Audience:** App authors (human and AI), integration
+> engineers
+>
+> **TL;DR** — A connector packages an external system behind named **actions**
+> that flows dispatch with the `connector_action` node. You get one in two
+> ways: a **plugin** registers it from host code, or — since ADR-0097 — you
+> **declare** it as pure metadata: a `connectors:` entry naming a `provider`
+> (`rest`, `openapi`, or `mcp`) is materialized into a live connector at boot,
+> no plugin code required. Credentials are **references** (`credentialRef`),
+> never secrets inlined in metadata. This page is the authoring guide for that
+> declarative path — what to write, how auth works, and how it fails.
+
+A connector is a **transport mechanism**: it knows how to reach one external
+system and exposes that system as a set of dispatchable actions. The runtime
+keeps a **connector registry** — `GET /api/v1/automation/connectors` lists it,
+the Studio flow palette browses it, and a flow's
+[`connector_action` node](/docs/automation/flows#node-types) dispatches against
+it. Connectors are *not* messaging channels (ADR-0022): "send a Slack message"
+is a connector action; routing, templating, and user notification preferences
+live in the messaging layer.
+
+## The three shapes of a `connectors:` entry
+
+| Shape | Behavior comes from | When to use |
+|:---|:---|:---|
+| **Plugin-registered connector** | Host code: a plugin calls `engine.registerConnector(def, handlers)` (brand connectors like Slack, or the executors' hand-wired instance options) | The integration needs custom logic, or ships as an installable plugin |
+| **Provider-bound instance** (ADR-0097) | An installed **generic executor** (`rest` / `openapi` / `mcp`) materializes the entry at boot | The upstream is an HTTP API, an OpenAPI document, or an MCP server — declare it as metadata, write no code |
+| **Catalog descriptor** | Nothing — the entry is inert documentation | Cataloguing a planned or externally-managed integration ([below](#catalog-descriptors-entries-without-a-provider)) |
+
+The first shape is code and belongs to [plugin development](/docs/plugins).
+This guide is about the other two — the entries a tenant (or an AI author)
+writes into stack metadata.
+
+## Declaring a provider-bound instance
+
+A declarative entry becomes a live connector the moment it names a `provider`:
+
+{/* os:check */}
+```typescript
+import { defineConnector } from '@objectstack/spec/integration';
+
+export const BillingApiConnector = defineConnector({
+ name: 'billing_api',
+ label: 'Billing API',
+ type: 'api',
+ provider: 'rest',
+ providerConfig: {
+ baseUrl: 'https://billing.example.com',
+ },
+ auth: { type: 'bearer', credentialRef: 'BILLING_API_TOKEN' },
+});
+```
+
+Wire it into the stack alongside your other metadata:
+
+```ts
+// objectstack.config.ts
+export default defineStack({
+ // ...
+ connectors: [BillingApiConnector],
+});
+```
+
+At boot, the automation service resolves each provider-bound entry:
+
+1. **Look up the provider factory** registered under `entry.provider`. The
+ three generic executors — `@objectstack/connector-rest`,
+ `@objectstack/connector-openapi`, `@objectstack/connector-mcp` — each
+ contribute one; scaffolded projects (`npm create objectstack`) ship all
+ three in `plugins:` by default, paired with the `automation` capability
+ that performs the materialization.
+2. **The factory validates `providerConfig`** and the automation service
+ resolves `auth.credentialRef` to its secret.
+3. **The result is registered** on the connector registry — indistinguishable
+ from a hand-written connector. Its **actions are derived from the
+ upstream** (the OpenAPI document's operations, the MCP server's
+ `tools/list`, the REST executor's generic `request`), never authored.
+
+Three authoring rules are enforced when the stack is validated:
+
+- `providerConfig` and `auth` **require** `provider` — on an entry without one
+ they are meaningless materialization inputs and are rejected.
+- A provider-bound instance must **not** inline secrets via `authentication`
+ (the runtime auth shape) — see [Authentication](#authentication).
+- A provider-bound instance must **not** author `actions` or `triggers` — the
+ provider derives actions from the upstream at boot; authoring both the
+ instance and its actions would let them drift apart (ADR-0097 §5).
+
+## Provider config contracts
+
+
+`providerConfig` is deliberately **not** validated by the stack schema — each
+provider factory validates its own config **at boot** (re-modelling an OpenAPI
+document or an MCP transport inside the stack schema is exactly what ADR-0023
+rejected). A wrong or missing key is therefore *not* flagged by `os validate`;
+it surfaces as a **hard boot error** naming the connector. Check the contracts
+below against what you wrote.
+
+
+### `provider: 'rest'` — a generic HTTP client
+
+Config: **`baseUrl`** (required, e.g. `https://api.example.com`) and
+**`defaultHeaders`** (optional string→string map merged into every request;
+request-level headers win).
+
+Materializes a single **`request`** action accepting
+`{ method?, path?, headers?, query?, body? }` — the flow supplies the path and
+payload per call. Use it when the upstream is "just HTTP" and you don't have a
+spec document.
+
+### `provider: 'openapi'` — one action per operation
+
+Config: **`spec`** (required) and **`baseUrl`** (optional — overrides the
+document's `servers[0].url`). The `spec` takes any of three forms:
+
+- an **inline OpenAPI 3.x document object** — no I/O at boot, the most
+ deterministic form;
+- an **`http(s)` URL**, fetched at materialization;
+- a **package-relative file path** like `'./specs/billing-openapi.json'`,
+ resolved against the directory containing `objectstack.config.ts` and
+ **confined to it** — absolute and `..`-escaping paths are rejected.
+
+{/* os:check */}
+```typescript
+import { defineConnector } from '@objectstack/spec/integration';
+
+export const CrmApiConnector = defineConnector({
+ name: 'crm_api',
+ label: 'CRM API',
+ type: 'api',
+ provider: 'openapi',
+ providerConfig: {
+ spec: './specs/crm-openapi.json',
+ },
+ auth: { type: 'api-key', credentialRef: 'CRM_API_KEY', headerName: 'X-API-Key' },
+});
+```
+
+Each operation becomes an action keyed by its `operationId` (or a
+`method_path` slug when the document omits one) — so `getInvoice` in the
+document is `actionId: 'getInvoice'` in your flow.
+
+### `provider: 'mcp'` — one action per tool
+
+Config: **`transport`** (required) and **`include`** (optional tool-name
+allowlist — only the listed tools become actions):
+
+- `{ kind: 'http', url, headers? }` — a remote MCP server over streamable
+ HTTP. Resolved `auth` is folded into the request headers.
+- `{ kind: 'stdio', command, args?, env? }` — a **local child process**,
+ policy-gated (below). Credentials for a stdio server ride `transport.env`,
+ not `auth`.
+
+{/* os:check */}
+```typescript
+import { defineConnector } from '@objectstack/spec/integration';
+
+export const SearchToolsConnector = defineConnector({
+ name: 'search_tools',
+ label: 'Search Tools (MCP)',
+ type: 'api',
+ provider: 'mcp',
+ providerConfig: {
+ transport: { kind: 'http', url: 'https://mcp.example.com/mcp' },
+ include: ['web_search', 'fetch_page'],
+ },
+ auth: { type: 'bearer', credentialRef: 'SEARCH_MCP_TOKEN' },
+});
+```
+
+At materialization the provider connects, calls `tools/list`, and maps each
+tool to an action — the MCP server's tool names are your `actionId`s.
+
+
+**Declarative stdio transports are denied by default.** Materializing one
+spawns a local process *from metadata* — which a runtime Studio publish can
+introduce — so anyone who can publish metadata could otherwise execute
+commands on the server. The host opts in per command:
+
+```ts
+new ConnectorMcpPlugin({ declarativeStdio: ['node'] }) // exact-match allowlist
+```
+
+The boot error you hit without the opt-in spells out this exact fix. The
+allowlist is a deliberately coarse boundary — allowlisting a launcher like
+`npx` effectively trusts anything it can run, so list the specific server
+binaries you trust. Hand-wired MCP connectors configured in host code are not
+subject to this policy (their command *is* host code), and `http` transports
+need no opt-in.
+
+
+## Authentication
+
+Connector auth answers "how do we authenticate **to the external system**" —
+it is unrelated to how users authenticate to ObjectStack. There are two shapes,
+and which one you write depends on which of the two connector worlds you are
+in:
+
+### Declarative instances: references, never secrets
+
+Stack metadata is authored, versioned, and shipped — a raw token must never
+live in it (ADR-0097 §3). The `auth` field on a provider-bound instance
+therefore has **no field that can hold a secret**. Its secret-bearing variants
+carry a **`credentialRef`** instead, resolved at materialization:
+
+```ts
+auth: { type: 'none' } // public upstream
+auth: { type: 'bearer', credentialRef: 'BILLING_API_TOKEN' }
+auth: { type: 'api-key', credentialRef: 'CRM_API_KEY',
+ headerName: 'X-API-Key' } // or paramName: 'api_key'
+auth: { type: 'basic', username: 'svc_objectstack', // username is not a secret
+ credentialRef: 'ERP_SVC_PASSWORD' } // the password is
+```
+
+Variant notes:
+
+- **`api-key`** — the key travels in a header (`headerName`, default
+ `X-API-Key`) or a query parameter (`paramName`); set one or the other.
+- **`basic`** — the `username` stays in metadata (it is not a secret); only
+ the password resolves through `credentialRef`.
+
+**What `credentialRef` resolves through.** In the open tier the ref is the
+name of an **environment variable** (`BILLING_API_TOKEN` above means
+`process.env.BILLING_API_TOKEN` on the server). A ref that resolves to nothing
+is a **hard boot error** — an app must not silently run with a dead connector.
+The enterprise tier swaps in a vault/KMS-backed resolver
+(`AutomationServicePluginOptions.credentialResolver`) without changing what
+you author: the entry still just names a ref.
+
+Inlining a secret is rejected when the stack is validated. Writing the runtime
+shape on a provider-bound instance —
+
+```ts
+// ✗ rejected at authoring/publish:
+// "must not inline secrets via `authentication`; reference credentials
+// with `auth: { type, credentialRef }` instead (ADR-0097 §3)."
+authentication: { type: 'bearer', token: 'sk_live_9f…' },
+
+// ✓ a reference, resolved at boot:
+auth: { type: 'bearer', credentialRef: 'BILLING_API_TOKEN' },
+```
+
+— fails validation, and `auth: { type: 'bearer', token: … }` is not even a
+legal shape (the declarative union has no `token` field to put a secret in).
+
+### Where is `oauth2`?
+
+Deliberately absent from declarative instances — **not** an oversight to work
+around. The runtime auth shape has five variants (`none`, `bearer`,
+`api-key`, `basic`, `oauth2`); the declarative shape stops at four. Static
+credentials resolved from env/config are the open tier; the OAuth2
+authorization-code/refresh **lifecycle** — token acquisition, rotation,
+per-tenant connections — is the enterprise tier (ADR-0015). If your upstream
+needs OAuth2, use an enterprise credential resolver or register the connector
+from a plugin that manages its own tokens; don't try to emulate it by stuffing
+a short-lived access token behind a `bearer` ref.
+
+### The runtime shape: `authentication`
+
+The `authentication` field is the **runtime** auth config — it carries the
+resolved secret *inline* (`{ type: 'bearer', token }`), because it is supplied
+by **host code** (a plugin calling `engine.registerConnector`, or the
+executors' hand-wired instance options reading `process.env`). That is a
+different trust anchor than metadata: code review guards what enters it, and
+it is never serialized into a versioned artifact. On declarative entries,
+leave `authentication` alone — provider-bound instances must use `auth`, and
+catalog descriptors documenting a planned integration must still never carry a
+live secret value.
+
+## Dispatching from a flow
+
+A materialized instance is dispatched exactly like any other connector — by
+`name` and action key:
+
+```ts
+{
+ id: 'ping',
+ type: 'connector_action',
+ label: 'GET /api/v1/health',
+ connectorConfig: {
+ connectorId: 'billing_api', // the connector's `name`
+ actionId: 'request', // rest: 'request' · openapi: operationId · mcp: tool name
+ input: { method: 'GET', path: '/api/v1/health' },
+ },
+},
+```
+
+See [Flows](/docs/automation/flows) for the node reference and how outputs
+flow into downstream nodes.
+
+## Failure modes at a glance
+
+Boot distinguishes **configuration faults** (your entry is wrong — fail loud,
+fix the metadata) from **operational faults** (the upstream is down — degrade,
+retry):
+
+| You wrote / it happened | What you get |
+|:---|:---|
+| `provider` names no installed factory | **Hard boot error** naming the entry, the provider, and the plugin that supplies it — add the executor to `plugins:` |
+| Invalid `providerConfig` (missing `baseUrl` / `spec` / `transport`, wrong shape) | **Hard boot error** from the provider factory |
+| `credentialRef` resolves to nothing | **Hard boot error** — set the env var (open tier) or fix the vault ref |
+| Instance `name` collides with a plugin-registered connector | **Hard boot error** — there is no silent precedence between the two worlds |
+| stdio transport without host opt-in | **Hard boot error** with the `declarativeStdio` fix inline — a security rejection is never retried |
+| MCP server unreachable / `tools/list` fails; remote spec URL unreachable or transiently failing (408/429/5xx) | **Not fatal**: the instance registers **degraded** (visible on `GET /api/v1/automation/connectors` with the reason), dispatches fail with that reason, and the platform retries on a 5s→5min backoff — recovery is automatic |
+| Wrong spec URL (other 4xx) or unparseable document | Configuration fault — **hard boot error** |
+| A bad entry arrives via **runtime reload** (Studio publish / dev reload) | Logged and **skipped**, never crashing the live server; a changed instance's old connector keeps serving until its replacement materializes |
+
+## Catalog descriptors: entries without a `provider`
+
+An entry with no `provider` never reaches the registry — it is an inert
+**descriptor** for discovery, documentation, or marketplace listing. It *may*
+author `actions` (as documentation of a planned surface, including I/O JSON
+Schemas). Because a declared-with-actions connector that nothing registers is
+usually a mistake, boot **warns** about it — mark a deliberate descriptor with
+`enabled: false` to state the intent and silence the audit:
+
+{/* os:check */}
+```typescript
+import { defineConnector } from '@objectstack/spec/integration';
+
+export const ErpCatalogConnector = defineConnector({
+ name: 'erp_catalog',
+ label: 'ERP Integration (planned)',
+ type: 'saas',
+ description: 'Planned ERP integration — catalogued, not yet dispatchable.',
+ actions: [
+ { key: 'get_invoice', label: 'Get Invoice' },
+ ],
+ enabled: false, // deliberate catalog-only descriptor: suppresses the boot audit warning
+});
+```
+
+A descriptor sharing a live connector's name stays legal — it is catalog
+metadata *about* that connector. (A provider-bound instance sharing one is the
+boot error from the table above.)
+
+## A complete, runnable example
+
+The showcase app exercises every shape on a real boot —
+[`examples/app-showcase/src/system/connectors/index.ts`](https://github.com/objectstack-ai/objectstack/blob/main/examples/app-showcase/src/system/connectors/index.ts)
+declares a `rest` instance pointing at the server's own health endpoint, an
+`openapi` instance whose spec is a package-relative file, an `mcp` instance
+spawning the in-repo stdio fixture under a `declarativeStdio` allowlist, and a
+catalog descriptor — with flows dispatching the `rest` and `mcp` instances
+end-to-end. Start from it rather than assembling the shapes from scratch:
+
+```bash
+pnpm dev
+```
+
+Related pages: [Flows](/docs/automation/flows) ·
+[Webhook Delivery](/docs/automation/webhooks) (the outbound-notification
+counterpart) · [Connect an MCP Client](/docs/ai/connect-mcp) (the **opposite
+direction**: exposing *this* platform to an external MCP client) ·
+[Connector schema reference](/docs/references/integration/connector)
+(generated from the schema).
diff --git a/content/docs/automation/flows.mdx b/content/docs/automation/flows.mdx
index ead8dbe7e2..c259753389 100644
--- a/content/docs/automation/flows.mdx
+++ b/content/docs/automation/flows.mdx
@@ -119,7 +119,7 @@ Each node performs a specific action in the flow.
| `map` | Sequential multi-instance — run a per-item subflow for each element, one at a time (each may pause); batch approval (ADR-0039) |
| `connector_action` | Execute an external connector action |
-`connector_action` dispatches against the runtime **connector registry**, which connector plugins populate. The three **generic executors** — `rest`, `openapi`, and `mcp` — double as provider factories: with them in your app's `plugins:`, a declarative `connectors:` entry that names a `provider` is materialized into a live, dispatchable connector at boot with no plugin code (ADR-0097). Scaffolded projects (`npm create objectstack`) ship all three executors by default — paired with the `automation` capability that performs the materialization — and the showcase wires them too. Brand connectors (Slack, …) are installed separately. One security note: a declarative `mcp` instance using a **stdio** transport spawns a local process from metadata and is therefore denied by default — the host opts in with `new ConnectorMcpPlugin({ declarativeStdio: [''] })`; `http` transports need no opt-in.
+`connector_action` dispatches against the runtime **connector registry**, which connector plugins populate. The three **generic executors** — `rest`, `openapi`, and `mcp` — double as provider factories: with them in your app's `plugins:`, a declarative `connectors:` entry that names a `provider` is materialized into a live, dispatchable connector at boot with no plugin code (ADR-0097). Scaffolded projects (`npm create objectstack`) ship all three executors by default — paired with the `automation` capability that performs the materialization — and the showcase wires them too. Brand connectors (Slack, …) are installed separately. One security note: a declarative `mcp` instance using a **stdio** transport spawns a local process from metadata and is therefore denied by default — the host opts in with `new ConnectorMcpPlugin({ declarativeStdio: [''] })`; `http` transports need no opt-in. [Connectors](/docs/automation/connectors) is the full authoring guide — provider config contracts, `credentialRef` auth, and failure modes.
### Node Structure
diff --git a/content/docs/automation/index.mdx b/content/docs/automation/index.mdx
index 25e64dd4a4..b67ed9df86 100644
--- a/content/docs/automation/index.mdx
+++ b/content/docs/automation/index.mdx
@@ -1,6 +1,6 @@
---
title: Automation
-description: Hooks, flows, workflows, approvals, scheduled jobs, and durable webhooks — the process engine that reacts to your data.
+description: Hooks, flows, workflows, approvals, scheduled jobs, durable webhooks, and connectors — the process engine that reacts to your data.
---
# Automation
@@ -33,6 +33,7 @@ export const OpportunityStageHook: Hook = {
- **Workflows** model a record's lifecycle as a **finite state machine**: states, transitions, and guards ([Workflows](/docs/automation/workflows)).
- **Approvals** are flow nodes with approver resolution, approve/reject decisions, and escalation ([Approvals](/docs/automation/approvals)).
- **Webhooks** deliver events to external systems through a **durable outbox** — exponential/linear/fixed retry with dead-lettering, HMAC signing, and an admin redeliver endpoint ([Webhook Delivery](/docs/automation/webhooks)).
+- **Connectors** package external systems behind named actions that flows dispatch — registered by plugins, or **declared as pure metadata** (`provider: 'rest' | 'openapi' | 'mcp'`) and materialized at boot, with reference-based credentials ([Connectors](/docs/automation/connectors)).
- **Scheduled jobs** run on `setInterval` or cron via the job service, alongside `schedule`-type flows.
Rule of thumb: model *state* with workflows, model *steps* with flows, use hooks for *code-level* reactions, and webhooks to *notify the outside world*.
@@ -46,6 +47,7 @@ Rule of thumb: model *state* with workflows, model *steps* with flows, use hooks
+
## Related
diff --git a/content/docs/automation/meta.json b/content/docs/automation/meta.json
index 011acd27dc..c50d547968 100644
--- a/content/docs/automation/meta.json
+++ b/content/docs/automation/meta.json
@@ -7,6 +7,7 @@
"flows",
"workflows",
"approvals",
- "webhooks"
+ "webhooks",
+ "connectors"
]
}
diff --git a/content/docs/capabilities/integrations.mdx b/content/docs/capabilities/integrations.mdx
index fe404e021e..c927c8db12 100644
--- a/content/docs/capabilities/integrations.mdx
+++ b/content/docs/capabilities/integrations.mdx
@@ -12,7 +12,7 @@ A business system earns its keep by fitting into the systems — and habits —
- **Generated REST APIs** — every object gets standard endpoints automatically, so your website, mini-program, or ERP reads and writes through the same permission gate as people.
- **Webhooks out** — data changes notify external systems (*deal won → billing system opens an invoice*).
- **Triggers in** — external systems start platform [flows](/docs/capabilities/automation).
-- **Connectors** — ready-made Slack and generic REST connectors drop into flows.
+- **Connectors** — ready-made Slack and generic REST connectors drop into flows; an HTTP API, OpenAPI document, or MCP server can be [declared as metadata](/docs/automation/connectors) and goes live at boot, no plugin code.
- **Federated external databases** — point at an existing database and query it in place: no migration, visible in views, usable in flows.
- **Marketplace templates** — install complete apps (HotCRM among them) with one click: objects, views, flows, dashboards, and seed data included, then customize in Studio.
- **Deploy anywhere** — cloud or fully self-hosted; your data and your app definition stay yours.
@@ -27,4 +27,4 @@ Interface text — labels, options, messages — is translatable per language, a
> **In HotCRM**: the interface ships in four languages (English, Chinese, Japanese, Spanish); the Slack connector and web-to-lead intake demonstrate both directions of integration.
-**For developers**: [API & SDK](/docs/api), [connectors and webhooks](/docs/automation), and [translations](/docs/ui/translations).
+**For developers**: [API & SDK](/docs/api), [connectors](/docs/automation/connectors), [webhooks](/docs/automation/webhooks), and [translations](/docs/ui/translations).
diff --git a/packages/spec/variant-docs.json b/packages/spec/variant-docs.json
index 9ba4a13dde..e2495c8c12 100644
--- a/packages/spec/variant-docs.json
+++ b/packages/spec/variant-docs.json
@@ -28,7 +28,8 @@
" - the remaining exemptions now state the gap plainly instead of pointing at the",
" generated page as if it settled the question. Connector authentication is the one",
" worth acting on: it is tenant-authored (ADR-0097 declarative `connectors:`) and the",
- " repo has no hand-written connector guide at all."
+ " repo has no hand-written connector guide at all. (Since closed: #4289 wrote",
+ " content/docs/automation/connectors.mdx and both connector-auth entries are governed.)"
],
"entries": [
{
@@ -80,14 +81,14 @@
{
"key": "type:api-key|basic|bearer|none|oauth2",
"label": "connector authentication",
- "exempt": "generated-reference-only",
- "reason": "DOCUMENTATION GAP, not a non-authorable surface — the strongest candidate among the exemptions for a real guide. Connector auth IS tenant-authored (ADR-0097 materializes a declarative `connectors:` entry naming a `provider` into a live connector at boot), but the repo has no hand-written connector page at all: the only prose is one paragraph inside automation/flows.mdx about the `connector_action` node. Until a guide exists there is nothing for this entry to bind to; when one is written, bind it and delete this exemption."
+ "docs": ["content/docs/automation/connectors.mdx"],
+ "note": "The runtime auth shape (ConnectorAuthConfigSchema) — five variants including the enterprise-tier `oauth2`. Was exempt generated-reference-only while no hand-written connector page existed; #4289 wrote the guide and bound it, per the old exemption's own instruction."
},
{
"key": "type:api-key|basic|bearer|none",
"label": "connector auth (environment-artifact projection)",
- "exempt": "generated-reference-only",
- "reason": "The declarative connector-instance auth shape (ADR-0097), reached via EnvironmentArtifactSchema.metadata.connectors[].auth. It carries a `credentialRef` rather than an inline secret, and omits `oauth2` because the authorization-code/refresh lifecycle is the enterprise tier (ADR-0015) — not because the artifact narrows it. Inherits the gap above; a connector guide would govern both."
+ "docs": ["content/docs/automation/connectors.mdx"],
+ "note": "The declarative connector-instance auth shape (ADR-0097), reached via EnvironmentArtifactSchema.metadata.connectors[].auth: `credentialRef` references only, and `oauth2` deliberately absent (enterprise tier, ADR-0015) — the guide states that absence explicitly so authors don't read it as an omission."
},
{
"key": "strategy:isolated_db|isolated_schema|shared_schema",