Skip to content

fix(service-datasource)!: a datasource update whose new configuration fails to connect stores none of it - #29981

Merged
objectstack-fleet[bot] merged 4 commits into
mainfrom
claude/project-thread-m2ruaq
Nov 3, 2026
Merged

objectstack-fleet[bot] merged 4 commits into
mainfrom
claude/project-thread-m2ruaq

Conversation

@objectstack-fleet

@objectstack-fleet objectstack-fleet Bot commented Nov 3, 2026 •

Copy link
Copy Markdown
Contributor

Requested by Yibo Wang · project thread

Fixes #29879

Clause-②: yes (narrowing)
Metadata-protocol: none
Skills-docs: none — no file under skills/** or content/docs/** teaches what a datasource update stores when its rebuild fails, or what a blank secret does at the admin doors; the fail-closed paragraph in external-datasources.mdx (§4 Credentials) stays true.

BREAKING accept-set narrowing at the datasource admin doors (@objectstack/service-datasource): a whitespace-only secret is refused.

维护者速读

  • 改了什么:PATCH /api/v1/datasources/NAME 在新配置连不上、旧连接池被保留时,现在什么都不落盘:数据源记录、external.credentialsRef、同一次 PATCH 里的其它键都保持原样,为这次 PATCH 新建的 sys_secret 行会被删掉。接口回答不变(200、status: "error"、「新配置未生效」),现在这句话是真的。另外,全空白的 secret(" "、制表符加换行)和空串一样在入口回 400「Nothing was written」,用的是同一个守卫和同一句话。
  • 为什么改:以前先存记录、换凭据、删旧凭据,再重建连接池;重建失败时旧池继续服务,接口说「未生效」,但下次重启读到的是失败的新凭据,能用的旧凭据已经被删了(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 记录 QA run · release-18.3.0-w5-p0 (23/23) · 589049b0 · 2026-11-03 · 20 PASS / 2 PARTIAL / 1 FAIL / 0 BLOCKED / 0 NOT-RUN #29877)。
  • 风险与回滚:中低。成功路径的结果不变(新凭据存盘并生效,旧凭据退役),只是顺序从「先存后连」变成「先连后存」,失败时不存。自己实现 reregisterPool 的宿主不回报结果时行为与以前相同。回滚即 revert。
  • 先看:packages/services/service-datasource/src/datasource-admin-service.ts 的 updateDatasource 尾部(previousPoolKept 分支、putDatasourceRecord 之后才删旧凭据)与 assertSecretNotEmpty;datasource-admin-plugin.ts 的 reregisterPool 如何判断旧池被保留。
  • 两分钟验证:pnpm --filter @objectstack/service-datasource exec vitest run src/__tests__/datasource-update-kept-pool-stores-nothing.test.ts src/__tests__/datasource-empty-secret-refusal.test.ts,预期 Tests 17 passed (17)(main 上 7 条红:3 条「重启后仍连得上」、4 条空白 secret;对照两边都绿)。
  • 需要你做的:无,席位落地(这是收窄,按规矩先做合同审查)。

What changes

  • The update stores nothing of a configuration that never came into force. updateDatasource used to seal the new secret, remove the working one, store the merged record, and only then rebuild the pool. It now seals the new secret (the rebuild dereferences the merged record's credentialsRef), rebuilds, and only then decides:
    • the reregisterPool seam reports previousPoolKept: true → nothing is stored, the sys_secret row sealed for this patch is removed, no cluster signal is published (there is nothing for a peer to converge on), and the answer is toSummary(existing): 200, status: "error", the reason ending "the new configuration is NOT in force";
    • otherwise → the merged record is stored, and the replaced secret is retired only after that write. A putDatasourceRecord that throws now takes the newly sealed secret back out instead of leaving the record pointing at a removed one.
    • Which of the two routes, and why: "never write until the reconnect succeeds", so the record has ONE write path (one putDatasourceRecord, after the pool settles). The restore route would have written the record twice on failure, and its second write can fail too.
    • active: false and the no-connectivity-change register keep their order (store, then pool), unchanged.
  • The seam reports the outcome. DatasourceAdminServiceConfig.reregisterPool may now answer { previousPoolKept: boolean } (new ReregisterPoolOutcome in datasource-admin-service.ts, not exported from the index). A seam that answers nothing reads as before: the record is stored whatever the rebuild did. This is the widening half of Clause-②: yes (narrowing).
  • The plugin's seam answers it from the state the rebuild left. DatasourceAdminServicePlugin's reregisterPool reads: the new configuration is not available AND a driver still answers the name. That driver is the previous pool reconnect put back, the only driver a failed rebuild leaves registered (a rebuild with no live pool to keep is a plain connect, which registers nothing on failure). It reads the state, not the verdict the rebuild started from: after a first failed PATCH the kept pool serves under a FAILED verdict, and a second failing PATCH must store nothing either (pinned). On a kept pool the plugin's livePoolRecords entry goes back to previous, so a later cluster convergence compares the shared row against the configuration actually serving. A reconnect that throws after restoring the pool is reported as kept (logged at warn, as the admin service logged it before); one that throws with nothing kept rethrows.
  • A binder that rewrites in place. The shipped sys_secret binder mints a fresh handle per write, so removing the new row restores the binding exactly. For a host binder that reseals under the SAME reference, the working credential is already overwritten: the row is never removed (it is the binding the stored record names) and the loss is logged once at error with the remedy.
  • Blank secrets. assertSecretNotEmpty now refuses a secret whose value is whitespace only, on create and on update, with the same message (The \secret` is empty. … Nothing was written.) and the same 400 DATASOURCE_ADMIN_ERROR`. No second guard. A value with whitespace around other characters is still sealed as sent.
  • datasource-connection-service.ts is unchanged: reconnect already kept the previous pool; the defect was the store write before it.
  • Changeset .changeset/29879-datasource-update-kept-pool-stores-nothing.md: patch on @objectstack/service-datasource, both declaration lines, BREAKING banner, the remedy, ADR-0087 marker not-required (no-migration-prescription) (as 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).

Why Clause-②: yes (narrowing) and not the claim's no. The narrowing is the blank-secret refusal, declared as #28928 declared the empty string (no (narrowing) there). This diff also widens an exported type (DatasourceAdminServiceConfig.reregisterPool's answer), so the value is yes; yes (narrowing) is the spelling for a diff that widens one surface and narrows another (scripts/pm/clause2-line.mjs). Metadata-protocol stays none: secret is the doors' credential channel, not an authorable key, so the level is patch.

Tests

  • New src/__tests__/datasource-update-kept-pool-stores-nothing.test.ts: the REAL DatasourceAdminServicePlugin and ExternalDatasourceServicePlugin behind the REAL admin routes, with the REAL sys_secret binder, over one in-memory store (sys_metadata + sys_secret) that outlives a boot. A restart is a second boot over the same store. The pool connects only to database qa with the real password.
    • the card's reproduction: create with the real password → PATCH {secret: wrong, label} → 200, status: "error", "NOT in force" → the stored record and the sys_secret ids equal what they were before the PATCH → /test ok on the kept pool → restart → /test ok, and the restart built its pool with the real password;
    • a failing config change: the same, the stored config is the one serving;
    • a second failing PATCH after a first one: still nothing stored;
    • control: a secret that connects is in force and stored, the replaced sys_secret row is gone, and the restart connects.
  • Extended src/__tests__/datasource-empty-secret-refusal.test.ts (the 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 harness, real routes over the real service, every seam call recorded): PATCH with " " and with { value: "\t\n" }, POST with " " and "\t\n" → 400 DATASOURCE_ADMIN_ERROR, message contains secret and "Nothing was written.", no seam call at all. New control: a secret with whitespace around other characters is sealed as sent.
Red on main, green on head, two ablations
  • Main (the two source files restored from 18112bfa, the tests at head): Tests 7 failed | 10 passed (17). The three kept-pool pins received the card's state, for example external.credentialsRef sys_secret:sec_2 for sec_1, label: "Renamed in the same patch" stored, secretIds [sec_2] for [sec_1]; the four blank-secret pins received { status: 200 | 201, success: true }. Every control green. Restored: git diff HEAD empty.
  • Head: Tests 17 passed (17).
  • Ablation 1 (scripts/ablation-replace.mjs, if (previousPoolKept) { → if (previousPoolKept && 'ABLATED_29879' === '') {): Tests 3 failed | 1 passed (4), exactly the three kept-pool pins; the good-secret control green. Restore: blob cea697a4 equals HEAD's.
  • Ablation 2 (value.trim() !== '' → value !== '', the 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 guard): Tests 4 failed | 9 passed (13), exactly the four blank-secret pins. Restore: blob cea697a4 equals HEAD's.

Acceptance notes

  • The other caller of the seam. convergePool (a peer's rebuild after a cluster signal) ignores the answer, as it should: it writes no record. It benefits from the livePoolRecords fix only.
  • The pre-datasource update never rebuilds its driver — already-registered short-circuits the reconfigure path, so the OLD pool stays live and the admin UI reports success #13804 fallback. A host without the reregisterPool seam keeps the incumbent pool through the idempotent register and stores the new record, as before; nothing in that path says "NOT in force".
  • A rebuild with no live pool to keep (the datasource was inactive, or its last connect failed) still stores the new record when it fails: there is no serving configuration to protect, and the answer does not claim one.
  • SDK/script callers that send a whitespace-only secret now get the 400; the remedy is in the message and the changeset.

Verification

All readings at head 2e46f163 (four commits on origin/main 18112bfa; main had not moved when the gates and the dogfood ran).

Package suite, typecheck, lint
  • pnpm --filter @objectstack/service-datasource test: Test Files 71 passed (71), Tests 1169 passed (1169), exit 0 (taken before the ledger commit, which touches only scripts/engine-double-contract.pinned.json).
  • pnpm --filter @objectstack/service-datasource typecheck (tsc over src, tests included): exit 0.
  • The repo's pnpm lint command scoped to packages/services/service-datasource: exit 0, no findings.
Derived gates (`dispatch-gates.mjs --commands`, 69 families)
  • 68 exit 0, check-adr-0087-registration --base origin/main and check-changeset-no-major --base origin/main included. check:engine-double-contract first answered 1 for the new restart rig's doubles (RETAINED [delete] [findOne] [update]), then 0 after --write and the ledger commit; the 8 families that commit added all exit 0. check-plugin-teardown-shape --self-test exit 0 after fetching its pinned fixture commit.
  • check:dual-build-cjs-loads: exit 3, PREREQUISITE NOT MET (it needs dists of packages outside this closure). NOT MEASURED locally; CI's.
  • dispatch-gates --ran: 69 derived, 68 run, 1 NOT-MEASURED, 0 UNRUN.
  • Repo-wide pnpm lint and the CI-only families are CI's; see the PR's check-runs.

Dogfood battery (owed: ! title and the (narrowing) arm), once at 2e46f163 after pnpm turbo run build --filter='@objectstack/dogfood...' (69 tasks, exit 0): pnpm --filter @objectstack/dogfood test, Test Files 415 passed | 3 skipped (418), Tests 3432 passed | 28 skipped (3460), lock VERDICT command-exit 0.


Generated by Claude Code

… previous pool stores none of the new configuration

A PATCH whose new configuration failed to connect answered that the
previous pool was kept and the new configuration was NOT in force, but had
already stored the merged record, repointed external.credentialsRef at the
new secret and removed the working one. The next restart connected the
configuration that failed.

The update now rebuilds the pool before it stores the record. When the
reregisterPool seam reports previousPoolKept, nothing is stored and the
secret sealed for the write is taken back out; the working secret is
retired only after the merged record is stored. The plugin's seam reports
the outcome from the state the rebuild left.

The blank-secret guard also refuses a whitespace-only secret, with the
same message.

Claude-Session: https://claude.ai/code/session_01WGufzfDo764sexAMz4Feui
Co-authored-by: Claude <noreply@anthropic.com>
…and blank secrets at the door

Claude-Session: https://claude.ai/code/session_01WGufzfDo764sexAMz4Feui
Co-authored-by: Claude <noreply@anthropic.com>
@github-actions github-actions Bot added size/l documentation Improvements or additions to documentation tests tooling labels Nov 3, 2026
@github-actions

github-actions Bot commented Nov 3, 2026

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 1 package(s): @objectstack/service-datasource, touching 10 documentable anchor(s).

2 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:

  • content/docs/data-modeling/external-datasources.mdx (via DatasourceAdminServicePlugin (symbol, a top-level class))
  • content/docs/data-modeling/odata-datasources.mdx (via DatasourceAdminServicePlugin (symbol, a top-level class))
What this run could not see
  • the SDK route bridge reached 61 of 219 client-bound route-ledger rows — the other 158 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 158: 0 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 55 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 103 are undecided (no in-repo declaration, on a ledger that has other in-repo registrars — absence and an unreadable spelling are not distinguishable here). The rows themselves: node scripts/docs-audit/affected-docs.mjs --bridge-coverage
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.
  • a key NAME is not a key, so the hand re-read the line above prescribes can land on the wrong schema. The same spelling is authorable on one governed type and a [REMOVED] tombstone on another for each of active, aria, joins, objects, template, tools and version (censused on [finding] tools is a key on BOTH AgentSchema (tombstoned, dead) and SkillSchema (live, cloud-attested), so a name-based search attributes skill examples to the agent key — it produced a false stop-the-line alarm on PR #19059 #19093 over the liveness ledger's governed types, top-level keys); nothing in a search result distinguishes the two, so a grep hit on a LIVE example reads as evidence about the DEAD key. Measured on fix(spec): the agent.tools liveness row says dead — it claimed live on a key the schema tombstoned #19059: content/docs/ai/agents.mdx was reported as contradicting the agent.tools tombstone over its tools: example at :161, which is inside the defineSkill({ block opened at :155 — the page was already correct. Settle ownership by PARSING the value against both schemas, never by the name: that literal PASSES SkillSchema, and as an AgentSchema it FAILS at tools with the tombstone prescription. ⛔ These names are not the whole class — a key retired through a .strict() guidance map leaves no tombstone in the walked shape and none of them here (tool.category, live as AIToolDefinition.category).

Coarse fallback — 3 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json 6f7df530cd1432d0d34e0ee5cbee6b4d669e3f77 → packageMentionDocs.

Which tree this was computed on

This run read content/docs from 6440384440b929c889a0aef080fb0f4537d3c4bc — the merge of head 2e46f1637f33ef14359ec783959fea38419b2016 into base 6f7df530cd1432d0d34e0ee5cbee6b4d669e3f77, which is what actions/checkout gives a pull_request run. Not the PR head.

A worktree cut from an older main holds a different content/docs, so re-deriving there can legitimately return a different list — that is a different tree, not a wrong row. To answer on the same tree:

# while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 6440384440b929c889a0aef080fb0f4537d3c4bc && git checkout 6440384440b929c889a0aef080fb0f4537d3c4bc
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 6f7df530cd1432d0d34e0ee5cbee6b4d669e3f77 2e46f1637f33ef14359ec783959fea38419b2016 && git checkout -B drift-repro 6f7df530cd1432d0d34e0ee5cbee6b4d669e3f77 && git merge --no-ff 2e46f1637f33ef14359ec783959fea38419b2016

node scripts/docs-audit/affected-docs.mjs --json 6f7df530cd1432d0d34e0ee5cbee6b4d669e3f77

⚠️ That checkout carried uncommitted changes, so the commit above does not fully identify what was read.

Advisory only, and a precision-first one (#9192): a page is listed because it names a
symbol, wire route or SDK method this diff touched — not because it mentions a changed
package. Each row says which anchor put it there, so a wrong row is reportable rather than
merely annoying. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs 6f7df530cd1432d0d34e0ee5cbee6b4d669e3f77 → pass the list as
args.docs, on the commit named under Which tree this was computed on.

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

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

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: 2e46f1637f33ef14359ec783959fea38419b2016
Local-runs: none
Scope: narrowing face, no path hits; floor = every diff path outside the prose face: packages/services/service-datasource/src/datasource-admin-service.ts, packages/services/service-datasource/src/datasource-admin-plugin.ts, packages/services/service-datasource/src/__tests__/datasource-update-kept-pool-stores-nothing.test.ts, packages/services/service-datasource/src/__tests__/datasource-empty-secret-refusal.test.ts, scripts/engine-double-contract.pinned.json + cited code read (not reviewed): datasource-connection-service.ts (connect, recordState, availabilityOf, disconnect, reconnect, amendRetainedReason), admin-routes.ts (splitSecret, the /test, POST and PATCH doors), index.ts (export list), datasource-secret-binder.ts (write mints sys_secret:<handle.id> per call), package.json exports, datasource-admin-plugin.ts rehydratePools/convergePool, __tests__/datasource-update-rebuild.test.ts (a void-answering seam); the prose face .changeset/29879-datasource-update-kept-pool-stores-nothing.md read for ②; precedent #28928 = PR #29064 (merge 0a41075a, changeset 28928-empty-datasource-secret-refused.md, its assertSecretNotEmpty hunk) read via the API because the local clone is shallow; scripts/pm/clause2-line.mjs, scripts/pm/record-recognisers.mjs (reviewFace), scripts/check-adr-0087-registration.mjs (CATEGORIES, no-migration-prescription) for the grammar.

Face: narrowing (Clause-②: yes (narrowing) on PR body and changeset), no published schema. Run in-seat as an isolated reviewer at CONTRACT_REVIEW_TIER, fed the card, the PR and the head's check-runs only.

① Derived judgments

  • No-persist on a kept pool: correct, one write path. At head updateDatasource seals the new secret (newRef), rebuilds through tryReregisterPool(existing, merged), and on previousPoolKept calls discardUnstoredSecret and returns toSummary(existing) before putDatasourceRecord and before publishDatasourceMutation. So no key of the patch (record, external.credentialsRef, label, config, …) reaches the store, the row sealed for the patch is removed when newRef !== prevRef, and no cluster signal is published (the plugin resets livePoolRecords to previous). The answer is the retained failed/blocked verdict with reconnect's "NOT in force" suffix, which is now true. Judged right.
  • Success path unchanged in effect. The old guard if (prevRef && prevRef !== credentialsRef) tryRemoveSecret(prevRef) becomes if (newRef && prevRef && prevRef !== newRef) tryRemoveSecret(prevRef) after the single putDatasourceRecord: same condition, later position. active: false (store → unregister) and the no-connectivity-change register (store → registerPool) keep their order. The new putDatasourceRecord catch removes the unreferenced newRef and rethrows; the stored record still names the working prevRef, which was not removed. Judged right.
  • Ordering hazards. The old order (seal → remove old → put → rebuild) could lose the working credential on a crash between remove and put; the new order (seal → rebuild → put → remove old) cannot. A crash before the decision, or between seal and discardUnstoredSecret, leaves at most an orphan encrypted sys_secret row with no referrer, and the stored record keeps pointing at the working row. A failing removeSecret on the discard path is warn + orphan, the pre-existing tryRemoveSecret shape. Strictly safer than before; no must-fix.
  • POST takes its own path, unchanged. createDatasource still seals → puts → tryRegisterPool; there is no previous pool and the create answer never says "NOT in force", so the kept-pool rule does not apply. Only assertSecretNotEmpty moves on that door. Judged right.
  • Misdetection of "kept". The plugin's reading is getConnectionState(name).availability !== 'available' && getDriverByName(name) !== undefined, taken after reconnect settles. A successful rebuild records connected ⇒ available ⇒ not kept ⇒ stored (pinned by the control). A failed rebuild with no live pool to keep (inactive, or failed at boot) is a plain connect whose rollback evicts its own registration ⇒ no driver ⇒ not kept ⇒ stored with a plain error, as before. An engine without getDriverByName never produces a kept pool in reconnect either, so the two readings agree. A skipped-policy (blocked) rebuild is also "kept" and stores nothing, consistent with reconnect appending the same suffix. Judged sound for the default seam.
  • Second failing PATCH. The first leaves failed + the old driver re-registered; the second evicts, fails, restores ⇒ kept ⇒ nothing stored. Pinned. Judged right.
  • Host seam that answers nothing / throws. outcome?.previousPoolKept === true reads void as false; a throwing seam is warned and read as false; the pre-datasource update never rebuilds its driver — already-registered short-circuits the reconfigure path, so the OLD pool stays live and the admin UI reports success #13804 fallback (registerPool only) returns false. All three keep today's "store whatever the rebuild did"; the existing datasource-update-rebuild.test.ts and the 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 harness's void seam still pass untouched. The return type Promise<ReregisterPoolOutcome | void> | ReregisterPoolOutcome | void keeps every () => Promise<void> implementer compiling. Judged right.
  • Rewrite-in-place binder. When newRef === prevRef the discard path removes nothing and logs once at error with the remedy; the shipped binder mints sys_secret:<handle.id> per write, so the branch is unreachable with it. Degradation level is right for a durability loss.
  • Whitespace refusal. Same module function assertSecretNotEmpty, same two call sites (create after the name check, update after the undeclared-key / inline-ssl.key checks, both before any write, as in 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), same message text and the same 400 DATASOURCE_ADMIN_ERROR via badRequest. splitSecret normalises "\t\n" and { value: "\t\n" } alike. typeof value !== 'string' || value.trim() !== '' returns for every value the old value !== '' returned for, plus refuses blank strings; x\t is sealed as sent (pinned on written). Judged right.
  • Widening. DatasourceAdminServiceConfig is exported from index.ts; reregisterPool's answer may now carry { previousPoolKept }. ReregisterPoolOutcome is not named in the index but is reachable from the entry type graph through that member, so it is part of the published accept set; the PR declares the widening on the right symbol. Judged right.

② Semver level

patch on @objectstack/service-datasource with Clause-②: yes (narrowing) and Metadata-protocol: none on both PR body and changeset, line-initial. yes (narrowing) is the documented spelling for a diff that widens one surface (reregisterPool's answer) and narrows another (blank secret refused): clause2-line.mjs; reviewFace maps any (narrowing) arm to this face at CONTRACT_REVIEW_TIER. secret is split off the body before the draft is judged and never appears in /api/v1/meta/datasource, so none ⇒ patch; a non-protocol breaking change keeps its BANNER and ADR-0087 disposition (AGENTS.md step 3). Title carries !. Changeset: **BREAKING** accept-set narrowing …, Before / Now / Who moves, The remedy (leave secret out or null; send the new credential), marker <!-- adr-0087: not-required (no-migration-prescription) … -->, the same shape #28928 shipped and passed check-adr-0087-registration with. Skills-docs: none — … present in the PR body. Declaration consistent with the diff and the changeset level. The seat Claim's Clause-②: no is superseded by the committed tree; the PR body explains the departure. Correct.

③ Boundary flags

  • Author objectstack-fleet[bot]: no non-fleet login to name.
  • Dev open_questions: none. out_of_scope_findings: (1) a host binder resealing under the same ref cannot have the working credential restored; answered in ① (unreachable with the shipped binder; logged at error), no card owed. (2) the attribution block names the card thread; not a contract matter.
  • Concurrent PATCHes on one name are not serialised; a bad PATCH finishing after a good one could read available and store. Pre-existing on this door (the old code stored unconditionally), not a regression of this diff; not a must-fix.
  • ReregisterPoolOutcome is reachable but not re-exported from index.ts; optional follow-up, not blocking.
  • scripts/engine-double-contract.pinned.json gains three rows for the new rig's engine doubles: a ledger pin, no contract face.

④ Narrowing face

  • The narrowing (blank secret refused at POST and PATCH) takes no declared capability away: the card measured that a whitespace-only rotation breaks the datasource at the next connect exactly as "" did, and the change makes the "Nothing was written" refusal real for the one blank shape that slipped past 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. Same guard, same message, same position relative to every write. Holds.
  • The no-persist change against the card's reproduction (steps 2–7): the new rig drives the REAL plugins and REAL routes over one store, PATCHes a wrong password plus a label, asserts 200 / error / "NOT in force", atRest(store) (record and sys_secret ids) equal to before, /test ok on the kept pool, then a second boot over the same store whose /test is ok and whose factory built exactly [REAL_PASSWORD]. A failing config change and a second failing PATCH are pinned the same way; the control pins the working rotation stored, in force, old row retired, restart ok.
  • Red on main / ablations as reported: 7 red on main (3 kept-pool, 4 blank); ablating if (previousPoolKept) reds exactly the 3 kept-pool pins, ablating .trim() reds exactly the 4 blank pins. Meaningful.
  • Gaps, none blocking: no explicit pin that a void-answering seam still stores (covered indirectly), no pin for the putDatasourceRecord-throws branch removing newRef.

Implemented-by: claude/project-thread-m2ruaq
Reviewed-by: session_01WGufzibcN9oHa8kDbTTnL5

VERDICT: PASS

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

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

席位核对

Verdict: ACCEPT at head 2e46f1637f33ef14359ec783959fea38419b2016 · 2026-11-03T22:22Z

  • Shape: 2 service files (datasource-admin-service.ts, datasource-admin-plugin.ts), one new real-plugin restart rig, the 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 harness extended, 3 engine-double ledger rows, one patch changeset with the BREAKING banner and ADR-0087 marker not-required (no-migration-prescription) as 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 shipped. No published schema, Metadata-protocol: none. CONTRACT_REVIEW_TIER review PASS 6616902048 at this head.
  • Clause-②: the Claim said no; the committed tree declares yes (narrowing) (exported seam answer widened, blank secret refused). The reviewer confirmed the spelling and the patch level; the committed declaration supersedes the Claim.
  • Premise measured: the restart rig reproduces the card's steps 2–7 through the real routes; 7 red on main (3 kept-pool, 4 blank secret), 17/17 green at head; each ablation reds exactly its own pins.
  • Gates: service-datasource 1169/1169, typecheck 0, scoped pnpm lint 0, dispatch-gates 68/69 exit 0, 1 NOT-MEASURED (check:dual-build-cjs-loads, CI's). Landing waits for every check on this head to be green or an expected skip.
  • Accepted deviations: harness-pinned branch claude/project-thread-m2ruaq; the attribution block links the card thread. The signed trailer is appended by the seat through the relay. Optional follow-ups the reviewer named (re-export ReregisterPoolOutcome, a void-seam pin, a put-throws pin) are not owed by this card.
  • Landing: on all-green at this head, the seat sends pr_ready + automerge_enable through the relay.

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

Labels

documentation Improvements or additions to documentation size/l tests tooling

Projects

None yet

1 participant