Repository navigation
feat(service-datasource): a code-defined datasource reads its credential from OS_DATASOURCE_<NAME>_PASSWORD - #28208
Conversation
…ial from OS_DATASOURCE_<NAME>_PASSWORD A datasource declared in code has no route to its password: the driver contracts refuse it inline, the Setup form cannot edit a code datasource and nothing mints a sys_secret handle for one. Per the maintainer ruling, the connection service now reads a variable derived from the datasource name at the boot's declared connect, for names in the host's code-datasource set, when the host switch OS_DATASOURCE_ENV_CREDENTIALS is on. os serve turns it on by default; every other composition leaves it off. A declared credentialsRef or connector wins, an unset variable's failure names the variable, and two names deriving one variable are refused. The credential refusals and the two password guidance strings end with one shared sentence naming the route, and the docs teach the slot, the switch and who can read the variable. Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WGufziLJNDCTSUH9pVN32p
…nce pages Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WGufziLJNDCTSUH9pVN32p
…env-credential fixture Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WGufziLJNDCTSUH9pVN32p
📓 Docs Drift CheckThis PR changes 3 package(s): 3 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:
⛔ 2 release-owned page(s) also name something this change touched. These are read-only:
What this run could not see
Coarse fallback — 177 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 43a99b08e5a5fc1cb719e64034a3aff7ba2082a8 && git checkout 43a99b08e5a5fc1cb719e64034a3aff7ba2082a8
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 70209b892a9baa5849cd887bf14107a28d668c7f 5eda07f1ece5039367337c329c052efb8091b41f && git checkout -B drift-repro 70209b892a9baa5849cd887bf14107a28d668c7f && git merge --no-ff 5eda07f1ece5039367337c329c052efb8091b41f
node scripts/docs-audit/affected-docs.mjs --json 70209b892a9baa5849cd887bf14107a28d668c7f
|
|
domain:spec#3 · os-tesla · session_01L9qibw58ehQ2gRHP2h2Puz Contract-review-claim: |
|
domain:spec#3 · os-tesla · session_01L9qibw58ehQ2gRHP2h2Puz Contract reviewServed-tier: Inputs: card #27687 (body and all nine comments: triage 6505611552, the claim 6506069472, the measurement report 6506246359, the option amendments 6520085549 and 6520139448, the maintainer's ruling record 6525963988 「选项五」, the claim amendment 6525977981, the build report 6528223897), PR #28208 (body, the one docs-drift comment, the file list of 24 files, the diff of 1395 lines against ① Derived judgments
② Semver level
③ Boundary flags
④ Design judgment
Verdict reasoning. The diff is the maintainer's option 5 with each of its ruled guards in place and pinned, the provenance gate is closed at every connect door in the tree, the switch is on only in the CLI host by a seam already used for Implemented-by: VERDICT: PASS |
|
domain:spec#3 · os-support-ai · session_01WGufzgbJhFoXYsLSe5WSPP 维护者速读 (席位终稿)
席位意见:接受。契约复核在当前 head 上 PASS(6537998014),没有 must-fix。两条 should-fix(S1:非认证类连接失败也会带上"变量未设置"的说明;S2:连接失败后冲突登记未释放)都不动裁决和信任边界,已另立卡 #28283,本 PR 不改。 席位核对 · ACCEPT ·
|
domain:spec#3 · os-support-ai · session_01WGufzgbJhFoXYsLSe5WSPP
Requested by Yibo Wang · project thread
Fixes #27687
Clause-②: yes (widening) —
@objectstack/service-datasourceexportsdatasourceEnvCredentialVar,resolveDatasourceEnvCredentialsSwitch,DATASOURCE_ENV_CREDENTIALS_SWITCHand theDatasourceEnvCredentialSourcetype, andDatasourceConnectionServiceConfiggains the optionalenvCredentials;@objectstack/spec/dataexportsCODE_DEFINED_CREDENTIAL_ROUTE. Nothing an author writes is added or refused, and no existing key changes meaning.Metadata-protocol: none
维护者速读
defineDatasource/ 包内*.datasource.ts)现在在连接时读OS_DATASOURCE_<名字>_PASSWORD,例如warehouse→OS_DATASOURCE_WAREHOUSE_PASSWORD。OS_DATASOURCE_ENV_CREDENTIALS=on时读;os serve/os start/os dev默认开,其他组合(云端、多租户、自建 kernel)默认关;其他取值启动即拒绝。origin);UI/API 创建的同名数据源、default都不读。credentialsRef或connector时它们优先,失败仍然 fail-closed,不回落到环境变量;变量没设时,失败原因会写出该设哪个变量;值不进日志、不进任何应答。os serve下本地安装的包也读这个槽,所以任何已安装的包都能声明任意空闲名字的数据源,从而读到对应变量。文档和 changeset 已写明。password提示共用一句新句子),accept 集合不变;changeset 全部patch(Metadata-protocol: none时门禁要求 patch,见下文)。The ruling
Maintainer ruling on #27687 (option 5, recorded by the seat in comment 6525963988, claim amended in 6525977981): a datasource defined in code reads its credential at connect time from a runtime environment variable whose name the platform derives from the datasource name. The seat's follow-ups: the switch is opt-in in
os serve/os startwith the library default off and no tenancy coupling, the off value is spelledoff, and the install-local route (#28132) keeps reading the slot, with its trust consequence stated and pinned.Measurements on
origin/maind70e94ae(a) The seam where the CLI host turns the switch on. No new public option was needed.
os serveloads@objectstack/service-datasourcedynamically and buildsDatasourceAdminServicePluginitself (packages/cli/src/commands/serve.ts, theadminModblock). The plugin'sinit()readsprocess.env, soservestampsOS_DATASOURCE_ENV_CREDENTIALS=oninto its own environment just before it builds the plugin, when the operator left it unset or empty. That is the same seamos startuses forOS_CRYPTO_AUTOKEY(start.ts:482).os startspawnsserve(start.ts:528) andos devspawnsserve --dev(dev.ts:848), so both inherit the stamp. Every other composition never runs that line and stays off.(b) The shape of
secret.DatasourceConnectionSpec.secret?: string(contracts/datasource-driver-factory.ts) is the cleartext the connection service hands tofactory.create. Its readers indefault-datasource-driver-factory.ts: postgres puts it inpassword(:603,:611), mysql the same (:830,:839), mongodb puts it in theMongoClientauthblock (:945,:1047), and turso reads it ahead ofconfig.authToken. sqlite, sqlite-wasm and memory have no credential slot. The slot fills exactly this value, so it reaches every built-in driver that takes a secret. A plugin driver that resolves its credential per request (OData, throughDATASOURCE_SECRET_RESOLVER_SERVICE) does not read it; the docs say so.(c) Where
CODE_DATASOURCE_NAMES_SERVICEis filled, and howconnect()gets it. Three producers, all in@objectstack/runtime, all throughcontributeCodeDatasourceNames(code-datasource-names.ts:156):app-plugin.ts:626: the boot artifact's declared datasources;default-datasource-plugin.ts:150:default;declared-datasources.ts:160: a package installed into the running kernel (install-local, since fix(cloud-connection): an install-local install connects and records the package's declared datasources #28132).connect()gets it as a callback. The admin plugin passesisCodeDatasource(name), which reads the set throughcodeDatasourceNamesOf(ctx), the same readerservedOriginuses. The set is read at each connect, never cached atinit(), so a name an install adds later is honoured.What changes
DatasourceConnectionService.connect(), after thecredentialsRefand connector steps: when neither is declared, the connect is notasDefault, the slot is configured (switch on), the trigger isdeclared-auto, and the name is in the code set, it readsOS_DATASOURCE_<NAME>_PASSWORD.<NAME>is the name upper-cased with each non-[A-Z0-9]character replaced by_. A non-empty value becomes the build'ssecret.resolveDatasourceEnvCredentialsSwitch(): unset or empty is off;on/offin any letter case; anything else throwsOS_DATASOURCE_ENV_CREDENTIALS must be 'on' or 'off', got '<value>'. …. The admin plugin calls it ininit(), so the boot is refused.os servestamps theondefault (packages/cli/src/utils/datasource-env-credentials-default.ts).planCredentialMigration's code-origin remedy names the row's variable and the switch.CODE_DEFINED_CREDENTIAL_ROUTE, appended to the credential refusals and the twopasswordguidance entries (before/after below).external-datasources.mdx§4 gains "A datasource defined in code: the environment variable". It covers the derivation table, an example, the switch and its default, the code-only rule, precedence, the unset case, collisions, the plugin-driver limit, and a warning callout on who can read the variable (installed packages; why the switch is off on multi-tenant hosts).drivers.mdx: the Multi-Datasource paragraph and the read-only sample now teach the variable instead of asys_secret:handle a code author cannot mint.Deviations from the order, stated
failed-degraded, notfailed-credentials. The order says "keep the existingfailed-credentialsverdict", but the existing verdict for a code datasource connecting with no password is the driver's own connect failure,failed-degraded, so keeping it meansfailed-degraded. Only the reason changes: it gains "— datasource '' is defined in code and reads its credential fromOS_DATASOURCE_<NAME>_PASSWORDat connect, which is not set on this host". If the database accepts the connection without a password, nothing changes. A bound datasource still refuses the boot, and the thrown message names the variable./^[a-z_][a-z0-9_]*$/, so two names that pass the schema can never derive one variable. The guard is defensive: the second connect is refused asfailed-credentialswith both names and the variable, and its variable is never read. On the boot path that refusal is a boot failure whenever the datasource is fail-fast.INLINE_CREDENTIAL_REFUSED,URL_EMBEDDED_CREDENTIAL_REFUSED,URL_CREDENTIAL_QUERY_PARAM_REFUSED,PASSTHROUGH_INLINE_CREDENTIAL_REFUSED, andPASSTHROUGH_NESTED_CREDENTIAL_REFUSED. The fifth prescribes the same "bind it instead" route and would otherwise send a code author to the same unfollowable door. The.describe()onrefusedInlineCredentialKeycarries it too, which is why four generated driver reference pages moved.patchon all three packages, not minor. The seat asked for service-datasource minor and cli minor, butcheck-changeset-no-majorrefuses aminorbesideMetadata-protocol: none("none-> every introduced entrypatch").noneis the measured kind: no key, value or metadata type an author writes changes.Before / after for each string
The shared sentence (
CODE_DEFINED_CREDENTIAL_ROUTE, inpackages/spec/src/data/driver/common.zod.ts):Each string below is unchanged up to its last sentence. The after-text is the before-text plus
+ the sentence above.INLINE_CREDENTIAL_REFUSEDconfig."URL_EMBEDDED_CREDENTIAL_REFUSEDURL_CREDENTIAL_QUERY_PARAM_REFUSEDPASSTHROUGH_INLINE_CREDENTIAL_REFUSED${…}span anywhere inoptionsis itself refused at publish."PASSTHROUGH_NESTED_CREDENTIAL_REFUSED${…}span anywhere inoptionsis itself refused at publish."refusedInlineCredentialKey.describe()config."ExternalDatasourceSettingsSchemaguidancepasswordDatasourceSchemaguidancepasswordEvery one keeps the Setup → Datasources and
external.credentialsRefroutes, and none carries a#NNNN; the test below checks both.planCredentialMigrationcode-origin remedy:external.credentialsRef, or supply it from the runtime environment."OS_DATASOURCE_<NAME>_PASSWORDin the runtime environment, which the datasource reads at connect when the host'sOS_DATASOURCE_ENV_CREDENTIALSswitch is on (the default underos serve/os start), or bind it throughexternal.credentialsRef." The real row's variable is substituted.Guards and their tests (each ablated)
Unit pins are in
packages/services/service-datasource/src/__tests__/datasource-env-credential.test.ts(24 tests). Kernel wiring is inpackages/runtime/src/datasource-env-credential.test.ts(3, a real admin plugin and the real driver factory). The CLI pins are inpackages/cli/src/utils/datasource-env-credentials-default.test.ts(4) and the spec pins inpackages/spec/src/data/driver/code-defined-credential-route.test.ts(16). Every ablation usedscripts/ablation-replace.mjsagainst the committed tree and restored toblob == HEAD.connectDeclared", runtime "switch on: the boot artifact's datasource…"isCodeDatasource→ false, rebuilt dist) → runtime 1 redconnectPackageDatasources(…, installed: true)registerPool/ runtime-admin never reads itdeclared-autocheck) → 2 redoriginorigin: 'code'is not provenance"isCodeDatasource) → 1 redon/offrefuses the boot"credentialsRefwins and stays fail-closedcredentialsRefis the secret", "an unresolvablecredentialsRef… does NOT fall back"!credentialsRef) → 1 reddefaultkeepsOS_DATABASE_URLasDefaultconnect never reads the slot"!opts.asDefault) → 1 reddescribe.eachover 7 refusal families: the example name is read out of the message, the credential is left out of the very artefact that was refused, and the result parses with no secret in itOS_DATASOURCE_WH_PASSWORD) → 1 reddatasourceEnvCredentialVar(name) === variable, and the switch name matches)dist; pinned against the rebuilt distos servestamps before the plugin is builtStale text under governed paths (named, not edited)
skills/objectstack-data/rules/datasources.md:63: "## Credentials — one channel, fail-closed". A code datasource now has the environment channel too.docs/adr/0015-external-datasource-federation.md§10 (:764), "Credentials never appear in metadata artefacts.credentialsRefpoints atservice-secrets…", and the D7 amendment note (:10), "§10 says credentials reach a datasource only throughcredentialsRef. That stays true for a database."docs/adr/0062-external-datasource-runtime.mdD3 (:92): the connection service resolvescredentialsRefthrough theSecretBinderbefore building. The env slot is a third route, after it and the D7 borrow.Verification
All on head
5eda07f1ec(the same tree as the measured4b85a06c79; only the commit trailers were reworded to the model-free pair) (based70e94ae;origin/mainmoved toc48cd2c1meanwhile, and none of its 51 changed files is in this diff, so no merge was made).node scripts/pm/dispatch-gates.mjs --commands: 110 families. All 110 were run and recordedCMD :: exit N, and--ranreconciles to "110 derived, 110 run, 0 NOT-MEASURED, 0 UNRUN". Four of them first exited 3 (prerequisite not met, nothing measured):check-plugin-teardown-shape --self-teston the shallow clone, andcheck:skill-examples,check:dual-build-cjs-loadsandcheck:i18n-coveragebefore a full build. Aftergit fetch --unshallowandpnpm build(115/115 tasks), all four exit 0.check-nul-bytes: OK.@objectstack/spec:typecheck0,test0 (757 files, 22537 tests),test:repo0 (87 files, 1448 tests).check:generated→--fixregenerated exactly the three it proved stale:api-surface/,export-origins/, andcontent/docs/references/data/driver-{mongo,mysql,postgres,turso}.mdx.@objectstack/service-datasource:typecheck0,test0 (66 files, 1093 tests).@objectstack/runtime:typecheck0,test0 (486 files, 7888 passed, 19 skipped).@objectstack/cli:typecheck0;vitest run --project unit0 (354 files, 5100 tests). The new CLI pin is in the unit tier.pnpm --filter "...^@objectstack/service-datasource" typecheck0, covering 27 packages including dogfood, rest, hono, the examples anddownstream-contract. The spec change is additive: one new export and longer strings. Every spec consumer under that filter, and spec's owntest/test:repo, are green.Generated by Claude Code