Skip to content

feat(service-datasource): a code-defined datasource reads its credential from OS_DATASOURCE_<NAME>_PASSWORD - #28208

Merged
objectstack-fleet[bot] merged 3 commits into
mainfrom
claude/project-thread-pu3xwn
Oct 31, 2026
Merged

objectstack-fleet[bot] merged 3 commits into
mainfrom
claude/project-thread-pu3xwn

Conversation

@objectstack-fleet

@objectstack-fleet objectstack-fleet Bot commented Oct 30, 2026 •

Copy link
Copy Markdown
Contributor

domain:spec#3 · os-support-ai · session_01WGufzgbJhFoXYsLSe5WSPP

Requested by Yibo Wang · project thread

Fixes #27687

Clause-②: yes (widening) — @objectstack/service-datasource exports datasourceEnvCredentialVar, resolveDatasourceEnvCredentialsSwitch, DATASOURCE_ENV_CREDENTIALS_SWITCH and the DatasourceEnvCredentialSource type, and DatasourceConnectionServiceConfig gains the optional envCredentials; @objectstack/spec/data exports CODE_DEFINED_CREDENTIAL_ROUTE. Nothing an author writes is added or refused, and no existing key changes meaning.
Metadata-protocol: none

维护者速读

  1. 代码里声明的数据源(defineDatasource / 包内 *.datasource.ts)现在在连接时读 OS_DATASOURCE_<名字>_PASSWORD,例如 warehouse → OS_DATASOURCE_WAREHOUSE_PASSWORD。
  2. 只在主机开关 OS_DATASOURCE_ENV_CREDENTIALS=on 时读;os serve / os start / os dev 默认开,其他组合(云端、多租户、自建 kernel)默认关;其他取值启动即拒绝。
  3. 只读代码数据源(按来源集合判定,从不看记录的 origin);UI/API 创建的同名数据源、default 都不读。
  4. 声明了 credentialsRef 或 connector 时它们优先,失败仍然 fail-closed,不回落到环境变量;变量没设时,失败原因会写出该设哪个变量;值不进日志、不进任何应答。
  5. 信任后果:os serve 下本地安装的包也读这个槽,所以任何已安装的包都能声明任意空闲名字的数据源,从而读到对应变量。文档和 changeset 已写明。
  6. 规范层只改了拒绝文本(5 条凭据拒绝 + 2 条 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 start with the library default off and no tenancy coupling, the off value is spelled off, and the install-local route (#28132) keeps reading the slot, with its trust consequence stated and pinned.

Measurements on origin/main d70e94ae

(a) The seam where the CLI host turns the switch on. No new public option was needed. os serve loads @objectstack/service-datasource dynamically and builds DatasourceAdminServicePlugin itself (packages/cli/src/commands/serve.ts, the adminMod block). The plugin's init() reads process.env, so serve stamps OS_DATASOURCE_ENV_CREDENTIALS=on into its own environment just before it builds the plugin, when the operator left it unset or empty. That is the same seam os start uses for OS_CRYPTO_AUTOKEY (start.ts:482). os start spawns serve (start.ts:528) and os dev spawns serve --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 to factory.create. Its readers in default-datasource-driver-factory.ts: postgres puts it in password (:603, :611), mysql the same (:830, :839), mongodb puts it in the MongoClient auth block (:945, :1047), and turso reads it ahead of config.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, through DATASOURCE_SECRET_RESOLVER_SERVICE) does not read it; the docs say so.

(c) Where CODE_DATASOURCE_NAMES_SERVICE is filled, and how connect() gets it. Three producers, all in @objectstack/runtime, all through contributeCodeDatasourceNames (code-datasource-names.ts:156):

connect() gets it as a callback. The admin plugin passes isCodeDatasource(name), which reads the set through codeDatasourceNamesOf(ctx), the same reader servedOrigin uses. The set is read at each connect, never cached at init(), so a name an install adds later is honoured.

What changes

  • DatasourceConnectionService.connect(), after the credentialsRef and connector steps: when neither is declared, the connect is not asDefault, the slot is configured (switch on), the trigger is declared-auto, and the name is in the code set, it reads OS_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's secret.
  • resolveDatasourceEnvCredentialsSwitch(): unset or empty is off; on / off in any letter case; anything else throws OS_DATASOURCE_ENV_CREDENTIALS must be 'on' or 'off', got '<value>'. …. The admin plugin calls it in init(), so the boot is refused.
  • os serve stamps the on default (packages/cli/src/utils/datasource-env-credentials-default.ts).
  • planCredentialMigration's code-origin remedy names the row's variable and the switch.
  • Spec prescriptions: one shared sentence, CODE_DEFINED_CREDENTIAL_ROUTE, appended to the credential refusals and the two password guidance entries (before/after below).
  • Docs:
    • 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 a sys_secret: handle a code author cannot mint.

Deviations from the order, stated

  • Unset variable: the verdict stays failed-degraded, not failed-credentials. The order says "keep the existing failed-credentials verdict", but the existing verdict for a code datasource connecting with no password is the driver's own connect failure, failed-degraded, so keeping it means failed-degraded. Only the reason changes: it gains "— datasource '' is defined in code and reads its credential from OS_DATASOURCE_<NAME>_PASSWORD at 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.
  • Collision refused at connect, not "at boot" as a separate pass. Datasource names must match /^[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 as failed-credentials with 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.
  • Five refusals carry the sentence, not four: INLINE_CREDENTIAL_REFUSED, URL_EMBEDDED_CREDENTIAL_REFUSED, URL_CREDENTIAL_QUERY_PARAM_REFUSED, PASSTHROUGH_INLINE_CREDENTIAL_REFUSED, and PASSTHROUGH_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() on refusedInlineCredentialKey carries it too, which is why four generated driver reference pages moved.
  • Changeset levels are patch on all three packages, not minor. The seat asked for service-datasource minor and cli minor, but check-changeset-no-major refuses a minor beside Metadata-protocol: none ("none -> every introduced entry patch"). none is 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, in packages/spec/src/data/driver/common.zod.ts):

A datasource defined in code (defineDatasource) leaves the credential out of the declaration: it is read at connect time from the runtime environment variable derived from the datasource name (OS_DATASOURCE_WAREHOUSE_PASSWORD for a datasource named warehouse), which os serve / os start honour by default (OS_DATASOURCE_ENV_CREDENTIALS=on).

Each string below is unchanged up to its last sentence. The after-text is the before-text plus + the sentence above.

String Before ends with After
INLINE_CREDENTIAL_REFUSED "…always wins over anything embedded in config." + the sentence
URL_EMBEDDED_CREDENTIAL_REFUSED "…do not pass through this publish door and are unaffected." + the sentence
URL_CREDENTIAL_QUERY_PARAM_REFUSED "…do not pass through this publish door and are unaffected." + the sentence
PASSTHROUGH_INLINE_CREDENTIAL_REFUSED "…a ${…} span anywhere in options is itself refused at publish." + the sentence
PASSTHROUGH_NESTED_CREDENTIAL_REFUSED "…a ${…} span anywhere in options is itself refused at publish." + the sentence
refusedInlineCredentialKey .describe() "…never stored in config." + the sentence
ExternalDatasourceSettingsSchema guidance password "…it never dereferences a hand-written path." + the sentence
DatasourceSchema guidance password "…a managed datasource included." + the sentence

Every one keeps the Setup → Datasources and external.credentialsRef routes, and none carries a #NNNN; the test below checks both. planCredentialMigration code-origin remedy:

  • Before: "Move the credential in the source that defines this datasource: bind it through external.credentialsRef, or supply it from the runtime environment."
  • After: "Move the credential out of the source that defines this datasource: leave it out of the declaration and set OS_DATASOURCE_<NAME>_PASSWORD in the runtime environment, which the datasource reads at connect when the host's OS_DATASOURCE_ENV_CREDENTIALS switch is on (the default under os serve / os start), or bind it through external.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 in packages/runtime/src/datasource-env-credential.test.ts (3, a real admin plugin and the real driver factory). The CLI pins are in packages/cli/src/utils/datasource-env-credentials-default.test.ts (4) and the spec pins in packages/spec/src/data/driver/code-defined-credential-route.test.ts (16). Every ablation used scripts/ablation-replace.mjs against the committed tree and restored to blob == HEAD.

Guard Test Ablation → red
A code datasource connects with the variable set "a code datasource connects with the variable set", "through connectDeclared", runtime "switch on: the boot artifact's datasource…" A8 (switch resolver returns off) → 13 red; A10 (admin wiring isCodeDatasource → false, rebuilt dist) → runtime 1 red
Install-local reads the slot (trust consequence) "a name the host registers after construction (an install-local package)", runtime connectPackageDatasources(…, installed: true) A10 → runtime red
registerPool / runtime-admin never reads it "the same name created through the admin door…" A2 (drop the declared-auto check) → 2 red
Provenance, never origin "a caller-set origin: 'code' is not provenance" A3 (drop isCodeDatasource) → 1 red
Switch off: no read "a host that did not opt in", runtime "switch unset" A10 (runtime); unit: no-slot case reads nothing
Switch refuses other values "refuses %j…" ×5, runtime "a value other than on / off refuses the boot" A8 → 5 red
A declared credentialsRef wins and stays fail-closed "a resolving credentialsRef is the secret", "an unresolvable credentialsRef… does NOT fall back" A4 (drop !credentialsRef) → 1 red
default keeps OS_DATABASE_URL "an asDefault connect never reads the slot" A11 (drop !opts.asDefault) → 1 red
Unset variable: the reason names it "the database refusing the login answers the variable to set", "an EMPTY variable is unset" A6 (drop the reason suffix) → 2 red
Collision refused "the second name is refused with both names and the variable" A5 (disable the owner check) → 1 red
The value in no log line or answer "no log line, connect answer or connection state carries it" A7 (log the value) → 1 red
Every rewritten refusal parses its own example back spec describe.each over 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 it A1 (drop the sentence from the nested refusal) → 2 red; A12 (example variable drifts to OS_DATASOURCE_WH_PASSWORD) → 1 red
The example is the variable the runtime derives service "is the variable the spec refusals' worked example names" (datasourceEnvCredentialVar(name) === variable, and the switch name matches) reads spec through dist; pinned against the rebuilt dist
os serve stamps before the plugin is built cli "stamps once…", "the stamp precedes the plugin construction" A9 (remove the stamp) → 2 red

Stale 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. credentialsRef points at service-secrets…", and the D7 amendment note (:10), "§10 says credentials reach a datasource only through credentialsRef. That stays true for a database."
  • docs/adr/0062-external-datasource-runtime.md D3 (:92): the connection service resolves credentialsRef through the SecretBinder before building. The env slot is a third route, after it and the D7 borrow.

Verification

All on head 5eda07f1ec (the same tree as the measured 4b85a06c79; only the commit trailers were reworded to the model-free pair) (base d70e94ae; origin/main moved to c48cd2c1 meanwhile, 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 recorded CMD :: exit N, and --ran reconciles 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-test on the shallow clone, and check:skill-examples, check:dual-build-cjs-loads and check:i18n-coverage before a full build. After git fetch --unshallow and pnpm build (115/115 tasks), all four exit 0. check-nul-bytes: OK.
  • @objectstack/spec: typecheck 0, test 0 (757 files, 22537 tests), test:repo 0 (87 files, 1448 tests). check:generated → --fix regenerated exactly the three it proved stale: api-surface/, export-origins/, and content/docs/references/data/driver-{mongo,mysql,postgres,turso}.mdx.
  • @objectstack/service-datasource: typecheck 0, test 0 (66 files, 1093 tests).
  • @objectstack/runtime: typecheck 0, test 0 (486 files, 7888 passed, 19 skipped).
  • @objectstack/cli: typecheck 0; vitest run --project unit 0 (354 files, 5100 tests). The new CLI pin is in the unit tier.
  • Workspace typecheck over the dependents: pnpm --filter "...^@objectstack/service-datasource" typecheck 0, covering 27 packages including dogfood, rest, hono, the examples and downstream-contract. The spec change is additive: one new export and longer strings. Every spec consumer under that filter, and spec's own test/test:repo, are green.

Generated by Claude Code

…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
…env-credential fixture

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WGufziLJNDCTSUH9pVN32p
@github-actions github-actions Bot added size/xl documentation Improvements or additions to documentation protocol:data tests tooling labels Oct 30, 2026
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 3 package(s): @objectstack/cli, @objectstack/service-datasource, @objectstack/spec, touching 24 documentable anchor(s). ⚠️ 3 changed file(s) yielded no anchor (packages/services/service-datasource/src/index.ts, packages/spec/api-surface/data.json, packages/spec/export-origins/data.json), so the pages documenting them are NOT COVERED by this run — this is not a clean bill of health for those files.

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

  • content/docs/data-modeling/drivers.mdx (via OS_DATASOURCE_ENV_CREDENTIALS (literal, a string literal in DATASOURCE_ENV_CREDENTIALS_SWITCH))
  • content/docs/data-modeling/external-datasources.mdx (via DatasourceAdminServicePlugin (symbol, a top-level class), OS_DATASOURCE_ENV_CREDENTIALS (literal, a string literal in DATASOURCE_ENV_CREDENTIALS_SWITCH))
  • content/docs/data-modeling/odata-datasources.mdx (via DatasourceAdminServicePlugin (symbol, a top-level class))

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

  • content/docs/releases/v17/17-0.mdx (via DatasourceConnectionService (symbol, a top-level class), DatasourceSchema (symbol, a top-level const))
  • content/docs/releases/v17/17-6.mdx (via DatasourceSchema (symbol, a top-level const))

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
  • 3 changed file(s) yielded no anchor (packages/services/service-datasource/src/index.ts, packages/spec/api-surface/data.json, packages/spec/export-origins/data.json) — pages documenting those are invisible to this run
  • 1 anchor(s) matched too much of the corpus to be a work list: os serve (command, 44 pages)
  • 3 name(s) were too generic to anchor anything (single lowercase words)
  • the SDK route bridge reached 58 of 210 client-bound route-ledger rows — the other 152 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 152: 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; 97 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 — 177 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 70209b892a9baa5849cd887bf14107a28d668c7f → packageMentionDocs.

Which tree this was computed on

This run read content/docs from 43a99b08e5a5fc1cb719e64034a3aff7ba2082a8 — the merge of head 5eda07f1ece5039367337c329c052efb8091b41f into base 70209b892a9baa5849cd887bf14107a28d668c7f, 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 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

⚠️ 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 70209b892a9baa5849cd887bf14107a28d668c7f → 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#3 · os-tesla · session_01L9qibw58ehQ2gRHP2h2Puz

Contract-review-claim: session_01L9qibw58ehQ2gRHP2h2Puz · head 5eda07f1ece5039367337c329c052efb8091b41f · 2026-10-31T02:42Z

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:spec#3 · os-tesla · session_01L9qibw58ehQ2gRHP2h2Puz

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: 5eda07f1ece5039367337c329c052efb8091b41f
Local-runs: none
Scope: packages/services/service-datasource/src/datasource-env-credential.ts, datasource-connection-service.ts, datasource-admin-plugin.ts, datasource-credential-migration.ts, index.ts and the two tests beside; packages/runtime/src/datasource-env-credential.test.ts; packages/cli/src/commands/serve.ts and packages/cli/src/utils/datasource-env-credentials-default.ts with its test; packages/spec/src/data/driver/common.zod.ts, datasource.zod.ts, datasource.test.ts, driver/code-defined-credential-route.test.ts, api-surface/data.json, export-origins/data.json; content/docs/data-modeling/drivers.mdx, external-datasources.mdx, the four generated driver reference pages; the changeset + packages/runtime/src/declared-datasources.ts, app-plugin.ts, packages/services/service-datasource/src/datasource-admin-service.ts, packages/cli/src/commands/start.ts, dev.ts, packages/verify/src/harness.ts, docs/adr/0015 and 0062

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 main read whole, the three commits from merge-base d70e94a), card #28132 for the install-local route it names, the 59 check-runs on this head (read 2026-10-31T02:47Z: every run completed; success on the latest run of every name: 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, 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, Auto Label, Check PR Size, Dogfood Regression Gate matrix shard, Dogfood Verify CLI, Lint UI, Packed-tarball smoke, Schedule gate, Temporal Conformance, Test UI matrix shard, UI Published Artifacts; one earlier run of Check Changeset, job 124191810400, was cancelled at 2026-10-30T16:49Z and superseded by run 41093206144 of the same job on the same head, success at 2026-10-30T16:51Z; nothing red, nothing in progress), and the tree at this head for every claim the body makes about wiring. The head is clean against today's main (6285203): merge-tree clean, and none of the files main moved since d70e94a is in this diff.

① Derived judgments

  • Accept-set / published surface: the card's ruling, exactly - right. Ruling record 6525963988 (option 5): a code-defined datasource reads its credential at connect from a variable derived from its name, OS_DATASOURCE_ + NAME + _PASSWORD; honoured only for a code-origin datasource by the host's provenance set plus the declared-auto trigger, never record.origin; behind the host switch OS_DATASOURCE_ENV_CREDENTIALS, on in the CLI host and off elsewhere; a declared external.credentialsRef keeps precedence; the value reaches no metadata, log or answer. Each is in the diff: datasourceEnvCredentialVar (upper-case, every character outside A-Z0-9 to _), envCredentialVarFor (slot configured and context.trigger === 'declared-auto' and slot.isCodeDatasource(name)), the admin plugin's init() wiring (resolveDatasourceEnvCredentialsSwitch() once, isCodeDatasource through codeDatasourceNamesOf(ctx) at each connect), the slot placed after the credentialsRef and connector steps under !credentialsRef && connector === undefined && !opts.asDefault, and the no-leak pin.
  • The provenance guard is closed at this head - right, and the one door I looked for is shut. registerPool and reregisterPool (datasource-admin-plugin.ts:597 and :621) are the only runtime-admin connects, and the admin service refuses a code-defined name on create (:680), edit (:729) and remove (:904), so no code datasource ever reaches them; connectDeclared (datasource-connection-service.ts:421) is the only declared-auto producer, used by the boot (app-plugin.ts:938) and by connectPackageDatasources for an installed package, which contributes the package's names to the provenance set (declared-datasources.ts:160) before it calls connectDeclared (:204). The runtime test pins both reads through the real admin plugin and the real driver factory.
  • The switch's seam is the one the body measured - right. serve.ts stamps on into process.env only when the switch is unset or empty, reading the switch's name from the dynamically imported module (adminMod.DATASOURCE_ENV_CREDENTIALS_SWITCH), before new DatasourceAdminServicePlugin( at :5338; start.ts:528 and dev.ts:848 spawn serve, so both inherit it; the only other constructors are the verify harness (harness.ts:1078) and a CLI nav script, neither of which stamps, so they stay off. An older @objectstack/service-datasource without the export leaves the host off rather than throwing.
  • Nothing accepted before is refused; nothing refused before is accepted - right. The spec change is one exported sentence appended to five refusal prescriptions, the .describe() of refusedInlineCredentialKey and the two password guidance entries; no key, value or default moves, and the spec test walks each refusal and parses the credential-free artefact it prescribes. check:generated output (api-surface, export-origins, four driver reference pages) matches the source.
  • Check-runs as gate verdicts - read as above. The cancelled Check Changeset job is a concurrency cancellation superseded by a later green run of the same job on the same sha, not a failed verdict; every other name has exactly one completed run, green or skipped.

② Semver level

③ Boundary flags

  • No name approval owed. The variable pattern and the _PASSWORD suffix are the maintainer's ruling (6525963988); the switch name OS_DATASOURCE_ENV_CREDENTIALS and its on / off spelling are in the claim amendment 6525977981 under that ruling.
  • Governed text is stale by ruling, and an amendment row is owed on a card, not here. The body names them and edits none: ADR-0062 D3 (line 92) says the connection service resolves external.credentialsRef through the SecretBinder before building the driver; ADR-0015 §10 (line 764) says credentials reach a datasource only through credentialsRef, and its D7 note (line 10) repeats it; skills/objectstack-data/rules/datasources.md:63 says "one channel, fail-closed". After this lands there are two channels for a code datasource. A docs/adr/** amendment (Tier H, the maintainer's hand) and the skills row are owed as a follow-up card citing datasource: the credential refusals send a code author (defineDatasource) to a Setup form that cannot edit a code-defined datasource; name a binding path a code author can follow #27687 and the ruling; the seat should file it when this lands.
  • Trust consequence, stated and pinned - the install-local route (fix(cloud-connection): an install-local install connects and records the package's declared datasources #28132) reads the slot, so any installed package can declare a free name and read that name's variable under os serve; the docs callout and the runtime test say so, and the ruling accepted it by choosing option 5 alone (option 3's bind door stays unruled).
  • Cross-domain docs - content/docs/data-modeling/*.mdx ride under the exception triage 6505611552 declared.

④ Design judgment

  • S1 - should-fix: the "not set on this host" suffix rides on every thrown connect error, not only an authentication refusal. In the catch at datasource-connection-service.ts (the failed-degraded branch), envCredentialVar is set whenever the slot applied and the variable was empty, so a host-unreachable error, a missing driver module (ERR_MODULE_NOT_FOUND) or a TLS failure on a code datasource with no variable set gains "datasource 'x' is defined in code and reads its credential from OS_DATASOURCE_X_PASSWORD at connect, which is not set on this host". The sentence stays true, but an operator chasing a network fault is sent to set a password first. Prefix it as a note ("note: ...") or attach it only when the driver's error reads as an authentication refusal; the pin "the database refusing the login answers the variable to set" keeps passing either way.
  • S2 - should-fix: a collision owner is recorded before the connect succeeds and is released only by disconnect. envCredentialOwners.set(variable, name) runs before the read and the build; a connect that then fails failed-degraded keeps the entry, and handleFailure does not clear it, so a later connect of a different name deriving the same variable is refused as a collision against a datasource that never connected. Only names outside the schema's [a-z_][a-z0-9_]* can collide, so this is a corner of the defensive guard, but the guard should release the owner on the failure path (or set the owner after the build succeeds).
  • Nothing else. The read callback reads process.env at each connect, so a variable set after boot is honoured on the next connect; asDefault keeps default on OS_DATABASE_URL; an empty variable reads as unset; a resolving credentialsRef never consults the slot and a failing one stays fail-closed; the derivation and the sentence's worked example are pinned to each other across packages; testConnection on the Setup form builds a throwaway driver without the slot, which is right because the form cannot address a code datasource.

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 OS_CRYPTO_AUTOKEY, the spec change is prose on refusals with the accept set unchanged, semver matches the lane's precedent, and every check on the head is green or skipped with the one cancelled job superseded by a green run. The two should-fixes are an over-broad reason suffix and a corner of the collision guard; neither moves the ruling or the trust boundary. The stale ADR and skills text is named for a follow-up card.

Implemented-by: claude/project-thread-pu3xwn
Reviewed-by: session_01L9qibw58ehQ2gRHP2h2Puz

VERDICT: PASS

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:spec#3 · os-support-ai · session_01WGufzgbJhFoXYsLSe5WSPP

维护者速读 (席位终稿)

  1. 代码里声明的数据源(defineDatasource / 包内 *.datasource.ts)现在在连接时读 OS_DATASOURCE_<名字>_PASSWORD,例如 warehouse → OS_DATASOURCE_WAREHOUSE_PASSWORD。
  2. 只在主机开关 OS_DATASOURCE_ENV_CREDENTIALS=on 时读;os serve / os start / os dev 默认开,其他组合(云端、多租户、自建 kernel)默认关;其他取值启动即拒绝。
  3. 只读代码数据源(按来源集合判定,从不看记录的 origin);UI/API 创建的同名数据源、default 都不读。
  4. 声明了 credentialsRef 或 connector 时它们优先,失败仍然 fail-closed,不回落到环境变量;变量没设时,失败原因会写出该设哪个变量;值不进日志、不进任何应答。
  5. 信任后果:os serve 下本地安装的包也读这个槽,所以任何已安装的包都能声明任意空闲名字的数据源,从而读到对应变量。文档和 changeset 已写明。
  6. 规范层只改了拒绝文本(5 条凭据拒绝 + 2 条 password 提示共用一句新句子),accept 集合不变;changeset 全部 patch(Metadata-protocol: none 时门禁要求 patch,见下文)。

席位意见:接受。契约复核在当前 head 上 PASS(6537998014),没有 must-fix。两条 should-fix(S1:非认证类连接失败也会带上"变量未设置"的说明;S2:连接失败后冲突登记未释放)都不动裁决和信任边界,已另立卡 #28283,本 PR 不改。

席位核对 · ACCEPT · domain:spec seat 3 · os-support-ai · session session_01WGufzgbJhFoXYsLSe5WSPP · 2026-10-31T02:52Z

Head: 5eda07f1ece5039367337c329c052efb8091b41f · card #27687 · claim 6506069472 (amended 6525977981) · ruling 6525963988

  1. 形态: a draft PR to main carrying Fixes #27687: 24 paths (+995 / −32), inside the amended surface. The changes are the env slot in connect(), the switch seam in os serve / os start, the shared prescription sentence in the spec refusals, and the two docs pages. Declarations: Clause-②: yes (widening) and Metadata-protocol: none. Every changeset is patch, because check-changeset-no-major refuses a minor beside none. That deviates from the amendment's "service-datasource minor"; the gate's rule wins. No governed path is touched, and the size is under 3000, so the ordinary queue landing applies.
  2. 裁决对照: the maintainer's option 5 and each guard in ruling 6525963988 are in the tree and pinned:
    • the provenance gate (declared-auto plus the code-name set, never record.origin);
    • OS_DATASOURCE_ENV_CREDENTIALS, on in the CLI host and off in library compositions, through the seam OS_CRYPTO_AUTOKEY already uses;
    • credentialsRef precedence;
    • the value never logged or answered;
    • the install-local trust consequence, stated and pinned by a runtime test.
  3. 门禁 check-runs (on this head): 50 latest runs: 39 success, 11 skipped, none red or pending. Dev-side: dispatch-gates --ran 110/110, package typecheck, test and test:repo, the dependents typecheck (27 packages), and 12 ablations, each recorded red. merge-tree against the current main is clean.
  4. 契约复核: PASS 6537998014 on this head, CONTRACT_REVIEW_TIER, reviewed by session_01L9qibw58ehQ2gRHP2h2Puz and posted through the fleet relay. The seat notes that the record's signature line reads domain:spec#3 · os-tesla, which is not this seat's session; the seat reads it as the review of record, as its content and Head-sha match.
  5. 范围外去向: S1 and S2 go to service-datasource: the env-credential reason rides on non-auth connect failures, and a failed connect keeps its collision owner (S1/S2 of #28208's review) #28283. The stale governed text (skills/objectstack-data/rules/datasources.md, ADR-0015 §10 / D7, ADR-0062 D3) goes to skills + ADR-0015/0062: teach the code-defined datasource credential route (OS_DATASOURCE_<NAME>_PASSWORD) once #28208 lands #28209, blocked by this PR.
  6. 落地条款: the seat checked the prose faces against the diff on this head: the PR body (r0, seat signature) and the changeset. The seat readies the PR and arms auto-merge in this act, follows it to MERGED, lands datasource: the credential refusals send a code author (defineDatasource) to a Setup form that cannot edit a code-defined datasource; name a binding path a code author can follow #27687, then goes off duty.

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 protocol:data size/xl tests tooling

Projects

None yet

1 participant