Repository navigation
Commit 64646ae
fix(rest,runtime)!: seeing a datasource's remote structure needs the read capability on every route, writes included (#29812)
Clause-②: yes (narrowing) — `POST
/api/v1/datasources/:name/external/refresh-catalog`, `POST
/api/v1/datasources/:name/external/tables/:remote/import` and the MCP
`import_object` tool now require `manage_platform_settings` as well as
`manage_metadata`; `@objectstack/rest` adds two exports,
`federationCapabilityRefusal` and the type `FederationCallKind`.
Metadata-protocol: none
Skills-docs: none — no skill or doc page states which capability the
federation routes require; the one agent-facing text that does, the MCP
`import_object` tool description, is updated in this diff.
## 维护者速读
- **改了什么**:联邦数据源的两个写接口(刷新目录 `refresh-catalog`、导入为对象 `import`)与 MCP 的
`import_object` 工具,现在除 `manage_metadata` 外还要求
`manage_platform_settings`;这条规则只写在一处(`federationCapabilityRefusal`),REST
与 MCP 都调用它。
- **为什么改**:维护者裁决「两个写接口同时要求读权限
(Recommended)」——看到数据源的远端结构在每条路由上都需要读权限,写接口也不例外。
- **风险与回滚**:这是收窄(BREAKING):只有 `manage_metadata`、没有
`manage_platform_settings` 的凭据调用这两个写接口改为 403。内置的 `admin_full_access`
两项都有,不受影响;Studio 数据源页本来就要求读权限才能列表和起草,导入流程不变。回滚 = revert 本 PR。
- **先看**:① `packages/rest/src/external-datasource-routes.ts` 的
`federationCapabilityRefusal` 与 `refuseFederationRequest` 中调用它的那几行;②
`packages/runtime/src/domains/mcp.ts` 的 `federationPairCallable` /
`federationService`;③
`packages/qa/dogfood/test/external-federation-read-capability.dogfood.test.ts`。
- **两分钟验证**:`pnpm --filter @objectstack/rest exec vitest run --project
local src/external-datasource-routes-auth-guard.test.ts`,预期 `Tests 12
passed (12)`,其中「`manage_metadata` alone is refused all five」一条通过。
- **需要你做的**:无,席位落地(收窄已申报,契约档复核由席位执行)。
## What changes
The rule: **seeing a datasource's remote structure needs the read
capability on every route, writes included.** Maintainer ruling,
2026-11-03 (verbatim: 「两个写接口同时要求读权限 (Recommended)」).
- `packages/rest/src/external-datasource-routes.ts` — the federation
family's capability decision moves into one exported function,
`federationCapabilityRefusal(kind, held)`. A `'read'` requires
`FEDERATION_READ_CAPABILITY` (`manage_platform_settings`); a `'write'`
requires it AND `FEDERATION_WRITE_CAPABILITY` (`manage_metadata`). The
refusal names every capability the caller lacks, read first, so a caller
lacking only the read capability gets the read routes' own refusal on a
write route: `403 PERMISSION_DENIED`, same message.
`refuseFederationRequest` calls it; it still runs before the service
lookup, so a refused import saves nothing. The two constants' docblocks
state the rule and the ruling.
- `packages/rest/src/index.ts` — exports `federationCapabilityRefusal`
and `FederationCallKind`, beside the two constants already exported for
the MCP bridge.
- `packages/runtime/src/domains/mcp.ts` — the MCP federation bridge asks
the same function per call (`federationService`), and carries the
`list_remote_tables` / `import_object` pair only where some call of it
can pass (`federationPairCallable`, the existing listing rule). An
envelope holding `manage_metadata` without `manage_platform_settings`
can pass neither call, so it is carried neither tool, as an envelope
holding neither capability already was.
- `packages/mcp/src/mcp-http-tools.ts` — the `import_object` tool
description and the bridge docblock say the import requires the read and
write capabilities.
- `packages/rest/src/meta-item-read-gate.ts` — the note on why
`external_catalog` has no `/meta` WRITE row stays true: the row matches
the write door's capability for the write (`manage_metadata`,
unchanged); the read capability the refresh now also requires is for
what it answers, and the `/meta` write doors answer a receipt, never the
stored document. No row changes.
- `docs/qa/platform-checklist/areas/integration-system.json` — the two
federation items' clauses that said the writes sit on `manage_metadata`
alone now name both capabilities (history entries added).
- `.changeset/federation-writes-read-capability.md` — patch on
`@objectstack/rest`, `@objectstack/runtime`, `@objectstack/mcp`,
BREAKING banner, ADR-0087 `not-required (no-migration-prescription)`.
Before: the refresh and the import required `manage_metadata` only. Now:
they require `manage_metadata` and `manage_platform_settings`. The three
read routes, the anonymous floor and the ADR-0069 posture gate are
unchanged.
## Tests
- `packages/rest/src/external-datasource-routes-auth-guard.test.ts` —
the route table lists every capability a route requires (`READS` /
`WRITES`). The case that pinned "`manage_metadata` alone clears the
writes" is FLIPPED, not deleted: `seeing remote structure needs the read
capability on every route, writes included: manage_metadata alone is
refused all five, the writes with the read routes' status and code`,
which also asserts no service ran and the service was never looked up.
New control: `…and holding both capabilities clears the writes`. The
holding-nothing case now asserts both capabilities are named on a write
route.
- `packages/runtime/src/domains/mcp-federation-bridge.test.ts` — the
import is served for a holder of both capabilities; a holder of the
write capability alone is carried neither call (two cases, one on a
hand-built envelope, one on the envelope the real `/mcp` door resolves);
a holder of the read capability alone keeps the pair, its listing served
and its import refused `403`.
-
`packages/qa/dogfood/test/external-federation-read-capability.dogfood.test.ts`
(new) — the HTTP door on the composed showcase with
`ExternalDatasourceServicePlugin`, grants authored at runtime through
`/meta/permission`: a member holding `manage_metadata` alone is refused
the refresh and the import with the status and code `GET
…/external/tables` answers it, the refused import leaves no object (`GET
/meta/object/:name` answers `404`); control, a member holding both
capabilities, gets `200` with a non-empty catalog, `201` for the import,
and its object reads back `200` through the same door.
- `packages/rest/src/meta-type-write-capability.test.ts` — one test
title updated to match the note; no assertion changed.
## Acceptance notes
- **Public surface.** `@objectstack/rest` gains two exports; measured on
the built `packages/rest/dist/index.d.ts` (`declare function
federationCapabilityRefusal`, `type FederationCallKind`, both in the
export list). That is why the declaration reads `yes (narrowing)` rather
than `no (narrowing)`.
- **Studio.** The datasource page's import flow lists (`GET
…/external/tables`) and drafts (`POST …/draft`) before saving through
`PUT /meta/object/:name`; it never calls the `/import` route, and both
of those reads already required `manage_platform_settings`. Its
Refresh-catalog button now needs the same capability its table listing
already needed. No stock permission set grants `manage_metadata` without
`manage_platform_settings` (`admin_full_access` grants both; the
organization-admin sets grant neither).
- **Other doors into the same operations.** The admin spelling
(`service-datasource` `admin-routes.ts`) requires
`manage_platform_settings` on all eleven routes, unchanged. The SDK
(`datasources.external.*`) and the CLI are clients of the REST routes
and change with them.
- `pnpm test:repo` (the vitest `repo` project) is not run locally; it
belongs to CI's `Test Core`.
## Verification
All local readings are at head `654d2d88d9` (this branch merged with
`origin/main` `6f094645b4`), dependency closure built with `turbo run
build --filter='@objectstack/dogfood^...'`.
<details><summary>Package suites and type checks (one locked run, each
exit captured before any pipe)</summary>
```text
REST_BUILD=0 PREFLIGHT_ABSENT=0 DOGFOOD_TARGETED=0
REST_TEST=0 @objectstack/rest Test Files 381 passed (381) · Tests 7131 passed | 332 skipped (7463)
REST_TYPECHECK=0 check:test-typecheck: OK — @objectstack/rest's test layer compiles
MCP_TEST=0 @objectstack/mcp Test Files 55 passed (55) · Tests 659 passed (659)
MCP_TYPECHECK=0
RUNTIME_TYPECHECK=0
RUNTIME_TEST=0 @objectstack/runtime Test Files 533 passed (533) · Tests 8542 passed | 19 skipped (8561)
dogfood (targeted: the new file + four federation siblings) Test Files 5 passed (5) · Tests 20 passed (20)
VERDICT command-exit 0
```
`@objectstack/runtime`'s type check imports
`federationCapabilityRefusal` and `FederationCallKind` from
`@objectstack/rest`, so its green also shows it read the rebuilt
declarations: against the previous ones that import has no exported
member to resolve.
</details>
<details><summary>One-shot ablation of the rule (committed first,
mutated through scripts/ablation-replace.mjs, restore proved)</summary>
Mutation: in `FEDERATION_REQUIRED_CAPABILITIES`, `write:
[FEDERATION_READ_CAPABILITY, FEDERATION_WRITE_CAPABILITY]` replaced with
`write: [FEDERATION_WRITE_CAPABILITY]` (the write kind asking the write
capability alone).
```text
ablation-replace: anchor x1 -> x0 · replace x0 -> x1 · blob 2bc4289 -> 864bce59d50f · ok mutation landed
BUILD=0 · ablation-dist-preflight: marker present in 2 built files -- the ablation is live in the artifact the suite consumes
DOGFOOD=1 Tests 2 failed | 1 passed (3) red: both "the write capability alone is refused …" cases; the both-capabilities control stayed green
REST=1 Tests 2 failed | 10 passed (12) red: the holding-nothing case and the manage_metadata-alone case
RUNTIME=1 Tests 2 failed | 8 passed (10) red: the two "write capability alone is carried neither" cases
ablation-replace: ok restored: blob == HEAD (2bc4289) and `git diff HEAD` is empty
restore leg: rest rebuilt · ablation-dist-preflight --absent: marker absent from all 6 built files · tree clean against HEAD
re-run after restore: dogfood targeted 5/5 files, 20/20 tests
```
</details>
<details><summary>Derived gate families (node
scripts/pm/dispatch-gates.mjs --commands, 67 commands)</summary>
```text
Run reconciliation — 67 derived, 65 run, 2 NOT-MEASURED, 0 UNRUN (every command recorded with its exit code)
65 commands :: exit 0 — among them check-adr-0087-registration (exemption no-migration-prescription accepted), check-changeset-no-major, check-empty-changeset, check:platform-checklist (16 areas, 359 items OK), check:route-envelope, check:doc-authoring, check:issue-citations, check:nul-bytes, check:engine-double-contract, check:cross-package-test-inputs, check:test-source-alias, check:published-files, check:dts-closure, check:type-check-coverage
NOT MEASURED: check:dual-build-cjs-loads, reason: PREREQUISITE NOT MET, it reads every package's dist/ and seven packages outside this diff's build closure had none (studio, client-react, embedder-openai, knowledge-memory, knowledge-ragflow, service-cluster-redis, service-knowledge)
NOT MEASURED: check:type-check-debt, reason: PREREQUISITE NOT MET, its own closure build never acquired the shared verify lock (queue-timeout, exit 99)
```
Both run in CI's required jobs on this PR.
</details>
<details><summary>Full dogfood battery</summary>
Owed by the `(narrowing)` arm and the `!` title; run once on head
`654d2d88d9`, the CI command under the shared lock, no shard and no
path:
```text
pnpm --filter @objectstack/dogfood test
VERDICT command-exit 0 · held the lock 1599s
Test Files 404 passed | 3 skipped (407)
Tests 3323 passed | 28 skipped (3351)
```
</details>
---
_Generated by [Claude
Code](https://claude.ai/code/session_01QSeKYn5CmmdShCJifm6WYj)_
---------
Co-authored-by: Claude <noreply@anthropic.com>1 parent 55a4898 commit 64646ae
11 files changed
Lines changed: 494 additions & 97 deletions
File tree
- .changeset
- docs/qa/platform-checklist/areas
- packages
- mcp/src
- qa/dogfood/test
- rest/src
- runtime/src/domains
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
1461 | 1461 | | |
1462 | 1462 | | |
1463 | 1463 | | |
1464 | | - | |
| 1464 | + | |
1465 | 1465 | | |
1466 | 1466 | | |
1467 | 1467 | | |
| |||
1515 | 1515 | | |
1516 | 1516 | | |
1517 | 1517 | | |
1518 | | - | |
| 1518 | + | |
1519 | 1519 | | |
1520 | 1520 | | |
1521 | 1521 | | |
| |||
1591 | 1591 | | |
1592 | 1592 | | |
1593 | 1593 | | |
| 1594 | + | |
| 1595 | + | |
| 1596 | + | |
| 1597 | + | |
| 1598 | + | |
| 1599 | + | |
1594 | 1600 | | |
1595 | 1601 | | |
1596 | 1602 | | |
| |||
2140 | 2146 | | |
2141 | 2147 | | |
2142 | 2148 | | |
2143 | | - | |
| 2149 | + | |
2144 | 2150 | | |
2145 | 2151 | | |
2146 | 2152 | | |
2147 | | - | |
| 2153 | + | |
2148 | 2154 | | |
2149 | 2155 | | |
2150 | 2156 | | |
| |||
2169 | 2175 | | |
2170 | 2176 | | |
2171 | 2177 | | |
2172 | | - | |
| 2178 | + | |
2173 | 2179 | | |
2174 | 2180 | | |
2175 | 2181 | | |
| |||
2192 | 2198 | | |
2193 | 2199 | | |
2194 | 2200 | | |
2195 | | - | |
| 2201 | + | |
2196 | 2202 | | |
2197 | 2203 | | |
2198 | 2204 | | |
| |||
2204 | 2210 | | |
2205 | 2211 | | |
2206 | 2212 | | |
2207 | | - | |
| 2213 | + | |
2208 | 2214 | | |
2209 | 2215 | | |
2210 | 2216 | | |
| |||
2284 | 2290 | | |
2285 | 2291 | | |
2286 | 2292 | | |
| 2293 | + | |
| 2294 | + | |
| 2295 | + | |
| 2296 | + | |
| 2297 | + | |
| 2298 | + | |
2287 | 2299 | | |
2288 | 2300 | | |
2289 | 2301 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
341 | 341 | | |
342 | 342 | | |
343 | 343 | | |
344 | | - | |
345 | | - | |
346 | | - | |
| 344 | + | |
| 345 | + | |
| 346 | + | |
| 347 | + | |
| 348 | + | |
| 349 | + | |
347 | 350 | | |
348 | 351 | | |
349 | 352 | | |
| |||
840 | 843 | | |
841 | 844 | | |
842 | 845 | | |
843 | | - | |
| 846 | + | |
844 | 847 | | |
845 | 848 | | |
846 | 849 | | |
| |||
0 commit comments