Skip to content

Commit 64646ae

Browse files
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
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
---
2+
'@objectstack/rest': patch
3+
'@objectstack/runtime': patch
4+
'@objectstack/mcp': patch
5+
---
6+
7+
fix(rest,runtime)!: seeing a datasource's remote structure needs the read capability on every route, writes included
8+
9+
Clause-②: yes (narrowing) — `POST /api/v1/datasources/:name/external/refresh-catalog` and `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`.
10+
Metadata-protocol: none
11+
12+
<!-- adr-0087: not-required (no-migration-prescription) No key of any schema is added, removed, renamed or re-shaped, no stored row or authorable spelling moves, and no export is removed or narrowed: two exports are added, and the federation family's two write doors and the MCP import tool ask one more platform capability of the caller. The remedy is a grant on the caller's permission set, never a rewrite of a package source or a stored row, so there is nothing for `objectstack migrate meta` to rewrite and no tombstone. -->
13+
14+
**BREAKING** accept-set narrowing on two published HTTP doors and one MCP tool. Maintainer ruling, 2026-11-03 (verbatim: 「两个写接口同时要求读权限 (Recommended)」).
15+
16+
- **The rule.** Seeing a datasource's remote structure needs the read capability, `manage_platform_settings`, on every route of the federation family, writes included. The two writes answer with that structure (the refresh with the catalog snapshot, the import with the definition it generated), so each requires the read capability AND the write capability, `manage_metadata`. The three read routes are unchanged.
17+
- **Before.** The refresh and the import required `manage_metadata` only.
18+
- **Now.** They require `manage_metadata` and `manage_platform_settings`. A caller holding `manage_metadata` without `manage_platform_settings` is refused with the read routes' own answer: `403 PERMISSION_DENIED`, the message naming `manage_platform_settings`. A caller holding neither is told both capabilities in one answer. The refusal comes before the service is looked up, so a refused import saves nothing.
19+
- **MCP.** `import_object` asks the same rule, through the same function. A connection whose user holds `manage_metadata` without `manage_platform_settings` can pass neither federation call now, so `tools/list` omits `list_remote_tables` and `import_object` for it, as it already did for a connection holding neither capability.
20+
- **One rule, one place.** `federationCapabilityRefusal(kind, held)` (`@objectstack/rest`) is the family's capability decision: the REST routes and the MCP bridge both call it, so no door admits a caller another refuses.
21+
- **Who sees it.** A credential (session, `sys_api_key`, or an MCP connection) whose permission sets grant `manage_metadata` without `manage_platform_settings` and that calls `datasources.external.refreshCatalog`, `datasources.external.importObject` or the MCP `import_object` tool. The shipped `admin_full_access` set grants both, so a stock administrator is unaffected; a purpose-built set is the case to check. Studio's datasource page already required `manage_platform_settings` to list and draft remote tables, so its import flow is unchanged.
22+
23+
**The remedy.** Grant `manage_platform_settings` to the calling credential's permission set, beside `manage_metadata`.

‎docs/qa/platform-checklist/areas/integration-system.json‎

