Skip to content

fix(service-datasource): an adopted plugin driver's failed-credentials verdict clears on the next granted borrow, and the connector path withholds the form-encoded credential spelling - #27990

Merged
objectstack-fleet[bot] merged 5 commits into
mainfrom
claude/project-thread-k4n62h
Oct 30, 2026
Merged

objectstack-fleet[bot] merged 5 commits into
mainfrom
claude/project-thread-k4n62h

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Requested by 田騏 · project thread

Fable review owed. Clause-②: yes (widening) (a new optional member on a published @objectstack/types contract), so it lands after a contract review PASS on the current head.

Fixes #27919

Clause-②: yes (widening)
Metadata-protocol: none

维护者速读

  • 改了什么: 插件驱动(driver-odata)的数据源若在启动时因为连接器实例借不出凭据(未声明、已停用、没有 auth)被判 failed-credentials,现在实例修好后,下一次出借该实例凭据成功时,这个判定和引擎的不可用标记就自动撤销,不用重启。出借方(自动化服务)新增可选的 onLent(listener),每次成功出借都按实例名通知;数据源连接服务订阅它。另外,连接器自身调用路径的 credentialForms 改为直接用 borrowedCredentialForms,补上了表单编码(空格写成 +)这种写法的脱敏。
  • 为什么改: feat(service-datasource): a datasource naming a connector instance in external.connector borrows its credential, and its driver presents it per request (ADR-0015 D7) #27819 契约复审(记录 6514494060)的两条 should-fix:判定在修复后仍挂到重启,Setup → Datasources 一直显示失败;两份凭据写法清单重复,且连接器路径漏了表单编码。
  • 风险与回滚: 只撤销「同一个实例」的判定,别的实例出借成功不影响(有测试)。驱动若已被移除、或数据源之后又被重连或删除,都不会被改写。没有出借方,或出借方没有 onLent,行为与现在相同,降级启动横幅仍写「需重启」。回滚即 revert。
  • 先看: packages/services/service-datasource/src/datasource-connection-service.ts 的已注册守卫、subscribeToLends、retractOnLend;packages/types/src/connector-credential-source.ts 的 onLent;packages/services/service-automation/src/connector-credentials.ts 的 credentialForms。
  • 两分钟验证: pnpm --filter @objectstack/service-datasource exec vitest run src/__tests__/datasource-connector-credential.test.ts,预期 24 个用例全绿,其中新的 5 个在 "clears on the next granted borrow" 下。
  • 需要您做的: 无。契约复审 PASS 后落地。

What changes

The contract (packages/types/src/connector-credential-source.ts): IConnectorCredentialSource gains an optional onLent(listener). The listener is called with the connector instance's name each time borrow grants a handle, whoever asked (a driver's request, a connect, a probe); it returns the unsubscribe; a listener's throw never fails the borrow. A borrow judges the declaration, so a grant proves the instance can lend now. The docblock of borrowedCredentialForms now says the connector call path takes its list from it.

The lender (packages/services/service-automation/src/connector-credential-source.ts): implements onLent. A granted borrow is announced after the declaration check passes and before the handle is built; a refusal is never announced; each listener is called in its own try/catch.

The borrower (packages/services/service-datasource/src/datasource-connection-service.ts): when the already-registered guard (the one connect an adopted plugin driver reaches) records failed-credentials, the service subscribes to the lender's onLent (once per lender) and keeps the datasource's name with the instance it names and the reason recorded. On a grant for THAT instance, retractOnLend re-records the datasource already-registered, which clears the engine's unavailable mark, but only while the retained state is still that verdict and the plugin's driver is still registered. A grant for any other instance leaves it standing. A connect, reconnect or disconnect of the datasource drops the entry (it records its own verdict), and disconnectAll releases the subscriptions. With no lender, or a lender without onLent, nothing changes: the verdict stays until a restart. handleFailure takes an optional recovery sentence for the degraded-boot banner, so the guard's banner says the first granted borrow clears it; every other caller keeps the "reachable AND the server is restarted" text.

The connector call path (packages/services/service-automation/src/connector-credentials.ts): credentialForms(value, username?) keeps its signature and its three callers, and now delegates to borrowedCredentialForms (a basic auth when a username is given, else a bearer one). Its local base64 helper is gone. The list gains the form encoding URLSearchParams writes.

Changeset: patch on @objectstack/types, @objectstack/service-automation and @objectstack/service-datasource (Metadata-protocol: none takes patch).

Why

The two non-blocking should-fixes of the delta contract review of #27819 (record 6514494060, VERDICT PASS on 3351ae35), filed as this card under epic #27261. The card preferred clearing the verdict on the next successful borrow over rewording the banner; this does that, and the banner states which recovery applies.

Verification

