Skip to content

fix(rest): relay a declared service outage from the external-datasource routes as 503 - #30952

Merged
objectstack-fleet[bot] merged 2 commits into
mainfrom
claude/project-thread-ticta6
Nov 6, 2026
Merged

objectstack-fleet[bot] merged 2 commits into
mainfrom
claude/project-thread-ticta6

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Requested by Elon · project thread

Fixes #30609

Clause-②: no
Metadata-protocol: none

domain:server#5 card thread · session session_016WV4cwJiP1ALmQ1rTttNGc · branch claude/project-thread-ticta6 (named in the seat's re-posted claim on the card). Branched from main at 33043bd.

维护者速读

  • 改了什么:外部数据源导入接口(POST /api/v1/datasources/:name/external/tables/:remote/import)遇到租户服务暂时不可用时,现在回 503 SERVICE_UNAVAILABLE(请稍后重试),不再回 400 EXTERNAL_IMPORT_ERROR(请求写错了)。同一组外部数据源接口共用这一处,所以它们遇到同类服务停摆也一样回 503。
  • 为什么改:导入要先读租户形态来决定对象落到哪个包;租户服务读不出时抛的是「服务不可用」,但这里把它当成调用方的错误,调用方会去改请求而不是重试。数据源管理接口和 MCP import_object 工具早就回 503,REST 这一处没跟上。
  • 风险与回滚:很低。只改了一个错误分支的状态码,只认同时带 status 503 和 code SERVICE_UNAVAILABLE 的错误;其它拒绝照旧 400,此前此后都不会保存任何东西。回滚即 revert 本 PR。
  • 先看:packages/rest/src/external-datasource-routes.ts 的 answerThrown(新增的 503 分支,在 404 分支之前);packages/rest/src/external-datasource-unknown-name.test.ts 末尾新增的两个用例。
  • 两分钟验证:pnpm --filter @objectstack/rest exec vitest run src/external-datasource-unknown-name.test.ts,预期 17 个用例全过,其中「answers 503 SERVICE_UNAVAILABLE and saves nothing」一条。
  • 需要你做的:无,席位落地。

What changes

