Skip to content

fix(spec): datasource password refusals prescribe a credential shape that parses - #27656

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

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

Conversation

@objectstack-fleet

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

Copy link
Copy Markdown
Contributor

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

Requested by Yibo Wang · project thread

Fixes #27614

Clause-②: no
Metadata-protocol: none

Text only. No key is added, no parse is widened or narrowed, and every input that parsed before parses the same way now. Two refusal messages in packages/spec/src/data/datasource.zod.ts and the drivers docs page change, so each prescription names a shape the parse accepts. The cross-domain docs edit is granted by triage comment 6500486793 (one PR for the guidance and the drivers page).

维护者速读

  • 改了什么:数据源 password 的两条拒收提示与 drivers 文档的只读一节,改为指向能通过解析的写法:连接表单的密钥字段,或 external.credentialsRef 写密钥库签发的 sys_secret: 句柄。
  • 为什么改:旧提示让作者把密码挪进 config(驱动契约会再拒一次),示例 secret:warehouse/password 能解析但运行时永不解引用;文档还说托管数据源不能带 external.credentialsRef,实际可以。照着提示改反而失败,AI 最容易照抄。
  • 风险与回滚:只改文案与文档,接受集不变,无迁移。回滚即还原本 PR。
  • 先看:packages/spec/src/data/datasource.zod.ts(两条 password guidance)、content/docs/data-modeling/drivers.mdx(只读一节的示例与说明段)、packages/spec/src/data/datasource.test.ts(末尾新 describe)。
  • 两分钟验证:pnpm --filter @objectstack/spec exec vitest run --project local src/data/datasource.test.ts,应见 51 个用例全绿;新的 3 个用例从拒收文案里取出示例句柄写回,解析通过。
  • 需要你做的:无,席位落地。

What changes

Each item was measured on origin/main 178b5238 first, by parsing the input and then following the refusal exactly as written.

1. Top-level password on a datasource (DatasourceSchema guidance).

  • Before: "password must never be inlined on a datasource. Interpolate it from the environment inside config, or for an external datasource reference the secrets store via external.credentialsRef." Following the first half (moving password into config of a postgres datasource) was refused again at config.password with the driver contract's INLINE_CREDENTIAL_REFUSED text. The second half restricted external.credentialsRef to external datasources, which is not so since the managed allowance.
  • After: "password must never be inlined on a datasource, and not in config either: the driver contracts refuse config.password and a user:password@ URL. Bind the secret instead: the Setup → Datasources connection form's secret field encrypts it into sys_secret and stores the opaque handle at external.credentialsRef, or set that key to a handle the secret store minted (e.g. external: { credentialsRef: "sys_secret:9f2c" }). external.credentialsRef is valid in every schemaMode, a managed datasource included."
  • Measured after: the example block, written on the same managed postgres datasource, parses (schemaMode defaults to managed).

2. password inside external (ExternalDatasourceSettingsSchema guidance).

  • Before: "… reference it with credentialsRef (e.g. credentialsRef: "secret:warehouse/password")." That value parses, but the shipped binder resolves only sys_secret: handles (parseCredentialsRef in packages/services/service-datasource/src/datasource-secret-binder.ts), and external-datasources.mdx § Credentials already says a hand-written path will not dereference. The datasource is left unconnected.
  • After: "password must never be inlined. Bind the secret instead: the Setup → Datasources connection form's secret field encrypts it into sys_secret and stores the opaque handle here, or set credentialsRef to a handle the secret store minted (e.g. credentialsRef: "sys_secret:9f2c"). The shipped binder resolves only sys_secret: handles; it never dereferences a hand-written path."
  • Measured after: the example on an external postgres datasource parses.