On the branch head 016e23cd (origin/main 9900366e merged in; its incoming commits touch none of these files, and the service-automation suite was rerun after the merge: 219 files, 2824 passed):

  • New tests. service-datasource datasource-connector-credential.test.ts (5): on a real ObjectQL engine and the real DatasourceAdminServicePlugin, a disabled instance records failed-credentials with the engine's mark; a grant for another instance (salesforce_crm) leaves both; after the repair, a grant for sap_s4 re-records already-registered, availability available, no reason, no mark, driver still registered. One subscription per lender for two waiting datasources, released by disconnectAll. A disconnect before the grant resurrects nothing. A driver evicted before the grant keeps the verdict. The degraded-boot banner names the next granted borrow with a lender that has onLent, and keeps the restart sentence with one that lacks it. service-automation connector-credential-source.test.ts (2): onLent names the instance on a grant, never on a refusal (disabled, undeclared), and stops after the unsubscribe; a throwing listener does not fail the borrow and later listeners still run. connector-credential-door.test.ts (1): an api-key instance whose upstream 401 body echoes the form-encoded query string; no form-encoded, percent-encoded or raw spelling reaches the degraded reason, the credential listing or the logs.
  • Ablations, each red then restored (git status --porcelain empty after each): retractOnLend made a no-op: the retraction case fails; the connector-name match removed: the same case fails (the other-instance grant clears it); the form-encoded spelling filtered out of credentialForms: the new door case fails.
  • Typecheck: @objectstack/types, @objectstack/service-automation (with check:test-typecheck), @objectstack/service-datasource: exit 0. Lint: eslint on the changed files, clean.
  • Suites: @objectstack/types 33 files, 832 passed; @objectstack/service-datasource 65 files, 1069 passed; @objectstack/service-automation 219 files, 2821 passed; @objectstack/driver-odata (the borrowing presenter, unchanged) 7 files, 727 passed.
  • Dispatch gates: dispatch-gates --commands derived 62 families on the merged head; all run with exit codes recorded before any pipe; --ran: 62 derived, 62 run, 0 NOT-MEASURED (a DERIVED zero). On the first pass check-plugin-teardown-shape --self-test and check:dual-build-cjs-loads exited 3 (fixture commit not fetched, packages not built) and passed once those prerequisites were met.

Acceptance notes

  • credentialForms is kept as the connector path's adapter, not removed. Its callers hold a value and an optional basic username, not a BorrowedConnectorAuth, and one of them (the credential door's write) has no auth at all. The adapter is two lines over borrowedCredentialForms, so there is one list of spellings.
  • The D2 gate is unchanged. A plugin-driver datasource no object binds to gets no connect verdict at all, as before, so there is nothing to retract.
  • A successful borrow is a declaration verdict, as the guard's was. It does not prove the credential value is set; the handle still refuses a missing value per request, which is the guard's own scope.