answerThrown, the one catch every /datasources/:name/external/* route answers through, relayed only sandbox faults and the 404 RESOURCE_NOT_FOUND envelope; every other throw became this family's 400. Since PR #30602 the import's landing reads the effective tenancy posture (classifyAdmissionTenancyPosture), which throws AuthzStoreUnavailableError (status 503, code SERVICE_UNAVAILABLE) when a registered tenancy service cannot answer. The import therefore told the caller its request was malformed when the truth was "retry".

It now relays an error carrying status 503 and code SERVICE_UNAVAILABLE as that envelope, ahead of the 404 relay, exactly as service-datasource's badRequest does.

  • Choice of read: the envelope (status + code), not isAuthzStoreUnavailableError. It is the read both twins make (badRequest in admin-routes.ts, and the MCP import_object tool's thrownRefusalEnvelope), so the three doors cannot drift on what counts as an outage. The brand predicate would relay only the authorization-store outage and keep answering 400 for any other service that declares the same envelope. Both fields are required, so an error carrying a stray status cannot re-route itself.
  • The EXTERNAL_DATASOURCE_ERROR routes get the same relay. They share the catch, and a declared dependency outage is the same truth on a read or a draft as on the import: 400 would tell the caller to change a request that was never wrong. This matches badRequest, which serves every admin route the same way. No refusal that is not a declared outage changes: a sandboxed refusal declaring a 4xx and the metadata door's 409 DESTRUCTIVE_CHANGE keep this family's 400 and code.

File surface as claimed: answerThrown only, its test beside it, one changeset. refuseFederationRequest and the route registrations are untouched (open draft PR #30702 edits those; hunks disjoint, re-checked before push).

Tests

New top-level describe in packages/rest/src/external-datasource-unknown-name.test.ts, the file that already pins answerThrown's 404 relay over the real ExternalDatasourceService and the real HonoHttpServer. Its mount() takes config overrides now; the existing pins are unchanged. The landing hook runs the real classifyAdmissionTenancyPosture over a tenancy factory that rejects, which is the read service-datasource's plugin makes.

  • the import on that outage answers 503 SERVICE_UNAVAILABLE, its message names the failed tenancy read, carries no data, and persistObject is never called;
  • a plain landing refusal from the same seam still answers 400 EXTERNAL_IMPORT_ERROR with its own sentence, nothing saved.

Reproduction and ablation, one run (node scripts/ablation-replace.mjs, anchor envelope?.status === 503 && envelope?.code === 'SERVICE_UNAVAILABLE' replaced by false, anchor x1 -> x0, blob b99785dd -> 6d0515af): the outage pin goes red with expected { status: 400, … } to deeply equal { status: 503, … } (1 failed, 16 passed), which is the defect as main answers it; restored, blob == HEAD, git diff HEAD empty. The test imports the routes from src/ by relative path, so no build sits between the mutation and the run.

Acceptance notes

  • None.

Verification

All local readings on head 3bccb0f74e (the final head), worktree off main 33043bd; origin/main has since moved 6 commits, none touching packages/rest, service-datasource or packages/core/src/security, so not merged.

Derived gate families: 60 derived, 59 run green, 1 NOT MEASURED
  • node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack derived 60 commands; each run with its exit code captured before any pipe (record: gates-ran-3bccb0f74e.txt).
  • dispatch-gates.mjs --ran: 60 derived famil(ies) accounted for — 59 run, 1 NOT-MEASURED.
  • NOT MEASURED: pnpm check:dual-build-cjs-loads, reason: exit 3 PREREQUISITE NOT MET (reads the whole tree's dist/, which this worktree has not built; CI's run is the reading).
Package and repo checks
  • pnpm --filter @objectstack/rest test (under the verify lock): exit 0, Test Files 393 passed (393), Tests 7458 passed | 332 skipped (7790).
  • pnpm --filter @objectstack/rest typecheck: exit 0.
  • pnpm lint: exit 0.
  • pnpm check:cli-command-ids: exit 0. pnpm check:adr-symbol-anchors: exit 0.
  • test:repo (the repo vitest project) not run locally: the diff touches no file in it and the derivation does not name it; CI's Test Core runs it.
  • Full dogfood not run: no ! in the title and no (narrowing) arm.

Generated by Claude Code

🤖 Generated with Claude Code

https://claude.ai/code/session_016WV4cwJiP1ALmQ1rTttNGc

@github-actions github-actions Bot added size/s documentation Improvements or additions to documentation tests tooling Fix lands in tooling, not a product package — enters pm:queue only with Unblocks: or a named surface labels Nov 6, 2026
@github-actions

github-actions Bot commented Nov 6, 2026

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 1 package(s): @objectstack/rest, touching 2 documentable anchor(s).

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

  • content/docs/api/client-sdk.mdx (via SERVICE_UNAVAILABLE (literal, a string literal in registerExternalDatasourceRoutes))
  • content/docs/api/error-catalog.mdx (via SERVICE_UNAVAILABLE (literal, a string literal in registerExternalDatasourceRoutes))
  • content/docs/api/index.mdx (via SERVICE_UNAVAILABLE (literal, a string literal in registerExternalDatasourceRoutes))
  • content/docs/automation/webhooks.mdx (via SERVICE_UNAVAILABLE (literal, a string literal in registerExternalDatasourceRoutes))
  • content/docs/deployment/self-hosting.mdx (via SERVICE_UNAVAILABLE (literal, a string literal in registerExternalDatasourceRoutes))
  • content/docs/permissions/authorization.mdx (via SERVICE_UNAVAILABLE (literal, a string literal in registerExternalDatasourceRoutes))
  • content/docs/permissions/sharing-rules.mdx (via SERVICE_UNAVAILABLE (literal, a string literal in registerExternalDatasourceRoutes))
  • content/docs/permissions/system-context.mdx (via registerExternalDatasourceRoutes (symbol, a top-level function))
  • content/docs/protocol/kernel/error-handling.mdx (via SERVICE_UNAVAILABLE (literal, a string literal in registerExternalDatasourceRoutes))
  • content/docs/ui/print-pages.mdx (via SERVICE_UNAVAILABLE (literal, a string literal in registerExternalDatasourceRoutes))

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

  • content/docs/releases/v17/17-0.mdx (via SERVICE_UNAVAILABLE (literal, a string literal in registerExternalDatasourceRoutes))
  • content/docs/releases/v17/17-3.mdx (via SERVICE_UNAVAILABLE (literal, a string literal in registerExternalDatasourceRoutes))
  • content/docs/releases/v17/17-4.mdx (via SERVICE_UNAVAILABLE (literal, a string literal in registerExternalDatasourceRoutes))
  • content/docs/releases/v17/17-7.mdx (via SERVICE_UNAVAILABLE (literal, a string literal in registerExternalDatasourceRoutes))
  • content/docs/releases/v18/18-2.mdx (via SERVICE_UNAVAILABLE (literal, a string literal in registerExternalDatasourceRoutes))

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 63 of 222 client-bound route-ledger rows — the other 159 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 159: 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; 104 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 — 19 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 6ddf827236902c0ccaee56b65431a4c8a9c7ac09 → packageMentionDocs.

Which tree this was computed on

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

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

⚠️ 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 6ddf827236902c0ccaee56b65431a4c8a9c7ac09 → 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#5 · os-elon-musk · session_016WV4cyvF7SP65frVFAYLv7

席位核对 · ACCEPT

PR #30952 · head 3bccb0f7 · 2026-11-06T14:08Z · card #30609 (④ should-fix of PR #30602's contract review, #30469 item 2) · claim re-posted with Branch claude/project-thread-ticta6 · os-dev report 6686736735

维护者速读(席位校正)

The body's six lines stand as written, read against the diff.

席位核对

  • Card fit: answerThrown relays an error carrying both status 503 and code SERVICE_UNAVAILABLE as that envelope, ahead of the 404 relay. This is the same envelope read service-datasource's badRequest and the MCP import_object tool's thrownRefusalEnvelope make. The import on a tenancy outage now answers 503 and saves nothing.
  • Seat's implementation choices, inside the card's Expected line: the envelope read rather than the isAuthzStoreUnavailableError brand, so the three doors cannot drift. The EXTERNAL_DATASOURCE_ERROR routes share the catch and get the same relay, which is the same truth on a read or a draft. Every refusal that does not declare the outage envelope keeps this family's 400 and code.
  • Pins (read): the outage pin drives the real classifyAdmissionTenancyPosture over a rejecting tenancy factory through the real HonoHttpServer. It asserts 503 SERVICE_UNAVAILABLE, no data, and persistObject never called. The control pin asserts that a plain landing refusal still answers 400 EXTERNAL_IMPORT_ERROR with its sentence. Ablation: the anchor replaced by false turns the outage pin red with main's 400, and the restore is byte-identical.
  • Declarations: Fixes #30609, Clause-②: no, Metadata-protocol: none, and one changeset with '@objectstack/rest': patch. It is not BREAKING: an outage's status is corrected and nothing refused is admitted.
  • Review face: none of the four faces is touched. There is no Clause-②: yes, no narrowing, no packages/spec/src/** and no governed text. Per contract-review.md, CI plus the seat's own read stands in, and no second agent is started. The changeset sentence was read against the diff and holds.
  • Overlap: open draft PR fix(rest,runtime)!: the import doors and the catalog refresh read the walled-catalog verdict the metadata save door reads #30702 edits the same file in refuseFederationRequest and the route registrations. The hunks are disjoint. Whichever lands second merges main.
  • Gates (report): 60 derived, 59 run green, 1 NOT-MEASURED (check:dual-build-cjs-loads, exit 3, needs the whole tree's dist/; CI's run is the reading). pnpm lint, check:cli-command-ids and check:adr-symbol-anchors exit 0. The rest suite is 393 files green.
  • Landing: check-governed-merges --pr 30952 says NOT governed, +90/-4. The seat lands with pr_ready + automerge_enable once every check on 3bccb0f7 is green or expected-skip.

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:server#5 · os-elon-musk · session_016WV4cyvF7SP65frVFAYLv7

CI red · not this PR's · 2026-11-06T14:17Z

Test Core (7/8) (run 42230858051, job 127937068429) failed on head 3bccb0f7 in @objectstack/service-automation. The failing test is src/oauth2-grant/authorization.test.ts:138: "the KV holds no verifier, user, organization, instance or redirect URI — only an envelope".

  • Not this PR's: this diff touches packages/rest only, and nothing in service-automation imports it.
  • A probabilistic flake in the test itself: the test asserts that the JSON of a freshly encrypted envelope does not contain begun.connector, which is the three-letter string crm. The ct field is random base64url. In this run it happened to contain …gpcrmVS1…, so the assertion fired on ciphertext rather than plaintext. A random string of about 230 base64url characters contains a given three-character run roughly once in a thousand runs.
  • Fix, outside this PR: assert on the plaintext fields that should be absent (the envelope's keys and a decrypt round-trip), or give the fixture a connector name long enough that a random match is impossible. The seat files a card for it and does not widen this PR.
  • Next: the failed job is re-run once through the relay once the run completes. A second failure would be real.

@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review November 6, 2026 14:40
@objectstack-fleet
objectstack-fleet Bot enabled auto-merge November 6, 2026 14:40
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Nov 6, 2026
Merged via the queue into main with commit ea41c49 Nov 6, 2026
72 of 74 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/project-thread-ticta6 branch November 6, 2026 15:33
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/s tests tooling Fix lands in tooling, not a product package — enters pm:queue only with Unblocks: or a named surface

Projects

None yet

Development

Successfully merging this pull request may close these issues.

External import over REST answers a tenancy-service outage as 400 instead of 503

2 participants