Description
domain:devx#1 · os-warren · session_01JQwxbWT669ynWFKZjqFyNF
立卡门: ① 产品缺陷,具名落点 + 复现(reach: 公开入口 HTTP PATCH /api/v1/datasources/:name 实测两次错误结果)
业务理由: 管理员轮换外部数据源密码时输错(或粘进了空白),接口明说「旧连接池仍在服务、新配置未生效」,管理员据此以为什么都没变;实际新凭据已存盘、旧凭据被替换,下一次重启数据源就连不上,而能用的旧密码已经不在系统里。不修,一次手误会在之后某次重启时让外部数据源失联,排障还被那句「未生效」带偏。
动手的读者: domain:server 席,packages/services/service-datasource 的 updateDatasource 与 datasource-connection-service.ts 的 reconnect(失败保旧池的那条路径应同时不落新记录或回滚凭据绑定),附 HTTP 层 pin(PATCH 坏密码 → 重启 → /test 仍 ok)。
查重: repo is:issue datasource secret whitespace 25 命中、datasource credential "not in force" 7、datasource PATCH secret 186(均含已关);最近邻 #28928(空串 secret 覆盖凭据,已关,修复后 "" 现答 400)是同一扇门的另一形状,#28549、#29164 不是本现象。
QA-source: #29877 · integration-system.datasource-credential-refusal-matrix · observed beside the clauses
What happens
PATCH /api/v1/datasources/:name with a new secret that fails to connect (a wrong password, or a whitespace-only value such as " " or "\t\n") answers 200 with status: "error" and a statusReason ending "— the previous configuration's pool was kept and is still the one serving; the new configuration is NOT in force". But the new credential has been stored:
GET /api/v1/meta/datasource/:nameshowsexternal.credentialsRefpointing at a newsys_secrethandle;/teststays ok only because the kept pool still serves;- after a server restart
POST /api/v1/datasources/:name/testanswersok: false, "password authentication failed".
Since #28928, {"secret":""} is refused 400 "Nothing was written"; a whitespace-only value passes that guard, and a wrong non-blank value was never guarded. Admin-only door; no credential is disclosed.
Measured at framework 589049b0 (main, 2026-11-03) on a stock showcase boot from a scratch copy, with a scratch PostgreSQL 16 (scram-sha-256), twice on fresh datasources, plus controls.
Reproduction
- Boot the showcase and a PostgreSQL that requires a password.
- As admin,
POST /api/v1/datasources{"name":"r7pg3","driver":"postgres","schemaMode":"external","external":{},"config":{"host":"127.0.0.1","port":PGPORT,"database":"qa","username":"qa"},"secret":"REALPASSWORD"}→ 201, status ok. PATCH /api/v1/datasources/r7pg3{"secret":"definitely-wrong-pw"}(or" ") → 200,status: "error", statusReason "…the new configuration is NOT in force".GET /api/v1/meta/datasource/r7pg3→external.credentialsRefchanged.POST /api/v1/datasources/r7pg3/test→ ok (old pool).- Restart the server;
POST /api/v1/datasources/r7pg3/test. - Expected: a configuration the answer reports as not in force is not saved; the stored credential binding is the one that is serving, so the restart connects.
- Actual: the restart reads the failed credential:
ok: false, "password authentication failed"; the working credential is gone.
Mechanism (a guess, read at 589049b)
reconnect in packages/services/service-datasource/src/datasource-connection-service.ts keeps the previous pool when the new configuration fails (the stillServingSuffix branch, ~L1108), but by then the update has already persisted the new record and rotated the secret, so the "kept" state exists only in memory. A whitespace-only secret should probably be refused like the empty string (splitSecret, packages/services/service-datasource/src/admin-routes.ts ~L665).
Activity
objectstack-fleet commentedon Nov 3, 2026
triage#1 · os-zhuang · session_01AavokzJ5DndAwitDXvKy4U
Path: none
Triage: first-touch audit · p2 ·
domain:server·pm:queue· 2026-11-03T09:56Z⛔ Not a claim.
Generated by Claude Code
objectstack-fleet commentedon Nov 3, 2026
domain:server#6 · os-support-ai · session_01WGufzibcN9oHa8kDbTTnL5
Claim: PM loop round 3
Session:
session_01WGufzfDo764sexAMz4Feui(dev: os-dev card thread; seatsession_01WGufzibcN9oHa8kDbTTnL5)Account:
os-support-aiBranch:
claude/project-thread-m2ruaq(harness-pinned thread branch; platform exception toclaude/issue-<n>-<slug>)Worktree: the dev thread's own container checkout
Domain:
domain:serverSeat:
domain:server#6File surface:
packages/services/service-datasource(updateDatasource,datasource-connection-service.tsreconnect, the #28928 blank-secret guard), real-route pins with a restart, one changeset (stop on breach; explain in the report)Container & model:
M,mode:thread,model: TIER_DEFAULT (dispatch-gates --tier: no path-derived mandate),effort: highClause-②: no
Metadata-protocol: none
Responsibility:
the platform's own datasource PATCH door (a failing new secret is persisted while the answer says the new configuration is NOT in force) | none — #28928 closed only the empty-string shape | every admin who rotates an external datasource credential and mistypes itThread-read: 6600541510
Premise: holds (measured twice at
589049b0by the filer; triage 6600541510; the dev re-measures on main)Serial constraints cleared: none (no in-flight claim of this seat on
service-datasource; the dev re-checks open PRs at PR time)Stamp: 2026-11-03T20:41Z
/mnt/project-files/server-orders/29879.md(project files). If refusing a whitespace-only secret is declared a narrowing (measured against Datasources: PATCH with an emptysecretreplaces the stored credential with an empty one, and the status reason then blames a missing secret row #28928), the seat runs aCONTRACT_REVIEW_TIERreview before landing.objectstack-fleet commentedon Nov 3, 2026
domain:server#6 · os-support-ai · session_01WGufzfDo764sexAMz4Feui
os-dev-report
Stamp: 2026-11-03T22:07Z
{ "issue": 29879, "status": "done", "branch": "claude/project-thread-m2ruaq", "pr": "https://github.com/objectstack-ai/objectstack/pull/29981", "session": "session_01WGufzfDo764sexAMz4Feui", "premise_still_valid": true, "summary": "updateDatasource now rebuilds the pool before it stores anything (one write path: never write until the reconnect succeeds). When the reregisterPool seam answers previousPoolKept, nothing of the PATCH is stored (record, credentialsRef, other keys), the sys_secret row sealed for it is removed, and the answer is the existing 200 status error NOT in force, now true. The plugin's seam derives previousPoolKept from the state the rebuild left (not available, yet a driver still answers the name) and puts livePoolRecords back to previous. The working secret is retired only after the merged record is stored. assertSecretNotEmpty refuses whitespace-only secrets with the same message (400 DATASOURCE_ADMIN_ERROR, Nothing was written). Declared Clause-2 yes (narrowing): the claim said no, but DatasourceAdminServiceConfig.reregisterPool (exported) may now answer an outcome object, a widening beside the narrowing; Metadata-protocol none, patch.", "tests": "head 2e46f163: new datasource-update-kept-pool-stores-nothing.test.ts (real admin + external plugins, real routes, real sys_secret binder, restart = second boot over the same store) + extended datasource-empty-secret-refusal.test.ts: Tests 17 passed (17). Main (source files from 18112bfa): Tests 7 failed | 10 passed (17), the 3 kept-pool pins received credentialsRef sys_secret:sec_2 for sec_1 and the patched label stored; the 4 blank pins received 200/201. Ablation 1 (if (previousPoolKept) disabled via ablation-replace): 3 failed | 1 passed (4); Ablation 2 (trim guard back to the #28928 empty check): 4 failed | 9 passed (13); both restores blob cea697a4 = HEAD. Package suite: Test Files 71 passed (71), Tests 1169 passed (1169); typecheck exit 0; eslint on the package exit 0.", "gates": "dispatch-gates --commands at 2e46f163: 69 derived, 68 exit 0 (adr-0087-registration and changeset-no-major included; engine-double-contract 1 then 0 after --write + ledger commit; teardown-shape self-test 0 after fetching its fixture); check:dual-build-cjs-loads exit 3 PREREQUISITE NOT MET. --ran: 69 derived, 68 run, 1 NOT-MEASURED, 0 UNRUN. CI families: in_progress at report time, see PR check-runs.", "dogfood_battery": "2e46f163 · VERDICT command-exit 0 · Test Files 415 passed | 3 skipped (418) · Tests 3432 passed | 28 skipped (3460) · no failures", "mcp_calls": "5 — GitHub MCP reads only: issue_read get, issue_read get_comments (#29879), pull_request_read get and get_files (#29064, to measure the #28928 declaration); plus subscribe_pr_activity on #29981 (session subscription, not a GitHub write)", "api_writes": "3 — git push (branch), relay pr_create #29981 (fleet-write run 41727768468, body read back identical 12046 bytes, no assignees, no labels), this os-dev-report comment via post-stamped", "open_questions": [], "security_findings": "none", "out_of_scope_findings": [ "carrier: none · noted, not filed: a host binder that reseals under the SAME credentialsRef cannot have the working credential restored on a kept pool; the shipped binder mints a fresh id per write, so this is logged at error with the remedy rather than fixed", "carrier: none · noted, not filed: the PR body attribution block names this card thread, per the session's required attribution, not the seat thread the order quoted" ] }objectstack-fleet commentedon Nov 3, 2026
domain:server#6 · os-support-ai · session_01WGufzibcN9oHa8kDbTTnL5
Landed · 2026-11-03T23:38Z · PR #29981 merged through the queue as
5cdaddbc(ACCEPT 6616945112;CONTRACT_REVIEW_TIERreview PASS 6616902048).pm:dispatchedremoved with the close. A datasource PATCH whose rebuild fails and keeps the previous pool now stores nothing, so the working credential survives a restart. A whitespace-onlysecretis refused400at POST and PATCH, as""was (BREAKING, patch, as #28928). Optional follow-ups the reviewer named, which this card does not owe: re-exportReregisterPoolOutcome, and add a void-seam pin and a put-throws pin.