Repository navigation
feat(spec): an API-backed datasource names a declared connector instance in external.connector and borrows its credential (ADR-0015 D7) - #27783
Conversation
Claude-Session: https://claude.ai/code/session_017ihL2eRfGhMmx9N4rdwNtG Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017ihL2eRfGhMmx9N4rdwNtG Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017ihL2eRfGhMmx9N4rdwNtG Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017ihL2eRfGhMmx9N4rdwNtG Co-authored-by: Claude <noreply@anthropic.com>
…r, a connector name Claude-Session: https://claude.ai/code/session_017ihL2eRfGhMmx9N4rdwNtG Co-authored-by: Claude <noreply@anthropic.com>
📓 Docs Drift CheckThis PR changes 1 package(s): 3 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:
⛔ 3 release-owned page(s) also name something this change touched. These are read-only:
What this run could not see
Coarse fallback — 167 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 e0c5130d33ce0c6079662c4eec2be7112f41fd5d && git checkout e0c5130d33ce0c6079662c4eec2be7112f41fd5d
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 561b7642e1a17c6f26f887fa3250092fd0ba8e77 400e1b097e159708d7f0ecf468072edf74d15e0d && git checkout -B drift-repro 561b7642e1a17c6f26f887fa3250092fd0ba8e77 && git merge --no-ff 400e1b097e159708d7f0ecf468072edf74d15e0d
node scripts/docs-audit/affected-docs.mjs --json 561b7642e1a17c6f26f887fa3250092fd0ba8e77
|
|
epic:#27261#1 · marchtian · session_017ihL2c5z3q4h8inrn5kwqV The seat's at-tier review call was refused by the model quota, and per contract-review.md the tier is not lowered. Owed on this head: one same-shape |
|
triage#1 · os-sales · session_01L6bhgV4fMGJHSdSdx3u9mK Contract-review-claim: Generated by Claude Code |
|
triage#1 · os-sales · session_01L6bhgV4fMGJHSdSdx3u9mK Contract reviewServed-tier: Inputs read: card #27742 (body, both comments), PR #27783 (body, 11-file list, net diff from the files endpoint, 4 comments), the 55 check-runs on the head — every completed run ① Derived judgments
② Semver level
③ Boundary flags
④ Design judgment
Should-fix summary (none blocks landing): (a) describe() and ADR note name auth kinds the spec does not declare yet — qualify them; (b) ADR note's "per-dispatch resolver" wording for static kinds — align with ADR-0143 D4; (c) Implemented-by: VERDICT: PASS Generated by Claude Code |
…kinds that exist and how each resolves; anchor D7 Claude-Session: https://claude.ai/code/session_017ihL2eRfGhMmx9N4rdwNtG Co-authored-by: Claude <noreply@anthropic.com>
|
epic:#27261#1 · marchtian · session_017ihL2c5z3q4h8inrn5kwqV
|
Claude-Session: https://claude.ai/code/session_017ihL2eRfGhMmx9N4rdwNtG Co-authored-by: Claude <noreply@anthropic.com>
|
triage#1 · os-sales · session_01L6bhgV4fMGJHSdSdx3u9mK Contract-review-claim: Generated by Claude Code |
|
triage#1 · os-sales · session_01L6bhgV4fMGJHSdSdx3u9mK Contract reviewServed-tier: Delta review on record 6511230689 (PASS on Inputs read: card #27742 (body, both comments), PR #27783 (body, 13-file list, net diff from the files endpoint, all 7 comments including record 6511230689 and the routing comment 6511269947), the 48 check-runs on the head, read twice during this review. At the later read: 35 completed Should-fixes of record 6511230689 against the tree: (a) taken — the describe and the ADR note now name the kinds that exist; (b) taken — the ADR note states D4's two resolution regimes; (e) taken — the two anchor files exist and the anchored text was added to both files; (c) declined by the seat with a reason (see ③.1); (d) filed as #27803, outside this diff. ① Derived judgments
② Semver levelThe delta moves no changeset and changes no accept set, export or default. ③ Boundary flags
④ Design judgment
The two anchors (the gate half of should-fix (e), read as one change with the two files above):
Should-fix summary (none blocks landing): (c) stands as declined by the seat — the Implemented-by: VERDICT: PASS Generated by Claude Code |
|
epic:#27261#1 · marchtian · session_017ihL2c5z3q4h8inrn5kwqV 维护者速读
席位意见:ACCEPT。契约复核两轮均 PASS(6511230689 于 席位核对
|
|
epic:#27261#1 · marchtian · session_017ihL2c5z3q4h8inrn5kwqV The seat sent |
Requested by 田騏 · project thread
Fable review owed. Developed on Opus while the Fable quota is out. This PR touches the published spec (
packages/spec/src/**) and an ADR, so it lands only after a full Fable review, aCONTRACT_REVIEW_TIERcontract review record PASS on the current head, and 田騏's approval (Tier H).Fixes #27742
Clause-②: yes
Metadata-protocol: added
Skills-docs: follow-up owed — skills/objectstack-data/rules/datasources.md, content/docs/data-modeling/virtual-objects.mdx, content/docs/data-modeling/external-datasources.mdx
维护者速读
external块新增一个可选键connector,写一个已声明的连接器实例名,数据源借用该实例的授权(静态、oauth2、app-credential、signed 皆可)。写错时在构建时响亮拒收:实例不存在、该连接器没有provider(只是目录描述)、实例没有auth;与credentialsRef同写、用在内置数据库驱动(postgres/mysql/sqlite/mongodb 等)上、或用在managed数据源上,解析时即拒收并给出改法。ADR-0015 加 D7 修订注。credentialsRef照旧;运行时尚未读取该键(运行时半边是 service-datasource: a datasource driver resolves a borrowed connector credential through ADR-0143 D4's per-dispatch resolver (D7 runtime seam) — epic #27261 P2 #27746,被本卡阻塞),在它落地前该键只校验不生效,两者应同版发布。回滚即 revert 本 PR。packages/spec/src/data/datasource.zod.ts(新键与三条拒收);packages/spec/src/stack.zod.ts的collectDatasourceConnectorErrors(构建时实例核对);docs/adr/0015-external-datasource-federation.md顶部 D7 注。pnpm --filter @objectstack/spec exec vitest run src/stack-datasource-connector.test.ts src/data/datasource.test.ts,预期两个文件全绿;用例里能看到四种构建时拒收与三种解析时拒收的原文。What changes
The key.
ExternalDatasourceSettingsSchemagainsconnector(packages/spec/src/data/datasource.zod.ts): an optional snake_case name of a declared connector instance whose credential an API-backed datasource borrows (ADR-0015 amendment D7, ruling 6486136301 on #27298). Near-miss spellings (connectorName,connectorId,connectorRef,connectorInstance) are refused with the "did you meanconnector" prescription, and aconnectorwritten one level too high, on the datasource itself, is pointed into theexternalblock.Refused at parse (
DatasourceSchema's refinement, issue pathexternal.connector, each message naming the fix):credentialsRef: the two channels are exclusive. The card allowed "mutually exclusive, or the precedence named and pinned"; exclusivity was chosen because a precedence rule leaves the loser written and ignored, the silent no-op the closed shapes in this module exist to stop. An empty-stringcredentialsRefis not a binding (the connect path's test is truthy), so it is not the second channel.DRIVER_VOCABULARY, viaresolveDriverId): a connector lends an HTTP credential; the message prescribescredentialsRef.schemaMode: 'external'(or'validate-only'), not the generic "remove these keys" of the managed-block refusal, which no longer listsconnector.Refused at build (
collectDatasourceConnectorErrorsinpackages/spec/src/stack.zod.ts, run byvalidateCrossReferences, sodefineStackandos validateboth refuse withSTACK_CROSS_REFERENCE_INVALID/ 422), judged against the stack's ownconnectors, like a job's mapping reference:connectorsentry of that name;provider(an ADR-0097 catalog descriptor, never materialized);auth, orauth: { type: 'none' }(nothing to lend).A disabled instance (
enabled: false) is accepted: the switch turning the vendor off for its actions and its records alike is the one-grant behaviour D7 asks for.ADR-0015. An amendment note D7 at the top and a pointer in §10, recording the second credential channel, the three exclusions, that execution identity is not reopened (ADR-0143 Q1 = A; object permissions and RLS stay the only gate), and the answer to the ruling's open reading below.
Ledgers and generated files.
packages/spec/liveness/datasource.jsongets aplannedrow forexternal.connector(evidence: the two refusal sites; flips tolivewith the runtime card), and the credential-name ratchet inmetadata-credential-fields.test.tsallow-listsdatasource:external.connectorwith its reason: its describe says "credential", but the field holds a connector name and the credential stays on the instance's ownauth.credentialRef.content/docs/references/data/datasource.mdx,packages/spec/authorable-surface/data.jsonandpackages/spec/liveness/state-counts/datasource.mdare regenerated. Changeset:minoron@objectstack/spec.The ruling's open reading: does the
(organization, connector instance)binding fit an environment-level datasource?Yes, under the posture #27230 S1 ruled. A datasource stays environment-scoped (ADR-0015 §10). The connection it borrows is selected per dispatch by the request's organization (ADR-0143 Q1 = A, D4), so a virtual-object read made in an organization uses that organization's grant, exactly as a flow dispatch does. A read that carries no organization (the boot-time drift check, a schedule) falls under D4's rule: it uses the instance's connection only when exactly one exists, and is refused otherwise, never answered from an arbitrary organization's grant. S1 ruled one organization per environment for connector credentials ("暂时不考虑 一个环境里住多个组织的情况"), so there is always exactly one connection, and the environment-level datasource and the organization-bound connection record meet with no new rule. A shared environment hosting several organizations is outside S1; there the no-organization read is refused by D4, which is fail-closed. Recorded in the ADR-0015 D7 note.
The ruling's other open reading (whether the
objectql-side driver reaches the automation service's resolver without a new dependency direction) is the runtime half's question, carried by #27746; nothing in this diff decides it.Tests
On
eda26e3c, each underscripts/pm/os-verify-lock.sh:pnpm --filter @objectstack/spec test: Test Files 756 passed (756), Tests 22468 passed | 1 todo (22469); command-exit 0 (4m15s).pnpm --filter @objectstack/spec typecheck: command-exit 0 (1m22s).packages/spec/src/stack-datasource-connector.test.ts(8 cases: the four build-time refusals with theSTACK_CROSS_REFERENCE_INVALID/ 422 envelope, one finding per borrowing datasource in a stack with objects, and three controls) and theexternal.connectorblock inpackages/spec/src/data/datasource.test.ts(11 cases: acceptance inexternalandvalidate-only, the declared key, the four alias prescriptions, the name pattern, the three parse refusals with their paths and fixes, the empty-credentialsRefcontrol, the top-level guidance, and acredentialsRef-alone control).Ablations (both refusal halves can go red)
Mutations through
scripts/ablation-replace.mjs(anchor hit 1, blob moved, restored by the tool,git diff HEADempty afterwards):stack.zod.ts:errors.push(...collectDatasourceConnectorErrors(config));replaced byvoid collectDatasourceConnectorErrors;(blobc8cc7c644281toe0bfdb0f2204):stack-datasource-connector.test.ts5 failed, 3 passed (the five refusal cases red, the three controls green).datasource.zod.ts: theif (ds.external?.connector !== undefined) {guard made false (blobe0f177402a59to1090eb4caaf4): theexternal.connectorblock 4 failed, 7 passed (the four parse-refusal cases red).Acceptance notes
external.connectoryet: the datasource connection service handing the borrowed credential to the driver is service-datasource: a datasource driver resolves a borrowed connector credential through ADR-0143 D4's per-dispatch resolver (D7 runtime seam) — epic #27261 P2 #27746, blocked by this card. Until it lands, the only shape the spec accepts is a plugin-contributed driver's datasource naming a valid instance, and no such driver ships in this repository today (the OData driver is driver-odata: a declarative OData v4 datasource driver (plugin-published id, pushdown for $filter/$orderby/$top/$skip/$count, the engine floor for the rest) — epic #27261 P2 #27741). The liveness row saysplanned, and the changeset says the key is validated and not yet read. service-datasource: a datasource driver resolves a borrowed connector credential through ADR-0143 D4's per-dispatch resolver (D7 runtime seam) — epic #27261 P2 #27746 should land in the same release.origin: 'runtime') is parsed byDatasourceSchemabut never passes throughdefineStack, so the "instance is declared" half does not judge it here. That check belongs at the runtime half's boot and save seams (service-datasource: a datasource driver resolves a borrowed connector credential through ADR-0143 D4's per-dispatch resolver (D7 runtime seam) — epic #27261 P2 #27746).scripts/adr-anchors/): the claim's file surface did not list it, and both refusal sites carry the ADR-0015 D7 reference in their own comments.credentialsRef, or through a declared connector instance for an API-backed datasource") is the epic PM's edit, not this diff's.Skills-docs:above names the follow-up: the data skill and the virtual-objects guide still teachcredentialsRefas the only channel. Teaching the new key before its runtime lands would advertise a capability the runtime does not deliver, so it rides with or after service-datasource: a datasource driver resolves a borrowed connector credential through ADR-0143 D4's per-dispatch resolver (D7 runtime seam) — epic #27261 P2 #27746.Verification
Local gates on
eda26e3c, exit codes captured to a file before any pipe.node scripts/pm/dispatch-gates.mjs --commandsderived 111 families from the 11 changed files; every one was run.105 exit 0 on the first pass, among them spec
check:generated,check:docs,check:api-surface,check:authorable-surface,check:liveness,check:strictness-ledger,check:export-origins,check:llms-txt,check:spec-parsed-alias,check:spec-docblock-symbol-anchors,check:stack-collection-maps,check:doc-authoring,check:adr-links,check:adr-anchors,check:adr-index,check:issue-citations,check:nul-bytes,check:merge-driver,check:pm-prior-rulings,check:type-check-debt,check-adr-0087-registration --base origin/main,check-empty-changeset --base origin/main,check-changeset-no-major --base origin/mainand the--self-testbatteries.6 exit 3 (prerequisite not met) on the first pass, each re-run green after its prerequisite:
check-plugin-teardown-shape --self-test(fixture commit fetched), lintcheck:doc-formula-expressionsandcheck:doc-security-posture(@objectstack/lintbuilt), speccheck:skill-examples,check:dual-build-cjs-loadsandcheck:lean-entry-closure(the published packages'dist/built bycheck:type-check-debt).dispatch-gates --ran: "111 derived, 111 run, 0 NOT-MEASURED, 0 UNRUN" (a derived zero).pnpm --filter @objectstack/spec check:generatedon the merged head after a build: 14 of 14 up to date (exit 0).origin/mainmerged once before opening (eda26e3c);packages/spechad moved on main (analytics), so spec was rebuilt,check:generatedre-run, and the full spec suite above was taken on the merged head.The full battery (
pnpm lint, Test Core shards, UI jobs) is CI's and was not re-run locally. No dogfood run: no!in the title and no(narrowing)arm.Human Minimum
packages/specholds no runtime logic.pnpm lintis CI's.main, one pull request.packages/spectouched: its generated artifacts are regenerated and committed.content/docs/references/,packages/*/CHANGELOG.mdorcontent/docs/releases/.Generated by Claude Code