Skip to content

feat(spec): an oauth2 authorization_code connector instance can take its OAuth client from the installer — clientSource: 'installer' - #28920

Merged
os-zhuang merged 6 commits into
mainfrom
claude/project-thread-ds43h1
Nov 2, 2026
Merged

os-zhuang merged 6 commits into
mainfrom
claude/project-thread-ds43h1

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Requested by Steve · project thread
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 只在发布者登记过的部署上能连上。[Decision] #28277 DocuSign app: where an open-edition OAuth listing's client id comes from, when clientId is fixed by the package and the redirect URI is per deployment #28702 裁决 B(「同意」)要求开源版 OAuth 应用不带官方客户端、由安装方自带,DocuSign 应用等这一步。
  • 风险与回滚:纯放宽,不写新键的实例解析结果完全不变,包里写 clientId 的私有应用照旧可用。服务端还不读这个键(automation: a sys_connector_connection stores and uses the installer-supplied OAuth client for an authorization_code instance (#28702 B, server slice) #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

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.

Package suites and type checks (packages/spec and every consumer that parses a connector entry)
  • 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.
Ablations (each landed on disk through ablation-replace and restored to the HEAD blob)
  • 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.

Derived gate families (dispatch-gates)
  • 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 621a4876… 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.

Human Minimum

  • 1. Schemas start as Zod; packages/spec holds no runtime logic.
  • 2. Every name I added uses the casing for its kind.
  • 3. No formatter ran; pnpm lint passes.
  • 4. One issue, one branch off an up-to-date main, one pull request.
  • 5. A change that publishes carries a changeset, and both declaration lines are filled in here and in the changeset.
  • 6. packages/spec touched: its generated artifacts are regenerated and committed.
  • 7. No hand edits to content/docs/references/, packages/*/CHANGELOG.md or content/docs/releases/.
  • 8. Title, body and comments are in English.
  • The Before Submitting list in CONTRIBUTING.md is done.

Generated by Claude Code

… from the installer — `clientSource: 'installer'`

ADR-0143 D2.1 kept the client in the definition: a packaged `clientId`, or
none on `provider: 'mcp'` over http, where the provider registers one. The
administrator of a deployment the publisher never saw had no way to bring
the client the vendor accepts for that deployment's callback.

`clientSource: 'installer'` declares that the administrator of each
deployment registers their own client and enters it on the instance's
connection. The definition then names no `clientId` and no
`credentialRef`, on any provider; the endpoints stay declared (or
discovered over MCP http). A packaged `clientId` without the key stays
accepted for private apps, so no existing input changes its result.
`client_credentials` refuses the key with guidance: it keeps no connection.

Claude-Session: https://claude.ai/code/session_01ADHkmPS39tCwZRCvghgPQf
Co-authored-by: Claude <noreply@anthropic.com>
…e for `clientSource`

Each grant's `clientId` row now says only where else a client may come
from for that grant.

Claude-Session: https://claude.ai/code/session_01ADHkmPS39tCwZRCvghgPQf
Co-authored-by: Claude <noreply@anthropic.com>
…supply the OAuth client

Appends a dated note under D2: an `authorization_code` instance that
declares `clientSource: 'installer'` takes its client from the
administrator of each deployment, kept on the instance's connection beside
a dynamically registered client. A packaged `clientId` is kept for private
apps; `client_credentials` is unchanged; a per-account API host is a
follow-up. Adds the changeset.

Claude-Session: https://claude.ai/code/session_01ADHkmPS39tCwZRCvghgPQf
Co-authored-by: Claude <noreply@anthropic.com>
@github-actions github-actions Bot added size/m documentation Improvements or additions to documentation tests tooling labels Nov 2, 2026
@github-actions

github-actions Bot commented Nov 2, 2026 •

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 1 package(s): @objectstack/spec, touching 9 documentable anchor(s). ⚠️ 2 changed file(s) yielded no anchor (packages/spec/authorable-surface/integration.json, packages/spec/liveness/connector.json), so the pages documenting them are NOT COVERED by this run — this is not a clean bill of health for those files.

6 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:

  • content/docs/automation/connectors.mdx (via authorizationUrl (literal, a string literal in DeclarativeConnectorEntrySchema), authorization_code (literal, a string literal in ConnectorInstanceOAuth2ClientCredentialsAuthSchema; a string literal in DeclarativeConnectorEntrySchema), clientId (literal, a string literal in DeclarativeConnectorEntrySchema), client_credentials (literal, a string literal in ConnectorInstanceOAuth2ClientCredentialsAuthSchema), tokenUrl (literal, a string literal in DeclarativeConnectorEntrySchema))
  • content/docs/kernel/contracts/auth-service.mdx (via clientId (literal, a string literal in DeclarativeConnectorEntrySchema))
  • content/docs/permissions/authentication.mdx (via clientId (literal, a string literal in DeclarativeConnectorEntrySchema))
  • content/docs/permissions/sso.mdx (via authorizationUrl (literal, a string literal in DeclarativeConnectorEntrySchema), clientId (literal, a string literal in DeclarativeConnectorEntrySchema), tokenUrl (literal, a string literal in DeclarativeConnectorEntrySchema))
  • content/docs/permissions/system-context.mdx (via authorization_code (literal, a string literal in ConnectorInstanceOAuth2ClientCredentialsAuthSchema; a string literal in DeclarativeConnectorEntrySchema))
  • content/docs/plugins/index.mdx (via clientId (literal, a string literal in DeclarativeConnectorEntrySchema))

⛔ 2 release-owned page(s) also name something this change touched. These are read-only:

  • content/docs/releases/v17/17-0.mdx (via DeclarativeConnectorEntrySchema (symbol, a top-level const))
  • content/docs/releases/v17/17-5.mdx (via client_credentials (literal, a string literal in ConnectorInstanceOAuth2ClientCredentialsAuthSchema))

content/docs/releases/ is RELEASE-OWNED (AGENTS.md "Documentation Guardrails"): release
notes are written centrally at release time, and a code PR that edits them is the exact PR
that guardrail exists to stop. They are still audited — read-only. If one of them is actually
wrong, file an issue or open a dedicated docs-only PR; do not edit it here.

What this run could not see
  • 2 changed file(s) yielded no anchor (packages/spec/authorable-surface/integration.json, packages/spec/liveness/connector.json) — pages documenting those are invisible to this run
  • the SDK route bridge reached 60 of 216 client-bound route-ledger rows — the other 156 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 156: 0 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 55 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 101 are undecided (no in-repo declaration, on a ledger that has other in-repo registrars — absence and an unreadable spelling are not distinguishable here). The rows themselves: node scripts/docs-audit/affected-docs.mjs --bridge-coverage
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.
  • a key NAME is not a key, so the hand re-read the line above prescribes can land on the wrong schema. The same spelling is authorable on one governed type and a [REMOVED] tombstone on another for each of active, aria, joins, objects, template, tools and version (censused on [finding] tools is a key on BOTH AgentSchema (tombstoned, dead) and SkillSchema (live, cloud-attested), so a name-based search attributes skill examples to the agent key — it produced a false stop-the-line alarm on PR #19059 #19093 over the liveness ledger's governed types, top-level keys); nothing in a search result distinguishes the two, so a grep hit on a LIVE example reads as evidence about the DEAD key. Measured on fix(spec): the agent.tools liveness row says dead — it claimed live on a key the schema tombstoned #19059: content/docs/ai/agents.mdx was reported as contradicting the agent.tools tombstone over its tools: example at :161, which is inside the defineSkill({ block opened at :155 — the page was already correct. Settle ownership by PARSING the value against both schemas, never by the name: that literal PASSES SkillSchema, and as an AgentSchema it FAILS at tools with the tombstone prescription. ⛔ These names are not the whole class — a key retired through a .strict() guidance map leaves no tombstone in the walked shape and none of them here (tool.category, live as AIToolDefinition.category).

Coarse fallback — 168 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json cae71fedfc8f04d35694da55ab74f9d680da0c5f → packageMentionDocs.

Which tree this was computed on

This run read content/docs from 0ebaf2d9135eabf4f92124a25f7e3fb5aede142e — the merge of head 82e993b57cac93ec12c5949c4163dde87528c45c into base cae71fedfc8f04d35694da55ab74f9d680da0c5f, which is what actions/checkout gives a pull_request run. Not the PR head.

A worktree cut from an older main holds a different content/docs, so re-deriving there can legitimately return a different list — that is a different tree, not a wrong row. To answer on the same tree:

# while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 0ebaf2d9135eabf4f92124a25f7e3fb5aede142e && git checkout 0ebaf2d9135eabf4f92124a25f7e3fb5aede142e
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin cae71fedfc8f04d35694da55ab74f9d680da0c5f 82e993b57cac93ec12c5949c4163dde87528c45c && git checkout -B drift-repro cae71fedfc8f04d35694da55ab74f9d680da0c5f && git merge --no-ff 82e993b57cac93ec12c5949c4163dde87528c45c

node scripts/docs-audit/affected-docs.mjs --json cae71fedfc8f04d35694da55ab74f9d680da0c5f

⚠️ That checkout carried uncommitted changes, so the commit above does not fully identify what was read.

Advisory only, and a precision-first one (#9192): a page is listed because it names a
symbol, wire route or SDK method this diff touched — not because it mentions a changed
package. Each row says which anchor put it there, so a wrong row is reportable rather than
merely annoying. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs cae71fedfc8f04d35694da55ab74f9d680da0c5f → pass the list as
args.docs, on the commit named under Which tree this was computed on.

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:spec#1 · os-steve · session_01ADHkmPi25XXKx92Xmg4viS

Head-sha: 23acaa6ae1db71f8e913a7eb31c0db0d8c4fd12d · read 2026-11-02T04:18Z

维护者速读

席位意见:可落地,等键名批准、契约复审和受管批准。

席位核对 — ACCEPT · domain:spec#1 · head 23acaa6a

  1. Shape: draft, base main. The first content line after the session attribution block is Fixes #28864, and it is followed by the Ruling-ref: 6566728470 line. 8 files, +249/-31, all inside the claimed surface as amended (6567679603). It is GOVERNED, Tier H, because it touches docs/adr/0143-outbound-connector-auth-is-open-tier.md.
  2. Premise and diff, read on the head:
    • ConnectorInstanceOAuth2AuthorizationCodeAuthSchema gains clientSource: z.enum(['installer']).optional().
    • client_credentials refuses the key through its guidance map.
    • In DeclarativeConnectorEntrySchema's D2.1 block, clientId beside clientSource: 'installer' is refused at auth.clientId on every provider. The clientId-required rule off MCP-over-http skips the installer form; tokenUrl and authorizationUrl stay required.
    • No narrowing: on main, clientSource was an unrecognized key on both variants, so every input without it keeps its verdict, and client_credentials with it is still refused, now with guidance.
    • The ADR amendment is append-only under D2 and cites path#symbol.
  3. Gate check-runs (pinned to head 23acaa6a): at reading 13 success, 7 skipped, 22 running, 0 failed. Dev readings at this head (main b15b634c merged in): dispatch-gates 107/107 exit 0, a DERIVED zero. spec 23324 tests pass, service-automation 3281, and all 5 connector-* packages plus the lint, objectql, metadata and ui consumers are green. Two ablations went red and were restored.
  4. Accepted deviations: ① The packaged clientId is KEPT for private apps, not refused. Refusing it would narrow an accept set the ruling did not ask to narrow, and the schema cannot tell a listing from a private app. The ruling's listing rule is held at listing review, as the ADR note says. ② connector.zod.ts is in the amended claim, not a deviation. ③ The main merge before the PR and a ui test re-run from ui/ are declared.
  5. Out of scope / follow-ups: Skills-docs follow-up owed: skills/objectstack-automation/references/integration-recipes.md (Tier H) and content/docs/automation/connectors.mdx. Both should land with the server slice automation: a sys_connector_connection stores and uses the installer-supplied OAuth client for an authorization_code instance (#28702 B, server slice) #28866. The per-account API host (DocuSign) is named as a follow-up in the ADR note. The server and ui slice surfaces are in the dev report's handoff_slices (6568922117), for the epic seat's automation: a sys_connector_connection stores and uses the installer-supplied OAuth client for an authorization_code instance (#28702 B, server slice) #28866 / console: the connector connection form asks the administrator for the OAuth client (client id + secret) when the instance takes an installer-supplied client (#28702 B, ui slice) #28869.
  6. Landing: three gates, all on this head.
    • (a) The maintainer approves the new public key name clientSource (spec lane rule; asked in the project).
    • (b) One CONTRACT_REVIEW_TIER review of the widening, handed to triage (line below).
    • (c) A GOVERNED_APPROVERS APPROVED review, for the ADR.
      The seat never readies, queues or arms auto-merge on this PR before (c). A head move voids this record.

Contract-review-handoff: triage — CONTRACT_REVIEW_TIER unavailable (maintainer standing word 2026-10-28T15:04Z: the Fable quota is short, so contract reviews wait for triage) · head 23acaa6

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

triage#1 · os-zhuang · session_01AavokzJ5DndAwitDXvKy4U

Contract-review-claim: session_01AavokzJ5DndAwitDXvKy4U · head 23acaa6ae1db71f8e913a7eb31c0db0d8c4fd12d · 2026-11-02T04:22Z

Taken on the lane's Request: contract-review doorbell (#28646, 6568953026). Face schema + rules (widening plus the ADR-0143 D2.1 amendment, Tier H), at CONTRACT_REVIEW_TIER.


Generated by Claude Code

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

triage#1 · os-zhuang · session_01AavokzJ5DndAwitDXvKy4U

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: 23acaa6ae1db71f8e913a7eb31c0db0d8c4fd12d
Local-runs: none
Scope: packages/spec/src/integration/connector.zod.ts, packages/spec/src/shared/connector-auth.zod.ts, docs/adr/0143-outbound-connector-auth-is-open-tier.md + packages/spec/src/integration/connector-auth-grant-kinds.test.ts (the gate of the D2.1 rule), packages/spec/src/integration/connector-credential-redaction.test.ts (the fail-closed key census), packages/spec/src/shared/strict-object.ts, packages/spec/src/shared/refinement-projection.ts, packages/spec/dropped-refinements.baseline.json, packages/spec/liveness/connector.json, packages/spec/scripts/liveness/check-liveness.mts, packages/services/service-automation/src/oauth2-grant/grant-handle.ts, packages/services/service-automation/src/plugin.ts, packages/services/service-automation/src/sys-connector-connection.object.ts, ui/packages/app-shell/src/views/setup/connectorConnections.ts, ruling 6566728470 on #28702, cards #28866 and #28869

Inputs read: card #28864 body and all three comments (claim 6567679603, triage, dev report 6568922117); PR #28920 body, file list, the three PR comments (docs-drift, seat ACCEPT 6568941768, this seat's claim), the net diff against main; ruling 6566728470 and the #28702 body; #28866 and #28869 bodies; the check-runs on the head, re-fetched at review time. PR reviews on the head: none.

Check-runs on 23acaa6a at review time: 29 success, 12 skipped, 0 failure, 26 pending (Test Core 1–8/8, Test UI 1–16/16, Type Check UI, Type Check · workspace). Green among them: Lint & Repo Gates, Check Changeset, Spec property liveness, Governed Surface Queue Guard, PR-body guards, Build Core, Build Docs, Type Check · source gates and · consumer gates, Live E2E UI, Build & E2E UI. The pending runs are stated pending, not assumed green; the seat's landing needs them.

① Derived judgments

  • Accept set, DeclarativeConnectorEntrySchema (integration/DeclarativeConnectorEntry). Read on the head's D2.1 block and the test file. Before: clientSource was an unrecognized_keys refusal on both oauth2 grants (both shapes are strictObject), and an oauth2 entry off MCP-over-http was refused at auth.clientId when clientId was absent. After: (a) authorization_code + clientSource: 'installer' is accepted without clientId on every provider, endpoints still required off MCP-over-http; (b) clientId beside clientSource is refused custom @ auth.clientId on every provider; (c) credentialRef without clientId stays refused at auth.credentialRef (the shape's dependentRequired({ credentialRef: ['clientId'] }), unchanged); (d) clientSource on client_credentials stays unrecognized_keys, now with a guidance sentence; (e) clientSource with any value but 'installer' is invalid_value @ auth.clientSource. Every input accepted on main is accepted unchanged, and every newly accepted input carries clientSource. Widening, named right. The two refusal messages that changed (auth.clientId missing; auth.credentialRef without clientId) change text only, not the set.
  • Public TypeScript surface. ConnectorInstanceOAuth2AuthorizationCodeAuth gains an optional clientSource?: 'installer'; ConnectorInstanceOAuth2Auth, ConnectorInstanceAuth, DeclarativeConnectorEntry widen by it. No export moved (api-surface/, export-origins/, declaration-map/ unchanged in the diff; check:api-surface is inside the green Lint & Repo Gates). Right.
  • Authorable surface. packages/spec/authorable-surface/integration.json gains integration/ConnectorInstanceOAuth2AuthorizationCodeAuth:clientSource and nothing else. Right — one key, the one the diff declares.
  • Published JSON Schema / dropped refinements. packages/spec/json-schema/ is gitignored (built at publish). The new exclusion rule sits in the entry's superRefine, which dropped-refinements.baseline.json already lists as integration/DeclarativeConnectorEntry → out, so the published file is wider than the runtime by exactly one more rule, in the one place already declared unstated. The auth shape's own .refine(dependentRequired(...)) stays projectable. The dev's reason for placing the rule on the entry (no ProjectableRefinement arm for "this key excludes that one"; refinement-projection.ts says growing the union is a public-contract decision) holds. Baseline unchanged, right.
  • Generated reference. content/docs/references/integration/connector.mdx regenerated from the new .describe() strings; the clientId row of the client_credentials table no longer mentions the installer form (oauth2SharedShape(clientIdNote)). Right — generated, not reviewed here.
  • Redaction census. DECLARED_NOT_A_SECRET gains clientSource; the value is a closed enum member, not a secret. Right.
  • Liveness ledger. packages/spec/liveness/connector.json is untouched. Read against check-liveness.mts#childShape: a union-typed property returns null, so connector/auth is a leaf row (live, verifiedAt 2026-09-17) and no member of either oauth2 shape is walked. clientSource therefore inherits a live verdict the gate never asked about and never could — it is not an undrilled container, not UNCLASSIFIED, and not in undrilled-containers.baseline.json. The green "Spec property liveness" run says nothing about this key. Not listed as planned anywhere — see ④ must-fix 1.
  • Interim runtime, read on the head (not reviewed; it is automation: a sys_connector_connection stores and uses the installer-supplied OAuth client for an authorization_code instance (#28702 B, server slice) #28866's). plugin.ts#oauth2NotServableReason holds a non-MCP installer-form instance degraded for a missing clientId, and its reason text says "Declare them to dispatch it" — a remedy the new schema refuses (clientId beside clientSource → auth.clientId). On provider: 'mcp' over http, mcpDiscoveryServerUrl sees clientId undefined and mcpAuthorizationClient runs dynamic registration, so an instance that declares the installer's client silently gets a registered one instead. The changeset states both; the spec surface an author reads does not (④ must-fix 1).
  • Hand-written pages falsified on merge (lane remainder, declared). content/docs/automation/connectors.mdx:557 ("oauth2 needs tokenUrl and clientId") and skills/objectstack-automation/references/integration-recipes.md:163-165 (an e-signature recipe pinning clientId: 'objectstack-prod', the publisher-owned client the ruling rules out for a listing). The PR body owes both as a Skills-docs follow-up with automation: a sys_connector_connection stores and uses the installer-supplied OAuth client for an authorization_code instance (#28702 B, server slice) #28866; the seat accepted that. Named, not reviewed here.

② Semver level

  • Changeset .changeset/28864-oauth2-installer-client.md: '@objectstack/spec': minor, body carries Clause-②: yes (widening) and Metadata-protocol: added, matching the PR body line for line. The diff publishes one new authorable key and one new accepted form in @objectstack/spec and moves no other package's published source. By AGENTS.md's rule ("a new key, accepted value or metadata type is added; a kind takes at least minor on a package the diff moves") minor is right, added is right, yes (widening) is right (① shows no input loses its verdict). Both Check Changeset runs are green.
  • The changeset body's "Not yet served" paragraph is the only author-facing place that says the service does not read the key yet. See ④.

③ Boundary flags

  • Dev open_questions[0] — the new public key name clientSource (value 'installer'), standard-order item 5, needs the maintainer's approval before landing. Options A/B/C stated, A built. No approval is recorded on the card, on the PR, on [Decision] #28277 DocuSign app: where an open-edition OAuth listing's client id comes from, when clientId is fixed by the package and the redirect URI is per deployment #28702 after the ruling, or as a PR review; the seat says it was asked "in the project" (the seat's own project thread). Escalated, not a FAIL reason: the approval is a landing gate the seat already lists as (a). The name itself holds against the card's own sentence ("its client comes from the installer") and is a closed enum that can grow, which is the ADR-0143 D2 shape for every other value list. This reviewer has no objection to A.
  • Governed surface. docs/adr/0143-outbound-connector-auth-is-open-tier.md is Tier H. No GOVERNED_APPROVERS APPROVED review exists on the head. Landing gate (c), the seat's and the maintainer's; this record does not lift it.
  • Dev deviation — connector.zod.ts outside the first claim. The seat amended the claim (6567679603) to the D2.1 block before the edit; the diff touches only that block. Answered.
  • Dev deviation — origin/main merged in before the PR. 23acaa6a is a merge of b15b634c; the head's check-runs cover the merge. Answered.
  • Packaged clientId kept. The card made this the slice's choice ("refused, or kept for private apps … must say why"). The PR body's measurement (no non-test source authors an oauth2 instance; refusing would narrow fixtures, the docs example, the recipe and ADR-0143 Q3's HubSpot case) is consistent with what this reviewer finds on the head (content/docs/automation/connectors.mdx:537,547, integration-recipes.md:165, ADR-0143 Q3). The reason is stated in the ADR note, the shape docblock and the entry's refusal message. Answered; the ruling's listing rule is correctly placed at listing review, where the ruling put the shelf (its letter A fixture clause).
  • Confidence gap carried from the ruling — DocuSign per-account production host. The ADR note names it "not decided here" and a follow-up, sandbox first, as the ruling allowed ("says how the host is discovered or leaves production as a named follow-up"). Answered.
  • Interim behaviour until automation: a sys_connector_connection stores and uses the installer-supplied OAuth client for an authorization_code instance (#28702 B, server slice) #28866 (degraded off MCP; dynamic registration on MCP-over-http). Declared in the changeset and the PR body; not declared on the spec surface. Carried into ④ must-fix 1.

④ Design judgment

packages/spec/src/shared/connector-auth.zod.ts — judged against ruling 6566728470, ADR-0143 D2/D3, and the first consumers (#28866 grant-handle.ts#createOAuth2GrantHandle declaredClient; #28869 connectorConnections.ts#readConnectableInstances).

  • Holds against the ruling: the installer form carries no client and no secret — the parsed installer definition is exactly authorizationUrl, clientSource, grant, scopes, tokenUrl, type (pinned). Holds against D3: no inline-secret path opens. Both shapes stay strictObject, so an undeclared secret key is still unrecognized_keys with the credentialRef prescription; clientSource is a one-member enum and cannot carry a value; credentialRef still requires clientId, so the installer form cannot smuggle a secret reference either. The client_credentials guidance entry for clientSource is a prescription on a refusal that already fired, so it narrows nothing. Tried and could not break: clientSource + credentialRef (refused at auth.credentialRef); clientSource: 'publisher' (refused invalid_value); clientSecret beside the installer form (still unrecognized_keys → credentialRef guidance, unchanged).
  • First consumers: grant-handle.ts#declaredClient is built from config.clientId; with the installer form clientId is undefined, which is the exact branch the handle already takes for a dynamically registered client (kept.clientId), so automation: a sys_connector_connection stores and uses the installer-supplied OAuth client for an authorization_code instance (#28702 B, server slice) #28866's seam is the one the PR names — read the installer's client from the connection record in that branch. connectorConnections.ts#readConnectableInstances filters on auth.type/auth.grant only, so the ui's list is unchanged until console: the connector connection form asks the administrator for the OAuth client (client id + secret) when the instance takes an installer-supplied client (#28702 B, ui slice) #28869 reads clientSource. Both consumers compile against an optional key with no change (Type Check · consumer gates green).
  • Must-fix 1 (ADR-0049 enforce-or-remove; the record ADR-0143 D2.3 applies to its own enum). The key is declared on the published spec, described in the generated reference as a working form ("the administrator … enters its id and secret on the instance's connection"), and read by nothing on the head: off MCP the service degrades the instance with a reason whose remedy ("declare clientId") the new schema refuses, and on MCP-over-http it silently substitutes dynamic registration for the client the author said the installer supplies. That is ADR-0049's fourth state — parsed, unmarked on the spec surface, unenforced — and the liveness ledger cannot record it as planned because the walker does not descend into the auth union (①). The house tool for exactly this case is the ledger's own second channel: a .describe() marker on the key in the [… — not enforced] family, naming automation: a sys_connector_connection stores and uses the installer-supplied OAuth client for an authorization_code instance (#28702 B, server slice) #28866 as the slice that serves it, regenerated into the reference; plus a dated sentence in connector.json's auth row note recording that clientSource is planned until automation: a sys_connector_connection stores and uses the installer-supplied OAuth client for an authorization_code instance (#28702 B, server slice) #28866 reads it (the row is the only ledger coordinate that covers the key, and its live verdict now silently covers one more member). The changeset's "Not yet served" paragraph is not the spec surface an author validates against. One describe string and one ledger note; the reference regenerates. FAIL until carried.
  • Should-fix: the clientId .describe() of the authorization_code shape now runs to three clauses; fine as a reference row, but the credentialRef row still says only "requires clientId" — the one row an author on the installer form will hit does not name the form (the refusal message does; the row could).

packages/spec/src/integration/connector.zod.ts (the D2.1 block of DeclarativeConnectorEntrySchema) — judged against ADR-0143 D2.1 as amended, and the test file that enforces it.

  • Holds. installerClient is computed once and read twice: the exclusion (refused loudly, located auth.clientId, with both remedies — delete clientId and its credentialRef, or delete clientSource for a private app) and the relaxation (clientId dropped from missing only for the installer form; tokenUrl/authorizationUrl still required off MCP-over-http, pinned). The missing-clientId message now names the installer form as the alternative. Issue order for the pre-existing refusal (tokenUrl, clientId, authorizationUrl) is preserved, so the existing pins hold. The exclusion runs on every provider, including MCP-over-http (pinned), so the "two clients" case cannot reach mcpAuthorizationClient, whose kept.registered === (auth.clientId === undefined) reuse check would otherwise see a packaged id. Tried and could not break: installer form on MCP stdio (endpoints required, no clientId asked — consistent with the rule's shape); clientSource on a catalog descriptor (refused earlier at auth requiring provider, unchanged).
  • The dev's two ablations (exclusion guard → false: 1 red; relaxation without !installerClient: 4 red) are reported, not re-run; the test file's CONTROL accepts make a fire-on-everything refusal impossible to pass. Read, consistent with the code.
  • Note, not a finding: ConnectorSchema (the in-code registerConnector carrier, same base shape) carries none of the D2.1 rules, so a plugin-built def may pair clientId with clientSource. This pre-dates the PR for every D2.1 rule and no engine reads auth off that carrier; the ledger's _note records the two-door asymmetry.

docs/adr/0143-outbound-connector-auth-is-open-tier.md — judged against the ruling, its first consumers (#28866, #28869) and the execution plan (three slices, spec first).

  • Holds against the ruling: letter B quoted with the maintainer's verbatim word; the three slices named; the DocuSign host gap carried as a named follow-up; the per-connection client record placed where the ruling put it (the registered_client shape, D3/Q2). Every path#symbol anchor resolves (check:adr-symbol-anchors inside the green Lint & Repo Gates). Append-only under D2, dated, after the 2026-10-31 note — the file's own amendment convention.
  • Should-fix (ADR): the note rewrites D3's persisted list ("the refresh token, a dynamically registered or installer-supplied client, and the grant's status, scopes and expiry") and D3's "Only what cannot be re-obtained unattended" sentence from inside a D2 note, while D3 itself carries no pointer. A reader of D3 alone still reads the old list. One line under D3 ("see the 2026-11-02 D2.1 note") keeps the record navigable; the 2026-10-31 precedent put its Acceptance change under Acceptance.
  • Should-fix (ADR, with must-fix 1): the note's "Slices" bullet says what the server and ui slices do but not that until they land the key is declared-not-served and how an instance behaves meanwhile (degraded off MCP; dynamic registration on MCP-over-http). The changeset says it; the record of decision should too, one sentence.
  • Consumer fit: automation: a sys_connector_connection stores and uses the installer-supplied OAuth client for an authorization_code instance (#28702 B, server slice) #28866's card reads the D2.1 shape it is told to implement (declaredClient, the connection record, "the secret goes in the credential store, never in metadata") — consistent with the note's "The client secret entered there is not a credentialRef" and D3's encryption clause. console: the connector connection form asks the administrator for the OAuth client (client id + secret) when the instance takes an installer-supplied client (#28702 B, ui slice) #28869 reads the server door shape; the note's "shows the deployment's redirect URI" matches D5 and the card.

Security. No security-relevant finding. The widening opens no inline-secret path (D3 holds on both shapes), the published spec stays strictly narrower than or equal to the runtime on every door, and the interim divergence on MCP-over-http substitutes a dynamically registered client, not a credential exposure.

Must-fix list

  1. Mark clientSource as not yet served on the spec surface until automation: a sys_connector_connection stores and uses the installer-supplied OAuth client for an authorization_code instance (#28702 B, server slice) #28866 reads it: a .describe() marker in the ledger's [… — not enforced] family on the key (regenerating content/docs/references/integration/connector.mdx), and a dated planned sentence for the key in packages/spec/liveness/connector.json's auth row note, since the walker cannot drill a union and the row's live verdict now covers it unseen.

Should-fix list

Implemented-by: claude/project-thread-ds43h1
Reviewed-by: session_01AavokzJ5DndAwitDXvKy4U

VERDICT: FAIL


Generated by Claude Code

… server slice reads it

Contract review 6569108322 on #28920:

- The key's describe carries `[PLANNED — not enforced]` and says what an
  instance that declares it does meanwhile (held degraded off MCP over
  http; dynamic registration on MCP over http). It names ADR-0143 D2.1,
  not the tracker id, because check:doc-authoring refuses an issue id in
  customer-facing spec prose; the adjacent comment names #28866.
  Reference regenerated.
- The liveness ledger's `connector/auth` row records the key as `planned`
  until #28866: the walker does not descend into the auth union, so that
  row is the only ledger coordinate over it.
- A test pins the marker, the ADR reference, no tracker id in the
  describe, and the ledger sentence, so #28866 drops them together.
- The authorization_code `credentialRef` describe names the installer form.
- ADR-0143: a pointer under D3's persisted list to the 2026-11-02 D2.1 note,
  and the note's Slices bullet states the interim behaviour.
- The changeset names the marker and the ledger row.

Claude-Session: https://claude.ai/code/session_01ADHkmPS39tCwZRCvghgPQf
Co-authored-by: Claude <noreply@anthropic.com>
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:spec#1 · os-steve · session_01ADHkmPi25XXKx92Xmg4viS

Head-sha: 82e993b57cac93ec12c5949c4163dde87528c45c · read 2026-11-02T06:01Z

维护者速读

  • 改了什么:按契约复审 6569108322 的意见修了第二轮。clientSource 的说明开头加上 [PLANNED — not enforced],讲明服务端还没读这个键时的过渡行为;liveness 台账的 connector/auth 行记下它在 automation: a sys_connector_connection stores and uses the installer-supplied OAuth client for an authorization_code instance (#28702 B, server slice) #28866 落地前是 planned。ADR-0143 的 D3 加了一行指向 D2.1 修订,Slices 段补了一句过渡说明;credentialRef 的说明也写明了安装方表单。
  • 为什么改:键已经声明但还没被读取,需要在 spec 层面写清楚(ADR-0049)。
  • 风险与回滚:只改说明文字、台账注记和 ADR 文字,接受与拒收的结果不变。回滚即撤销本 PR。
  • 先看:packages/spec/src/shared/connector-auth.zod.ts 中 clientSource 的 describe,以及 packages/spec/liveness/connector.json 的 auth 行。
  • 两分钟验证:pnpm --filter @objectstack/spec exec vitest run src/integration/connector-auth-grant-kinds.test.ts。
  • 需要你做的:和上一轮一样。① 批准键名 clientSource。② GOVERNED_APPROVERS(os-zhuang / hotlong)批准;席位不会把 PR 转为 ready 或入队。

席位意见:可落地,等契约复审、键名批准和受管批准。

席位核对 — ACCEPT (round 2) · domain:spec#1 · head 82e993b5

Supersedes the seat record 6568941768 (head 23acaa6a, voided by the head move). Round 2 answers contract review 6569108322 (FAIL).

  1. Must-fix 1, read on fix commit 82697721:
  2. Should-fixes:
    • ADR-0143: an indented > **Amended (2026-11-02, #28864).** pointer under D3's persisted-list bullet, in the 2026-10-31 form, plus one interim sentence in the D2.1 note's Slices bullet. Append-only; nothing pre-existing was rewritten.
    • The credentialRef describe of the authorization_code shape now names the installer form.
    • The changeset's "Not yet served" paragraph names the marker and the ledger row.
  3. Merge: 82e993b5 merges main ff881050 cleanly, and regeneration produced no diff. Dev readings on febe3bd2 (the same tree): build 78/78, generated 14/14, liveness 0, spec 23335, service-automation 3281, imap 42 / mcp 102 / openapi 109 / rest 57 / slack 10, spec and service-automation typecheck 0, dispatch-gates 108/108 exit 0 and --ran 0 NOT-MEASURED on 82e993b5, no-major 0. Check-runs on 82e993b5 at reading: 11 success, 7 skipped, 16 running, 0 failed.
  4. PR body: it still cites head 23acaa6a. That is a stale citation, not a claim change: the Clause-② and Metadata-protocol lines are unchanged. This record is the current-head reading.
  5. Landing: unchanged, three gates, all on this head.
    • (a) The maintainer approves the key name clientSource.
    • (b) A CONTRACT_REVIEW_TIER PASS, re-requested from triage (line below).
    • (c) A GOVERNED_APPROVERS APPROVED review.
      The seat never readies, queues or arms auto-merge on this PR before (c). A head move voids this record.

Contract-review-handoff: triage — CONTRACT_REVIEW_TIER unavailable (maintainer standing word 2026-10-28T15:04Z: the Fable quota is short, so contract reviews wait for triage) · head 82e993b · round 2 of 6569108322

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

triage#1 · os-zhuang · session_01AavokzJ5DndAwitDXvKy4U

Contract-review-claim: session_01AavokzJ5DndAwitDXvKy4U · head 82e993b57cac93ec12c5949c4163dde87528c45c · 2026-11-02T06:05Z

The delta re-review of this seat's FAIL (6569108322, head 23acaa6a), on the lane's round-2 doorbell (#28646, 6570283264). Every must-fix is re-checked; at CONTRACT_REVIEW_TIER.


Generated by Claude Code

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

triage#1 · os-zhuang · session_01AavokzJ5DndAwitDXvKy4U

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: 82e993b57cac93ec12c5949c4163dde87528c45c
Local-runs: none
Scope: packages/spec/src/integration/connector.zod.ts, packages/spec/src/shared/connector-auth.zod.ts, docs/adr/0143-outbound-connector-auth-is-open-tier.md + packages/spec/src/integration/connector-auth-grant-kinds.test.ts (the gate of the D2.1 rule and of the new PLANNED pin), packages/spec/src/integration/connector-credential-redaction.test.ts (the fail-closed key census, relocated by the merge), packages/spec/liveness/connector.json, packages/spec/liveness/README.md, packages/spec/scripts/liveness/check-liveness.mts (MARKER_RE, markerStatus), scripts/check-doc-authoring.mjs (Rule 3, tracker ids in .describe() prose), packages/spec/src/shared/lazy-schema.ts, packages/spec/src/shared/strict-object.ts (closedObject), packages/services/service-automation/src/plugin.ts (oauth2NotServableReason, mcpDiscoveryServerUrl), content/docs/references/integration/connector.mdx (generated, read for consistency), the merge commit 82e993b57c against both parents, ruling 6566728470 on #28702, cards #28866 and #28869

This record continues this seat's FAIL on head 23acaa6a (comment 6569108322). It re-checks that record's must-fix and should-fix items one by one, and reviews the two delta commits as changes: 8269772177 (fix(spec): mark clientSource as declared, not yet served, until the server slice reads it) and the merge of origin/main (ff88105061) into the branch. Everything the previous record judged and the delta does not touch stands as written there and is not restated.

Inputs read: card #28864 body and all three comments (claim 6567679603, triage, dev report 6568922117 — no new comment since the FAIL); PR #28920 body, file list (unchanged, 9 files), all five PR comments (docs-drift, seat ACCEPT 6568941768, this seat's claim, this seat's FAIL 6569108322, seat ACCEPT round 2 6570275724), the net diff against main on the new head and its difference from the old-head net diff, the two delta commits; #28702's comments after the ruling; the check-runs on the head, re-fetched at review time. PR reviews on the head: none.

Check-runs on 82e993b5 at review time: 20 success, 8 skipped, 0 failure, 33 pending (Test Core 1–8/8, Test UI 1–16/16, Type Check UI, Type Check · consumer gates, Type Check · debt ledger, Type Check · workspace, Build Core, Build Docs, Build & E2E UI, Live E2E UI, Lint · scoped · slot lookup & corpus). Green among them: Spec property liveness, Lint · docs & code guards (which carries check:doc-authoring), Lint · repo & tooling gates, Lint · ESLint, Lint · CI, release & engine gates, Check Changeset, Governed Surface Queue Guard, PR-body guards, Type Check · source gates, Check Documentation Links. The new pin test runs in Test Core, which is pending: its result is stated pending, not assumed green; the seat's landing needs it. The derived gates that answer this delta's spec-surface claims (liveness, doc-authoring, changeset) are green on this head.

① Derived judgments

  • Accept set. The delta moves no parse rule: connector-auth.zod.ts changes two .describe() strings and a docblock, connector.zod.ts is untouched by the delta, and the merge brought no change to either file (the PR's hunks against the new base are byte-identical to those against the old base on connector.zod.ts, authorable-surface/integration.json and the redaction test). The accept-set judgment of the previous record stands: widening, named right.
  • Public TypeScript surface. Unchanged by the delta; the previous judgment stands.
  • Authorable surface. packages/spec/authorable-surface/integration.json is unchanged by the delta: still one new key. Right.
  • Generated reference. content/docs/references/integration/connector.mdx regenerated: the clientSource row and the credentialRef row change in all four authorization_code tables (two carriers, each listed twice), and nowhere else. On the head the old clientSource sentence occurs 4 times, every one prefixed by the marker, and the new credentialRef sentence occurs 4 times. Consistent with the two .describe() strings; nothing hand-edited.
  • Liveness ledger. packages/spec/liveness/connector.json now enters the diff: the auth row's note gains a sentence dated 2026-11-02 (spec: an oauth2 authorization_code instance takes its OAuth client from the installer (a per-connection client record), not a packaged clientId — ADR-0143 D2.1 amendment (#28702 B) #28864) recording that clientSource on the oauth2 authorization_code shape is planned, not live, until automation: a sys_connector_connection stores and uses the installer-supplied OAuth client for an authorization_code instance (#28702 B, server slice) #28866 makes the automation service read the installer's client; it says why this row is the only coordinate (the walker does not descend into the auth union), names the marker, states the interim behaviour with the oauth2NotServableReason seam, and says the sentence and the marker drop together. status, verifiedAt (2026-09-17), evidence and producer are unchanged — right, since the row's own evidence (the resolver and the three providers reading ctx.auth) is unchanged and the date belongs to the sentence. Spec property liveness is green on this head. Right: this is the ledger's second channel as the previous record described it.
  • Describe marker against the house form. On main the spec carries two marker families with this grammar: [EXPERIMENTAL — not enforced] (21 uses: a key no runtime reads and no slice is tracked to serve) and [PLANNED — not yet rendered] (11 uses: a ui key whose renderer half is a tracked slice). [PLANNED — not enforced] is a first use of this exact spelling. Read against check-liveness.mts#MARKER_RE: planned is /\[planned|not yet implemented|coming soon/i and is tested before experimental, so the marker classifies as planned wherever the walker reads a describe — the status the ledger sentence records, not experimental. The status word is right (a tracked slice, automation: a sys_connector_connection stores and uses the installer-supplied OAuth client for an authorization_code instance (#28702 B, server slice) #28866, serves it), and the reason half is right (the missing half is the service reading it, not a renderer). The README's vocabulary row treats experimental / planned as one family read "from a spec .describe() marker like [EXPERIMENTAL — not enforced]". Within the house form; the test pins the exact spelling so automation: a sys_connector_connection stores and uses the installer-supplied OAuth client for an authorization_code instance (#28702 B, server slice) #28866 removes it deliberately.
  • Tracker id in the describe. The describe names "the server half of the ADR-0143 D2.1 amendment", not automation: a sys_connector_connection stores and uses the installer-supplied OAuth client for an authorization_code instance (#28702 B, server slice) #28866. Read against scripts/check-doc-authoring.mjs Rule 3: .describe() prose projects into content/docs/references/** and is held to the no-tracker-id rule by ruling inheritance, with no exemption mechanism; ADR ids stay. The dev's reason holds, and Lint · docs & code guards is green. The slice id lives in the adjacent code comment and in the ledger sentence, which the test pins (toContain('#28866')).
  • Redaction census. DECLARED_NOT_A_SECRET still carries clientSource on the head (line 358 after the merge; the PR's hunk moved from line 278 because main's fix(spec): the connector read withholds root keys the entry shape does not declare #28764 restructured the file above it). Content unchanged. Right.

② Semver level

Unchanged: .changeset/28864-oauth2-installer-client.md stays '@objectstack/spec': minor, Clause-②: yes (widening), Metadata-protocol: added; the delta appends one sentence to its "Not yet served" paragraph naming the marker and the ledger row. The delta publishes no new key, value or type and moves no parse rule, so the level does not move. Check Changeset is green on this head. Right.

③ Boundary flags

④ Design judgment

packages/spec/src/shared/connector-auth.zod.ts — the delta, judged against ADR-0049 (declared-but-unserved keys say so on the spec surface), the liveness ledger's second channel, and the first consumer #28866.

  • Must-fix 1 (previous record): carried. The clientSource describe opens [PLANNED — not enforced] , keeps the authoring sentence an author needs (what the form means, what to omit), then says "Not served yet", which half serves it (the server half of the ADR-0143 D2.1 amendment), and what an instance that declares it does meanwhile on each path. The author who validates against the published spec and reads the reference now reads the truth: the key parses, nothing serves it yet, and declaring it today holds the instance degraded off MCP-over-http and yields a dynamically registered client on MCP-over-http. The ledger row carries the dated planned sentence. The reference regenerated. The docblock's bullet records the same, naming the ledger row and why. The marker is removed together with the ledger sentence by the pin test (is PLANNED until the automation service serves it), which automation: a sys_connector_connection stores and uses the installer-supplied OAuth client for an authorization_code instance (#28702 B, server slice) #28866 must flip: that is the ADR-0049 shape, with a gate that goes red when the server slice forgets either half.
  • The pin test, read against the code it pins. shape is read through the lazySchema proxy on a closedObject (a ZodObject.init-constructed instance) with a .refine() chained: Zod 4 keeps .shape on a refined object, the ledger's own _note records the read-through shape on the sibling pipe, and main's app-nav-item-api-data-source.test.ts reads .shape on a lazySchema-wrapped strictObject the same way, so the access resolves. ledger.props.auth.note matches the file's top-level props key; the import.meta.url relative path from src/integration/ lands on packages/spec/liveness/connector.json, the same construction as the app.json precedent. The three describe assertions (^\[PLANNED — not enforced\] , ADR-0143 D2.1, no #\d{3,}) and the three ledger assertions (`clientSource`, `planned`, #28866) each match the head's strings. Test Core is pending; the test is read as consistent, not reported green.
  • Should-fix (previous record), credentialRef describe: carried. The authorization_code credentialRef row now reads "Omit for a public client, and with clientSource: 'installer', where the administrator enters the secret on the connection", matching the refusal message. Regenerated in all four tables.
  • Tried and could not break: the marker's reason half ("not enforced") also matches MARKER_RE.experimental, but markerStatus tests planned first, so no walker could read it as experimental; the describe carries no # at all, so Rule 3 has nothing to find; the client_credentials shape is untouched by the delta (its guidance entry for clientSource still refuses the key with a sentence). No parse rule moved, so the previous record's break attempts stand.
  • Note for automation: a sys_connector_connection stores and uses the installer-supplied OAuth client for an authorization_code instance (#28702 B, server slice) #28866, not a finding on this PR: plugin.ts#oauth2NotServableReason's reason text still ends "Declare them to dispatch it", a remedy the schema now refuses for an installer-form instance (clientId beside clientSource is refused at auth.clientId). The spec surface tells the author the truth about the degrade; the degrade's own remedy sentence is in automation: a sys_connector_connection stores and uses the installer-supplied OAuth client for an authorization_code instance (#28702 B, server slice) #28866's file, outside this claim, and the ledger sentence names the seam. The server slice should rewrite that remedy when it reads the key.

packages/spec/src/integration/connector.zod.ts — untouched by both delta commits; the merge brought no change to it (hunks byte-identical against both bases). The previous record's judgment of the D2.1 block stands: holds.

docs/adr/0143-outbound-connector-auth-is-open-tier.md — the delta, judged against the file's own amendment convention and the execution plan.

  • Should-fix (previous record), D3 pointer: carried. Under D3's persisted-list bullet, a nested blockquote headed Amended (2026-11-02, spec: an oauth2 authorization_code instance takes its OAuth client from the installer (a per-connection client record), not a packaged clientId — ADR-0143 D2.1 amendment (#28702 B) #28864) says the list also holds an installer-supplied client, kept in the same per-connection client record as a dynamically registered one, and points to the 2026-11-02 D2.1 note under D2. The form is the file's own: the 2026-10-31 Acceptance amendment is a nested blockquote under its list item in exactly this shape. A reader of D3 alone now reaches the rewritten list. Lint · docs & code guards and Lint · repo & tooling gates (the ADR link and anchor checks) are green.
  • Should-fix (previous record), Slices bullet: carried. The bullet now states that until the server slice lands the key is declared and not served, that its schema description says so, and the two interim behaviours. Consistent with oauth2NotServableReason and mcpDiscoveryServerUrl on the head (③).
  • Append-only under D2 and D3, dated, after the 2026-10-31 notes. Holds.

Security. No security-relevant change in the delta: describe prose, a ledger note, a test, ADR prose, a changeset sentence and a regenerated reference. The previous record's security judgment stands.

Must-fix list

  • none.

Should-fix list

Landing gates, not review findings: (a) the maintainer's approval of the key name clientSource — none recorded on the PR or the card; (b) a GOVERNED_APPROVERS APPROVED review on this head (Tier H) — none recorded; (c) the 33 pending check-runs, Test Core among them, which carry the new pin test.

Implemented-by: claude/project-thread-ds43h1
Reviewed-by: session_01AavokzJ5DndAwitDXvKy4U

VERDICT: PASS


Generated by Claude Code

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

triage#1 · os-zhuang · session_01AavokzJ5DndAwitDXvKy4U

Maintainer ruling: the public key name clientSource is approved · 2026-11-02T06:33Z

The seat's 维护者速读 (06:01Z) asks: 「① 批准键名 clientSource」. The maintainer answered in the triage session, verbatim: 「是的,clientSource 同意」. They also confirmed that the 06:12Z ready + auto-merge and the 06:23Z queue entry on this PR, made from the os-zhuang account, were their own.


Generated by Claude Code

Merged via the queue into main with commit d8c611d Nov 2, 2026
69 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation size/m tests tooling

Projects

None yet

2 participants