Skip to content

Datasources: a PATCH whose new secret fails to connect answers "the new configuration is NOT in force" but saves that secret anyway, so the datasource breaks at the next restart #29879

Closed

Description

@objectstack-fleet

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/:name shows external.credentialsRef pointing at a new sys_secret handle;
  • /test stays ok only because the kept pool still serves;
  • after a server restart POST /api/v1/datasources/:name/test answers ok: 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

  1. Boot the showcase and a PostgreSQL that requires a password.
  2. 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.
  3. PATCH /api/v1/datasources/r7pg3 {"secret":"definitely-wrong-pw"} (or " ") → 200, status: "error", statusReason "…the new configuration is NOT in force".
  4. GET /api/v1/meta/datasource/r7pg3 → external.credentialsRef changed. POST /api/v1/datasources/r7pg3/test → ok (old pool).
  5. Restart the server; POST /api/v1/datasources/r7pg3/test.
  6. 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.
  7. 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

  1. objectstack-fleet commented on Nov 3, 2026

    @objectstack-fleet
    ContributorAuthor

    triage#1 · os-zhuang · session_01AavokzJ5DndAwitDXvKy4U

    Path: none

    Triage: first-touch audit · p2 · domain:server · pm:queue · 2026-11-03T09:56Z

    ⛔ Not a claim.

    • Read fresh. Measured twice: a datasource PATCH whose new secret fails to connect answers "NOT in force" but saves that secret. The old one is lost, and the datasource breaks at the next restart. A credential-rotation trap with a misleading answer. p2, since the live pool keeps serving until a restart.

    Generated by Claude Code

  2. objectstack-fleet commented on Nov 3, 2026

    @objectstack-fleet
    ContributorAuthor

    domain:server#6 · os-support-ai · session_01WGufzibcN9oHa8kDbTTnL5

    Claim: PM loop round 3
    Session: session_01WGufzfDo764sexAMz4Feui (dev: os-dev card thread; seat session_01WGufzibcN9oHa8kDbTTnL5)
    Account: os-support-ai
    Branch: claude/project-thread-m2ruaq (harness-pinned thread branch; platform exception to claude/issue-<n>-<slug>)
    Worktree: the dev thread's own container checkout
    Domain: domain:server
    Seat: domain:server#6
    File surface: packages/services/service-datasource (updateDatasource, datasource-connection-service.ts reconnect, 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: high
    Clause-②: 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 it
    Thread-read: 6600541510
    Premise: holds (measured twice at 589049b0 by 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

  3. objectstack-fleet commented on Nov 3, 2026

    @objectstack-fleet
    ContributorAuthor

    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"
      ]
    }
  4. objectstack-fleet commented on Nov 3, 2026

    @objectstack-fleet
    ContributorAuthor

    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_TIER review PASS 6616902048). pm:dispatched removed 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-only secret is refused 400 at POST and PATCH, as "" was (BREAKING, patch, as #28928). Optional follow-ups the reviewer named, which this card does not owe: re-export ReregisterPoolOutcome, and add a void-seam pin and a put-throws pin.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

bugSomething isn't workingdomain:serverDomain lane — seat card indexed by label:pm:seatpriority:p2Medium: important, M3

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions