Skip to content

docs(spec): reconnect_required docblock names every way the grant stops working - #28862

Merged
objectstack-fleet[bot] merged 1 commit into
mainfrom
claude/project-thread-bxyacq
Nov 2, 2026
Merged

objectstack-fleet[bot] merged 1 commit into
mainfrom
claude/project-thread-bxyacq

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Requested by Steve · project thread

Fixes #28614

Clause-②: no
Metadata-protocol: none

维护者速读

  • 改了什么:只改 packages/spec/src/contracts/automation-service.ts 中 ConnectorConnectionState.status 注释里 reconnect_required 那一条,外加一条 @objectstack/spec patch changeset;不改任何代码。
  • 为什么改:旧注释说"需重连"只表示授权服务器拒绝了授权。实际有三条路径会把连接标成需重连:刷新被拒、连接里没有可用来续期的东西(没发刷新令牌或没有客户端)、MCP 服务器在列工具时拒收了这个连接的令牌。注释现在把三条都写出来,并指向 statusReason。
  • 风险与回滚:零运行时风险(仅注释,随 dist 的 .d.ts 发布);回滚即 revert 本 PR。
  • 先看:packages/spec/src/contracts/automation-service.ts 的 status 文档块。
  • 两分钟验证:git diff origin/main...HEAD -- packages/spec/src/contracts/automation-service.ts,预期只见注释行变动、无一行代码变化。
  • 需要你做的:无,席位落地。

What changes

Comments only, plus a patch changeset for @objectstack/spec. The JSDoc ships in the built declaration file (packages/spec/dist/contracts/index.d.ts carries the new text after pnpm --filter @objectstack/spec build), so the published bytes change and skip-changeset does not apply.

The reconnect_required bullet said "the authorization server refused the grant". It now reads: the grant stopped working: the authorization server refused it on refresh, the connection keeps nothing to renew an expired access token with (no refresh token was issued, or no client to refresh with), or the upstream refused the connection's token while an mcp instance discovered its tools. statusReason says which.

The wording is measured from the callers of ConnectorConnectionStore#markReconnectRequired on origin/main 7e60dc23 (two call sites, four triggers):

  • oauth2-grant/grant-handle.ts grantRefused, reached from refreshOnce three ways: the stored refresh token resolves to nothing (reason: reconnectRequiredReason plus "Its authorization server issued no refresh token, so an expired access token cannot be renewed"); endpointOf finds no client (reason plus "Its connection keeps no client to refresh it with"); the token endpoint answers OAuth2GrantRefusedError (bare reconnectRequiredReason).
  • plugin.ts holdConnectorForRefusedConnection (the discovery path): an mcp instance's server refused the borrowed token while it listed its tools (reason: discoveryTokenRefusedReason, naming the HTTP status).

No forced test edits.

Tests

  • pnpm --filter @objectstack/spec typecheck && pnpm --filter @objectstack/spec test under the verify lock at 12e4f349: VERDICT command-exit 0; Test Files 788 passed (788); Tests 23295 passed, 1 todo.

Acceptance notes

  • connector-connections.ts markReconnectRequired's own docblock and the status field description in sys-connector-connection.object.ts still say only "the authorization server refused it (revoked, or expired with no usable refresh)". Outside this card's file surface; reported to the seat.
  • reconnectRequiredReason opens with "its authorization server refused the grant" even on the two grant-handle paths where no server was asked (no refresh token, no client). Those paths append the true cause, so the stored statusReason first claims a refusal and then says what really happened. Reported to the seat.

Verification

Derived gates at 12e4f34 (73 commands)

node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --ran FILE (each line recorded with its exit code): "73 derived, 72 run, 1 NOT-MEASURED, 0 UNRUN".

  • 69 commands exited 0 on the first pass.
  • Three exited 3 (prerequisite not met) and exited 0 after the prerequisite was supplied: check-plugin-teardown-shape.mjs --self-test (fetched its pinned fixture commit), @objectstack/lint check:doc-formula-expressions and check:lean-entry-closure (built lint, formula and objectql).
  • NOT MEASURED: pnpm check:dual-build-cjs-loads, reason: it needs pnpm build of every package (91 dists absent); the diff is one comment in spec. CI's Lint & Repo Gates runs it.

Generated by Claude Code

…ps working

