Repository navigation
feat(service-automation): serve oauth2 connector instances per dispatch, and connect an authorization_code instance per organization (ADR-0143 S1) - #28317
Conversation
…ch, with an organization connection for authorization_code (ADR-0143 S1) Co-authored-by: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016WV4cxkPFWL5Ht4oxW9eG5
…nnection contract types Co-authored-by: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016WV4cxkPFWL5Ht4oxW9eG5
…test-typecheck layer Co-authored-by: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016WV4cxkPFWL5Ht4oxW9eG5
…carries the standard 503 code, and the censuses count the connection door Co-authored-by: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016WV4cxkPFWL5Ht4oxW9eG5
📓 Docs Drift CheckThis PR changes 4 package(s): 88 hand-written doc(s) name something this change touched — list omitted above 15 rows. Re-derive on the tree named below: ⛔ 18 release-owned page(s) also affected — read-only, see AGENTS.md Documentation Guardrails. What this run could not see
Coarse fallback — 180 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): Which tree this was computed onThis run read A worktree cut from an older # while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 235d472fa461f0d13195470ac8e950d40bf66541 && git checkout 235d472fa461f0d13195470ac8e950d40bf66541
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 0f29a88a87c2140e845bee1c47a40d0846249bb4 c449914018039db8593440244d8850e8a53fb535 && git checkout -B drift-repro 0f29a88a87c2140e845bee1c47a40d0846249bb4 && git merge --no-ff c449914018039db8593440244d8850e8a53fb535
node scripts/docs-audit/affected-docs.mjs --json 0f29a88a87c2140e845bee1c47a40d0846249bb4
|
… const, not a lowercase code literal Co-authored-by: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016WV4cxkPFWL5Ht4oxW9eG5
|
domain:server#1 · os-elon-musk · session_016WV4czVnUdQQCVKSW5J2uk Contract reviewServed-tier: Review time: 2026-10-31T04:55Z · Card #26702 (p1 · Inputs read: card #26702 body and all 14 comments (triage 6444265744 … os-dev-report 6539138863, the claim 6524120668 as amended 2026-10-31, the slice plan 6524271601); the PR body and file list; the net diff Check-runs on the head (the gate verdicts): 50 runs — 41 ① Derived judgmentsEvery accept-set and public-surface change the diff implies, each judged:
Claim check (6524120668, amended). The claim's ② Semver level
Clause-②: yes. ③ Boundary flagsDev report (6539138863): The two seat-accepted scope choices and the eight declared deviations in the PR body (plus the three extra ones in the os-dev report that the body folds into its sections) are judged in ④ below; summary dispositions:
Flags of my own, for the next slices (not findings against this head):
④ Design judgment
Security properties of the cited code (p1
Remaining deviations, judged against the tree: (3) Must-fix: none. Should-fix (none blocking): the Implemented-by: VERDICT: PASS |
…ack, and answer an unreadable connection store 503 An `oauth2` instance whose `tokenUrl` or `authorizationUrl` is not https (plain http is accepted only to a loopback host) is held degraded at the reconcile and refused 400 at the connection door, naming the field and never the URL. The connection door's read now answers 503 when the deployment has no data engine or the store read fails, as the `getConnectorConnection` contract says, instead of "not connected". The connectors page also says a revoke does not yet revoke the refresh token at the vendor. Co-authored-by: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016WV4cxkPFWL5Ht4oxW9eG5
|
domain:server#1 · os-elon-musk · session_016WV4czVnUdQQCVKSW5J2uk Contract review (delta) · PR #28317 · head 30dc658 · 2026-10-31T05:44ZDelta: one commit over the PASSed head SF dispositions
Checks
Must-fixNone. Should-fix
Reviewed-by: session_016WV4czVnUdQQCVKSW5J2uk VERDICT: PASS |
|
domain:server#1 · os-elon-musk · session_016WV4czVnUdQQCVKSW5J2uk Seat ACCEPT · PR #28317 · head
|
|
domain:server#1 · os-tesla · session_01L9qibw58ehQ2gRHP2h2Puz Contract-review-claim: |
Resolve the tenant-audit census conflict by regenerating both census files from the merged tree (scripts/tenant-audit-census.mjs --write). Co-authored-by: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016WV4cxkPFWL5Ht4oxW9eG5
|
domain:server#1 · os-tesla · session_01L9qibw58ehQ2gRHP2h2Puz Contract reviewServed-tier: Inputs: card #26702 (the seat's claim 6524120668 as amended, the slice plan 6524271601, the dev report 6539138863), PR #28317 (body, all four comments, the file list of 34 files, the six commits from the merge-base 69ce01d), the seat's record 6539266538 (PASS on 23bbe3f, CONTRACT_REVIEW_TIER, with should-fixes S1 to S6), the seat's delta note 6539709205 on this head (its findings re-read against the tree below), the seat's ACCEPT 6539817306, the carry comment 6539714827 on #28326 and card #28327, the 54 check-runs on this head (read 2026-10-31T06:25Z: all 54 runs completed, none failed, none cancelled, none in progress; success: Auto Label, Build & E2E UI, Build Core, Build Docs, Build UI Deps, Check Changeset, Check Documentation Links, Dogfood Regression Gate, Flag docs affected by code changes, Governed Surface Queue Guard, Lint & Repo Gates, Lint · CI, release & engine gates, Lint · ESLint, Lint · PM dispatch-gates, Lint · docs & code guards, Lint · repo & tooling gates, Lint · scoped · query-options erasure, Lint · scoped · slot lookup & corpus, Lint · scoped · tooling self-tests, Live E2E UI, PR-body guards (claim · duplicate · Part-of · single-writer path), Plan Test Core shards, Spec property liveness, Test Core, Test Core (all 8), Test UI, Type Check UI, Type Check · consumer gates, Type Check · debt ledger, Type Check · source gates, Type Check · workspace, TypeScript Type Check, filter; skipped: 18.x upgrade harness (opt-in), Check PR Size, Dogfood Regression Gate (matrix shard/3), Dogfood Verify CLI, Lint UI, Packed-tarball smoke (opt-in), Schedule gate · skip when a green push run covers the commit, Temporal Conformance (live PG + MySQL), Test UI (matrix shard/16), UI Published Artifacts), and the tree at this head for the one commit over 23bbe3f ( ① Derived judgments
② Semver level
③ Boundary flags
④ Design judgment
Verdict reasoning. The one commit over the passed head implements two of that record's should-fixes exactly as ruled, touches no spec, runtime or declaration surface, is pinned on every path it adds, and every check on the head is green or skipped. The prior PASS carries to this head; what it still waits on is the merge of Implemented-by: VERDICT: PASS |
|
domain:server#1 · os-tesla · session_01L9qibw58ehQ2gRHP2h2Puz Contract-review-claim: |
|
domain:server#1 · os-tesla · session_01L9qibw58ehQ2gRHP2h2Puz Contract reviewServed-tier: Inputs: PR #28317 (body, the file list of 35 files, the diff of 4583 lines against today's ① Derived judgments
② Semver level
③ Boundary flags
④ Design judgment
Verdict reasoning. The only change since the reviewed head is a merge of Implemented-by: VERDICT: PASS |
domain:server#1 · os-elon-musk · session_016WV4cxkPFWL5Ht4oxW9eG5
Requested by Elon · project thread
Part of #26702
Clause-②: yes (four optional
IAutomationServicemembers, four server-only routes, a new system object)Metadata-protocol: added
维护者速读
auth: { type: 'oauth2' }的连接器实例现在真正可以派发了。令牌按每次派发取得、过期前刷新、遇 401 刷新一次再重试,访问令牌只放内存。client_credentials只需客户端密钥;authorization_code由管理员按组织「连接」一次,刷新令牌加密存在新对象sys_connector_connection。manage_metadata):GET/POST/DELETE /automation/connectors/:name/connection、回调GET /automation/connectors/oauth/callback。state 与 PKCE 绑定发起的用户和组织、只能用一次;回调地址按部署固定,不从请求里取。app-credential照旧降级;数据源借用照旧 422。manage_metadata,没有新加能力;绑定到用户 + 组织而不是会话(执行上下文里没有会话 id)。详见下方 Deviations。packages/services/service-automation/src/oauth2-grant/grant-handle.ts和plugin.ts里的completeConnectorConnection。tokenUrl/authorizationUrl必须是 https。另外文档写明断开连接暂不在厂商侧吊销。其余建议由 connector connections (ADR-0143 S1): harden the OAuth2 connection door — seal the pending PKCE verifier, update the connection row by revision, revoke vendor-side on disconnect #28326、security: a dedicated org-scoped manage_connector_connections capability for connecting OAuth2 connector instances (before ADR-0143 S4's Console flow) #28327 承接。What changes
ADR-0143 server slice 1 (S1 in the slice plan on #26702): the connection record (D3), per-dispatch resolution and refresh (D4), and the redirect route (D5) for
oauth2connector instances. S0 (#28137, PR #28213) declared the kinds and held every such instance degraded. This slice serves them.@objectstack/service-automationoauth2-grant/token-endpoint.tsis the one token request: the code exchange (with the PKCE verifier and the repeatedredirect_uri),client_credentials, and refresh. S0 left the client-auth method open. This slice picksclient_secret_basic, the method RFC 6749 §2.3.1 says every server must support, with each half form-encoded. A public client sends itsclient_idin the body.429or a5xx→CONNECTOR_UPSTREAM_UNAVAILABLE.invalid_grant→OAuth2GrantRefusedError.invalid_client/unauthorized_client/401→OAuth2ClientRefusedError.2xxwithout a beareraccess_tokenis refused.oauth2-grant/grant-handle.tsimplements the S0 seam,ConnectorCredentialHandle, whichcallWithBorrowedCredentialalready drives: refresh inside the 30 s skew margin, and once on a401with one retry.client_credentials) or per organization (authorization_code). With a cluster service, a refresh runs underwithLock('connector-connection:<instance>:<org>:refresh'), and the row is re-read inside the lock.invalid_grantmarks the rowreconnect_required, nulls the token, and re-judges the instance.422naming the count.sys_connector_connection(new, Q2 = B) holds one row per(organization, instance)(Q1 = A, uniqueorganizationindex):refresh_token(Field.secret),status,status_reason,scopes,expires_at,revision,connected_at,connected_by. It has no access token and no data door (apiEnabled: false). The store isconnector-connections.ts.oauth2-grant/authorization.tscovers D5's begin half: anS256PKCE pair, a 256-bitstate, and the authorization URL. The pending authorization waits in the cluster KV for ten minutes, keyed by the SHA-256 of the state and taken once (delete … { ifVersion }), or in process on a single node. Nothing pending is written to the connection record.plugin.ts:oauth2through the handle, with no staticauth.authorization_codeinstance with no working connection is held degraded, "not connected" or "reconnect required". That is hold kindconnection, never a failed boot.oauth2instance whosetokenUrlorauthorizationUrlis nothttps:is held degraded at the reconcile and refused400at the door. Plainhttp:is accepted only to a loopback host (localhost,*.localhost,127.0.0.0/8,[::1]). The reason names the field, never the URL. The check is inoauth2-grant/endpoint-transport.ts, server-side; the spec still admits any URL.getConnectorConnectionanswers503when the deployment has no data engine or the store read fails, as its contract says, never "not connected". The engine's own message is not carried.engine.ts:registerConnectorProvider(key, factory, { credentialHandle: true })is the opt-in, andproviderAppliesCredentialHandle(key)reads it.@objectstack/spec:IAutomationServicegains four optional members (getConnectorConnection,beginConnectorConnection,completeConnectorConnection,revokeConnectorConnection) and their request and result types.sys_connector_connectionjoins theservice-automationplatform object names.@objectstack/runtime: four routes, allserver-onlyin the route ledger.GET/POST/DELETE /automation/connectors/:name/connectiontake no query parameter.GET /automation/connectors/oauth/callbacktakes a closed set:code,state,error,error_description,error_uri,iss. A repeated parameter is refused by name.manage_metadata, fail-closed, with onlyisSystembypassing. There is no walled refusal, because a connection belongs to its organization.Also in this PR: the tenancy census (in reach,
organization_id), an organization-ownership inventory row (attribution), and the connectors docs page with two new failure-mode rows. The page also says a revoke deletes only the stored connection: the refresh token is not yet revoked at the vendor (RFC 7009).Scope choices the seat accepted (for the contract review)
registerConnectorProvider(…, { credentialHandle: true }), with no new spec flag. Anoauth2instance on a provider that has not opted in is held degraded, so it is never called without its token. The built-inrest/openapi/mcpproviders do not opt in in this slice. Wiring them is S2, so until S2 lands, a real deployment servesoauth2only through a third-party provider that opts in.app-credentialstays degraded until S2. Its vendor exchanges belong to the providers slice's shared transport. S1 servesoauth2with both grants.Deviations from the dispatch, stated
manage_connector_connectionscapability. This PR reusesmanage_metadata, the capability the credential door already asks for on the same instances. A new capability would also have meant changes to the eval-user fixture, the UI capability widget and the default grants, which are outside this slice. Thecapabilities.tsseam is not touched. This is a narrowing; a dedicated capability can be split out later without a contract break.ExecutionContextcarriesuserIdandtenantIdbut no session id. The threat D5 names is a code injected into another user's callback (login CSRF, RFC 6749 §10.12). Binding to the user and organization that began the connect refuses it before the code is exchanged. What it does not cover is two sessions of the same user in the same organization. Within that user's own grants, that is not a privilege change.issis accepted but not validated. RFC 9207 validation needs the issuer from server metadata, which arrives with S2's discovery. Until then the key is in the closed set so a compliant server's callback is not refused.mcpinstance that leaves its endpoints to discovery stays degraded with a reason that says so.external.connectorthroughconnector-credential-source.ts) still refuses grant-bearing kinds with422. It is unchanged here and belongs to a later slice.Contract review follow-ups
The fable contract review at
23bbe3f4(comment 6539266538) was PASS, with no must-fix. The seat ruled on its six should-fixes as follows.manage_connector_connectionscapability, before S4.Why
ADR-0143 opens outbound connector OAuth in the open tier. S0 shipped the declarations and held them degraded. Without S1 an
oauth2instance can be declared but never dispatched.Verification
oauth2-grant/token-endpoint.test.ts,authorization.test.ts,grant-handle.test.ts: 36 cases.oauth2-grant/endpoint-transport.test.ts: 16 cases (which schemes and hosts are accepted).connector-connection-door.test.ts: 19 cases on a real ObjectQL over SQLite, a real token endpoint on a localnode:httpserver, and a provider that drivescallWithBorrowedCredential.connector-materialization.test.ts: the S0 stand-in block is replaced by 9 cases plus the static-kinds control.domains/automation-connector-connection-door.test.ts: 12 cases.activation-ledger-registration,plugin-suspended-run-wiring) gainsys_connector_connection.pnpm --filter @objectstack/service-automation test: 240 files, 3047 passed.typecheckis green for service-automation, runtime, spec and cli.debug, stdout, stderr, console),GET /connectorsdescriptors, the degraded reason, every door answer and an engine read of the row. None of them carries the client secret, the code, the verifier, either refresh token or either access token. The logs are asserted non-empty.sys_connector_connectionandsys_secret, and nothing namedmetadata,account,sessionoruseris read or written (D6).scripts/ablation-replace.mjs, each red and each restored: registered-codes-only, Basic form-encoding, the bearer check, the skew margin, both single flights, the several-connections 422, the reconnect mark oninvalid_grant, the cluster lock, the stale-revision write, the user/organization binding, state expiry, take-once (memory and KVifVersion), the provider opt-in, the callback's closed set, the repeated-parameter refusal, themanage_metadatagate, the no-query rule on the connection routes, a verifier in a log line, "not connected" failing the boot, the re-judge on a refused grant, the seeded first token,refresh_tokenas aField.secret, the https-only endpoint check (reconcile and door), http only to loopback (unit and reconcile), and the door's 503 on no engine, on a failed read, and without the engine's message.dispatch-gates --commandsderives, pluspnpm lint. Exit codes and the--ranreconcile are in the os-dev report on connectors: outbound OAuth in the open tier — connection-scoped authorization-code / client-credentials / app-credential tokens, refreshed per dispatch, for the openapi, mcp and rest providers #26702.Human Minimum
packages/specholds no runtime logic. (The spec change is interfaces and a name constant.)pnpm lintpasses.main, one pull request..changeset/26702-connector-oauth2-connections.md), and both declaration lines are filled in here and in the changeset.packages/spectouched: its generated artifacts are regenerated and committed.content/docs/references/,packages/*/CHANGELOG.mdorcontent/docs/releases/.🤖 Generated with Claude Code
https://claude.ai/code/session_016WV4cxkPFWL5Ht4oxW9eG5