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: PATCH with an empty secret replaces the stored credential with an empty one, and the status reason then blames a missing secret row #28928
QA-source: #28927 · integration-system.datasource-credential-refusal-matrix · observed beside the clauses
What happens
An admin PATCH /api/v1/datasources/:name whose body carries "secret": "" answers 200 and destroys the datasource's working credential:
the server mints a NEW sys_secret holding an empty value (a 43-byte ciphertext), points external.credentialsRef at it, and deletes the previous secret row;
statusReason then reads "credential 'sys_secret:…' could not be resolved or decrypted (missing sys_secret row, or the encryption key changed)", which is wrong: the row exists, it just holds an empty value;
hasSecret still reads true, and the already-open pool keeps serving (/test stays ok), so the datasource fails only at the next connect or reboot.
{"secret": null} is a no-op, and a real secret re-binds correctly. The console only sends secret when it is non-empty (ui/packages/app-shell/src/views/metadata-admin/datasource/DatasourceResourcePage.tsx around L450), so only a direct API caller reaches this. Admin-only door; no credential is disclosed.
Measured at framework 7fa9873f (main, 2026-11-02) on a stock showcase boot from a scratch copy, with a scratch PostgreSQL 16 (scram-sha-256).
Reproduction
Boot the showcase (scratch copy) and a PostgreSQL that requires a password.
As admin, POST /api/v1/datasources{"name":"r7pgb","driver":"postgres","config":{"host":"127.0.0.1","port":PGPORT,"database":"qa","username":"qa"},"secret":"REALPASSWORD"} → 201, status ok; POST /api/v1/datasources/r7pgb/test → ok.
Expected: an empty secret is treated as absent (no change), or refused 400 naming secret.
Actual:GET /api/v1/datasources/r7pgb shows a new credentialsRef; the old sys_secret row is gone and the new one holds an empty value; statusReason names a missing row. Restart the server: the datasource no longer connects.
splitSecret in packages/services/service-datasource/src/admin-routes.ts (around L664) turns "" into {value: ""} instead of treating it as absent, so the rotate path runs. A sibling of the literal-mask PATCH recorded on #24579 §7.
Read fresh. splitSecret (packages/services/service-datasource/src/admin-routes.ts) treats "secret": "" as a value. An admin PATCH therefore rotates the working credential into an empty one, and the status reason then blames a missing row. The door is admin-only and no credential is disclosed, so this is not a SECURITY flag. It is a user-visible behaviour defect that breaks the datasource at the next reconnect, so it is p2 in domain:server, and it goes to pm:queue.
Claim: PM loop round 1 · card thread · 2026-11-02T05:19Z
Session: session_01R1q2xdtYgU6FKrZ7LgjPCC (the seat); the card thread is session_01FEkQ5mTYb1BTJNRqf3bdJ8 (the #28735 / #28776 / #28831 / #28863 card thread, continuing; it restarts from main only after PR #28955 merges)
Account: os-project-manager (the seat's linked user; the card's assignee)
Branch: claude/project-thread-xqgh8b (the card thread's harness-pinned branch; it names no issue, so this claim is the identity record)
Worktree: the card thread's own container checkout
Domain: domain:server
Seat: domain:server#1
Ruling-ref: triage#1 graded the card p2 (05:07Z): admin-only, no credential disclosed, not a SECURITY flag. Route (the dispatching seat's call): an empty secret is refused 400 naming secret at the door, on create and update; null stays a no-op. Dispatched in the seat-1 split of the server queue under 「队列进了很多任务,抓紧派发」 (Leehom, 2026-11-02T05:15Z) and 「定级你可以自己决定」 (Leehom, 2026-11-01T12:11Z).
File surface: splitSecret and the create/update doors in packages/services/service-datasource/src/admin-routes.ts, and the credential status reason if an empty stored value is reachable; their tests and changesets. ⛔ No packages/spec/**, no new error code, no new gate.
Container & model: S, mode:thread, model and effort as the thread was opened
Clause-②: no (narrowing) (expected: "secret": "" that rotated the credential into an empty one is refused; the dev measures)
Metadata-protocol: per AGENTS.md, measured by the dev (likely none)
Responsibility: the code is the datasource admin doors of @objectstack/service-datasource · the platform path is an admin PATCH that silently replaces a working credential with an empty one · reached by every direct API caller of PATCH /api/v1/datasources/:name
Thread-read: none
Premise: holds on main7fa9873f (the card's PostgreSQL repro)
Serial constraints cleared: PR #28955 (this thread's previous card) must merge before the branch restarts. No open server card edits admin-routes.ts.
Resume-from: none — fresh from origin/main
State: pm:queue → pm:dispatched, assigned to os-project-manager, recorded by this claim.
os-dev-report
{
"issue": 28928,
"status": "done",
"branch": "claude/project-thread-xqgh8b",
"pr": "#29064",
"session": "session_01FEkQ5mTYb1BTJNRqf3bdJ8",
"premise_still_valid": true,
"summary": "POST /api/v1/datasources and PATCH /api/v1/datasources/NAME refuse a secret whose value is the empty string ("" or {value:""}) with 400 DATASOURCE_ADMIN_ERROR naming secret, before anything is written: no sys_secret row, no credentialsRef repoint, no removeSecret, no record put. assertSecretNotEmpty sits in datasource-admin-service.ts beside the #28549/#28612 undeclared-key and inline ssl.key refusals and reuses their shape (plain Error answered by badRequest); the doors have no VALIDATION_ERROR, no new code (seat confirmed). null, absent and whitespace-only secrets unchanged (whitespace is sealed and resolve() returns it as sent, so it is usable; seat confirmed not refusing it). Ruling 4: an empty stored row is reachable (legacy rows; resolve() returns ''), so DatasourceConnectionService.connect now says the credential is empty instead of blaming a missing row or a changed key; still failed-credentials, no driver built. Changeset patch on service-datasource, Clause-②: no (narrowing), Metadata-protocol: none, BREAKING banner, ADR-0087 not-required (no-migration-prescription). No packages/spec, no new gate.",
"step1_measurement": "main f7c545a, real routes over the real service: "" create 201 seals empty and binds it; "" update 200 writes a new sys_secret holding '', repoints credentialsRef, removes the working row; " " sealed as the 3-space value on both, resolve() returns it unchanged; null create binds no ref, update no-op. Callers: none sends an empty secret on purpose (console DatasourceResourcePage.tsx sets body.secret only when truthy, save and test; client SDK has no datasource create/update; hotcrm none).",
"tests": "New datasource-empty-secret-refusal.test.ts (real routes, real service, in-memory stores): PATCH "" and {value:''} refused 400 naming secret, record equals seed, nothing written, zero seam calls; POST "" same, nothing stored; controls: real secret rotates on PATCH (new ref, working ref removed) and binds on POST, null and absent on PATCH change no credential, null on POST binds no ref. New D3 case in datasource-connection-service.test.ts: resolver '' fails closed with the empty-credential reason. Base (source at f7c545a, tests at head): Tests 4 failed | 107 passed (111), the 3 refusal pins got {status:200,success:true}, the reason pin got the 'could not be resolved or decrypted' text; 5 controls green. Head 111/111. Ablation (ablation-replace, guard replaced by return;): Tests 3 failed | 5 passed (8), exactly the 3 refusal pins; restore blob = HEAD 1933a09e, git diff HEAD empty. At 66a746c: service-datasource Test Files 69 passed, Tests 1152 passed; typecheck (tsc over src, tests included) exit 0; repo lint command scoped to packages/services/service-datasource exit 0.",
"gates": "dispatch-gates --commands derived 62 at 66a746c; all 62 exit 0 (record pm-orders/server/28928-gates-ran.txt); --ran: 62 derived, 62 run, 0 NOT-MEASURED; check:type-check-debt OK. CI families: see PR #29064's check-runs.",
"dogfood_battery": "owed (! title, (narrowing) arm); once at final head 66a746c after turbo build --filter=@objectstack/dogfood... (exit 0): pnpm --filter @objectstack/dogfood test, Test Files 383 passed | 3 skipped (386), Tests 3163 passed | 28 skipped (3191), exit 0.",
"mcp_calls": "3 — read only: issue_read x3 (#28928 get, #28928 comments, #28927 get). No write tool.",
"api_writes": "3 — git push (claude/project-thread-xqgh8b restarted from origin/main 00d0668 after PR #28955 merged; the remote branch had been deleted, so a plain new-branch push to 66a746c), relay pr_create (#29064, draft, no assignees, no labels: a changeset is present), and this os-dev-report comment through post-stamped.",
"open_questions": [],
"out_of_scope_findings": [
"class: b · reach: named producer: migrateCredential drop-inline refusal (service-datasource datasource-admin-service.ts, the resolved == null || resolved === '' check) says the existing ref 'could not be resolved or decrypted (a missing sys_secret row, or a changed encryption key)' when the row decrypts to empty; outcome right, cause text conflates · evidence: read at 66a746c · dedupe words: migrateCredential empty credential could not be resolved · carrier: seat 1 · noted, not filed"
]
}
Release: server seat 1 shift close · 2026-11-02T07:38Z
Session: session_01R1q2xdtYgU6FKrZ7LgjPCC (the seat that claimed this card); card thread session_01FEkQ5mTYb1BTJNRqf3bdJ8 (paused)
Who / what / where: Leehom (os-project-manager) in the server-lane project chat — 「进展中的开发先提交代码然后暂停,先确保已有的pr都合并」 (2026-11-02T07:11Z), 「自动合并中的也交给下一任跟进」 (2026-11-02T07:23Z), 「我说的是pm-dispatch 中车道下班的流程,都要走完」 (2026-11-02T07:33Z). Shift close returns every claim to the queue (seat-post-protocol.md 交接收尾清单 step 2); no stay-behind.
Resume-from: claude/project-thread-xqgh8b@66a746c6f45bcac47f811b0408ad2e341306d7fa — PR #29064, ACCEPTed by the seat on this head. Enqueued for merge at handoff; auto-merge not armed at release time — if it left the queue, re-arm.
Successor: read the handoff 6571543819 on seat post #6367 first; land PR #29064 on green, write the landing record, close this card. ⛔ Do not re-dispatch the card from scratch.
State: pm:dispatched → pm:queue, assignee os-project-manager cleared, recorded by this release.
Claim: PM loop round 1 · takeover of a released card · 2026-11-02T08:16Z
Session: session_01ADHkmQjPiXgNoSaEpqeGvY
Account: os-steve (the card's assignee, set in the same round)
Branch: claude/project-thread-xqgh8b
Worktree: the predecessor card thread's (paused); no new dev is dispatched
Domain: domain:server
Seat: domain:server#1
File surface: unchanged from the predecessor's claim 6569714758 (packages/services/service-datasource/src/admin-routes.ts); the open PR #29064 is the surface.
Container & model: S, mode:thread, landing only (no new build)
Clause-②: no (narrowing)
Metadata-protocol: none
Responsibility: as in the predecessor's claim (unchanged)
Thread-read: 6571729059
Premise: holds (PR #29064 ACCEPTed by the predecessor seat)
Serial constraints cleared: none new; the seven PRs this seat inherited are its only in-flight surface.
Resume-from: claude/project-thread-xqgh8b@66a746c6f45bcac47f811b0408ad2e341306d7fa
Takeover record: the predecessor (os-project-manager, session_01R1q2xdtYgU6FKrZ7LgjPCC) released this card in 6571729059 at its shift close (Leehom: 「自动合并中的也交给下一任跟进」). This seat takes it on the maintainer's summons in the Claude Code project "objectstack spec" (「server 1 号座位你也接手开席位」), seat 2 (#20966) having left it to seat 1. Last pushed head: 66a746c6f4 (PR #29064). In the merge queue since 2026-11-02T07:01Z; follow to MERGED.
Activity
objectstack-fleet commentedon Nov 2, 2026
triage#1 · os-zhuang · session_01AavokzJ5DndAwitDXvKy4U
Path: integration-system.datasource-credential-refusal-matrix
Triage: priority:p2 · domain:server · pm:queue · 2026-11-02T05:07Z
⛔ Not a claim.
splitSecret(packages/services/service-datasource/src/admin-routes.ts) treats"secret": ""as a value. An admin PATCH therefore rotates the working credential into an empty one, and the status reason then blames a missing row. The door is admin-only and no credential is disclosed, so this is not a SECURITY flag. It is a user-visible behaviour defect that breaks the datasource at the next reconnect, so it is p2 indomain:server, and it goes topm:queue.Generated by Claude Code
objectstack-fleet commentedon Nov 2, 2026
domain:server#1 · os-project-manager · session_01R1q2xdtYgU6FKrZ7LgjPCC
Claim: PM loop round 1 · card thread · 2026-11-02T05:19Z
Session:
session_01R1q2xdtYgU6FKrZ7LgjPCC(the seat); the card thread issession_01FEkQ5mTYb1BTJNRqf3bdJ8(the #28735 / #28776 / #28831 / #28863 card thread, continuing; it restarts frommainonly after PR #28955 merges)Account:
os-project-manager(the seat's linked user; the card's assignee)Branch:
claude/project-thread-xqgh8b(the card thread's harness-pinned branch; it names no issue, so this claim is the identity record)Worktree: the card thread's own container checkout
Domain:
domain:serverSeat:
domain:server#1Ruling-ref: triage#1 graded the card p2 (05:07Z): admin-only, no credential disclosed, not a SECURITY flag. Route (the dispatching seat's call): an empty
secretis refused400namingsecretat the door, on create and update;nullstays a no-op. Dispatched in the seat-1 split of the server queue under 「队列进了很多任务,抓紧派发」 (Leehom, 2026-11-02T05:15Z) and 「定级你可以自己决定」 (Leehom, 2026-11-01T12:11Z).File surface:
splitSecretand the create/update doors inpackages/services/service-datasource/src/admin-routes.ts, and the credential status reason if an empty stored value is reachable; their tests and changesets. ⛔ Nopackages/spec/**, no new error code, no new gate.Container & model:
S,mode:thread, model and effort as the thread was openedClause-②: no (narrowing) (expected:
"secret": ""that rotated the credential into an empty one is refused; the dev measures)Metadata-protocol: per AGENTS.md, measured by the dev (likely none)
Responsibility: the code is the datasource admin doors of
@objectstack/service-datasource· the platform path is an admin PATCH that silently replaces a working credential with an empty one · reached by every direct API caller ofPATCH /api/v1/datasources/:nameThread-read: none
Premise: holds on
main7fa9873f(the card's PostgreSQL repro)Serial constraints cleared: PR #28955 (this thread's previous card) must merge before the branch restarts. No open server card edits
admin-routes.ts.Resume-from: none — fresh from
origin/mainState:
pm:queue→pm:dispatched, assigned toos-project-manager, recorded by this claim.Generated by Claude Code
objectstack-fleet commentedon Nov 2, 2026
domain:server#1 · os-project-manager · session_01FEkQ5mTYb1BTJNRqf3bdJ8
os-dev-report
{
"issue": 28928,
"status": "done",
"branch": "claude/project-thread-xqgh8b",
"pr": "#29064",
"session": "session_01FEkQ5mTYb1BTJNRqf3bdJ8",
"premise_still_valid": true,
"summary": "POST /api/v1/datasources and PATCH /api/v1/datasources/NAME refuse a secret whose value is the empty string ("" or {value:""}) with 400 DATASOURCE_ADMIN_ERROR naming
secret, before anything is written: no sys_secret row, no credentialsRef repoint, no removeSecret, no record put. assertSecretNotEmpty sits in datasource-admin-service.ts beside the #28549/#28612 undeclared-key and inline ssl.key refusals and reuses their shape (plain Error answered by badRequest); the doors have no VALIDATION_ERROR, no new code (seat confirmed). null, absent and whitespace-only secrets unchanged (whitespace is sealed and resolve() returns it as sent, so it is usable; seat confirmed not refusing it). Ruling 4: an empty stored row is reachable (legacy rows; resolve() returns ''), so DatasourceConnectionService.connect now says the credential is empty instead of blaming a missing row or a changed key; still failed-credentials, no driver built. Changeset patch on service-datasource, Clause-②: no (narrowing), Metadata-protocol: none, BREAKING banner, ADR-0087 not-required (no-migration-prescription). No packages/spec, no new gate.","step1_measurement": "main f7c545a, real routes over the real service: "" create 201 seals empty and binds it; "" update 200 writes a new sys_secret holding '', repoints credentialsRef, removes the working row; " " sealed as the 3-space value on both, resolve() returns it unchanged; null create binds no ref, update no-op. Callers: none sends an empty secret on purpose (console DatasourceResourcePage.tsx sets body.secret only when truthy, save and test; client SDK has no datasource create/update; hotcrm none).",
"tests": "New datasource-empty-secret-refusal.test.ts (real routes, real service, in-memory stores): PATCH "" and {value:''} refused 400 naming secret, record equals seed, nothing written, zero seam calls; POST "" same, nothing stored; controls: real secret rotates on PATCH (new ref, working ref removed) and binds on POST, null and absent on PATCH change no credential, null on POST binds no ref. New D3 case in datasource-connection-service.test.ts: resolver '' fails closed with the empty-credential reason. Base (source at f7c545a, tests at head): Tests 4 failed | 107 passed (111), the 3 refusal pins got {status:200,success:true}, the reason pin got the 'could not be resolved or decrypted' text; 5 controls green. Head 111/111. Ablation (ablation-replace, guard replaced by return;): Tests 3 failed | 5 passed (8), exactly the 3 refusal pins; restore blob = HEAD 1933a09e, git diff HEAD empty. At 66a746c: service-datasource Test Files 69 passed, Tests 1152 passed; typecheck (tsc over src, tests included) exit 0; repo lint command scoped to packages/services/service-datasource exit 0.",
"gates": "dispatch-gates --commands derived 62 at 66a746c; all 62 exit 0 (record pm-orders/server/28928-gates-ran.txt); --ran: 62 derived, 62 run, 0 NOT-MEASURED; check:type-check-debt OK. CI families: see PR #29064's check-runs.",
"dogfood_battery": "owed (! title, (narrowing) arm); once at final head 66a746c after turbo build --filter=@objectstack/dogfood... (exit 0): pnpm --filter @objectstack/dogfood test, Test Files 383 passed | 3 skipped (386), Tests 3163 passed | 28 skipped (3191), exit 0.",
"mcp_calls": "3 — read only: issue_read x3 (#28928 get, #28928 comments, #28927 get). No write tool.",
"api_writes": "3 — git push (claude/project-thread-xqgh8b restarted from origin/main 00d0668 after PR #28955 merged; the remote branch had been deleted, so a plain new-branch push to 66a746c), relay pr_create (#29064, draft, no assignees, no labels: a changeset is present), and this os-dev-report comment through post-stamped.",
"open_questions": [],
"out_of_scope_findings": [
"class: b · reach: named producer: migrateCredential drop-inline refusal (service-datasource datasource-admin-service.ts, the resolved == null || resolved === '' check) says the existing ref 'could not be resolved or decrypted (a missing sys_secret row, or a changed encryption key)' when the row decrypts to empty; outcome right, cause text conflates · evidence: read at 66a746c · dedupe words: migrateCredential empty credential could not be resolved · carrier: seat 1 · noted, not filed"
]
}
Generated by Claude Code
objectstack-fleet commentedon Nov 2, 2026
domain:server#1 · os-project-manager · session_01R1q2xdtYgU6FKrZ7LgjPCC
Release: server seat 1 shift close · 2026-11-02T07:38Z
Session:
session_01R1q2xdtYgU6FKrZ7LgjPCC(the seat that claimed this card); card threadsession_01FEkQ5mTYb1BTJNRqf3bdJ8(paused)Who / what / where: Leehom (
os-project-manager) in the server-lane project chat — 「进展中的开发先提交代码然后暂停,先确保已有的pr都合并」 (2026-11-02T07:11Z), 「自动合并中的也交给下一任跟进」 (2026-11-02T07:23Z), 「我说的是pm-dispatch 中车道下班的流程,都要走完」 (2026-11-02T07:33Z). Shift close returns every claim to the queue (seat-post-protocol.md 交接收尾清单 step 2); no stay-behind.Resume-from:
claude/project-thread-xqgh8b@66a746c6f45bcac47f811b0408ad2e341306d7fa— PR #29064, ACCEPTed by the seat on this head. Enqueued for merge at handoff; auto-merge not armed at release time — if it left the queue, re-arm.Successor: read the handoff 6571543819 on seat post #6367 first; land PR #29064 on green, write the landing record, close this card. ⛔ Do not re-dispatch the card from scratch.
State:
pm:dispatched→pm:queue, assigneeos-project-managercleared, recorded by this release.Generated by Claude Code
objectstack-fleet commentedon Nov 2, 2026
domain:server#1 · os-steve · session_01ADHkmQjPiXgNoSaEpqeGvY
Claim: PM loop round 1 · takeover of a released card · 2026-11-02T08:16Z
Session:
session_01ADHkmQjPiXgNoSaEpqeGvYAccount:
os-steve(the card's assignee, set in the same round)Branch:
claude/project-thread-xqgh8bWorktree: the predecessor card thread's (paused); no new dev is dispatched
Domain:
domain:serverSeat:
domain:server#1File surface: unchanged from the predecessor's claim 6569714758 (
packages/services/service-datasource/src/admin-routes.ts); the open PR #29064 is the surface.Container & model:
S,mode:thread, landing only (no new build)Clause-②: no (narrowing)
Metadata-protocol: none
Responsibility: as in the predecessor's claim (unchanged)
Thread-read: 6571729059
Premise: holds (PR #29064 ACCEPTed by the predecessor seat)
Serial constraints cleared: none new; the seven PRs this seat inherited are its only in-flight surface.
Resume-from:
claude/project-thread-xqgh8b@66a746c6f45bcac47f811b0408ad2e341306d7faTakeover record: the predecessor (
os-project-manager,session_01R1q2xdtYgU6FKrZ7LgjPCC) released this card in 6571729059 at its shift close (Leehom: 「自动合并中的也交给下一任跟进」). This seat takes it on the maintainer's summons in the Claude Code project "objectstack spec" (「server 1 号座位你也接手开席位」), seat 2 (#20966) having left it to seat 1. Last pushed head:66a746c6f4(PR #29064). In the merge queue since 2026-11-02T07:01Z; follow to MERGED.objectstack-fleet commentedon Nov 2, 2026
domain:server#1 · os-steve · session_01ADHkmQjPiXgNoSaEpqeGvY
Landed · 2026-11-02T08:34Z
0a41075ad1, through the merge queue (queued by the predecessor seat)completedbyFixes #28928pm:dispatchedis removed. Seat record: contract review PASS 6571023263, ACCEPT 6571043498 (predecessor), takeover claim 6572358100.pm:queue; domain:server seat 3 has said it takes it next.