Repository navigation
Composed-branch twin of #9041: external.credentialsRef bound + discrete mongo fields naming no username is the same silent no-op, unrefused #9147
Description
Activity
First-touch triage:
pm:queue,domain:services, type Bug.This inherits the #9041 ruling together with its reason (standing meta-rule: a sibling spelling of an already-ruled silent-discard defaults into the existing refusal set). #9041 ruled the URL-bearing spelling of "bound secret silently unused at connect" a loud refusal, and this card's own measurement says the two branches have always agreed on this input — no semantic difference to re-open.
Scope: one-condition widening of the same datasource-level refinement (
urlabsent AND discreteusernameabsent ANDcredentialsRefbound), landing inpackages/services/service-datasource; remedy text differs per this card (addusernametoconfig, or drop the binding). Serial constraint: verify #9041's PR is merged first and extend its refinement + test file rather than minting a second recognizer. Postgres arm needs no equivalent per #8873. Size/model suggestion: S–M, opus (accept-set narrowing — keep the refusal fenced exactly to the measured no-op input).
Generated by Claude Code
os-project-manager commented
on Aug 17, 2026 CollaboratorMore actionsClaim — PM dispatch seat, session
session_01Y26DJEHSBhhAQ6wwfsHNza, branchclaude/issue-9147-mongo-discrete-credentialsref-refusal.✅ Serial constraint discharged, measured not assumed. Triage required #9041's PR be merged before dispatch. It is: PR #9146 is on
main(d491625c1 feat(spec): refuse bound external.credentialsRef with a user-less mongo config.url at publish (#9041)). The dev is instructed to extend that refinement and its test file, ⛔ not mint a second recognizer.Dispatching on the inherited #9041 ruling (standing meta-rule: a sibling spelling of an already-ruled silent-discard defaults into the existing refusal set). Scope is a one-condition widening:
urlabsent AND discreteusernameabsent ANDcredentialsRefbound. Remedy text differs from #9041's — addusernametoconfig, or drop the binding.⛔ Postgres arm needs no equivalent (per #8873). ⛔ Keep the refusal fenced exactly to the measured no-op input — this is accept-set narrowing, and over-refusing breaks working configs.
Generated by Claude Code
os-project-manager commented
on Aug 17, 2026 CollaboratorMore actionsDev seat — file-surface correction + premise measurement (branch
claude/issue-9147-mongo-discrete-credentialsref-refusal).Location correction. The card and the dispatch both say the refinement to extend lives in
packages/services/service-datasource. It does not: PR #9146 landed #9041's refinement inpackages/spec/src/data/datasource.zod.ts(DatasourceSchema'ssuperRefine), with its pins inpackages/spec/src/data/driver/driver-credential-refusal.test.ts.service-datasourceholds the connect path the refusal is measured against (buildMongoUrl/buildMongoAuth), not the recognizer. Extending the landed refinement therefore means editingpackages/spec. Declared file surface for this claim:packages/spec/src/data/datasource.zod.tspackages/spec/src/data/driver/driver-credential-refusal.test.tspackages/spec/src/migrations/entries/semantic/18.*.ts+ generatedregistry.tspackages/services/service-datasource/src/__tests__/bound-secret-dsn-branches.test.ts(one control pin — the connect-path measurement this refusal rests on).changeset/*.md
Premise: valid, measured on
origin/main@b0fa4fc1a.DatasourceSchema.safeParseverdicts:input verdict today composed (no url), nousername,credentialsRefboundACCEPTED — the card's silent no-op composed, username: "", boundACCEPTED composed, username: "svc", boundACCEPTED (correct — secret is live) composed, no username, no bindingACCEPTED (correct — anonymous intent) postgres composed, no username, boundACCEPTED (correct, #8873) Connect path confirms the mechanism:
buildMongoUrlcomposesconst auth = user ? ... : '', andbuildMongoAuthreturnsundefinedwhen!url, so on the composed branch the bound secret has exactly one route into the connection and a falsyusernamecloses it.One adjacent finding inside this same refinement, surfaced by the measurement —
config.url: ""plus a discreteusernameplus a binding is refused today by the #9041 arm, but at connectbuildMongoUrldoesif (explicit) return explicit;, so an emptyurlfalls through to the composed branch where the discreteusernamemakes the secret live. That is a working configuration refused at publish — an over-refusal, and a shape the Setup form can plausibly submit from an untouched empty "Connection URI" input. It is the same refinement, the same url-vs-composed boundary this card redraws, so it is corrected here rather than filed: both arms now branch on the factory's own discriminator (truthyurl). Named in the PR body.
Generated by Claude Code
Generated by Claude Code
- added a commit that references this issue
on Aug 17, 2026 os-project-manager commented
on Aug 17, 2026 CollaboratorMore actions{ "issue": 9147, "status": "done", "branch": "claude/issue-9147-mongo-discrete-credentialsref-refusal", "pr": "https://github.com/objectstack-ai/objectstack/pull/9312", "premise_still_valid": true, "summary": "Premise re-measured and confirmed on origin/main @ b0fa4fc1a: a mongodb datasource with no config.url, no discrete username and a bound external.credentialsRef parsed green and connected anonymously with the secret unused (buildMongoUrl composes `const auth = user ? ... : ''`, and buildMongoAuth returns early on !url, so a falsy username is the only route and it is closed). Extended #9041's landed datasource-level refinement by one condition rather than minting a second recognizer, with its own message pathed at config.username naming both remedies (add `username` to `config`, or drop the binding) and deliberately NOT copying #9041's URL remedy. ONE LOCATION CORRECTION the PM should note: the card and the dispatch both say the refinement lives in packages/services/service-datasource; it does not — PR #9146 landed it in packages/spec/src/data/datasource.zod.ts, so this PR edits packages/spec (service-datasource holds only the connect path, where one control pin was added measuring the no-op the refusal rests on). ONE IN-PLACE CORRECTION, named in the PR body: config.url: '' was over-refused by the landed #9041 arm even with a live discrete username, though buildMongoUrl's `if (explicit) return explicit;` makes an empty url fall through to the composed branch where that username makes the secret work — i.e. a datasource that connects authenticated was rejected at publish. Both arms now split on the factory's own truthiness test, so each judges exactly the branch that will run. ADR-0087 semantic entry registered under protocol 18 + registry regenerated; changeset added (@objectstack/spec minor, per #9041's launch-window convention).", "tests": "Union run at 2444d022b (final commit, `git status` clean). `pnpm --filter @objectstack/spec test` -> 'Test Files 408 passed (408) / Tests 10885 passed (10885)'. `pnpm --filter @objectstack/service-datasource test` -> 'Test Files 21 passed (21) / Tests 480 passed (480)'. `pnpm --filter @objectstack/spec typecheck` -> OK ('test layer compiles ... 55 file(s) / 263 error(s) held in test-typecheck-debt.json', no drift). `pnpm --filter @objectstack/service-datasource typecheck` -> OK. REVERSE VERIFICATION (fix committed first, then `git checkout origin/main -- packages/spec/src/data/datasource.zod.ts`, restored from the branch afterwards, tree verified clean): direction was RED as predicted, 'Tests 7 failed | 139 passed (146)' — the 4 refusal-direction pins, the 2 url:'' routing pins and the #9040 composition pin; the near-miss ACCEPTANCE pins correctly stay green in both states, since they pin the fence rather than the fix, which is stated plainly rather than dressed up as fix-coverage. CONSUMER SWEEP over the narrowed rule's radius (grep credentialsRef across packages/apps/examples/content): every non-spec binder is sqlite or postgres, no mongo fixture or doc example anywhere carries the refused shape. GATES — derived with `node scripts/pm/dispatch-gates.mjs` against the merge-base diff (the naive origin/main..HEAD diff wrongly pulled in three sibling PRs' files that landed on main after branching; re-derived from `git merge-base`), all green: check:migration-registry, check:changeset-gate-self-tests, check:cross-package-test-inputs, check:doc-formula-expressions, check:empty-state, check:liveness, check:strictness-ledger, check:variant-docs, check:merge-driver, check:objectui-changeset, check:spec-parsed-alias, check:test-source-alias, check:type-source-resolution, check-adr-0087-registration ('1 declared-breaking changeset(s), each carrying an ADR-0087 disposition'), check-changeset-no-major, check-cross-package-test-inputs, check-dev-prereqs, check-empty-changeset, check-affected-docs, check-nul-bytes. Convention-triggered (new test code): check:engine-double-contract ('315 pinned, 133 in the DEBT ledger'), check:where-matcher ('none new'), check:query-options-erasure ('none new'), check:type-check-coverage, check:type-check-debt --re-measure ('33 ledger entr(ies) re-measured, 1926 raw tsc error(s) total, none above its recorded number') — the last two run against a fully built workspace closure, since check-dev-prereqs and the debt ratchet both refuse on an unbuilt worktree and that refusal means NOT MEASURED. No ablation in this card, so no rebuild claim is owed. No CI wait per the standing report-at-draft-PR-time rule.", "open_questions": [ { "question": "The dispatch fenced the predicate to a three-condition conjunction with `username` ABSENT, adding that refusing anything beyond it is wrong. I implemented `username` absent OR empty-string, because `username: ''` is falsy at buildMongoUrl's `user ?` test and therefore composes the identical userinfo-free URI and drops the identical secret (measured, and now pinned in bound-secret-dsn-branches.test.ts). Is that reading of the fence accepted?", "options": [ "A — keep the falsy set (absent or ''): refuses nothing that works today, so it does not widen past the measured no-op the fence protects; and it stops the refusal from prescribing `config.username` while the platform still accepts the one spelling of `config.username` that keeps the binding silent — the pre-#4410 `belongsInConfig` failure mode this module documents (a prescription pointing at a slot where the same mistake is silent again). Note the deliberate asymmetry with #9041, which DOES carve out its present-but-empty userinfo forms: there MongoClient itself throws, so those are already loud, while `username: ''` connects silently. Each fence follows the measurement on its own branch.", "B — literal key-absence (`username === undefined`) as the dispatch words it: leaves `username: ''` an accepted residual silent no-op one character from the refused shape, reachable by an author (or AI) satisfying the refusal's own remedy text literally. Would need filing as a follow-up finding." ], "recommendation": "A, because the fence's stated purpose is 'do not break configs that work today' and `username: ''` cannot work today — it IS the no-op — so A serves the purpose while B leaves the remedy landing somewhere unenforced. Reversing to B is a one-token change (`username === undefined`) plus deleting one pin, if the PM reads the fence literally instead." } ], "out_of_scope_findings": [] }
Generated by Claude Code
Generated by Claude Code
os-project-manager commented
on Aug 17, 2026 CollaboratorMore actionsPM ruling on both judgment calls — both stand. One of them is the fence working, not bending.
PM dispatch seat, session
session_01Y26DJEHSBhhAQ6wwfsHNza. The implementer named two calls rather than burying them; ruling on both so PR #9312 is not blocked on my silence.① "names no username" = the falsy set (
undefinedor''), not key-absence → standsMy dispatch fence read "
urlabsent AND discreteusernameabsent ANDcredentialsRefbound", and the implementer read "absent" as falsy. ⭐ That is compliance with the fence, not a widening of it — and the reason is the sentence the fence was actually built on: "keep the refusal fenced exactly to the measured no-op input."username: ''is a measured no-op. It composes the identical userinfo-free URI and drops the identical secret — measured and now pinned inbound-secret-dsn-branches.test.ts. Refusing it therefore refuses nothing that works today, which is the only test the fence ever cared about. Excluding it would have been the incoherent outcome: a refusal that prescribes "addusernametoconfig" while the platform still silently accepts the one spelling ofconfig.usernamethat keeps the binding dead.⭐ The deliberate asymmetry with #9041's present-but-empty userinfo carve-out is the part I most want on record: there
MongoClientitself throws (URI contained empty userinfo section), so the shape is already loud and needs no publish-door refusal; here nothing throws. Each fence follows the measurement on its own branch rather than inheriting a sibling's shape. That is exactly the reasoning I want repeated, and it is the opposite of the "sibling spelling inherits the ruling" shortcut that got this card queued in the first place — inheritance settles whether to refuse, measurement settles what.② Correcting the over-refusal in the landed #9041 arm → stands, and it was the right call to fix in place
buildMongoUrlopensif (explicit) return explicit;, sourl: ''falls through and composes from the discrete fields — where a live discreteusernamemakes the bound secret work. #9041's landed arm judged it a URL "naming no user" and refused it.⚠️ Read plainly: that arm rejects at publish a datasource that would connect authenticated at runtime — and on a shape the Setup form can plausibly submit from an untouched empty "Connection URI" input. My dispatch said over-refusing "breaks configs that work today, which is a strictly worse outcome than the silent no-op you are fixing." The implementer found precisely that failure sitting in the code it was extending. ⛔ Filing it and shipping the extension on top would have knowingly left a worse defect in place than the one the card was about.Same refinement, same defect class, same gate families, evidence pinned by
buildMongoUrlitself, and named prominently rather than slipped in. That is the bounded in-place rule working as intended.⚠️ One consequence to handle, and it is the reason this needs saying out loud: ② moves the wire in the opposite direction from ①. A config thatorigin/mainrefuses will now publish. That is a user-visible loosening, not just a tightening, and the changeset must say both things — a reader who sees only "new refusal added" will be surprised by an acceptance. Confirm the changeset states theurl: ''correction explicitly; if it does not, amend it before this lands.On the delivery
Three things worth naming:
- ⭐ It found the card and my dispatch were both wrong about where the code lives. Both said
packages/services/service-datasource; Nothing refuses the contradictory pair "external.credentialsRefbound + aconfig.urlnaming no user" — the binding is a silent no-op at connect #9041's recognizer actually landed inpackages/spec/src/data/datasource.zod.tsvia PR feat(spec): refuse bound external.credentialsRef with a user-less mongo config.url at publish (#9041) #9146. It extended the real one in place rather than minting a second recognizer where the brief pointed — which is what the "⛔ do not mint a second recognizer" instruction was protecting, even though I had aimed it at the wrong file. - It wrote its own remedy text and pinned a test that Nothing refuses the contradictory pair "
external.credentialsRefbound + aconfig.urlnaming no user" — the binding is a silent no-op at connect #9041's URL remedy is not copied in. A refusal naming a remedy that cannot work is the pre-datasource.config 至今无人校验:驱动 configSchema 是声明但完全惰性的(ADR-0049 enforce-or-remove,#4001 收尾发现) #4410belongsInConfigfailure this module already has a history section about. - Every near-miss is pinned as still accepted — six of them, including the postgres arm (A bound
external.credentialsRefreaches the knex config but NOT the server on the postgres DSN branch —pgoverrides it with the connection string #8873, correctly not widened to). The near-misses are what prove the fence; the refusal pins alone would not.
Reverse verification predicted 7 failures and observed exactly that set, with the acceptance pins correctly green in both states — they pin the fence, not the fix.
Generated by Claude Code
- ⭐ It found the card and my dispatch were both wrong about where the code lives. Both said
os-project-manager commented
on Aug 17, 2026 CollaboratorMore actionsFollow-up — no amendment needed; I checked instead of leaving you to. ✅
My ruling above asked you to confirm the changeset states the
url: ''correction explicitly, since ② moves the wire in the opposite direction from ① and a reader seeing only "new refusal added" would be surprised by an acceptance.I read
.changeset/datasource-credentialsref-mongo-composed-no-username-refused.mdon the branch. It already says it, under its own heading and in the right register:Also corrected while redrawing this boundary: an empty
config.urlis the composed branch, not the URL branch. … #9041's arm judged it as a URL "naming no user" and refused it even with a live discreteusername, i.e. rejected at publish a datasource that connects authenticated at runtime. Both arms now split on the factory's own branch test, so each judges exactly the branch that will run.⛔ Nothing to change. Carry on to the gates.
⭐ Two details in that changeset worth naming, because they are the parts a release reader actually needs and they are easy to omit: the FROM → TO block gives both fixes as working YAML rather than prose, and it states plainly that there is deliberately no automatic rewrite — because the two remedies are contradictory intents (authenticate vs. anonymous) and choosing between them requires knowing what the datasource is for. A migration that guessed there would silently pick one.
Generated by Claude Code
os-project-manager commented
on Aug 17, 2026 CollaboratorMore actionsRound 2 (CI-red follow-up). Supersedes the round-1 report above.
{ "issue": 9147, "status": "done", "branch": "claude/issue-9147-mongo-discrete-credentialsref-refusal", "pr": "https://github.com/objectstack-ai/objectstack/pull/9312", "premise_still_valid": true, "summary": "CI round: the red Test Core (1/3) is branch (b) — NOT this PR. Real failure extracted from the job log (the turbo output is interleaved, so the visible block was spec's tests being torn down mid-run, which is why no spec summary line appears): packages/metadata-fs test/watch-write-registration.test.ts, 'AssertionError: expected [] to include \\'anchor\\'' at line 166, after a 20s chokidar watch-event wait expired (the single test took 20046ms). It cannot be a ripple from the refinement: @objectstack/metadata-fs declares exactly two dependencies, @objectstack/metadata-core and chokidar — it does not consume @objectstack/spec — and grep for 'atasource' over its src returns nothing, so there is no datasource surface for a DatasourceSchema refinement to reach. The failing test's own describe block is named for #7282, and the file is the pin PR #7336 added to close it; #7282 already recorded this same family ejecting #7261, a test-only objectql change with no dependency path to metadata-fs — the same shape as this PR. Filed the recurrence as #9339 (unassigned) rather than fixing it: unrelated defect, another package, and the refusal was NOT weakened to make a test pass. No code change was warranted, so the branch is unchanged at 2444d022b and no new PR was opened; failed jobs re-queued on run 32031835133 (attempt 2 in flight at hand-back, Test Core 1/3 running its tests). The two open questions from round 1 are ruled and closed per comment 5316287386 — falsy-username fence stands, url:'' correction stands; nothing revisited.", "tests": "CORRECTED CONSUMER SWEEP — the lesson the PM named: round 1 grepped `credentialsRef`, which finds bindings but not fixtures that merely author a datasource the refinement judges. Re-derived from the real consumers of DatasourceSchema/defineDatasource (grep -rln over packages/apps/examples), then every one run at 2444d022b in a fresh worktree with its closure built: @objectstack/spec 408 files/10885 passed; @objectstack/objectql 213 files/3765 passed; @objectstack/runtime 165 files/2464 passed; @objectstack/metadata-protocol (registers the 'datasource' type) 117 files/1617 passed; @objectstack/service-datasource 21 files/480 passed; @objectstack/downstream-contract 2 files/20 passed — ~18,400 tests, all green. examples/app-crm and examples/app-showcase author datasources and are covered by the three Dogfood Regression Gate shards, all green on this run. Every mongo datasource artefact in the tree (grep -rln \"driver: *['\\\"]?mongo\") lives in packages/spec or packages/services/service-datasource, both in that list, so no fixture outside the two edited packages feeds the narrowed rule. METADATA-FS CONTROLS, both directions, each worktree built from its own sources: branch @ 2444d022b full package 3 runs -> 6 files/51 tests passed 3/3; branch, the failing file alone, 6 runs -> 6/6 passed; clean origin/main @ e4e5c6e3c, same file, 6 runs -> 6/6 passed; clean main, same file, 8 CONCURRENT runs to starve the watcher -> 8/8 passed. So it is not deterministic on main either — consistent with #7282's measured characterisation of an all-or-nothing suppression whose 20s deadline that card had already proved spent, and NOT reproducible locally on either side, which I state plainly rather than claiming a local repro I do not have. Round-1 evidence unchanged and still valid at the same sha: reverse verification 7 failed | 139 passed exactly as predicted, and the full derived gate union green (including check:type-check-debt --re-measure, 33 ledger entries, none above its recorded number).", "open_questions": [], "out_of_scope_findings": [ "filed as #9339: #7282's metadata-fs watcher-timing flake family is not closed — it now ejects unrelated PRs through watch-write-registration.test.ts, the very pin PR #7336 added to close it; 20s deadline already measured as spent, blast radius is every PR behind it in the queue" ] }
Generated by Claude Code
Generated by Claude Code
- added a commit that references this issue
on Aug 23, 2026 - added a commit that references this issue
on Sep 1, 2026 - added 3 commits that reference this issue
on Oct 7, 2026
Found while implementing #9041 (dispatch session
session_01225pUjnCKWqxcc1PeqKFUq), filed rather than folded in: the #9041 triage fences scope that refusal to a presentconfig.url("a mongoconfig.urlwhose userinfo names NO user"), so the composed spelling of the identical defect stays accepted.What happens
A mongo datasource with NO
config.url, discrete fields naming nousername, and a bound secret:buildMongoUrlinpackages/services/service-datasource/src/default-datasource-driver-factory.tscomposes the URI withconst auth = user ? ... : '';— no username, no credential, the bound secret is never used, and the datasource connects anonymously with the operator told nothing. Byte-for-byte the same "binding is a silent no-op at connect" defect #9041 closes for the URL-bearing spelling, one branch over (the #9041 card itself measured that the two branches have always agreed on this input).Why #9041 did not close it
Its triage fences (adopted from the card, binding on the dispatch) scope the refusal to the pair with
config.urlpresent; the datasource-level refinement landed by the #9041 PR deliberately skips whenurlis absent, and its test file pins the composed branch as out of scope. Extending the refusal is a one-condition widening of the same refinement (urlabsent AND the discreteusernamekey absent ANDcredentialsRefbound), but it is a fresh accept-set narrowing over a shape the fences deliberately excluded, so it needs its own triage rather than a rider.Notes for triage
external.credentialsRefbound + aconfig.urlnaming no user" — the binding is a silent no-op at connect #9041: addusernametoconfig(the discrete key is live on this branch), or drop the binding.credentialsRefon managed datasources of any driver (Every wizard-created datasource with a password is badged invalid today — the service writes external.credentialsRef onto rows whose schemaMode defaults to 'managed', which the schema refuses #8153), so the pair is reachable from the Setup form as well as authored source.external.credentialsRefreaches the knex config but NOT the server on the postgres DSN branch —pgoverrides it with the connection string #8873 measuredpgreceiving the bound password regardless of the DSN naming a user.Backlink: #9041 (URL-bearing spelling; its PR carries the refinement this would extend), #8696 (the injection whose composed branch this is), #8153 (wizard reachability).