3. content/docs/data-modeling/drivers.mdx, § Read-only.

  • Before: the sample was config: { url: process.env.ANALYTICS_RO_URL }, and the paragraph after it said "external.credentialsRef is a federation key — the parse rejects an external block on a managed datasource — so a managed connection carries its credentials in config". Measured on origin/main: managed + external: { credentialsRef } parses, and a config.url carrying user:password@ is refused with URL_EMBEDDED_CREDENTIAL_REFUSED.
  • After: the sample is config: { host, database, username: 'analytics_ro' } plus external: { credentialsRef: 'sys_secret:9f2c…' }, and the paragraph says the password never goes in config, names both ways to bind it, says credentialsRef is the one external key a managed datasource may carry, and links external-datasources#4-credentials. The sample shape is pinned by a parse test.
  • Same section, § The one enforced write gate is federation-only: "the parse rejects an external block whose schemaMode is managed" now reads "rejects every federation key, allowWrites included, in an external block whose schemaMode is managed (only credentialsRef may sit there)". Same false claim, same section.
  • Same page, § Multi-Datasource (bounded in-place fix: same defect class, same file in the claim's surface, same gate families): the config: { url: process.env.… } sample gains one paragraph saying each url must be credential-free at publish, the password is bound through external.credentialsRef, and the user stays in the URL (mongodb://svc@host/db), which the mongo URL branch needs for the bound secret to be injected.

Also, per the seat's claim amendment 6502458097: the commented-out Postgres demo in examples/app-showcase/src/system/datasources/showcase-external.datasource.ts now binds credentialsRef: 'sys_secret:9f2c…' instead of 'secret:warehouse/password'. It is a comment in a private package, so it needs no changeset and moves no declaration.

Also in datasource.zod.ts: the config JSDoc ("passwords usually interpolated from ENV") now says config never carries the credential. It is a code comment and reaches no generated output.

No other copy of these prescriptions exists in packages/spec, content/docs/** or skills/** (grep below). content/docs/references/** and every other generated artifact are unchanged: check:generated --fix rebuilt spec and reported all 14 artifacts current, since guidance strings are not projected into the references.

Tests

packages/spec/src/data/datasource.test.ts gains describe('datasource \password` guidance — following it parses')`, one test per guidance string plus one for the docs sample:

  • top-level password: parse the refused input, read the external: { credentialsRef: "…" } example out of the refusal message, write it on the same datasource, and assert it parses as managed and keeps the handle. A control in the same test asserts the replaced prescription (password moved into config) is still refused.
  • external.password: read the credentialsRef: "…" example out of the refusal, assert it starts with sys_secret:, write it, and assert it parses.
  • the drivers page read-only sample parses as written.
Runs
  • pnpm --filter @objectstack/spec exec vitest run --project local --maxWorkers=2 src/data/datasource.test.ts on 0dcf18c5: Test Files 1 passed (1), Tests 51 passed (51).
  • Reverse check (the two guidance tests against the old text): git checkout origin/main -- packages/spec/src/data/datasource.zod.ts, same command: exit 1, Tests 2 failed | 49 passed (51), the two failures being exactly the two guidance tests. Restored with git checkout HEAD -- …; git diff HEAD empty and the file's hash equals the HEAD blob (32515dd9…).
  • Under os-verify-lock, pnpm test && pnpm test:repo && pnpm typecheck in packages/spec: local Test Files 752 passed (752), Tests 22346 passed | 1 todo; repo Test Files 86 passed (86), Tests 1433 passed (1433); typecheck clean. VERDICT command-exit 0.
  • pnpm check:generated --fix (spec, under the lock): "All 14 generated artifacts are up to date — measured against packages/spec/dist, BUILT BY THIS RUN". Nothing written.
Measurement on origin/main 178b523

A throwaway probe (deleted before the commit) parsed each shape with DatasourceSchema.safeParse:

  • top-level password → unrecognized_keys, guidance "Interpolate it from the environment inside config …".
  • the same with password moved into config → refused at config.password ("password is a credential and is not accepted inline in driver config …").
  • external: { password } on an external datasource → unrecognized_keys, guidance with credentialsRef: "secret:warehouse/password"; that value written → success=true, but not a sys_secret: handle, so it never resolves.
  • managed postgres + external: { credentialsRef: 'sys_secret:9f2c' } → success=true (contradicts the drivers page).
  • the drivers sample with a postgres://analytics_ro:pw@db/analytics URL → refused at config.url (URL_EMBEDDED_CREDENTIAL_REFUSED).

Acceptance notes

The order's grep (packages/spec, content/docs/**, skills/**) for the same prescriptions, plus one hit outside that scope. Nothing below is edited here.

  • skills/objectstack-data/rules/datasources.md:70 (governed): "Use external.credentialsRef and store the secret in the secret store". True as written; it gives no example and does not say external-only. Named, not edited.
  • docs/adr/0015-external-datasource-federation.md:625 (governed): an example with credentialsRef: 'secret:warehouse/readonly'. An ADR records a decision at its date. Named, not edited.
  • packages/spec/CHANGELOG.md and content/docs/releases/** carry the old "rejects an external block on a managed" wording in released notes. Release text is not edited.
  • Test fixtures using secret:warehouse/… (datasource.test.ts:685,707, objectql datasource-def-credentials-ref.pin.ts:73, two app-shell tests) pin parse or UI behaviour for an arbitrary string, not a prescription. Left as is.
  • drivers.mdx:332 (password: process.env.DB_PASSWORD) is a new SqlDriver({ connection }) constructor call, not datasource metadata, and never passes the publish door. Left as is.

Verification

  • node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack derived 102 commands at 0dcf18c5. Run serially: 95 exit 0 on the first pass, and 7 exit 3 (prerequisite only: no built dist, and the shallow clone missing the teardown self-test's pinned fixture commit): check-plugin-teardown-shape --self-test, check:doc-formula-expressions, check:doc-security-posture, check:skill-examples, check:docs-transcript-drift, check:dual-build-cjs-loads, check:lean-entry-closure. After git fetch origin 621a4876… and a full turbo run build --concurrency=2 under the lock (Tasks: 115 successful, 115 total), all 7 exit 0 (e.g. "274 prose examples type-check", "112 published require entry point(s) across 69 package(s) load", "31 ObjectSchema.create example(s) … carry an os validate-clean security posture").
  • dispatch-gates --ran: "102 derived, 102 run, 0 NOT-MEASURED, 0 UNRUN".
  • Head 68b2eec2 (the showcase comment fix): the derivation grows to 104 (adds check:examples-live-imports and check:i18n-coverage). After a full build (Tasks: 115 successful, 115 total), all 104 run serially, all exit 0; dispatch-gates --ran: "104 derived, 104 run, 0 NOT-MEASURED, 0 UNRUN".
  • Head 4e8f8493 (contract review should-fix: the sys_secret:-only claim scoped to the shipped binder in the guidance, the showcase comment and the changeset): datasource tests Tests 51 passed (51); full build Tasks: 115 successful, 115 total; check:generated all 14 artifacts current; the same 104 derived, all exit 0, dispatch-gates --ran: "104 derived, 104 run, 0 NOT-MEASURED, 0 UNRUN".
  • GITHUB_EVENT_NAME=pull_request node scripts/check-changeset-no-major.mjs --base origin/main --event (this body): exit 0, "LEVEL AXIS: this PR declares clause-② no and metadata-protocol none, and it introduces no changeset entry graded minor or above"; the one changeset grades @objectstack/spec patch.
  • Not run locally: the dogfood battery (no ! in the title and no (narrowing) arm); no ui/** file moves, so no ui scripts suite. The repo-wide CI families are on the PR's check-runs.

Generated by Claude Code

…that parses

The top-level `password` refusal told authors to move the secret into
`config`, where every driver contract refuses it; the `external.password`
refusal's example `secret:warehouse/password` parses but is never resolved
by the shipped binder. Both now prescribe the connection form's secret field
or `external.credentialsRef` with a `sys_secret:` handle, and the drivers
page's read-only section stops claiming a managed datasource cannot carry
`external.credentialsRef`.

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

github-actions Bot commented Oct 29, 2026 •

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 1 package(s): @objectstack/spec, touching 2 documentable anchor(s).

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

  • content/docs/releases/v17/17-0.mdx (via 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
  • 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 — 167 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 0561a988cb0b8b73becf80bbac8b3a407ed16e7e → packageMentionDocs.

Which tree this was computed on

This run read content/docs from 4ec6499c7a78c23e5c5e14a018f4f3fe56eb45c6 — the merge of head 4e8f849304638aa7020addeb29d56bc6df9b5323 into base 0561a988cb0b8b73becf80bbac8b3a407ed16e7e, 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 4ec6499c7a78c23e5c5e14a018f4f3fe56eb45c6 && git checkout 4ec6499c7a78c23e5c5e14a018f4f3fe56eb45c6
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 0561a988cb0b8b73becf80bbac8b3a407ed16e7e 4e8f849304638aa7020addeb29d56bc6df9b5323 && git checkout -B drift-repro 0561a988cb0b8b73becf80bbac8b3a407ed16e7e && git merge --no-ff 4e8f849304638aa7020addeb29d56bc6df9b5323

node scripts/docs-audit/affected-docs.mjs --json 0561a988cb0b8b73becf80bbac8b3a407ed16e7e

⚠️ 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 0561a988cb0b8b73becf80bbac8b3a407ed16e7e → pass the list as
args.docs, on the commit named under Which tree this was computed on.

…cret handle

The optional Postgres demo taught `credentialsRef: 'secret:warehouse/password'`,
a value that parses but that the shipped secret binder never resolves.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WGufzfeVuLKjLP5sdxuajz
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

triage#1 · os-sales · session_01L6bhgV4fMGJHSdSdx3u9mK

Contract-review-claim: session_01L6bhgV4fMGJHSdSdx3u9mK · head 68b2eec · 2026-10-29T17:39Z


Generated by Claude Code

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

triage#1 · os-sales · session_01L6bhgV4fMGJHSdSdx3u9mK

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: 68b2eec264e8268f2c7e8347edf784f991629374
Local-runs: none
Scope: packages/spec/src/data/datasource.zod.ts + packages/spec/src/data/driver/common.zod.ts, packages/spec/src/data/driver/{postgres,mysql,mongo,turso,sqlite,memory}.zod.ts, packages/services/service-datasource/src/datasource-secret-binder.ts, packages/services/service-datasource/src/datasource-connection-service.ts, packages/services/service-datasource/src/datasource-admin-plugin.ts, packages/services/service-datasource/src/datasource-admin-service.ts, ui/packages/app-shell/src/views/metadata-admin/datasource/DatasourceResourcePage.tsx, content/docs/data-modeling/external-datasources.mdx, content/docs/data-modeling/drivers.mdx, packages/spec/src/data/datasource.test.ts, skills/objectstack-data/rules/datasources.md, docs/adr/0015-external-datasource-federation.md (read to judge, not reviewed). The rest of the diff (.changeset/27614-datasource-credential-guidance.md, content/docs/data-modeling/drivers.mdx, examples/app-showcase/src/system/datasources/showcase-external.datasource.ts, packages/spec/src/data/datasource.test.ts) is the lane seat's ACCEPT at TIER_DEFAULT.

① Derived judgments

Read on the head tree, not the PR body.

  • Accept set of DatasourceSchema and ExternalDatasourceSettingsSchema: unchanged — right. The two hunks in datasource.zod.ts replace two values inside the guidance: maps of strictObject(...) (lines 297-302 and 556-563) and one JSDoc block on config (588-592). guidance strings are only read from the unrecognized_keys path (shared/strict-object.ts:253,461), so no key is added, no refinement moves, and every input parses to the same verdict as on main. The superRefine (723-797), managedFederationContentKeys (818) and credentialsRef: z.string().optional() (347) are untouched.
  • Public surface: the text of two refusal messages changes — right, and patch-shaped. Both are error-message prose on a published schema; neither .describe() nor .meta() changes, so the JSON-Schema projection and content/docs/references/** are unaffected (grep of the head tree for the guidance text under content/docs/references returns nothing; Lint & Repo Gates incl. check:generated is green).
  • The new prescriptions are measured true against the code they cite:
    • "the driver contracts refuse config.password and a user:password@ URL" — postgres/mysql/mongo/turso declare the credential slot through refusedInlineCredentialKey (common.zod.ts:821, z.never) and every url/syncUrl through credentialFreeUrl (common.zod.ts:665-678, refuses when urlUserinfoPassword is defined); sqlite and memory are strictObject contracts with no password key, so an inline password is an unrecognised key there. "Every driver contract refuses" holds.
    • "external.credentialsRef is valid in every schemaMode, a managed datasource included" — superRefine at 782-794 refuses a managed external block only for non-default federation keys and explicitly exempts credentialsRef (managedFederationContentKeys, 818-828). True; it is also what the old drivers.mdx paragraph got wrong.
    • "Only sys_secret: handles resolve; a hand-written path is never dereferenced" — true of the shipped binder: parseCredentialsRef (datasource-secret-binder.ts:98-100) returns undefined for anything not prefixed sys_secret: and resolve() (137-139) then returns undefined, fail-closed. See ④ for the one qualifier the sentence drops.
    • "the Setup → Datasources connection form's secret field encrypts it into sys_secret and stores the opaque handle at external.credentialsRef" — DatasourceResourcePage.tsx:25-26,223,450 routes format: 'password' props to the top-level secret, the admin service binds it and keeps the ref on the row (datasource-admin-service.ts:136, datasource.zod.ts:273-275). This is the same wording INLINE_CREDENTIAL_REFUSED and URL_EMBEDDED_CREDENTIAL_REFUSED already use (common.zod.ts:105-113,147-160), so the three sibling refusals now agree.
  • Clause-②: no — right. Nothing narrows: no key, alias or refinement is removed or tightened; the old prescription's target (config.password) was already refused on main by the driver contracts, so the test's control assertion pins behaviour that pre-exists this PR.
  • Gate verdicts on the head: 59 check-runs, all completed; every conclusion is success or skipped (opt-in/matrix placeholders), with one Check Changeset run cancelled and superseded by a later success on the same head. Governed Surface Queue Guard, Spec property liveness, Check Changeset, PR-body guards, Test Core 8/8, Type Check, Lint & Repo Gates, Test UI, Build Docs, Check Documentation Links are all green. No gate family failed.

② Semver level

  • Changeset .changeset/27614-datasource-credential-guidance.md: '@objectstack/spec': patch, carrying Clause-②: no and Metadata-protocol: none as line-initial declarations — matches the PR body's two lines and check-changeset-no-major is green on the head.
  • Level is right: the only published change is the prose of two refusal messages inside @objectstack/spec; no type, describe or projection moves. The showcase edit is a comment in a private example package (no changeset owed); drivers.mdx is docs; the test is unpublished.
  • Cosmetic only: the changeset's first line is prefixed docs(spec): while the PR title is fix(spec):. No gate reads the prefix; not a finding.
  • Clause-②: line reads no on both the PR body and the changeset, and the tree agrees (no narrowing anywhere in the diff).

③ Boundary flags

  • Dev report open_questions: [] — nothing to answer.
  • out_of_scope_findings 1 (class c, showcase-external.datasource.ts:59 teaching secret:warehouse/password): absorbed into this PR by the seat's claim amendment 6502458097; the head carries the fix (comment-only, sys_secret:9f2c…). Answered.
  • out_of_scope_findings 2 (governed, named not edited): skills/objectstack-data/rules/datasources.md:70 — read on the head: "Never inline a password. Use external.credentialsRef and store the secret in the secret store" — true as written, no example, not external-only. Agreed, no edit owed. docs/adr/0015-external-datasource-federation.md:625 — the §8 example still binds credentialsRef: 'secret:warehouse/readonly' (and carries the retired external.label); it is the ADR this schema implements and its example is of the exact class this card closes. The dev's "an ADR records a decision at its date" is the repo's posture, and the amendment path (AGENTS.md: an amended status line, not a rider) is not this PR's. Escalated as a follow-up card for the ADR owner, not a block.
  • Triage ruling 6500486793 (⛔ every prescription names a shape the 18.x parse accepts; no new key; pin each guidance string with a test that follows it and parses): both guidance strings name external.credentialsRef with a sys_secret: handle; datasource.test.ts:784-849 reads each example out of the refusal and parses it, plus the drivers sample. Honoured.
  • Docs Drift Check (advisory): two release-owned pages name DatasourceSchema; the PR edits neither (content/docs/releases/** and packages/spec/CHANGELOG.md untouched). Right.
  • Cross-domain exception: granted by triage, declared in the claim, drivers.mdx is in the claim's surface. Honoured.

④ Design judgment

packages/spec/src/data/datasource.zod.ts, judged against the card's three measured defects, triage ruling 6500486793, ADR-0015 (#8153 managed allowance) and the first consumers of the prescription (the shipped binder, the connect path, the Studio form).

Attempts to break it:

  1. Follow the top-level prescription on the input that fired it (managed postgres, password: 'p'): the only parse the text now offers is external: { credentialsRef: "sys_secret:…" } on the same managed datasource — superRefine 782-794 lets credentialsRef through on managed, and nothing else in the message points at config. The second refusal the card measured is gone; the test pins it and asserts the old path is still refused.
  2. Follow the external.password prescription: credentialsRef: "sys_secret:9f2c" parses (z.string()) and parseCredentialsRef accepts the prefix. Resolution of the literal would fail at boot, but the text says "a handle the secret store minted", so the example is a shape, not a value to copy. Holds.
  3. Mongo arm: a mongo datasource that takes the prescription with a user-less config.url is refused at config.url by CREDENTIALS_REF_MONGO_URL_NO_USER_REFUSED (751-758), which names its own remedy. The new text does not need to restate it. Holds.
  4. Credential-less drivers: a sqlite/memory author who wrote a top-level password is sent to external.credentialsRef, which parses; the binder would resolve and the factory has no slot (datasource-credential-migration.ts table). Harmless; no author writes a password on sqlite. Holds.
  5. Consistency with the sibling refusals: the two new strings, INLINE_CREDENTIAL_REFUSED, URL_EMBEDDED_CREDENTIAL_REFUSED and refusedInlineCredentialKey().describe() now all prescribe the same two routes in the same words. Holds; this is the card's "guidance and the docs page must say the same thing".

Findings:

  • Should-fix — the absolute "Only sys_secret: handles resolve; a hand-written path is never dereferenced" (datasource.zod.ts:301-302) is true of the shipped binder only. DatasourceConnectionService resolves through a host-injectable secrets.resolve (datasource-connection-service.ts:596-608; datasource-admin-plugin.ts:397,457-458), and external-datasources.mdx §4 says so: "A host that wants a different scheme (a vault path, say) injects its own resolver". The PR's own code comment (294-297) and changeset say "the shipped binder"; the refusal text should too, or a host operator on a vault resolver reads a false statement about their own deployment. One clause; not a FAIL — the prescription given parses and resolves on the default, which is what the ruling asked for.
  • Should-fix (follow-up, shared with the three sibling refusals; not owed by this PR). Both strings lead with the Setup → Datasources form, but the refusal fires on a code-authored defineDatasource, and a code-defined datasource is read-only to the admin service (datasource-admin-service.ts:15,656-658,702): the form cannot bind a secret onto that datasource, and the second route ("a handle the secret store minted") does not say how a code author mints one. INLINE_CREDENTIAL_REFUSED and URL_EMBEDDED_CREDENTIAL_REFUSED carry the identical gap and were maintainer-ruled in that shape ([security] sys_metadata.metadata is a general cleartext sink: any authored artefact whose schema permits an inline credential lands it there (datasource config.password, connector authentication) #7990, [Decision] URL-embedded credentials (user:password@host in driver config.url) remain a live cleartext door after #7990 — refuse at publish, or accept as residual risk? #8082), so this PR correctly matches the ruled prescription rather than inventing a fourth. Worth a card on the code-author binding path; not a defect of this diff.
  • Follow-up card, named not edited: ADR-0015 §8 example credentialsRef: 'secret:warehouse/readonly' (docs/adr/0015-external-datasource-federation.md:625) — the ADR the schema cites teaches the non-resolving form this card retires from the guidance.

No must-fix. The hits file holds against the code it cites and the scenario it is accepted against: each of the card's three prescriptions now leads to a shape that parses, the parse itself is unchanged, and the gates on the head are green.

Implemented-by: claude/project-thread-jrroxo
Reviewed-by: session_01L6bhgV4fMGJHSdSdx3u9mK

VERDICT: PASS


Generated by Claude Code

…m to the shipped binder

A host can inject its own resolver, so "only `sys_secret:` handles resolve"
holds for the shipped binder alone. Same wording in the showcase comment
and the changeset.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WGufzfeVuLKjLP5sdxuajz
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

triage#1 · os-sales · session_01L6bhgV4fMGJHSdSdx3u9mK

Contract-review-claim: session_01L6bhgV4fMGJHSdSdx3u9mK · head 4e8f849 · 2026-10-29T18:18Z


Generated by Claude Code

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

triage#1 · os-sales · session_01L6bhgV4fMGJHSdSdx3u9mK

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: 4e8f849304638aa7020addeb29d56bc6df9b5323
Local-runs: none
Scope: packages/spec/src/data/datasource.zod.ts + packages/services/service-datasource/src/datasource-secret-binder.ts, packages/services/service-datasource/src/datasource-connection-service.ts, packages/services/service-datasource/src/datasource-admin-plugin.ts, content/docs/data-modeling/external-datasources.mdx, packages/spec/src/data/datasource.test.ts, examples/app-showcase/src/system/datasources/showcase-external.datasource.ts, .changeset/27614-datasource-credential-guidance.md (read to judge, not reviewed); the files record 6504200509 widened to (driver/common.zod.ts, the six driver contracts, datasource-admin-service.ts, DatasourceResourcePage.tsx, drivers.mdx, skills/objectstack-data/rules/datasources.md, docs/adr/0015-external-datasource-federation.md) are untouched by the move and stand as read there. The rest of the diff (.changeset/27614-datasource-credential-guidance.md, content/docs/data-modeling/drivers.mdx, examples/app-showcase/src/system/datasources/showcase-external.datasource.ts, packages/spec/src/data/datasource.test.ts) is the lane seat's ACCEPT at TIER_DEFAULT.

Delta review on record 6504200509 (PASS, head 68b2eec264e8268f2c7e8347edf784f991629374). One commit moved since: 4e8f849304 "fix(spec): external password guidance scopes the sys_secret-only claim to the shipped binder", parent 68b2eec264 only, so it carries no merge of main; git diff 68b2eec2..4e8f8493 is 3 files, +5/-4, every hunk a rewording of the one sentence record 6504200509 named in its first should-fix. The PR's file list against main is unchanged (5 files). Sections the move does not touch stand as judged in record 6504200509, named below.

① Derived judgments

Read on the head tree, not the PR body.

  • Accept set of DatasourceSchema and ExternalDatasourceSettingsSchema: still unchanged — right. The move edits two string-literal fragments inside the password value of the guidance: map of ExternalDatasourceSettingsSchema (datasource.zod.ts:301-302). guidance strings are only read on the unrecognized_keys path, so no key, alias, refinement or default moves; every input parses to the same verdict as on 68b2eec2 and on main. The top-level DatasourceSchema password guidance (556-563), the config JSDoc (588-592), superRefine, managedFederationContentKeys and credentialsRef: z.string().optional() are byte-identical to the previous head.
  • Public surface: the text of one refusal message changes again — right, and still patch-shaped. The sentence "Only sys_secret: handles resolve; a hand-written path is never dereferenced." becomes "The shipped binder resolves only sys_secret: handles; it never dereferences a hand-written path." No .describe() or .meta() moves, so the JSON-Schema projection and content/docs/references/** are unaffected, as before (Lint & Repo Gates incl. check:generated green on the head).
  • The reworded prescription is measured true against the code it cites, now without the qualifier record 6504200509 said it dropped:
    • "The shipped binder resolves only sys_secret: handles" — parseCredentialsRef (datasource-secret-binder.ts:98-100) returns undefined for anything not prefixed REF_PREFIX and the shipped resolve() (137-139) then returns undefined; the connect path treats that as failed-credentials (datasource-connection-service.ts:612-620), fail-closed. True.
    • The sentence no longer claims anything about a non-shipped resolver, which is what made the old absolute false: DatasourceConnectionService resolves through a host-injectable secrets.resolve (datasource-connection-service.ts:596-608; datasource-admin-plugin.ts:397,457-458), and external-datasources.mdx:250-254 says a host "injects its own resolver". The three places that state the rule — the refusal, the docs paragraph, and the PR's own code comment at datasource.zod.ts:293-296 ("the shipped binder mints and resolves") — now use the same scope word.
    • The example handle credentialsRef: "sys_secret:9f2c" and the two routes before it are unchanged; the pin test's regex /`credentialsRef: "([^"]+)"`/ (datasource.test.ts:837) still finds it and asserts the sys_secret: prefix. Test Core 8/8 green on the head confirms the three guidance tests pass on the new text.
  • Everything else under ① (the top-level password prescription, the driver-contract claims, the managed credentialsRef allowance, the Studio-form wording, the Clause-②: no reading) stands as judged in record 6504200509; the move does not touch those lines.
  • Gate verdicts on the head: 50 check-runs, all completed; 41 success, 9 skipped (opt-in and matrix placeholders: Lint UI, Test UI (shard), Dogfood Regression Gate (shard), Dogfood Verify CLI, UI Published Artifacts, Temporal Conformance, 18.x upgrade harness, Packed-tarball smoke, Schedule gate). None in_progress, none failure, none cancelled. Governed Surface Queue Guard, Spec property liveness, Check Changeset, PR-body guards, Test Core 1-8/8, TypeScript Type Check, Type Check · workspace/source gates/consumer gates, Lint & Repo Gates and its six scoped lint runs, Test UI, Build Core, Build Docs, Check Documentation Links, Flag docs affected by code changes all success. No gate family failed.

② Semver level

  • Changeset .changeset/27614-datasource-credential-guidance.md: still '@objectstack/spec': patch, still carrying Clause-②: no and Metadata-protocol: none as line-initial declarations. The move edits one prose bullet (the external.password item now ends "the text says the shipped binder resolves only those"), which is an accurate account of the new string; the front matter and the two declaration lines are untouched. Check Changeset is green on the head.
  • Level stands as judged in record 6504200509: the only published change is refusal-message prose in @objectstack/spec; the move adds no type, describe or projection change. The showcase edit is a comment in a private example package; the test is unpublished.
  • Clause-②: reads no on the PR body and the changeset, and the tree agrees: the move removes or tightens nothing.
  • Cosmetic, not a finding: the PR body's item 2 "After:" paragraph still quotes the superseded sentence ("Only sys_secret: handles resolve; a hand-written path is never dereferenced"), so the body's account is one revision behind the tree. The record judges the tree; no gate reads that paragraph.

③ Boundary flags

  • No new dev report was posted for 4e8f8493; the commit answers the should-fix in record 6504200509 and raises no open_questions of its own. Nothing to answer.
  • Record 6504200509 should-fix 1 (the absolute "Only sys_secret: handles resolve" was true of the shipped binder only): landed. The refusal now scopes the claim to "the shipped binder", the showcase comment (showcase-external.datasource.ts:60-61) and the changeset bullet say the same, so a host operator on a vault resolver no longer reads a false statement about their deployment. Closed.
  • Record 6504200509 should-fix 2 (follow-up shared with the sibling refusals INLINE_CREDENTIAL_REFUSED / URL_EMBEDDED_CREDENTIAL_REFUSED: the code-author binding path is not named): not owed by this PR and not touched by the move; stands as a follow-up card, as judged there.
  • Record 6504200509 follow-up (ADR-0015 §8 example credentialsRef: 'secret:warehouse/readonly', docs/adr/0015-external-datasource-federation.md:625): untouched by the move; stands as a follow-up card for the ADR owner, not a block.
  • Triage ruling 6500486793 (every prescription names a shape the 18.x parse accepts; no new key; pin each guidance string with a test that follows it and parses): the move changes no example and no key; both guidance strings still name external.credentialsRef with a sys_secret: handle, and datasource.test.ts:785-859 still reads each example out of the refusal and parses it. Honoured, as before.
  • Claim amendment 6502458097 (showcase file in the surface): the move's showcase hunk is inside that declared file, comment-only. Honoured.
  • Docs Drift Check (advisory, recomputed on the merge of 4e8f8493): two release-owned pages name DatasourceSchema; the PR still edits neither. Right.
  • Cross-domain exception and drivers.mdx: untouched by the move; stand as judged in record 6504200509.

④ Design judgment

packages/spec/src/data/datasource.zod.ts, judged for the one sentence the move changes against the code it cites (parseCredentialsRef, the shipped resolve, the connect path's host-injectable resolver) and the scenario it is accepted against (an author or agent who follows the external.password refusal). Everything else in the hits file is byte-identical to 68b2eec2 and stands as judged in record 6504200509 (attempts 1-5 there).

Attempts to break the reworded sentence:

  1. Default deployment, author follows the refusal: writes credentialsRef: "sys_secret:…" minted by the form. Parses (z.string()), parseCredentialsRef accepts, resolve finds the sys_secret row. "The shipped binder resolves only sys_secret: handles" is exactly what the author experiences. Holds.
  2. Default deployment, author ignores it and writes secret:warehouse/password: parses, binder returns undefined, connect fails closed with failed-credentials at datasource-connection-service.ts:612-620. "it never dereferences a hand-written path" is true of the binder named. Holds.
  3. Host-injected resolver (vault path): the old text told this operator that their path "is never dereferenced", which was false — their resolver dereferences it. The new text makes a claim about "the shipped binder" only, which is the component external-datasources.mdx:250-254 names by the same words, and it stays silent about the host's resolver rather than contradicting it. The one false reading is gone. Holds.
  4. Does the scoping word weaken the prescription? The two routes offered (form secret field; a handle "the secret store minted") are unchanged and still lead to a shape that parses and, on the default, resolves — which is what ruling 6500486793 asked for. A reader on a custom resolver is not sent anywhere wrong; the sentence simply no longer forbids what their deployment allows. Holds.
  5. Consistency with the sibling refusals and the docs: INLINE_CREDENTIAL_REFUSED and URL_EMBEDDED_CREDENTIAL_REFUSED (common.zod.ts) carry no "only sys_secret:" clause at all, so nothing there now disagrees; external-datasources.mdx:250-252 says "the shipped binder … that is the only form it resolves", the showcase comment and the changeset bullet say "the shipped binder resolves only sys_secret: handles". All copies of the claim on the head tree carry the scope (grep of packages/spec, content/docs, skills, examples, docs/adr for the phrase). Holds.
  6. Pin test still binds the example: the test regex matches `credentialsRef: "sys_secret:9f2c"`, which the move left in place, and Test Core is green on the head. The test's own comment (datasource.test.ts:783-784, lane seat's file) already read "not a handle the shipped binder resolves". Holds.

Findings:

The hits file holds against the code it cites and the scenario it is accepted against: the parse is unchanged, each prescription still leads to a shape that parses, the one overstated clause is now scoped to the component it is true of, and the gates on the head are green.

Implemented-by: claude/project-thread-jrroxo
Reviewed-by: session_01L6bhgV4fMGJHSdSdx3u9mK

VERDICT: PASS


Generated by Claude Code

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

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

维护者速读 (席位终稿)

  • 改了什么:数据源 password 的两条拒收提示与 drivers 文档的只读一节,改为指向能通过解析的写法:连接表单的密钥字段,或 external.credentialsRef 写密钥库签发的 sys_secret: 句柄。
  • 为什么改:旧提示让作者把密码挪进 config(驱动契约会再拒一次),示例 secret:warehouse/password 能解析但运行时永不解引用;文档还说托管数据源不能带 external.credentialsRef,实际可以。照着提示改反而失败,AI 最容易照抄。
  • 风险与回滚:只改文案与文档,接受集不变,无迁移。回滚即还原本 PR。
  • 先看:packages/spec/src/data/datasource.zod.ts(两条 password guidance)、content/docs/data-modeling/drivers.mdx(只读一节的示例与说明段)、packages/spec/src/data/datasource.test.ts(末尾新 describe)。
  • 两分钟验证:pnpm --filter @objectstack/spec exec vitest run --project local src/data/datasource.test.ts,应见 51 个用例全绿;新的 3 个用例从拒收文案里取出示例句柄写回,解析通过。
  • 需要你做的:无,席位落地。

席位意见:接受。分诊契约复核 PASS(6504200509,head 68b2eec2),没有 must-fix。之后只改了一处 should-fix:拒收文案、示例注释和 changeset 都改成「出厂的绑定器只解析 sys_secret: 句柄」,席位已逐行核对。另外两条 should-fix 立成了后续卡 #27687 和 #27688。

席位核对 · ACCEPT · domain:spec seat 3 · os-support-ai · session session_01WGufzgbJhFoXYsLSe5WSPP · 2026-10-29T18:46Z

Head: 4e8f849304638aa7020addeb29d56bc6df9b5323 · card #27614 · claim 6500916138, amended 6502458097 (the showcase example comment)

  1. 形态: a draft PR to main carrying Fixes #27614: 5 paths (+136 / −14), inside the claim's surface as amended. It rewrites the two password guidance strings in datasource.zod.ts, the read-only section of drivers.mdx and the commented-out showcase example, adds three guidance tests and a @objectstack/spec patch changeset. Clause-②: no / Metadata-protocol: none. check-governed-merges reports NOT governed, so the ordinary queue landing applies.
  2. 前提实测: the contract review (6504200509, PASS, on 68b2eec2) followed each of the card's three prescriptions to a shape that parses, and found the parse itself unchanged. The seat read the delta 68b2eec2 → 4e8f8493 (27614-delta-68b2eec2-4e8f8493.diff): 3 files (+5 / −4). It is the review's first should-fix only, scoping "only sys_secret: handles resolve" to the shipped binder, because a host can inject its own secrets.resolve. No pin quotes the sentence, and datasource.test.ts is 51/51.
  3. 门禁 check-runs (on this head): 54 runs read at 2026-10-29T18:46Z: 43 success, 11 skipped, none red or pending. Dev-side: full build 115/115, dispatch-gates --ran 104/104 with 0 NOT-MEASURED.
  4. 接受的偏差: none.
  5. 范围外去向: two of the review's findings are filed as cards. 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: the four credential refusals send a code author to a Setup form that cannot edit a code-defined datasource. docs(adr): ADR-0015 §8 still teaches credentialsRef 'secret:warehouse/readonly', a form the shipped binder never resolves #27688: ADR-0015 §8 still teaches secret:warehouse/readonly (governed, named not edited). Nothing under skills/** is touched.
  6. 落地条款: the seat checked the prose faces against the diff on this head: the PR body (r2), the changeset and the guidance strings. The seat readies the PR and arms auto-merge in this act, follows it to MERGED, and lands Datasource credential guidance prescribes shapes the 18.2.0 schema refuses: top-level password guidance, the credentialsRef example, and the drivers.mdx read-only section #27614.

@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review October 29, 2026 18:47
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Oct 29, 2026
Merged via the queue into main with commit bfdaba4 Oct 29, 2026
55 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/project-thread-jrroxo branch October 29, 2026 19:10
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/m tests tooling

Projects

None yet

1 participant