Skip to content

fix(service-datasource)!: external validate answers an unknown datasource 404 RESOURCE_NOT_FOUND - #27911

Merged
objectstack-fleet[bot] merged 1 commit into
mainfrom
claude/project-thread-rsztyz
Oct 30, 2026
Merged

objectstack-fleet[bot] merged 1 commit into
mainfrom
claude/project-thread-rsztyz

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Requested by Elon · project thread

Fixes #27696

Clause-②: no (narrowing)
Metadata-protocol: none
Skills-docs: none — no skills/** file or authored doc teaches what validate answers for an unknown name (git grep "external/validate" -- skills content docs hits only release notes and QA checklist items about the auth floor and the in-sync report)

BREAKING for a caller of POST /api/v1/datasources/:name/external/validate, or of ExternalDatasourceService.validateDatasource, that addresses a datasource name that does not exist: 200 { ok: true, results: [] } becomes 404 RESOURCE_NOT_FOUND. The changeset carries the before / now / fix and its ADR-0087 marker (not-required (no-migration-prescription)).

维护者速读

  • 改了什么:外部数据源校验接口 POST /datasources/NAME/external/validate 遇到不存在的数据源名时,改为返回 404「数据源不存在」,与查看数据源和其它联邦接口一致。
  • 为什么改:以前返回 200 和一份空的「全部通过」报告,调用方分不清「这个数据源没有联邦对象」和「这个数据源根本不存在」。
  • 风险与回滚:只影响不存在的名字;已保存的数据源(含托管数据源与主数据源)照旧 200。后台漂移检查若碰到已删除的数据源,会多记一条 warn。回滚即还原本 PR。
  • 先看:packages/services/service-datasource/src/external-datasource-service.ts 的 validateDatasource 开头三行;再看 packages/rest/src/external-datasource-unknown-name.test.ts 新增的三个用例。
  • 两分钟验证:pnpm --filter @objectstack/rest exec vitest run src/external-datasource-unknown-name.test.ts,预期 15 个用例全绿,其中未知名字 404、warehouse 与 default 仍 200 空报告。
  • 需要你做的:无,席位落地。

What changes

Tests

  • packages/rest/src/external-datasource-unknown-name.test.ts: validate on no_such_ds answers 404 RESOURCE_NOT_FOUND in GET /datasources/:name's words; validate on a saved managed datasource (warehouse) and on default keeps 200 { ok: true, results: [] }. Real ExternalDatasourceService over the real HonoHttpServer.
  • packages/rest/src/external-datasource-validate-scope.test.ts: the cell that pinned the old empty 200 for an unknown name now pins the 404, still with no introspection and no validateAll call.
  • packages/services/service-datasource/src/__tests__/external-datasource-service.test.ts: validateDatasource on an unknown name rejects with status 404 / code RESOURCE_NOT_FOUND / datasourceNotFoundError's message, with listObjects never called and nothing introspected; a saved datasource nothing is bound to still answers the empty report.
Red on main, ablation, results
  • On main (da94c11) with only the new pins: rest validate cell red, expected { status: 200, success: true, … } to deeply equal { status: 404, success: false, … }; the two 200 cells green.
  • Ablation (the three-line check removed from the service, then restored): service pin red (1 failed / 42), rest pin red (1 failed / 14); restored: 43/43 and 15/15. Both suites resolve @objectstack/service-datasource to src/ (rest's vitest alias), so no rebuild was in the path.
  • packages/rest: Test Files 352 passed (352), Tests 7023 passed / 350 skipped.
  • packages/services/service-datasource: Test Files 63 passed (63), Tests 1029 passed.
  • packages/runtime src/external-validation*: Test Files 4 passed, Tests 44 passed.
  • tsc --noEmit: rest exit 0, service-datasource exit 0.

Acceptance notes

  • None outside the card.

Verification

Gates at bdbdaae
  • node scripts/pm/dispatch-gates.mjs --commands: 62 families derived. All run; 61 exit 0; pnpm check:dual-build-cjs-loads exit 3 (PREREQUISITE NOT MET, needs every package's dist), NOT MEASURED locally, CI runs it.
  • dispatch-gates --ran: 62 derived, 61 run, 1 NOT-MEASURED, 0 UNRUN.
  • pnpm lint: exit 0.
  • node scripts/check-commit-card-trailers.mjs --range origin/main..HEAD: exit 0.
  • Full dogfood battery at the final head: reported in the os-dev-report comment.

Generated by Claude Code

…rce 404

validateDatasource(name) returned an empty, vacuously ok report for a name
no saved datasource has, so POST /api/v1/datasources/:name/external/validate
answered 200 where GET /datasources/:name and the other federation doors
answer 404 RESOURCE_NOT_FOUND. It now checks existence first, as
testConnection does, and throws the package's one datasourceNotFoundError
before anything is listed or introspected; the route relays it as such.
A saved datasource with nothing federated, and the primary one, keep their
200 empty report.

Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016WV4cx56cEs32AZSfABRR8
@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 2 package(s): @objectstack/rest, @objectstack/service-datasource, touching 4 documentable anchor(s).

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

  • content/docs/data-modeling/drivers.mdx (via /datasources/:name (route, a path literal in a comment in ExternalDatasourceService))
  • content/docs/kernel/cluster.mdx (via /datasources/:name (route, a path literal in a comment in ExternalDatasourceService))
  • content/docs/permissions/system-context.mdx (via registerExternalDatasourceRoutes (symbol, a top-level function))
  • content/docs/protocol/kernel/error-handling.mdx (via /datasources/:name (route, a path literal in a comment in ExternalDatasourceService))

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

  • content/docs/releases/v17/17-1.mdx (via /datasources/:name (route, a path literal in a comment in ExternalDatasourceService))
  • content/docs/releases/v17/17-6.mdx (via /datasources/:name (route, a path literal in a comment in ExternalDatasourceService))
  • content/docs/releases/v17/17-7.mdx (via /datasources/:name (route, a path literal in a comment in ExternalDatasourceService))
  • content/docs/releases/v18/18-2.mdx (via /datasources/:name (route, a path literal in a comment in ExternalDatasourceService))

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
  • 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 — 21 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 d770b1505e5b172c5a0cea2c22878067da2966b2 → packageMentionDocs.

Which tree this was computed on

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

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

⚠️ 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 d770b1505e5b172c5a0cea2c22878067da2966b2 → 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#3 · os-elon-musk · session_016WV4czuTWNgKR22fKv7jia

Contract-review-claim: session_016WV4czuTWNgKR22fKv7jia · head bdbdaae521a95b8baf08e553ac499060b3ccd2d0 · 2026-10-30T03:48Z

The lane seat runs the CONTRACT_REVIEW_TIER review this PR owes (Clause-②: no (narrowing)).

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:server#3 · os-elon-musk · session_016WV4czuTWNgKR22fKv7jia

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: bdbdaae521a95b8baf08e553ac499060b3ccd2d0
Local-runs: none
Scope: .changeset/27696-external-validate-unknown-name-404.md, packages/rest/src/external-datasource-routes.ts, packages/rest/src/external-datasource-unknown-name.test.ts, packages/rest/src/external-datasource-validate-scope.test.ts, packages/services/service-datasource/src/tests/external-datasource-service.test.ts, packages/services/service-datasource/src/external-datasource-service.ts + packages/services/service-datasource/src/datasource-admin-service.ts, packages/services/service-datasource/src/datasource-admin-plugin.ts, packages/services/service-datasource/src/plugin.ts, packages/runtime/src/external-validation-plugin.ts, packages/runtime/src/app-plugin.ts, packages/runtime/src/default-datasource-plugin.ts, packages/cli/src/commands/datasource/validate.ts, packages/client/src/index.ts, ui/packages/app-shell/src/views/metadata-admin/external/api.ts, ui/packages/app-shell/src/views/metadata-admin/external/ValidationPanel.tsx

① Derived judgments

  • Face: narrowing (Clause-②: no (narrowing) line-initial in PR body and changeset; record-recognisers --paths defers to that line, as its usage text says). No governed path, no published schema in the diff.
  • Ruling match (card datasources: POST /datasources/NAME/external/validate answers an unknown name 200 with an empty report, where every other by-name datasource door answers 404 RESOURCE_NOT_FOUND #27696, triage 6506948888): unknown name ⇒ 404 through the one datasourceNotFoundError, before any validation; an existing datasource with nothing federated keeps 200 { ok: true, results: [] }. The diff does exactly that: a three-line guard at the top of validateDatasource (external-datasource-service.ts:1231-1233), byte-identical to testConnection's (datasources: POST /datasources/NAME/test answers an unknown name 200 {ok:false} where every sibling NAME route answers 404 (the #25486 family) #27510, line 723), ahead of listObjects() and validateEach.
  • Q1 (who is refused): only a name for which config.getDatasource resolves nothing, with default short-circuited. getDatasource is wired to metadata.get('datasource', n) (service-datasource/src/plugin.ts:181), the same read GET /datasources/:name makes (datasource-admin-plugin.ts:437, getDatasourceRecord). Every datasource that exists has a record there: admin-created ones via metadata.register('datasource', …) (datasource-admin-plugin.ts:448), code-defined / app-bundle ones via registerInMemory('datasource', …) (app-plugin.ts:1335), the primary via DefaultDatasourcePlugin.registerVisibility (default-datasource-plugin.ts:288) and, independently, the DEFAULT_DATASOURCE_NAME short-circuit. A bare DriverPlugin registers a driver service, not a datasource (driver-plugin.ts:50-58), so it is not a datasource the sibling doors answer either. No new false-404 source beyond what testConnection and GET already have.
  • Q1 (who keeps 200): managed datasources (warehouse, no schemaMode) and default are pinned at 200 { ok: true, results: [] } over the real service and real HonoHttpServer (external-datasource-unknown-name.test.ts); the default pin runs with getDatasource returning undefined, so it pins the short-circuit itself. Existence only, not assertFederationSource (which would 400 managed and primary) — correct per ruling.
  • Q2 (before listing/introspection): the service pin asserts listObjects never called and nothing introspected; the rest scope test asserts introspected === [] and validateAll not called.
  • Q3 (consistency): datasourceNotFoundError is the single factory (datasource-admin-service.ts:352: status 404, code RESOURCE_NOT_FOUND, Datasource "NAME" does not exist.), used by GET/update/delete, assertFederationSource (datasources: GET /datasources/NAME/remote-tables and POST /datasources/NAME/object-draft answer an unknown name 400 EXTERNAL_DATASOURCE_ERROR where GET /datasources/NAME answers 404 (the #25486 / #27510 family) #27618) and testConnection (datasources: POST /datasources/NAME/test answers an unknown name 200 {ok:false} where every sibling NAME route answers 404 (the #25486 family) #27510). The route relays it unchanged via refused → answerThrown (external-datasource-routes.ts:479-480), which already special-cases {status: 404, code: RESOURCE_NOT_FOUND}; the route diff is comment-only, consistent with the PR's claim.
  • Q4 (drift check): runDriftCheck wraps validateDatasource in try/catch (external-validation-plugin.ts:~500): one warn "drift check could not be performed", return 0, no events, no throw; the timer is fire-and-forget (void this.runDriftCheck) so no unhandled rejection. The throwing path is already pinned (external-validation-plugin.test.ts:218). Timers are only cleared at plugin stop (lines 254-255), never on datasource delete — that is a pre-existing lifecycle gap; the PR turns a silent empty report into one warn per configured interval for a deleted datasource, which is the honest degradation the file's own doc comment prescribes. Not a crash loop.
  • Q6 (clients): packages/client datasources.external.validate uses unwrapResponse (throws on a failing envelope); CLI os datasource validate reads the envelope and this.error(envelope.message) on ok:false — a 404 now reports Datasource "x" does not exist. instead of "No federated objects to validate." (an improvement, exit non-zero); UI ValidationPanel catches and shows the message in error state, and is only ever invoked for a datasource it is rendering (one that exists). No caller relied on the 200 for an unknown name. Sibling repos (objectui, hotcrm) hold the same UI code only.

② Semver level

  • @objectstack/service-datasource: patch with BREAKING banner — correct: Metadata-protocol: none ⇒ patch; a non-protocol narrowing keeps its banner and ADR-0087 disposition (AGENTS.md Post-Task §3).
  • @objectstack/rest publishes no code change (comment + tests), so no second changeset is owed. The behaviour change reaches REST via the dependency; the changeset body names the route explicitly.
  • Before / Now / Fix carried in the changeset body; marker adr-0087: not-required (no-migration-prescription) is a recognised category (check-adr-0087-registration.mjs:147) and fits: no authorable key, value or stored shape moves; CI gates at the head are green (35 success / 13 skipped, 0 failing).

③ Boundary flags

  • Tier: no governed surface, no packages/spec touch, no IExternalDatasourceService interface change (validateDatasource stays off the spec interface, per its doc comment).
  • Declarations present and line-initial in both PR body and changeset; Skills-docs: none is grep-backed in the body; title carries !.
  • File surface: wider than the dispatch order (service file + test added) — amended on the claim before push, and the dev report measured why (the route already relays; the empty report was the service's). Accepted.
  • Head repo is objectstack-ai/objectstack (not a fork). PR is draft; landing is the seat's after this record and the seat's ACCEPT.
  • Note for a follow-up card, not a must-fix: scheduleDriftChecks never disarms a timer when its datasource is deleted at runtime; with this PR that datasource logs one warn per interval until restart.

④ Design judgment

  • The narrowing takes away nothing a consumer could use: the old 200 { ok: true, results: [] } for a nonexistent name was indistinguishable from "exists, nothing federated", and the ruling closes exactly that ambiguity. The spec's own refusal (RESOURCE_NOT_FOUND for an unaddressable datasource, as every by-name door answers) is made real on the last by-name door that lacked it.
  • Reusing testConnection's existence check verbatim (not assertFederationSource) is the right narrowing: it refuses only absence, and keeps managed and primary datasources at their empty report, which is what the ruling asks and what the 200 pins hold.
  • Pins are placed on both sides: service (throw shape, nothing listed/introspected) and REST (status, envelope, message in GET's words, data absent), plus the pre-existing scope test flipped rather than deleted, so the "dials nothing" guarantee survives the status change.

Implemented-by: claude/project-thread-rsztyz
Reviewed-by: session_016WV4czuTWNgKR22fKv7jia (seat subagent)

VERDICT: PASS

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:server#3 · os-elon-musk · session_016WV4czuTWNgKR22fKv7jia

Seat ACCEPT · PR #27911 · head bdbdaae521a95b8baf08e553ac499060b3ccd2d0 · 2026-10-30T03:55Z

维护者速读

POST /api/v1/datasources/:name/external/validate now answers a datasource name that does not exist with 404 RESOURCE_NOT_FOUND, in the same words as GET /datasources/:name and testConnection. Before, it answered 200 { ok: true, results: [] }, which a caller could not tell apart from "exists, nothing federated". A datasource that exists keeps its 200 empty report: the primary one, managed ones, and ones with nothing bound. The runtime drift check meets the new refusal only for a datasource deleted after its timer was armed. It lands in its existing catch: one warn, no events.

席位核对

ACCEPT

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

1 participant