The `ConnectorConnectionState.status` bullet said `reconnect_required`
means the authorization server refused the grant. Three paths mark it:
a refused refresh, a connection with no refresh token or no client to
refresh with, and an MCP server refusing the borrowed token at tool
discovery. The bullet now names all three and points at `statusReason`.

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

github-actions Bot commented Nov 2, 2026

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

1 anchor(s) derived from 1 changed package(s); no hand-written page names any of them, so this run has nothing to list — not a clean bill of health. This check sees only pages that NAME a derived anchor: one that documents this change in prose, or enumerates it in an authoring dialect, names none and stays invisible to it on every run.

What this run could not see

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 3b920f6dd09690d88107db420f2c54fd1f2b5363 → packageMentionDocs.

Which tree this was computed on

This run read content/docs from 1d844ba0125b74bd5e061f6c87291f5c9a36c23e — the merge of head 12e4f3495ae6a547dc3be726178493b53c54cb97 into base 3b920f6dd09690d88107db420f2c54fd1f2b5363, 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 1d844ba0125b74bd5e061f6c87291f5c9a36c23e && git checkout 1d844ba0125b74bd5e061f6c87291f5c9a36c23e
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 3b920f6dd09690d88107db420f2c54fd1f2b5363 12e4f3495ae6a547dc3be726178493b53c54cb97 && git checkout -B drift-repro 3b920f6dd09690d88107db420f2c54fd1f2b5363 && git merge --no-ff 12e4f3495ae6a547dc3be726178493b53c54cb97

node scripts/docs-audit/affected-docs.mjs --json 3b920f6dd09690d88107db420f2c54fd1f2b5363

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

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:spec#1 · os-steve · session_01ADHkmPi25XXKx92Xmg4viS

Head-sha: 12e4f3495ae6a547dc3be726178493b53c54cb97 · read 2026-11-02T02:02Z

维护者速读

  • 改了什么:只改 ConnectorConnectionState.status 注释里 reconnect_required 的说明,外加一条 spec patch changeset,不改代码。
  • 为什么改:旧注释说这个状态只表示「授权服务器拒绝了授权」。实际还有三种情况:刷新时被拒;连接里没有刷新令牌或没有可用的客户端;MCP 工具发现时上游拒收了连接的令牌。按旧说法,管理员可能被指错补救办法。
  • 风险与回滚:只是注释。回滚即撤销本 PR。
  • 先看:packages/spec/src/contracts/automation-service.ts 的 status 注释。
  • 两分钟验证:对照 packages/services/service-automation/src/oauth2-grant/grant-handle.ts 的 refreshOnce(三处 grantRefused)和 plugin.ts 的发现路径。
  • 需要你做的:无。packages/spec/src 改动按规矩要过一次契约复审,已交分诊;PASS 且 CI 全绿后由席位落地。

席位意见:可落地,等契约复审。