Lines changed: 19 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1461,7 +1461,7 @@
14611461
"title": "External-datasource schema introspection happy path: remote-tables lists live tables with columnCount (?schema= honoured since #7955), object-draft renders a reviewable *.object.ts, both route spellings answer as one operation behind the platform auth floor",
14621462
"since": "v16",
14631463
"status": "active",
1464-
"revision": 8,
1464+
"revision": 9,
14651465
"priority": "P1",
14661466
"surface": "api",
14671467
"personas": [
@@ -1515,7 +1515,7 @@
15151515
"evidence": "the test run output + the paired live reads (table sets, drafts, and the two refusals)"
15161516
},
15171517
{
1518-
"clause": "the auth floor holds and BOTH spellings answer it IDENTICALLY — the #9593 asymmetry this item recorded through rev 1 is CLOSED, so a federation 200 to an unentitled caller is a FAIL, not the documented behaviour: an anonymous caller is refused 401 UNAUTHENTICATED on BOTH spellings (admin: requireDatasourceAdmin/#9391; federation: refuseFederationRequest's shouldDenyAnonymous arm/#9686 — fail-closed, before any service lookup so an anonymous probe cannot learn what is wired), and an authenticated caller WITHOUT manage_platform_settings is refused 403 PERMISSION_DENIED naming the capability on BOTH spellings, keyed on that capability rather than on holding merely some grant. Federation READS (tables, draft) took FEDERATION_READ_CAPABILITY = manage_platform_settings in #9901 — maintainer ruling 2026-08-20, verbatim: 「其他接受你的建议。」— and validate joined them in #10255 (verbatim: 「同意你的意见。」); the family's WRITES (import, refresh-catalog — outside this item's probes) sit on FEDERATION_WRITE_CAPABILITY = manage_metadata above the same 401 floor",
1518+
"clause": "the auth floor holds and BOTH spellings answer it IDENTICALLY — the #9593 asymmetry this item recorded through rev 1 is CLOSED, so a federation 200 to an unentitled caller is a FAIL, not the documented behaviour: an anonymous caller is refused 401 UNAUTHENTICATED on BOTH spellings (admin: requireDatasourceAdmin/#9391; federation: refuseFederationRequest's shouldDenyAnonymous arm/#9686 — fail-closed, before any service lookup so an anonymous probe cannot learn what is wired), and an authenticated caller WITHOUT manage_platform_settings is refused 403 PERMISSION_DENIED naming the capability on BOTH spellings, keyed on that capability rather than on holding merely some grant. Federation READS (tables, draft) took FEDERATION_READ_CAPABILITY = manage_platform_settings in #9901 — maintainer ruling 2026-08-20, verbatim: 「其他接受你的建议。」— and validate joined them in #10255 (verbatim: 「同意你的意见。」); the family's WRITES (import, refresh-catalog — outside this item's probes) sit on FEDERATION_WRITE_CAPABILITY = manage_metadata AND this same FEDERATION_READ_CAPABILITY above the same 401 floor (seeing a datasource's remote structure needs the read capability on every route, writes included — maintainer ruling 2026-11-03, verbatim: 「两个写接口同时要求读权限 (Recommended)」)",
15191519
"oracle": "api",
15201520
"verify": "the four probes of step 8 against admin-routes.ts (401 floor then manage_platform_settings 403) and external-datasource-routes.ts refuseFederationRequest (the same two decisions out of ONE resolveExecutionContext call — anonymous first, then FEDERATION_READ_CAPABILITY); the agreement is pinned in the twin test's 'listRemoteTables twins agree on WHO may ask (#9686, #9593, #9901)' describe, whose two unentitled cases compare the two spellings' status AND code to each other rather than to a literal. Those cases compare status and code only: that the 403 message names manage_platform_settings is read off the live responses",
15211521
"evidence": "the 401/403/200 responses"
@@ -1591,6 +1591,12 @@
15911591
"date": "2026-10-31",
15921592
"change": "acceptance[1].verify and fixtures.knownGaps[0] said a live ?schema=<x> on the SQLite fixture answers 200 with the full set. QA run #27438 measured 400 EXTERNAL_DATASOURCE_ERROR 'Cannot narrow the remote tables … named no schema for 2 of its 2 table(s)' on both spellings, and the empty and repeated forms still 200 with the full set. external-datasource-service.ts listRemoteTables throws unattributedSchemaFilterError (the #24664 fix) when a filter is in force and a table has no schema; the route reads only a string ?schema=, so a repeated key is no filter. Stronger: the verify now requires the refusal, and the full set no longer passes. Narrowing stays scored by the twin pin. Read from source at base 22ce3d13fe. Wave anchor #27377, wave-3 accuracy slice S7 (#28341)",
15931593
"ref": "#27438"
1594+
},
1595+
{
1596+
"revision": 9,
1597+
"date": "2026-11-03",
1598+
"change": "acceptance[4]: the family's writes (import, refresh-catalog) sit on BOTH capabilities now, not on FEDERATION_WRITE_CAPABILITY alone. Seeing a datasource's remote structure needs the read capability on every route, writes included (maintainer ruling 2026-11-03, verbatim: 「两个写接口同时要求读权限 (Recommended)」); the rule is federationCapabilityRefusal in external-datasource-routes.ts. This item's own probes (the two read twins) are unchanged",
1599+
"ref": "claude/sec-hardening-d"
15941600
}
15951601
]
15961602
},
@@ -2140,11 +2146,11 @@
21402146
"title": "Setup → Datasources federation UI (ADR-0015 §6.4): SchemaBrowser mirrors the remote-table listing, ImportObjectDialog lands a REAL object through the metadata channel, Refresh catalog and ValidationPanel drive the three previously-uncovered federation routes (import / refresh-catalog / validate) behind their capability floors",
21412147
"since": "v16",
21422148
"status": "active",
2143-
"revision": 7,
2149+
"revision": 8,
21442150
"priority": "P1",
21452151
"surface": "mixed",
21462152
"personas": [
2147-
"admin — authenticated, holding manage_platform_settings (federation reads: tables/validate) AND manage_metadata (federation writes: import/refresh-catalog, and the /meta write the UI import performs)",
2153+
"admin — authenticated, holding manage_platform_settings (federation reads: tables/validate) AND manage_metadata (federation writes: import/refresh-catalog, which require manage_platform_settings as well, and the /meta write the UI import performs)",
21482154
"a second authenticated persona holding NO capability, plus an anonymous probe (for the floor clause)"
21492155
],
21502156
"fixtures": {
@@ -2169,7 +2175,7 @@
21692175
"Import: pick customers → ImportObjectDialog fires POST.../external/tables/customers/draft, renders the suggested name, any review flags, and the generated *.object.ts source; click 'Import as Object' and capture the PUT /api/v1/meta/object/<name> it performs (the UI's import door IS the metadata channel — api.ts importObjectDraft); confirm the object now exists over GET /api/v1/meta and answers a read-only /data query against the external rows",
21702176
"the server-side import twin: POST /api/v1/datasources/showcase_external/external/tables/orders/import (a second, scratch import) → 201 { object }; confirm it too landed and is queryable; then delete/clean up both imported objects",
21712177
"Validation tab: run validation; capture POST .../external/validate → { ok, results } (URL-scoped work, #10537) and compare the rendered per-object verdict rows against it; on the in-sync stock fixture expect all-green; if an induced-drift scratch copy is available, confirm each reported diff kind renders with its DIFF_LABEL text",
2172-
"the floor, both sides: replay tables + validate as the no-capability session (403 PERMISSION_DENIED naming manage_platform_settings) and import + refresh-catalog as the same session (403 naming manage_metadata — FEDERATION_WRITE_CAPABILITY), plus one anonymous probe of each family (401 UNAUTHENTICATED, decided before any service lookup); also probe the UI import door's floor: the no-capability session's PUT /api/v1/meta/object/<name> refuses 403 FORBIDDEN 'Saving a metadata item requires the `manage_metadata` capability.' (packages/metadata-core/src/meta-write-capability.ts#metaWriteCapabilityVerdict, the item save door's sentence; 'Rewriting stored metadata …' is POST /meta/_migrate-stored's, and the federation routes answer PERMISSION_DENIED)",
2178+
"the floor, both sides: replay tables + validate as the no-capability session (403 PERMISSION_DENIED naming manage_platform_settings) and import + refresh-catalog as the same session (403 PERMISSION_DENIED naming manage_platform_settings AND manage_metadata — a write requires the read capability as well as FEDERATION_WRITE_CAPABILITY), plus one anonymous probe of each family (401 UNAUTHENTICATED, decided before any service lookup); also probe the UI import door's floor: the no-capability session's PUT /api/v1/meta/object/<name> refuses 403 FORBIDDEN 'Saving a metadata item requires the `manage_metadata` capability.' (packages/metadata-core/src/meta-write-capability.ts#metaWriteCapabilityVerdict, the item save door's sentence; 'Rewriting stored metadata …' is POST /meta/_migrate-stored's, and the federation routes answer PERMISSION_DENIED)",
21732179
"run the objectui api.test.ts 503 cases (acceptance[6]). ONLY on a deliberately unwired boot (see knownGaps), also drive the Tables tab into the 503 and record that the designed 'Federation is not enabled on this server.' state renders"
21742180
],
21752181
"acceptance": [
@@ -2192,7 +2198,7 @@
21922198
"evidence": "the network captures + the post-import meta/data reads"
21932199
},
21942200
{
2195-
"clause": "Refresh catalog round-trips: the header button fires POST .../external/refresh-catalog (a federation WRITE — manage_metadata floor), and the returned catalog.snapshotAt renders as the snapshot timestamp",
2201+
"clause": "Refresh catalog round-trips: the header button fires POST .../external/refresh-catalog (a federation WRITE — manage_metadata AND manage_platform_settings floor), and the returned catalog.snapshotAt renders as the snapshot timestamp",
21962202
"oracle": "network",
21972203
"verify": "the captured POST + the rendered timestamp (objectui ExternalDatasourcePanel.tsx: its handleRefresh callback, and the header's catalog.snapshotAt span; route at external-datasource-routes.ts)",
21982204
"evidence": "the network capture + the header screenshot"
@@ -2204,7 +2210,7 @@
22042210
"evidence": "the response + the rendered rows (and the unlabelled-kind capture if drift/outage was induced)"
22052211
},
22062212
{
2207-
"clause": "the floor holds on BOTH sides for all three newly-covered routes: anonymous → 401 UNAUTHENTICATED before any service lookup (#9686 shouldDenyAnonymous); the no-capability session → 403 PERMISSION_DENIED naming manage_platform_settings on validate (read) and manage_metadata on import/refresh-catalog (write, FEDERATION_WRITE_CAPABILITY); and the UI's own import door (PUT /meta/object) refuses the same session server-side — the dialog's success path must be unreachable for an unentitled operator by anything but pixels",
2213+
"clause": "the floor holds on BOTH sides for all three newly-covered routes: anonymous → 401 UNAUTHENTICATED before any service lookup (#9686 shouldDenyAnonymous); the no-capability session → 403 PERMISSION_DENIED naming manage_platform_settings on validate (read) and both manage_platform_settings and manage_metadata on import/refresh-catalog (write: FEDERATION_WRITE_CAPABILITY on top of the read capability); and the UI's own import door (PUT /meta/object) refuses the same session server-side — the dialog's success path must be unreachable for an unentitled operator by anything but pixels",
22082214
"oracle": "api",
22092215
"verify": "the probes of step 8 against refuseFederationRequest (external-datasource-routes.ts — 'read' for validate, 'write' for import/refresh) and the /meta manage_metadata gate (rest-server.ts)",
22102216
"evidence": "the 401/403 responses, keyed by route and persona"
@@ -2284,6 +2290,12 @@
22842290
"date": "2026-10-27",
22852291
"change": "automated.ref names the auth-guard unit pin under acceptance[5] and negative[2]. Checklist-accuracy finding of the 18.2.0 wave 2 (QA run #26450, release-18.2.0-w2-tier1-b, at 756220c4; wave anchor #25807, slice A): the mapping pass scored both partial from it, and the ref did not name it. No clause changed",
22862292
"ref": "#25807"
2293+
},
2294+
{
2295+
"revision": 8,
2296+
"date": "2026-11-03",
2297+
"change": "personas[0], steps[7], acceptance[3] and acceptance[5]: import and refresh-catalog require manage_platform_settings as well as manage_metadata. Seeing a datasource's remote structure needs the read capability on every route, writes included (maintainer ruling 2026-11-03, verbatim: 「两个写接口同时要求读权限 (Recommended)」), so the no-capability session's 403 on the writes names both capabilities. The admin persona already holds both; no step's expected success changes",
2298+
"ref": "claude/sec-hardening-d"
22872299
}
22882300
]
22892301
},