Human Minimum

  • 1. Schemas start as Zod; packages/spec holds no runtime logic. (Not touched.)
  • 2. Every name I added uses the casing for its kind.
  • 3. No formatter ran; eslint on the changed files is clean.
  • 4. One issue, one branch off an up-to-date main, one pull request.
  • 5. A change that publishes carries a changeset, and both declaration lines are filled in here and in the changeset.
  • 6. packages/spec touched: not touched.
  • 7. No hand edits to content/docs/references/, packages/*/CHANGELOG.md or content/docs/releases/.
  • 8. Title, body and comments are in English (the 维护者速读 block is the repo's own Chinese reader section).
  • The Before Submitting list in CONTRIBUTING.md is done.

Generated by Claude Code

🤖 Generated with Claude Code

https://claude.ai/code/session_017ihL2eVFL4vpCbzypgy7JU

…edentials verdict on the next granted borrow

The connector credential source announces each granted borrow (onLent),
the datasource connection service retracts the already-registered guard's
verdict for that instance, and the connector call path takes its withheld
spellings from borrowedCredentialForms.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017ihL2eVFL4vpCbzypgy7JU
@github-actions github-actions Bot added size/m documentation Improvements or additions to documentation tests tooling labels Oct 30, 2026
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 3 package(s): @objectstack/service-automation, @objectstack/service-datasource, @objectstack/types, touching 13 documentable anchor(s).

⛔ 1 release-owned page(s) name something this change touched. These are read-only:

  • content/docs/releases/v17/17-0.mdx (via DatasourceConnectionService (symbol, a top-level class))

content/docs/releases/ is RELEASE-OWNED (AGENTS.md "Documentation Guardrails"): release
notes are written centrally at release time, and a code PR that edits them is the exact PR
that guardrail exists to stop. They are still audited — read-only. If one of them is actually
wrong, file an issue or open a dedicated docs-only PR; do not edit it here.

What this run could not see
  • 1 name(s) were too generic to anchor anything (single lowercase words)
  • the SDK route bridge reached 58 of 210 client-bound route-ledger rows — the other 152 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 152: 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; 97 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 — 16 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 b6db3692e2f3103c2a06d3cb53320d706dc42ddc → packageMentionDocs.

Which tree this was computed on

This run read content/docs from 0ffea305ede5a118e43037eff9569ec632908cff — the merge of head 016e23cd5c389a8642ef5f514b7c90aa8b558620 into base b6db3692e2f3103c2a06d3cb53320d706dc42ddc, 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 0ffea305ede5a118e43037eff9569ec632908cff && git checkout 0ffea305ede5a118e43037eff9569ec632908cff
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin b6db3692e2f3103c2a06d3cb53320d706dc42ddc 016e23cd5c389a8642ef5f514b7c90aa8b558620 && git checkout -B drift-repro b6db3692e2f3103c2a06d3cb53320d706dc42ddc && git merge --no-ff 016e23cd5c389a8642ef5f514b7c90aa8b558620

node scripts/docs-audit/affected-docs.mjs --json b6db3692e2f3103c2a06d3cb53320d706dc42ddc

⚠️ 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 b6db3692e2f3103c2a06d3cb53320d706dc42ddc → 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

epic:#27261#1 · marchtian · session_017ihL2c5z3q4h8inrn5kwqV

Contract review

Served-tier: TIER_DEFAULT
Head-sha: 016e23cd5c389a8642ef5f514b7c90aa8b558620
Local-runs: none

① Derived judgments

  • Published surface widened, nothing narrowed. IConnectorCredentialSource (@objectstack/types) gains an OPTIONAL onLent(listener): () => void. A source that does not implement it still type-checks and keeps working, and every reader guards with typeof source.onLent === 'function'. Correct as Clause-②: yes (widening). No packages/spec/src/** or governed path is in the file list (check-governed-merges --pr 27990: not governed), so the declaration face is the only face and the tier is TIER_DEFAULT.
  • Lender. createConnectorCredentialSource announces the instance name after lendRefusal passes and before the handle is built, so only a granted borrow is announced. Listener throws are swallowed per listener, and iterating over a copy keeps unsubscribe-during-announce safe. This matches the interface text ("a listener's throw never fails the borrow").
  • Consumer. DatasourceConnectionService subscribes once per lender (keyed by the source object), and only on the already-registered guard's failure path. retractOnLend re-records already-registered only if three things hold: the grant is for the SAME connector, the retained state is still that failed-credentials verdict with the same reason, and the plugin's driver is still registered. I read recordState at this head: already-registered maps to an available status, so clearDatasourceUnavailable runs. The claim "clears the unavailable mark" holds. attemptConnect, disconnect and shutdown each drop the entry, so a later verdict is never retracted by a stale entry, and shutdown unsubscribes every lender.
  • Banner. The new recovery argument replaces only the "until … restarted" clause, and only when a lender with onLent exists. With no lender, the old text stands. The text is accurate.
  • credentialForms. It now delegates to borrowedCredentialForms. The new list is a superset of the old one: it keeps value, encodeURIComponent, unpadded base64 and base64(user:pass), and adds the URLSearchParams form encoding. It applies the same 4-character floor and the same longest-first order. Nothing previously withheld is now leaked.

② Semver level

Metadata-protocol: none with a widening of a non-protocol surface ⇒ patch (AGENTS.md). The changeset has patch on @objectstack/types, @objectstack/service-automation and @objectstack/service-datasource, the three packages the diff moves. Consistent.

③ Boundary flags

  • The dev report's open_questions is empty.
  • Retraction trigger. Any granted borrow of the instance counts, whoever asks (the driver's request, another datasource, a connector call). That is right: a borrow judges the live declaration, so any grant proves the instance can lend now. It is documented on the interface.
  • Author. The PR user.login is objectstack-fleet[bot].

Implemented-by: claude/project-thread-k4n62h
Reviewed-by: session_017ihL2c5z3q4h8inrn5kwqV

VERDICT: PASS

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

epic:#27261#1 · marchtian · session_017ihL2c5z3q4h8inrn5kwqV
ACCEPT · PR #27990 @ 016e23cd5c389a8642ef5f514b7c90aa8b558620 · 2026-10-30T06:53Z

维护者速读

席位意见:ACCEPT。契约复核 PASS 在案(6516578586,TIER_DEFAULT,声明面,就是当前 head)。无 must-fix。

席位核对

  1. Shape: +365/−23 across 8 files: the optional onLent on the types contract, the lender's announce, the connection service's subscription and retraction with the banner's recovery text, credentialForms delegating, tests, and the changeset.
  2. Premise measured: on main the already-registered guard records failed-credentials and nothing ever re-records it, while the adopted driver borrows on every request (os-dev-report 6516553744).
  3. Gate check-runs on 016e23cd: 48 runs: 38 success, 10 skipped, 0 failed, none pending. Local: 62 derived gates, all exit 0.
  4. Accepted deviations: none.
  5. Out of scope: none.
  6. Landing: not governed (check-governed-merges --pr 27990; 388 lines ≤ 3000). A same-shape PASS stands on the current head. The seat lands it 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/m tests tooling

Projects

None yet

2 participants