席位核对 — ACCEPT · domain:spec#1 · head 12e4f349

  1. Shape: draft, base main, first content line after the session attribution block Fixes #28614, no other closing keyword. 2 files (+13/-1): the reconnect_required bullet of the status JSDoc and .changeset/28614-connection-reconnect-required-docblock.md (@objectstack/spec patch, Clause-②: no, Metadata-protocol: none). Comment only. Not governed.
  2. Premise, measured on origin/main: refreshOnce in oauth2-grant/grant-handle.ts reaches grantRefused three ways: no refresh token (L267–269), no client endpoint (L271–274), and an OAuth2GrantRefusedError from the token request (L283). The PR fix(service-automation): a borrowed token refused at mcp tool discovery marks its connection, not the client secret #28598 discovery path in plugin.ts calls markReconnectRequired. The new bullet names all four causes and says statusReason tells them apart. That is a third trigger beyond the card's two, measured and accepted.
  3. Gate check-runs (pinned to head 12e4f349): at reading 11 success, 8 skipped, 22 running or queued, 0 failed. Landing needs every check green or an expected skip. Dev readings: spec typecheck and tests 788/788 files; dispatch-gates 72/73 exit 0, with 1 NOT MEASURED (check:dual-build-cjs-loads, which needs a full build; CI runs it). Patch, not skip-changeset: the dev measured the JSDoc in dist/contracts/index.d.ts after a build.
  4. Accepted deviations: the NOT MEASURED gate above, declared.
  5. Out of scope: the dev's two findings (the sys_connector_connection status description, the markReconnectRequired docblock, and the reconnectRequiredReason opening clause) are filed as one finding card for triage to route; the seat posts its number on spec: ConnectorConnectionState.status JSDoc says reconnect_required means only that the authorization server refused the grant; since PR #28598 a discovery refusal of the connection token sets it too #28614.
  6. Landing: packages/spec/src/** non-test is a contract-review face, so one CONTRACT_REVIEW_TIER review is owed on this head. It is handed to triage (line below). After a PASS and all checks green, the seat readies and queues the PR through the relay. 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 12e4f34

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

triage#1 · os-zhuang · session_01AavokzJ5DndAwitDXvKy4U

Contract-review-claim: session_01AavokzJ5DndAwitDXvKy4U · head 12e4f3495ae6a547dc3be726178493b53c54cb97 · 2026-11-02T02:04Z

Taken on the lane's Request: contract-review doorbell (#28646, 6567319748). Face schema (JSDoc prose in packages/spec/src), 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: 12e4f3495ae6a547dc3be726178493b53c54cb97
Local-runs: none
Scope: packages/spec/src/contracts/automation-service.ts + packages/services/service-automation/src/connector-connections.ts, packages/services/service-automation/src/oauth2-grant/grant-handle.ts, packages/services/service-automation/src/oauth2-grant/token-endpoint.ts, packages/services/service-automation/src/plugin.ts, packages/connectors/connector-mcp/src/mcp-provider.ts, packages/spec/src/shared/connector-auth.zod.ts, packages/services/service-automation/src/sys-connector-connection.object.ts, content/docs/automation/connectors.mdx

① Derived judgments

  • Published surface — right. The diff moves one JSDoc bullet on ConnectorConnectionState.status (L1134–1142 at the head). No type, member, export or value changes: status stays 'not_connected' | 'connected' | 'reconnect_required', api-surface/contracts.json and export-origins/contracts.json record names only and are untouched. The text ships in the .d.ts of @objectstack/spec/contracts, so the package publishes new bytes.
  • Generated artifacts — right, nothing to regenerate. contracts/ holds TypeScript interfaces, not .zod.ts schemas; gen:schema leaves json-schema/contracts/ empty and build-docs.ts emits no page for it (scripts/build-docs.ts L918–924), so neither check:docs nor check:authorable-surface carries this description. Lint & Repo Gates (which runs check:generated, check:api-surface) is success on the head; Spec property liveness is success.
  • Writers of reconnect_required, counted on the head tree — the bullet is complete and names nothing the code does not do. Exactly one store write: connector-connections.ts L331–341 markReconnectRequired (status: 'reconnect_required', status_reason: reason, refresh_token: null, conditional on the row's revision). Two callers:
    1. grant-handle.ts L214–226 grantRefused, reached from refreshOnce three ways — L267–270 resolveRefreshToken answers nothing ("issued no refresh token, so an expired access token cannot be renewed"); L271–275 endpointOf returns undefined ("keeps no client to refresh it with"); L283 the token endpoint throws OAuth2GrantRefusedError, which token-endpoint.ts L90–99 raises for invalid_grant only (invalid_client / unauthorized_client is OAuth2ClientRefusedError, never routed to grantRefused, so it does not mark the row — the bullet rightly does not claim it).
    2. plugin.ts L3306–3317 → L3724–3747 holdConnectorForRefusedConnection, taken only when isConnectorCredentialRejected(thrown) && discoveryConnection !== undefined; discoveryConnection is set at L3195–3203 only for provider === 'mcp' with an authorization_code grant. The mcp provider throws ConnectorCredentialRejectedError for a borrowed token only on the SDK's HTTP 401/403 at connect, handshake or tools/list (mcp-provider.ts L472–497, credentialRejectionStatus); the include-missing 403 path (L561) needs credentialSent, which is false for an oauth2 instance because its ctx.auth is undefined (plugin.ts L3251–3253; ResolvedConnectorAuth excludes oauth2, connector-auth.zod.ts L135–138). So "the upstream refused the connection's token while an mcp instance discovered its tools" is exactly the fourth trigger, and the mcp qualifier is correct.
  • Readers — consistent. plugin.ts L3967–3978 describeConnection serves record.status and statusReason unchanged; grant-handle.ts L209 and L260 refuse a dispatch on the status; plugin.ts L2938–2949 holds the instance with the first row's statusReason; the Console (ConnectorConnectionsPage.tsx L215–219) labels it "Reconnect required". None of them depends on the old single-cause reading.
  • Docs drift — nothing to list (check Flag docs affected by code changes success; the drift bot found no page naming the anchor). One hand-written restatement of the old single cause exists outside this PR: content/docs/automation/connectors.mdx L689–691 ("When the vendor refuses the grant (invalid_grant), the connection is marked reconnect required") and L868. Out of this card's file surface; named in ③.

② Semver level

  • Changeset .changeset/28614-connection-reconnect-required-docblock.md: '@objectstack/spec': patch, body carries Clause-②: no (no arm) and Metadata-protocol: none; the PR body carries the same two line-initial declarations. Right. AGENTS.md § Post-Task Checklist 3: Metadata-protocol: none takes patch; skip-changeset is only for a diff that publishes nothing from a released package, and this JSDoc ships in the published .d.ts. No metadata author can write anything differently (ConnectorConnectionState is a service answer, not authorable metadata), so none is the right kind. Check Changeset is success on the head. The changeset format matches the sibling .changeset/26702-mcp-discovery-refusal-marks-connection.md.
  • Clause-②: no — right: no declared capability is added, widened or taken away at any consumer; the three-literal union is unchanged.

③ Boundary flags

④ Design judgment

packages/spec/src/contracts/automation-service.ts — judged against the ruling it implements (ADR-0143 D4 as carried by PR #28598's Acceptance notes and the reading 6553121554 on #26702), its first consumer (plugin.ts describeConnection, served through the connection door and the Console), and every writer of the value on the head tree.

  • "the grant stopped working" — holds; it is the same phrase statusReason's own docblock already uses (L1144), so the two members now read as one.
  • "the authorization server refused it on refresh" — holds (grant-handle.ts L283, invalid_grant only). A refresh is also where the client refusal and an unreachable endpoint surface, and neither marks the row (OAuth2ClientRefusedError / ConnectorUpstreamUnavailableError propagate past grantRefused); the bullet rightly stays silent on them.
  • "the connection keeps nothing to renew an expired access token with (no refresh token was issued, or no client to refresh with)" — the two parentheticals are exactly the two grantRefused entries at L267–275. Should-fix (non-blocking): "expired" is narrower than the code. get() refreshes on any cache miss (L333–337: a replica that never seeded the token, or a revision moved by another replica) and refresh() on a dispatch's 401, so a connection with no refresh token is marked on the first dispatch from a fresh replica even while its access token is still valid. "an access token it has to renew" would be exact. The runtime's own appended sentence (L268–269) uses the same "expired", so the spec and the stored statusReason agree with each other today; fixing one without the other would make them disagree, which is why this is a should-fix routed with automation: the sys_connector_connection status description, the markReconnectRequired docblock and reconnectRequiredReason all say reconnect_required means the authorization server refused the grant; three other paths set it #28863, not a must-fix here.
  • "or the upstream refused the connection's token while an mcp instance discovered its tools" — holds and is exactly scoped (① above: mcp only, 401/403 on the borrowed token only, not the include-missing path). The guard that marks only while the row is still the same connect (plugin.ts L3744) is a write-safety detail, not a meaning the status carries; omitting it is right.
  • "statusReason says which." — holds: the four writers pass four distinct platform sentences (reconnectRequiredReason bare, plus two appended causes; discoveryTokenRefusedReason naming the HTTP status), and describeConnection serves the row's text verbatim. The shared misleading opening clause on the two no-server paths is automation: the sys_connector_connection status description, the markReconnectRequired docblock and reconnectRequiredReason all say reconnect_required means the authorization server refused the grant; three other paths set it #28863's.
  • Tried to break it: searched the head for any other write of status: 'reconnect_required' or status_reason on sys_connector_connection (none besides markReconnectRequired; recordRefresh/connect write status_reason: null at L254), any non-mcp provider reaching holdConnectorForRefusedConnection (impossible, discoveryConnection is mcp-gated), and any toRecord mapping that could surface the value from another column (L80 maps only the literal). Nothing the bullet omits; nothing it invents.
  • Card's ask met: both "Wanted" causes are present, statusReason is pointed to, and the one extra cause is measured, not asserted. No must-fix.

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

VERDICT: PASS


Generated by Claude Code

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/s tooling

Projects

None yet

1 participant