‎packages/mcp/src/mcp-http-tools.ts‎

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -341,9 +341,12 @@ export interface McpActionBridge {
341341
* page makes (`GET …/external/tables`, `POST …/tables/:remote/import`).
342342
*
343343
* The host binds both to the caller and refuses as the REST routes refuse:
344-
* listing needs the federation READ capability, importing the WRITE one
345-
* (`FEDERATION_READ_CAPABILITY` / `FEDERATION_WRITE_CAPABILITY`,
346-
* `@objectstack/rest`), thrown with the ADR-0112 `code` / `status`.
344+
* listing needs the federation READ capability, importing the READ capability
345+
* AND the WRITE one — the import answers with the remote table's structure,
346+
* which needs the read capability on every door
347+
* (`FEDERATION_READ_CAPABILITY` / `FEDERATION_WRITE_CAPABILITY`, judged by
348+
* `federationCapabilityRefusal`, `@objectstack/rest`), thrown with the
349+
* ADR-0112 `code` / `status`.
347350
*
348351
* OPTIONAL as a pair: a bridge without both registers neither tool. The
349352
* runtime's bridge carries both and answers `503 SERVICE_UNAVAILABLE` per call
@@ -840,7 +843,7 @@ export function registerFederationTools(
840843
'read-only unless writable is true (which also needs the datasource to allow writes), and maps the ' +
841844
'remote types to field types. A navigation becomes a lookup only when its target is already imported, ' +
842845
'so import targets first; the result\'s review lists lossy columns and navigations left out. ' +
843-
'Requires the federation write capability.',
846+
'Requires the federation read and write capabilities.',
844847
inputSchema: strictToolInput(
845848
{
846849
surface: 'this import_object call',

0 commit comments

Comments
 (0)