Skip to content

Commit d745bb5

Browse files
docs(adr): ADR-0143 accepted — the maintainer's Q1–Q4 decisions recorded; D2.1, D2.3, D3, D4 and D7 state what they settle (#27231)
<!-- claude-projects-attribution: {"github_login":"os-steve"} --> _Requested by **Steve** · [project thread](https://claude.ai/code/project/chan_01FdL1XeUyvVNxLreh3F4Qi9?thread=cmsg_01FdL1XeUyvVNxLreh3F4Qi9BaU5Bf8FsJVKEf1GxktFtv)_ Part of #26768 Clause-②: no Metadata-protocol: none Ruling-ref: #26768 (comment) ## 维护者速读 - 改了什么:把你 10-28 对四个决定的答复(「同意」)写进 ADR-0143,状态从 Proposed 改为 Accepted;同时让 D2.1、D2.3、D3、D4、D7 与答案一致。 - 四个答案:Q1 组织共享(补「无组织的运行」规则);Q2 专用连接对象,只存 refresh token、动态注册的客户端和状态;Q3 MCP 实测改用 Notion,HubSpot 作第二案例;Q4 `alipay-v3` / 阿里云 ACS3 / 腾讯云 TC3,签名接口带响应验签。 - 为什么改:#26702、#26710 的 spec / 运行时 / 控制台切片都在等这份记录变成 Accepted。 - 风险与回滚:纯文档,无代码、无 spec 变更。回滚即撤掉本 PR,ADR-0143 回到 Proposed。 - 先看:`docs/adr/0143-outbound-connector-auth-is-open-tier.md` 里四个 **Decision (2026-10-28)** 段落,以及 D4 末尾新加的无组织运行规则。 - 需要你做的:受管 PR,等 os-zhuang 或 hotlong 批准。 ## What changes `docs/adr/0143-outbound-connector-auth-is-open-tier.md`: - **Status** is now Accepted, dated 2026-10-28. **Deciders** quotes the ruling 「同意」, given to an independent analysis of Q1–Q4. - **Q1–Q4.** Each keeps its options as drafted and gains a `**Decision (2026-10-28)**` paragraph. Q3 departs from the drafted recommendation, and says so. - Q1: A, organization-shared. - Q2: B, a dedicated connection object, scoped by D3. - Q3: A revised. Notion's hosted MCP server is the live dynamic-registration proof, and HubSpot is the pre-registered-`clientId` case, live or as a contract test. Then Feishu, an Alipay v3 sandbox query over https, and `client_credentials` plus a registration contract test against the embedded `@better-auth/oauth-provider`. - Q4: A, with three conditions: `alipay-v3` replaces `alipay-rsa2`; the signer seam carries response verification; the test-vector rule is relaxed. - **D2.1.** On `provider: 'mcp'`, endpoints and `clientId` may also be supplied, for vendors without dynamic client registration (HubSpot). - **D2.3.** Test vectors reproduce a complete vendor example byte for byte where one is published; otherwise they come from the vendor's official SDK test suite, with SDK and version named. The signer seam gains an optional response-verification step, which `alipay-v3` implements. - **D3.** Only what cannot be re-obtained unattended is persisted: the refresh token, the dynamically registered client, and status, scopes and expiry. Access tokens, including every `client_credentials` and `app-credential` token, stay in an in-process cache. - **D4** gains the organization-less run rule. A run with no organization (a schedule) uses the instance's connection only when exactly one exists; otherwise it is refused. It never falls back to an arbitrary connection. - **D7.** `ConnectorOAuth2Schema` is retired through ADR-0087, not redefined. - **Acceptance** follows Q3 and Q4. That means Notion via dynamic registration with refresh-token rotation, Alipay v3 over https on `alipay-v3` with response verification, and the embedded-server contract test. `docs/adr/README.md` is regenerated by `node scripts/adr-index.mjs`: ADR-0143's row, and Accepted 125→126 / Proposed 5→4. Forced test edits: none. ## Tests Docs-only diff with no package touched, so no build, unit test or typecheck is owed. The derived gate family ran (see Verification). ## Acceptance notes - **Vendor facts behind the Q3 / Q4 decisions** (pages fetched 2026-10-28; the ADR cites the first two): - HubSpot remote MCP: PKCE required, no dynamic client registration; the client is pre-registered in HubSpot. - Notion MCP: RFC 7591 registration, S256 PKCE, refresh token rotated on every refresh. - Aliyun ACS3: publishes a complete worked example with its final signature. - Tencent TC3: its example masks the SecretKey. - Alipay's v3 protocol page is robots-blocked. Its `openapi.yaml` (alipay-sdk-java-all, `v3/api/`) declares no `securitySchemes`, and its sandbox server entry is plain http. - New WeChat Pay merchants verify against a fixed platform public key. - **Code facts, re-read on `origin/main` at `cdec7473`.** - `sys-account.object.ts`: `(provider_id, account_id)` is indexed `unique: 'global'`. The comment beside the token columns says they are deliberately not retyped to `Field.secret()` and that `encryptOAuthTokens` is unset. - `runtime-identity.ts#resolveRunDataContext`: a schedule resolves no user and no organization. - `auth-manager-plugin-list.ts` sets `allowDynamicClientRegistration`. - **Ruling.** Steve's two messages, the 09:39Z request for an independent analysis with a recommendation per question and the 11:22Z reply 「同意」, are recorded verbatim on #26768 in ruling record 6467737978. The ADR quotes 「同意」 and describes the request in words. - **Not in this PR:** card item 3, the cloud-repository record with a `## Provenance` pointer. It is still owed, and this lane cannot reach that repository. - **Tier H.** This PR is a draft and waits for an approval from a `GOVERNED_APPROVERS` account. It is not readied, armed or queued from here. ## Verification <details><summary>Derived gate family: 21/21 exit 0, --ran reconciles 21/21, 0 NOT-MEASURED (derived zero)</summary> The gates ran on `97b63d2b`. The pushed head `5fada82b` is the same tree; only the commit message was reworded, to drop a card trailer that the pre-push `check:commit-card-trailers` refused. `node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack` listed 21 commands: ``` node scripts/check-adr-links.mjs :: exit 0 node scripts/check-adr-links.mjs --self-test :: exit 0 node scripts/check-closing-keyword-parity.mjs :: exit 0 node scripts/check-closing-keyword-parity.mjs --self-test :: exit 0 node scripts/check-comment-mask-corpus.mjs :: exit 0 node scripts/ci/plan-test-core-shards.mjs --self-test :: exit 0 pnpm --filter @objectstack/lint run check:doc-formula-expressions :: exit 0 pnpm check:adr-anchors :: exit 0 pnpm check:adr-index :: exit 0 pnpm check:adr-symbol-anchors :: exit 0 pnpm check:ci-filter-parity :: exit 0 pnpm check:closed-ceremony-text :: exit 0 pnpm check:cross-package-test-inputs :: exit 0 pnpm check:doc-authoring :: exit 0 pnpm check:driver-memory-census :: exit 0 pnpm check:gitlink-declared :: exit 0 pnpm check:nul-bytes :: exit 0 pnpm check:pm-governed-merges :: exit 0 pnpm check:pm-prior-rulings :: exit 0 pnpm check:refd-timer-probe :: exit 0 pnpm check:watch-hint-literal :: exit 0 ``` `--ran` result: "21 derived famil(ies) accounted for — 21 run, 0 NOT-MEASURED (a DERIVED zero — all 21 recorded an exit code and none of them is 3)". </details> --- _Generated by [Claude Code](https://claude.ai/code/session_01ADHkmP2wqmTYar443bTqhK)_ Co-authored-by: Claude <noreply@anthropic.com>
1 parent 2594f5b commit d745bb5

2 files changed

Lines changed: 103 additions & 27 deletions

File tree

‎docs/adr/0143-outbound-connector-auth-is-open-tier.md‎

Lines changed: 100 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,15 @@
11
# ADR-0143: Outbound connector auth is open tier — OAuth2 grants, vendor app-credential exchange and request signing in one declarative auth contract
22

3-
**Status**: Proposed — 2026-10-27. `docs/adr/` is Tier H (Prime Directive #14): the maintainer's merge,
4-
with the four [open decisions](#decisions-put-to-the-maintainer) answered in this file, is the acceptance
5-
act. The six amendment notes this record names land in the same PR and take effect with it.
3+
**Status**: Accepted — 2026-10-28. Proposed 2026-10-27 (#26833, which also landed the six amendment
4+
notes this record names). The maintainer answered the four [decisions](#decisions-put-to-the-maintainer)
5+
on 2026-10-28. Each answer is recorded under its question, and D2.1, D2.3, D3, D4 and D7 state what the
6+
answers settle. `docs/adr/` is Tier H (Prime Directive #14).
67
- **Deciders**: ObjectStack maintainer, 2026-10-27, verbatim and untranslated. On outbound connector OAuth:
78
「OAuth 放开源层,先起草 ADR 修订」, 「平台基于 better-auth ,怎么会有没有 oauth」, 「创建issue 就行」. On request
89
signing, the three recommendations of the MCP / OpenAPI directory survey: 「三条行动建议 立卡」 →
9-
「建议 1+2立卡」 → 「3 不考虑」 (recommendation 2 is request signing).
10+
「建议 1+2立卡」 → 「3 不考虑」 (recommendation 2 is request signing). On Q1–Q4, 2026-10-28: the
11+
maintainer asked for an independent analysis of the four decisions with a recommendation for each, then
12+
accepted the recommendations as written there: 「同意」.
1013
- **Amends**: the outbound-connector-auth tier placement — and nothing else — in
1114
[ADR-0018](./0018-unified-node-action-registry.md) "Out of scope (deliberately not baseline)",
1215
[ADR-0022](./0022-connectors-vs-messaging-channels.md) §4,
@@ -148,6 +151,9 @@ carry and forbid; exact key spelling is the spec slice's, reviewed against this
148151
runs the MCP authorization flow (protected-resource metadata, authorization-server metadata,
149152
dynamic client registration, PKCE), and the registered client lives in the connection record (D3),
150153
never in metadata.
154+
- They may also be supplied. A vendor MCP server without dynamic client registration takes a
155+
pre-registered `clientId` and a client-secret `credentialRef`, as for any other `oauth2` instance.
156+
HubSpot's remote server is one such vendor (Q3).
151157
2. **`app-credential`** — the non-interactive vendor exchange of an app id and secret for a short-lived
152158
access token, which is not an RFC 6749 grant: each vendor spells the request, the response fields
153159
and where the token rides differently.
@@ -163,20 +169,28 @@ carry and forbid; exact key spelling is the spec slice's, reviewed against this
163169
- `keyId` and scheme-specific **non-secret** parameters (an app id, a region, a vendor
164170
public-key `credentialRef` where the scheme verifies responses).
165171
- The private key or HMAC secret is a `credentialRef`.
166-
- A scheme enters the enum together with its signer and with test vectors reproducing the vendor's
167-
published examples byte for byte. An enum member without a shipped signer is a declared-but-
168-
unenforced property (ADR-0049) and is not admitted. A "generic HMAC" member is admitted only with a
172+
- A scheme enters the enum together with its signer and its test vectors. Where the vendor publishes
173+
a complete worked example, the vectors reproduce it byte for byte. Where it does not (a masked
174+
secret, an unreachable page), the vectors are taken from the vendor's official SDK test suite, and
175+
the test names that SDK and its version. An enum member without a shipped signer is a
176+
declared-but-unenforced property (ADR-0049) and is not admitted.
177+
- The signer seam carries an optional response-verification step beside request signing. A scheme
178+
whose vendor signs its responses implements that step in its first release; `alipay-v3` does
179+
(Q4). A "generic HMAC" member is admitted only with a
169180
canonical form pinned in the spec; without one, it is a dialect.
170181

171182
### D3 — Secrets are references; tokens are runtime state in a connection record
172183

173184
- Every secret an author names is a `credentialRef`, resolved through `CredentialResolver` (ADR-0097 §3,
174185
unchanged). Publish keeps refusing an inline secret, now on the three new variants too.
175-
- Access tokens, refresh tokens, registered MCP clients and their expiry are **not metadata**. They live
176-
in a connection record bound to the instance, encrypted at rest through the `secret` channel
177-
(ADR-0100) or an equivalent the spec slice names. They never appear in `sys_metadata`, in
178-
`GET /api/v1/automation/connectors`, in a connector descriptor, or in a log line.
179-
- Where that record lives is the [second decision](#q2--where-grants-are-stored).
186+
- Tokens, registered MCP clients and their expiry are **not metadata**. They never appear in
187+
`sys_metadata`, in `GET /api/v1/automation/connectors`, in a connector descriptor, or in a log line.
188+
- Only what cannot be re-obtained unattended is persisted: the `authorization_code` refresh token, the
189+
client a dynamic registration created, and the grant's status, scopes and expiry. These live in a
190+
dedicated connection record bound to the instance (Q2), their secrets encrypted at rest through the
191+
`secret` channel (ADR-0100).
192+
- Access tokens are never persisted. They live in an in-process cache. This includes every
193+
`client_credentials` and `app-credential` token, which the configured secret re-derives at any time.
180194

181195
### D4 — Grant-bearing kinds resolve per dispatch, not at materialization
182196

@@ -197,6 +211,11 @@ materialization.
197211
- An `authorization_code` instance whose connection has never been authorized is not an authoring fault
198212
and never fails a boot. It materializes `degraded` ("not connected") until an administrator connects
199213
it.
214+
- A connection is selected by the run's organization (Q1). A run that carries no organization, such as
215+
a schedule (`packages/services/service-automation/src/runtime-identity.ts#resolveRunDataContext`),
216+
uses the instance's connection only when exactly one exists. Otherwise the dispatch is refused with a
217+
reason naming the missing organization. It never falls back to an arbitrary connection: this is
218+
ADR-0073's "missing identity is refused" applied to the organization.
200219

201220
### D5 — Connecting is an authorized, server-side act
202221

@@ -213,12 +232,13 @@ materialization.
213232
A grant obtained to call a vendor on the platform's behalf must not let anyone sign in to ObjectStack,
214233
and must not be revoked or replaced by an unrelated sign-in. Whatever the storage decision, this holds.
215234

216-
### D7 — The unused runtime `oauth2` shape is settled by the spec slice
235+
### D7 — The unused runtime `oauth2` shape is retired
217236

218237
`ConnectorOAuth2Schema` (`packages/spec/src/shared/connector-auth.zod.ts#ConnectorOAuth2Schema`) inlines a client secret and a refresh token
219-
and has no consumer. The spec slice that adds D2's `oauth2` variant either retires it or redefines it
220-
as the resolved, post-refresh shape a provider receives. Either way it goes through ADR-0087 and does
221-
not survive as a second authored dialect.
238+
and has no consumer. The spec slice that adds D2's `oauth2` variant **retires** it through ADR-0087. It
239+
is not redefined as a resolved shape. Under D4 a provider receives a credential source answered per
240+
dispatch, an interface rather than a schema, and that source is separate from `CredentialResolver`, which
241+
keeps resolving `credentialRef`s.
222242

223243
### What this record does not decide
224244

@@ -234,8 +254,9 @@ not survive as a second authored dialect.
234254

235255
## Decisions put to the maintainer
236256

237-
Four questions stay open in this draft. Each is answered here, in place, before the merge that
238-
accepts the record. The recommendations are the drafting seat's and bind nothing.
257+
The maintainer answered all four on 2026-10-28 by accepting an independent analysis's recommendations
258+
(see Deciders). Each question keeps its options as drafted, followed by the **Decision**. Where the
259+
decision departs from the drafted recommendation, it says so.
239260

240261
### Q1 — Connection ownership and execution identity
241262

@@ -259,6 +280,10 @@ principal (D1/D2) is unbuilt, and many runs carry no user, so B fails the common
259280
A covers every acceptance scenario #26702 names, and adding a scope key later is additive
260281
(`Metadata-protocol: added`), not a migration.
261282

283+
**Decision (2026-10-28): A.** It comes with D4's rule for a run that carries no organization. Per-user
284+
connections stay a later additive key. The `oauth2` and `app-credential` variants carry no scope key
285+
now, and Console Connect / Reconnect is organization-level.
286+
262287
### Q2 — Where grants are stored
263288

264289
Reuse better-auth's `account` storage and refresh, or a dedicated connection object?
@@ -282,6 +307,19 @@ Reuse better-auth's `account` storage and refresh, or a dedicated connection obj
282307
**Recommendation: B.** A fails three of D3 / D6's requirements on today's columns, and closing them
283308
would bend a sign-in table into a second purpose.
284309

310+
**Decision (2026-10-28): B, scoped as D3 states.** Only the refresh token, the dynamically registered
311+
client and the grant's status, scopes and expiry are persisted. Access tokens are not. Further reasons
312+
`sys_account` does not fit:
313+
- `(provider_id, account_id)` is a globally unique index, so two organizations connected to one vendor
314+
tenant collide.
315+
- The token columns are kept out of the `secret` channel deliberately, because better-auth reads them
316+
back through its own adapter.
317+
- Both points are in `packages/platform-objects/src/identity/sys-account.object.ts`.
318+
319+
The connection object follows the shape of
320+
`packages/platform-objects/src/identity/sys-scim-connection-credential.object.ts`: an organization
321+
column, a revocation switch, an expiry, engine-owned, and a declared permission.
322+
285323
### Q3 — First products for the first acceptance
286324

287325
Which vendors does the first acceptance target? One per grant kind keeps every new path exercised by a
@@ -298,6 +336,27 @@ real upstream:
298336

299337
**Recommendation: A.** It is the union of the acceptance lines both parent cards already wrote.
300338

339+
**Decision (2026-10-28): A, revised. This departs from the drafted recommendation.** HubSpot's remote
340+
MCP server has no dynamic client registration. Clients are pre-registered in HubSpot
341+
([HubSpot docs](https://developers.hubspot.com/docs/apps/developer-platform/build-apps/integrate-with-the-remote-hubspot-mcp-server)),
342+
so HubSpot alone cannot prove D2.1's discovery-and-registration path, the hardest new path. The first
343+
acceptance therefore targets:
344+
- `authorization_code` over MCP:
345+
- **Notion's hosted MCP server is the live proof**
346+
([Notion docs](https://developers.notion.com/guides/mcp/build-mcp-client)). It provides dynamic
347+
client registration, mandatory S256 PKCE, and a refresh token rotated on every refresh, so it
348+
exercises D2.1, D4's single-flight refresh and atomic rotation, and the revoked → `degraded` path
349+
at once.
350+
- HubSpot is the second case, live or as a contract test, for the pre-registered `clientId` form.
351+
- `app-credential`: Feishu. WeCom's early token invalidation is a unit case for D4's refresh-once-on-401
352+
rule.
353+
- `signed`: an Alipay v3 sandbox query, called over https. `signed` is instance-level `auth`: the
354+
Alipay document declares no `securitySchemes`.
355+
- `client_credentials`, plus a dynamic-registration / PKCE contract test: against the platform's own
356+
embedded OAuth authorization server (`@better-auth/oauth-provider`), which already issues
357+
`client_credentials` tokens and has dynamic registration enabled
358+
(`packages/plugins/plugin-auth/src/auth-manager-plugin-list.ts`).
359+
301360
### Q4 — Which signing schemes ship first
302361

303362
The secret is always a `credentialRef` (D2.3); the question is the first members of the closed enum.
@@ -312,6 +371,19 @@ The secret is always a `credentialRef` (D2.3); the question is the first members
312371
**Recommendation: A.** The other schemes join the enum when a consumer declares one, each with its
313372
signer and vectors (D2.3). An enum listing schemes nobody implements would be declared-not-enforced.
314373

374+
**Decision (2026-10-28): A, with three conditions.**
375+
1. **The Alipay member is named `alipay-v3`.** `RSA2` is the v2 gateway's `sign_type`; the v3 protocol
376+
header is `ALIPAY-SHA256withRSA`. The first enum is
377+
`'alipay-v3' | 'aliyun-acs3-hmac-sha256' | 'tencentcloud-tc3-hmac-sha256'`.
378+
2. **The signer seam carries response verification from the start (D2.3).** `alipay-v3` implements it
379+
in its first release. WeChat Pay then joins later as one more signer, with no seam change.
380+
3. **The test-vector rule takes D2.3's relaxed form.** Of the three, only Aliyun publishes a complete,
381+
reproducible worked example; Tencent masks its SecretKey. The rule as first drafted would have
382+
admitted Aliyun alone.
383+
384+
Option B's cost statement is partly out of date. New WeChat Pay merchants verify against a fixed
385+
platform public key rather than rotating certificates, but the response-verification half remains.
386+
315387
---
316388

317389
## Consequences
@@ -336,14 +408,18 @@ signer and vectors (D2.3). An enum listing schemes nobody implements would be de
336408

337409
## Acceptance (pinned from #26702 and #26710)
338410

339-
- A declarative `provider: 'mcp'` instance pointing at an OAuth-protected vendor MCP server is
340-
authorized once from the Console, and a flow `connector_action` dispatches end to end. After the
341-
access token expires, the next dispatch refreshes it with no operator action.
411+
- A declarative `provider: 'mcp'` instance pointing at Notion's hosted MCP server, with no endpoints
412+
and no `clientId` authored, is authorized once from the Console through dynamic client registration.
413+
A flow `connector_action` then dispatches end to end. After the access token expires, the next
414+
dispatch refreshes it with no operator action, and the rotated refresh token replaces the old one.
415+
HubSpot covers the pre-registered `clientId` form, live or as a contract test (Q3).
342416
- A Feishu (or DingTalk) instance declared with `app-credential` dispatches through `openapi` or
343417
`rest`, and its token renews without a restart.
344-
- An `openapi` instance over the Alipay v3 document with `auth: { type: 'signed', scheme:
345-
'alipay-rsa2', … }` dispatches a sandbox query. Signer unit tests reproduce the vendor's published
346-
signatures byte for byte.
418+
- An `openapi` instance over the Alipay v3 document, normalized to JSON first, with
419+
`auth: { type: 'signed', scheme: 'alipay-v3', … }`, dispatches a sandbox query over https and
420+
verifies the response signature. Signer unit tests meet D2.3's vector rule.
421+
- `client_credentials` and the dynamic-registration flow pass a contract test against the embedded
422+
OAuth authorization server.
347423
- An unknown `grant`, `vendor` or `scheme`, or an inline secret on any variant, is refused at publish
348424
with a located error.
349425
- No token, client secret or private key appears in `sys_metadata`, in

‎docs/adr/README.md‎

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,8 @@
1717

1818
| Status | Records |
1919
|---|---|
20-
| Accepted | 125 |
21-
| Proposed | 5 |
20+
| Accepted | 126 |
21+
| Proposed | 4 |
2222
| Superseded | 4 |
2323
| Draft | 3 |
2424
| Deferred | 2 |
@@ -210,7 +210,7 @@ A bare `ADR-0006` names the decision; its current text is [0006-project-environm
210210
| [ADR-0139](./0139-engine-internal-semi-join-leaf-readable-parent-ids.md) | One engine-internal semi-join leaf — "the ids of object X this caller may read" — pushed down by ObjectQL, ne… | Accepted — 2026-10-11, accepted by the merge that landed it on `main` | 2026-10-11 | — | — |
211211
| [ADR-0141](./0141-inter-app-dependency-declared-checked-before-install.md) | An app may depend on another installed app — declared in `manifest.dependencies`, checked once before install… | Proposed — 2026-10-25; first draft 2026-10-24. `docs/adr/` is Tier H (Prim… | 2026-10-25 | — | — |
212212
| [ADR-0142](./0142-ownership-queue-membership-claim-and-assignment-rules.md) | An ownership queue is a platform primitive — a queue entity, membership through the existing recipient kinds,… | Proposed — 2026-10-25 · Amended (2026-10-28, ruling `6457475657` on #25769… | 2026-10-25 | — | — |
213-
| [ADR-0143](./0143-outbound-connector-auth-is-open-tier.md) | Outbound connector auth is open tier — OAuth2 grants, vendor app-credential exchange and request signing in o… | Proposed — 2026-10-27. `docs/adr/` is Tier H (Prime Directive #14): the ma… | 2026-10-27 | — | — |
213+
| [ADR-0143](./0143-outbound-connector-auth-is-open-tier.md) | Outbound connector auth is open tier — OAuth2 grants, vendor app-credential exchange and request signing in o… | Accepted — 2026-10-28. Proposed 2026-10-27 (#26833, which also landed the… | 2026-10-28 | — | — |
214214

215215
## ObjectUI registry — `ui/docs/adr/` (`ui-` prefix)
216216

0 commit comments

Comments
 (0)