You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
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
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
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.credentialsRef changed. 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.
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).
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.
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.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: 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
{
"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"
]
}
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.
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.