Repository navigation
Commit d8c611d
feat(spec): an
<!-- claude-projects-attribution: {"github_login":"os-steve"} -->
_Requested by **Steve** · [project
thread](https://claude.ai/code/project/chan_01FdL1XeUyvVNxLreh3F4Qi9?thread=cmsg_01FdL1XeUyvVNxLreh3F4Qi97JUcgFYCr7uuKzrKbe2paU)_
Fixes #28864
Clause-②: yes (widening)
Metadata-protocol: added
Skills-docs: follow-up owed —
`skills/objectstack-automation/references/integration-recipes.md` (its
e-signature recipe pins `clientId: 'objectstack-prod'`, the
publisher-owned client ruling 6566728470 rules out for a listing) and
`content/docs/automation/connectors.mdx` (the `oauth2` section). Both
should teach `clientSource: 'installer'` once the server slice (#28866)
serves it: taught today, the key sends an author to an instance the
service holds degraded. The skill file is governed (Tier H).
Ruling-ref: 6566728470
## 维护者速读
- 改了什么:`oauth2` 的 `authorization_code` 连接器实例新增可选键 `clientSource:
'installer'`,声明「OAuth 客户端由安装方自带」:每个部署的管理员自己到厂商处登记客户端(回调填本部署的地址),把 client
id 和 secret 填在连接上;定义里不写 `clientId`、`credentialRef`,两者同写会被拒。ADR-0143 D2.1
追加一条修订说明。
- 为什么改:厂商只认登记过回调地址的客户端,而回调地址每个部署都不同;包里写死的 `clientId`
只在发布者登记过的部署上能连上。#28702 裁决 B(「同意」)要求开源版 OAuth 应用不带官方客户端、由安装方自带,DocuSign
应用等这一步。
- 风险与回滚:纯放宽,不写新键的实例解析结果完全不变,包里写 `clientId` 的私有应用照旧可用。服务端还不读这个键(#28866
再做):非 MCP 实例声明它会被标为降级,绝不会无客户端发请求。回滚即撤掉本 PR。
- 先看:`packages/spec/src/integration/connector.zod.ts` 的 ADR-0143 D2.1
规则块(两处
`installerClient`);`packages/spec/src/shared/connector-auth.zod.ts` 的
`clientSource` 与上方
docblock;`docs/adr/0143-outbound-connector-auth-is-open-tier.md` D2
末尾的新修订说明。
- 两分钟验证:`pnpm --filter @objectstack/spec exec vitest run
src/integration/connector-auth-grant-kinds.test.ts`,应看到 29 条全过(其中 9
条是本次新增)。
- 需要你做的:批准本 PR,连同新公开键名 `clientSource`(值 `'installer'`)。ADR 受管,合并要等你的批准。
## What changes
- **`clientSource` on the `authorization_code` instance shape**
(`ConnectorInstanceOAuth2AuthorizationCodeAuthSchema`,
`@objectstack/spec` and `@objectstack/spec/integration`): an optional
closed enum with one member, `'installer'`. The administrator of each
deployment registers their own client with the vendor, against that
deployment's callback, and enters its id and secret on the instance's
connection. The definition then names no client and no secret.
- A `clientId` beside it is refused at publish, `custom @
auth.clientId`, on every provider: the definition would name two
clients. The rule is in `DeclarativeConnectorEntrySchema`'s ADR-0143
D2.1 block (claim amended to include it), beside the other client-key
rules.
- Off `provider: 'mcp'` over http, the installer form no longer needs
`clientId`. `tokenUrl` and `authorizationUrl` are still required there:
the installer owns the client, not the vendor's authorization server.
- A `credentialRef` without `clientId` stays refused at
`auth.credentialRef` by the shape's existing `dependentRequired`; its
message now names the installer form.
- On `grant: 'client_credentials'` the key is refused as an unknown key
with a guidance entry: that grant keeps no connection record, so there
is nowhere to enter a client.
- The `clientId` row of each grant now says only where else a client may
come from for that grant (`oauth2SharedShape(clientIdNote)`).
- **What the D2.1 block accepted before and after**, for the seat:
before, an `oauth2` entry off MCP-over-http was refused at
`auth.clientId` whenever `clientId` was absent. After, the same holds
unless the entry is `authorization_code` with `clientSource:
'installer'`. A new refusal fires only for an entry that carries
`clientSource`, a key every earlier input was refused for
(`unrecognized_keys`). No input that parsed before is refused now, and
no input refused before is accepted unless it carries `clientSource`.
- **ADR-0143 D2.1 amendment** (append-only, dated 2026-11-02, under D2
after the 2026-10-31 note): why, what the variant carries, where the
installer's client lives (the connection record's per-connection client
record, beside a dynamically registered client; D3's persisted list
grows by it), why a packaged `clientId` is kept, `client_credentials`
unchanged, and the per-account API host left as a follow-up.
- **Generated:** `content/docs/references/integration/connector.mdx`
(regenerated), `packages/spec/authorable-surface/integration.json` gains
`ConnectorInstanceOAuth2AuthorizationCodeAuth:clientSource`. No export
moved (`check:api-surface` green). `docs/adr/README.md` does not move
(`adr-index` OK).
- **Changeset:** `.changeset/28864-oauth2-installer-client.md`,
`@objectstack/spec: minor`.
### Forced test edits
-
`packages/spec/src/integration/connector-credential-redaction.test.ts`:
the fail-closed census `DECLARED_NOT_A_SECRET` lists `clientSource`. The
census reds on any newly declared instance-auth key until it is
confirmed not to be a secret; `clientSource` is a closed enum value.
## Why
Ruling 6566728470 on #28702 (letter B, maintainer 「同意」 2026-11-02): an
open-edition OAuth connector app ships no ObjectStack-owned `clientId`
or secret, and the administrator of each deployment registers their own
integration key against that deployment's callback and enters it on the
connection. Today the client is a literal of the definition
(`clientId`), so whoever publishes the package fixes it, and the
vendor's consent page refuses it on every deployment whose callback that
client's owner never registered. This is the spec slice; #28866 (server)
and #28869 (ui) are `Blocked-by` it, and #28277 (DocuSign) waits on all
three.
## Tests
`packages/spec/src/integration/connector-auth-grant-kinds.test.ts`, a
new describe with 9 tests, each refusal pinned by `code @ path`:
- CONTROL accepts: the installer form with endpoints on `openapi`; the
installer form on `mcp` over http with no endpoints; a packaged
`clientId` without `clientSource` (the accept set does not move).
- Refusals: `clientId` beside `clientSource` (alone, with
`credentialRef`, and on `mcp` over http) at `auth.clientId`;
`credentialRef` with the installer form at `auth.credentialRef`; the
endpoints still required off MCP-over-http (`auth.tokenUrl`,
`auth.authorizationUrl`, and no `auth.clientId`); an unknown value at
`auth.clientSource` (`invalid_value`); `clientSource` on
`client_credentials` (`unrecognized_keys`).
- The parsed installer definition carries exactly `authorizationUrl`,
`clientSource`, `grant`, `scopes`, `tokenUrl`, `type`: no client, no
secret.
## Acceptance notes
- **The packaged-`clientId` choice: kept, for private apps.** Measured
on `origin/main` `76bf6c50`: no example, packaged app or `connector-*`
source authors an `oauth2` instance; the authors are test fixtures
(spec, service-automation, connector-openapi, lint, ui app-shell), the
docs example in `content/docs/automation/connectors.mdx`, the skill
recipe above, and ADR-0143 Q3's HubSpot case (a pre-registered client on
an MCP server without dynamic registration). Refusing a packaged
`clientId` would refuse every one of those inputs, a narrowing beside
this widening, and the schema cannot tell a private app (whose author
registers every callback) from a listing. So it stays accepted, and the
ruling's listing rule is held where listings are reviewed (suggested
carrier: #27293).
- **Why the `clientId` exclusion is on the entry, not the shape.** The
closed list in `shared/refinement-projection.ts` has no arm for "this
key excludes that one", so a shape-level `.refine` would add a
dropped-refinement site to every published schema that embeds the auth
shape. The entry's `superRefine` is already marked as not stated
(`integration/DeclarativeConnectorEntry` `out` in
`dropped-refinements.baseline.json`), holds the other provider-dependent
client-key rules, and is what both authoring doors parse. A standalone
`ConnectorInstanceAuthSchema.parse` accepts the pair; no runtime door
parses the auth shape alone.
- **Interim, until #28866.** The automation service does not read
`clientSource`. Off MCP, `oauth2NotServableReason`
(`packages/services/service-automation/src/plugin.ts`) holds such an
instance degraded for a missing `clientId`, never called without a
client. On `mcp` over http, `mcpDiscoveryServerUrl` sees `clientId`
undefined and runs the MCP authorization flow, registering a client
where the server allows it. The changeset says both.
- **Server-slice seams** (for #28866, read on this head):
`plugin.ts#oauth2NotServableReason` and `#mcpDiscoveryServerUrl` (treat
the installer form as supplying the client), `#mcpAuthorizationClient`
(`kept.registered === (auth.clientId === undefined)`, and no
registration for the installer form), the connection door's begin step
(take the client id and secret),
`oauth2-grant/grant-handle.ts#createOAuth2GrantHandle`
(`declaredClient`), and `sys-connector-connection.object.ts`'s
`registered_client` description. UI slice (#28869):
`ui/packages/app-shell/src/views/setup/connectorConnections.ts` and
`ConnectorConnectionsPage.tsx` (the form, with the deployment's redirect
URI).
- **DocuSign production host.** Not measured (the container cannot reach
the vendor). The ADR note leaves a per-account API host to a follow-up;
endpoints stay in the definition, sandbox first.
## Verification
All readings on head `23acaa6a` (this branch with `origin/main`
`b15b634c` merged in) unless a line says otherwise; every heavy run went
through `os-verify-lock.sh`, whose `VERDICT command-exit` line is
quoted.
<details><summary>Package suites and type checks (packages/spec and
every consumer that parses a connector entry)</summary>
- `pnpm turbo run build --concurrency=2 --filter='./packages/**'`: Tasks
78 successful, 78 total. VERDICT command-exit 0.
- `pnpm --filter @objectstack/spec check:generated`: all 14 generated
artifacts up to date (exit 0). Before the merge it named
`content/docs/references/**` stale once; `gen:docs` regenerated it, and
the build wrote `authorable-surface/integration.json`. Both are
committed.
- `pnpm --filter @objectstack/spec test`: Test Files 789 passed (789),
Tests 23324 passed, 1 todo. VERDICT command-exit 0.
- `pnpm --filter @objectstack/spec typecheck` (includes
`check:test-typecheck`): VERDICT command-exit 0.
- `pnpm --filter @objectstack/service-automation test`: Test Files 255
passed (255), Tests 3281 passed (3281). `typecheck`: VERDICT
command-exit 0.
- Every connector package, `pnpm --filter @objectstack/connector-NAME
test`: imap 42/42, mcp 102/102, openapi 109/109, rest 57/57, slack
10/10, each VERDICT command-exit 0. Their `typecheck` (on `497c4a04`,
before the merge, which did not touch them): all five exit 0.
- Other consumers that parse a connector entry or the auth union (on
`497c4a04`): `@objectstack/lint`
`src/lint-flow-credential-literals.test.ts` 17/17;
`@objectstack/objectql` `src/protocol-meta.test.ts` 93/93;
`@objectstack/metadata`
`src/serializers/typescript-serializer-annotation.test.ts` 7/7; `ui`
`packages/app-shell/src/views/metadata-admin/clientValidation.optOuts.test.ts`
and
`packages/app-shell/src/views/setup/ConnectorConnectionsPage.test.tsx`
32/32 (run from `ui/`, as its vitest guard requires), each VERDICT
command-exit 0.
- `eslint --no-inline-config` over the four changed `.ts` files: exit 0.
</details>
<details><summary>Ablations (each landed on disk through
ablation-replace and restored to the HEAD blob)</summary>
- `connector.zod.ts`, the exclusion guard `if (installerClient &&
entry.auth.clientId !== undefined) {` → `if (false && …) {`:
`connector-auth-grant-kinds.test.ts` 1 failed | 28 passed ("refuses a
packaged `clientId` beside `clientSource`"). Restored: blob `eebb0a85`
== HEAD.
- `connector.zod.ts`, the relaxation `if (entry.auth.clientId ===
undefined && !installerClient) {` → without `&& !installerClient`: 4
failed | 25 passed (the openapi CONTROL accept, the `credentialRef`
refusal's single issue, the endpoints-still-required list, the
declared-keys pin). Restored: blob == HEAD.
- Reverse type verification: a probe in
`packages/services/service-automation` typed `ConnectorInstanceAuth`
from `@objectstack/spec/integration`: `clientSource: 'installer'`
compiles, `clientSource: 'publisher'` is TS2322 (`'"publisher"' is not
assignable to type '"installer"'`), so the consumer reads the rebuilt
`.d.ts`. Probe deleted; `git status` clean.
Ablation and probe readings were taken on `497c4a04`; the merge brought
no change to either file.
</details>
<details><summary>Derived gate families (dispatch-gates)</summary>
- `node scripts/pm/dispatch-gates.mjs --commands --repo
objectstack-ai/objectstack` at `23acaa6a`: 107 commands, change set 8
paths vs merge base `b15b634c1`, 280 changed lines (+249 / -31). Same
list as at `497c4a04`.
- All 107 exit 0 on `23acaa6a` (and all 107 on `497c4a04`). On the first
run `check-plugin-teardown-shape.mjs --self-test` exited 3: its pinned
fixture commit was outside the shallow clone. After `git fetch --depth=1
origin 621a487…` it reported 48 cases pass, exit 0.
- `dispatch-gates --ran`: "107 derived famil(ies) accounted for — 107
run, 0 NOT-MEASURED (a DERIVED zero …)".
- `check-changeset-no-major.mjs --base origin/main --event` (this body,
offline): exit 0, LEVEL AXIS `yes (widening)` + `added`,
`@objectstack/spec: minor`.
- ADR families: `check:adr-symbol-anchors` (2977 anchors resolve),
`check:adr-links`, `check:adr-anchors`, `check:adr-index` (README
current): all exit 0.
- Local-only families: `check:skill-frame-freshness` exit 0.
`check:dev-prereqs` exit 1 is a box precondition (35 `ui/` workspace
dists not built in this container; the `packages/**` build is 78/78),
not a reading about this diff.
- Full dogfood not run: no `!` in the title and no `(narrowing)` arm.
</details>
## Human Minimum
- [x] 1. Schemas start as Zod; `packages/spec` holds no runtime logic.
- [x] 2. Every name I added uses the casing for its kind.
- [x] 3. No formatter ran; `pnpm lint` passes.
- [x] 4. One issue, one branch off an up-to-date `main`, one pull
request.
- [x] 5. A change that publishes carries a changeset, and both
declaration lines are filled in here and in the changeset.
- [x] 6. `packages/spec` touched: its generated artifacts are
regenerated and committed.
- [x] 7. No hand edits to `content/docs/references/`,
`packages/*/CHANGELOG.md` or `content/docs/releases/`.
- [x] 8. Title, body and comments are in English.
- [x] The *Before Submitting* list in CONTRIBUTING.md is done.
---
_Generated by [Claude
Code](https://claude.ai/code/session_01ADHkmPS39tCwZRCvghgPQf)_
---------
Co-authored-by: Claude <noreply@anthropic.com>oauth2 authorization_code connector instance can take its OAuth client from the installer — clientSource: 'installer' (#28920)1 parent ee7518f commit d8c611d
9 files changed
Lines changed: 295 additions & 37 deletions
File tree
- .changeset
- content/docs/references/integration
- docs/adr
- packages/spec
- authorable-surface
- liveness
- src
- integration
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
267 | 267 | | |
268 | 268 | | |
269 | 269 | | |
270 | | - | |
| 270 | + | |
271 | 271 | | |
272 | 272 | | |
273 | | - | |
| 273 | + | |
| 274 | + | |
274 | 275 | | |
275 | 276 | | |
276 | 277 | | |
| |||
280 | 281 | | |
281 | 282 | | |
282 | 283 | | |
283 | | - | |
| 284 | + | |
284 | 285 | | |
285 | 286 | | |
286 | 287 | | |
| |||
561 | 562 | | |
562 | 563 | | |
563 | 564 | | |
564 | | - | |
| 565 | + | |
565 | 566 | | |
566 | 567 | | |
567 | | - | |
| 568 | + | |
| 569 | + | |
568 | 570 | | |
569 | 571 | | |
570 | 572 | | |
| |||
580 | 582 | | |
581 | 583 | | |
582 | 584 | | |
583 | | - | |
| 585 | + | |
584 | 586 | | |
585 | 587 | | |
586 | 588 | | |
| |||
599 | 601 | | |
600 | 602 | | |
601 | 603 | | |
602 | | - | |
| 604 | + | |
603 | 605 | | |
604 | 606 | | |
605 | | - | |
| 607 | + | |
| 608 | + | |
606 | 609 | | |
607 | 610 | | |
608 | 611 | | |
| |||
617 | 620 | | |
618 | 621 | | |
619 | 622 | | |
620 | | - | |
| 623 | + | |
621 | 624 | | |
622 | 625 | | |
623 | 626 | | |
| |||
870 | 873 | | |
871 | 874 | | |
872 | 875 | | |
873 | | - | |
| 876 | + | |
874 | 877 | | |
875 | 878 | | |
876 | | - | |
| 879 | + | |
| 880 | + | |
877 | 881 | | |
878 | 882 | | |
879 | 883 | | |
| |||
883 | 887 | | |
884 | 888 | | |
885 | 889 | | |
886 | | - | |
| 890 | + | |
887 | 891 | | |
888 | 892 | | |
889 | 893 | | |
| |||
0 commit comments