Skip to content

Commit adb2b89

Browse files
fix(rest,runtime)!: the import doors and the catalog refresh read the walled-catalog verdict the metadata save door reads (#30702)
Clause-②: no (narrowing) — under the `isolated` and `group` tenancy postures, importing a remote table as an object (`POST /api/v1/datasources/:name/external/tables/:remote/import`, and the MCP `import_object` tool) and refreshing a federated datasource's cached catalog (`POST /api/v1/datasources/:name/external/refresh-catalog`) are writes to the environment's metadata catalog, and they are the platform operator's, as every other write to that catalog is. Metadata-protocol: none Skills-docs: none — no `skills/**` text teaches any of the three doors or who may call them (0 hits for the import route, the refresh route, `refreshCatalog` and `import_object` under `skills/`; `datasource` itself is named there, the positive control); the rule is stated in the docs this diff updates (`content/docs/deployment/tenancy-modes.mdx`, `content/docs/data-modeling/odata-datasources.mdx`). ## 维护者速读 - 改了什么:三道写入门——REST 远程表导入路由、MCP `import_object` 工具、REST 外部目录刷新路由——在 `isolated` / `group` 租户姿态下,与元数据保存门问同一个“围墙目录写入”裁决。 - 为什么改:导入把一个对象存进环境共享的元数据目录,目录刷新把数据源的 `external_catalog` 快照存进同一个目录;围墙姿态下这个目录只归平台运营方(与引擎自身)写,所以这三道门与保存门读同一条裁决。 - 风险与回滚:围墙姿态下,平台运营方以下的调用方导入或刷新目录改为 `403 PERMISSION_DENIED`,什么都不读取、不保存;租户服务已注册却答不出时回 `503`;`single` 与未注册租户服务的部署不变;回滚即还原本 PR,已导入的对象与已存的快照不受影响。 - 先看:`packages/rest/src/external-datasource-routes.ts`(`refuseFederationRequest` 的 `catalogWrite` 段,导入与刷新两条路由共用)、`packages/runtime/src/domains/mcp.ts`(`federationService` 的 `catalogWrite` 段)、`packages/qa/dogfood/test/external-import-walled-catalog-refusal.dogfood.test.ts`。 - 两分钟验证:`pnpm --filter @objectstack/dogfood exec vitest run test/external-import-walled-catalog-refusal.dogfood.test.ts`,预期 9 个用例全部通过(围墙下三道门拒绝、运营方可导入与刷新、`single` 下照常导入与刷新)。 - 需要你做的:无,席位落地。 ## What changes - **The rule.** Under `isolated` and `group` every organization shares the environment's metadata catalog, and a write to it is admitted only for the platform operator and for the engine's own writes (ADR-0131 D3, D12 items 7–8, D11). Three doors of the federation family write that catalog: the import of a remote table as an object, at the REST route and at the MCP `import_object` tool, saves an `object`; the catalog refresh at the REST route saves the datasource's `external_catalog` snapshot. Each of them now asks the walled-catalog verdict the metadata save door asks (`walledCatalogWriteVerdict`, `@objectstack/metadata-core`) for the type it writes, after the federation family's capability rule and before the federation service is looked up. - **The answer.** A refused call answers `403 PERMISSION_DENIED` with the verdict's sentence for the type written, which is the sentence the metadata save door answers, naming the posture. Nothing is introspected and nothing is saved. A `tenancy` service that is registered and cannot answer refuses the call `503 SERVICE_UNAVAILABLE`, never a permit. - **Unchanged.** The platform operator imports and refreshes under a wall as before. Under `single`, and on a deployment with no `tenancy` service, every caller holding both federation capabilities imports and refreshes as before. Listing remote tables, drafting an object and validating are unchanged, and the federation capability rule answers first, as before. - **Docs.** `deployment/tenancy-modes` lists the import and the catalog refresh among the walled writes; `data-modeling/odata-datasources` states who imports and refreshes under a wall; row 55b of the system-context census names the doors, with its register regenerated. The `walledCatalogWriteVerdict` doc comment and its ADR anchor name the three doors among the doors that ask it. ## Tests - `packages/rest/src/external-datasource-routes-auth-guard.test.ts`: for the import route and for the catalog refresh route alike, under `group` and `isolated` an administrator holding both federation capabilities is refused `403 PERMISSION_DENIED` before the service is looked up; the sentence equals the verdict's sentence for the type the route writes (`object`, `external_catalog`); the capability rule still answers first; listing is still served; the platform operator and the `single` / no-tenancy controls are served; a failing `tenancy` service answers `503 SERVICE_UNAVAILABLE`. - `packages/runtime/src/domains/mcp-federation-bridge.test.ts`: the same rule, refusals and controls at the MCP `import_object` bridge. - `packages/rest/src/rest-write-route-hook-refusal-sentence.ledger.test.ts`: the ledger's context registers no `tenancy` service, as on a deployment that is not walled, so its import rows still reach the federation service the ledger reads. - `packages/qa/dogfood/test/external-import-walled-catalog-refusal.dogfood.test.ts`: a booted showcase under `group` with the federation service and the MCP server mounted. An organization administrator holding both capabilities is refused at the REST import door and at the catalog refresh with the save door's status and sentence for the type written, and at `import_object` (a real OAuth agent token granted `metadata:write` and `actions:execute`) with the same code and sentence; nothing is saved or stored. Controls: the platform operator imports at both import doors and refreshes the catalog under the same wall; under `single` the same kind of administrator does each as before. ## Acceptance notes - **Where the rule sits: at the doors, not in the shared save path.** The save path below the doors (`saveMetaItem` behind `importObject`, the catalog persistence behind `refreshCatalog`) receives the item, not the caller's rung or whether the caller is the engine, so judging there would mean threading the caller into the federation service contract. The doors are where caller admission already lives in this family (the capability rule runs there), so the walled verdict runs beside it and refuses before any landing is resolved or any remote schema is read. On the REST side the two write routes share the one guard (`refuseFederationRequest`), each naming the type it writes. - **The code.** The status and the sentence are the save door's. The code is `PERMISSION_DENIED`, the standard-catalog member this family already answers for a 403; the REST `/meta` door answers its grandfathered synonym (`STANDARD_SYNONYM_WAIVERS`, ADR-0112 D3), which keeps a wire value and is not endorsed for new code. - **Breaking, patch.** An accept-set narrowing under a walled posture only; no key of any schema moves and nothing stored changes, so both packages take a patch with a BREAKING banner (ADR-0087 marker `not-required (no-migration-prescription)`). The changeset names all three doors. ## Verification All readings below are at head `b5648f2fb463a1d4c70e8198d98fe3093711c5a4`, after `origin/main` was merged in (merge commit, no conflicts; `main` had moved `rest`, `runtime` and `service-datasource`), the closure rebuilt (`pnpm --filter '@objectstack/dogfood^...' build`) and every build and test run under the shared verify lock. <details><summary>Pins at the head</summary> - `external-datasource-routes-auth-guard.test.ts`: Tests 27 passed (27). - `mcp-federation-bridge.test.ts`: Tests 17 passed (17). - `external-import-walled-catalog-refusal.dogfood.test.ts`: Tests 9 passed (9). </details> <details><summary>Ablations</summary> Each through `scripts/ablation-replace.mjs` (anchor hit once, blob changed, restored blob-equal to HEAD with `git diff HEAD` empty), on the committed tree. Where the dogfood pin was read, the package was rebuilt and `scripts/ablation-dist-preflight.mjs` proved the mutation in `dist/` first; after each restore the package was rebuilt and the preflight proved `dist/` back at the head with the tree clean. | subject | ablation | unit reading | dogfood reading | | --- | --- | --- | --- | | the shared REST refusal (import and refresh) | the walled refusal made unreachable | 6 failed \| 21 passed (27): `group`, `isolated` and the verdict sentence, for the import and for the refresh | 3 failed \| 6 passed (9): the REST import, the MCP import (its sentence is compared with the REST door's) and the refresh refusal pins | | the MCP refusal | the same, in `federationService` | 3 failed \| 14 passed (17): `group`, `isolated`, verdict sentence | 1 failed \| 8 passed (9): the MCP refusal pin | | the REST outage arm | an unreadable posture read as no posture | 2 failed \| 25 passed (27): the import and the refresh `503` pins | not read (no `503` pin there) | | the MCP outage arm | the same, at the MCP posture read | 1 failed \| 16 passed (17): the `503` pin | not read | | the catalog refresh's wiring | the refresh route stops naming the type it writes | 4 failed \| 23 passed (27): the refresh's `group`, `isolated`, verdict-sentence and `503` pins; every import pin green | 1 failed \| 8 passed (9): the refresh refusal pin; every import pin and every control green | After the last restore: `git status --porcelain` empty and `git diff HEAD` empty. </details> <details><summary>Package tests and typecheck</summary> Under the verify lock at the head: - `pnpm --filter @objectstack/rest test`: Test Files 391 passed (391), Tests 7409 passed | 332 skipped. - `pnpm --filter @objectstack/runtime test`: Test Files 547 passed (547), Tests 8762 passed | 19 skipped. - `pnpm --filter @objectstack/metadata-core test`: Test Files 23 passed (23), Tests 911 passed. - `pnpm --filter @objectstack/service-datasource test` (the service behind all three doors, not in this diff): Test Files 72 passed (72), Tests 1239 passed. - `typecheck` for rest, runtime, metadata-core, service-datasource and dogfood: exit 0, 0 `error TS` lines each. </details> <details><summary>Derived gates</summary> `node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack` (no paths) at `b5648f2fb4`: 99 commands, each run as printed with stdin from `/dev/null` and its exit code recorded. `--ran`: 99 derived, 99 run, 0 NOT-MEASURED, 0 UNRUN. Two first answered exit 3 (`check:skill-examples` and `check:dual-build-cjs-loads`, prerequisite packages outside this diff unbuilt) and were re-run after building those packages: exit 0. Every recorded exit is 0. `pnpm --filter @objectstack/spec check:generated` after the merge (`main` moved `packages/spec`): all 14 generated artifacts up to date. </details> <details><summary>Full dogfood battery at the head</summary> `b5648f2fb463a1d4c70e8198d98fe3093711c5a4`, the closure built at that head, then under the lock `pnpm --filter @objectstack/dogfood test` with no shard and no path: - `os-verify-lock: VERDICT command-exit 0` - Test Files 424 passed | 3 skipped (427) - Tests 3541 passed | 41 skipped (3582) - no failures; `git status --porcelain` empty after the run </details> This body was rewritten for head `b5648f2fb4` in session `session_01L9qibvynRbCtMndW8EHNGc`, after the catalog refresh joined the two import doors. <!-- signed: security#1 · os-tesla · session_01L9qibvynRbCtMndW8EHNGc --> --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent f84205b commit adb2b89

12 files changed

Lines changed: 938 additions & 14 deletions
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
---
2+
'@objectstack/rest': patch
3+
'@objectstack/runtime': patch
4+
---
5+
6+
fix(rest,runtime)!: the import doors and the catalog refresh read the walled-catalog verdict the metadata save door reads
7+
8+
Clause-②: no (narrowing) — under the `isolated` and `group` tenancy postures, importing a remote table as an object (`POST /api/v1/datasources/:name/external/tables/:remote/import`, and the MCP `import_object` tool) and refreshing a federated datasource's cached catalog (`POST /api/v1/datasources/:name/external/refresh-catalog`) are writes to the environment's metadata catalog, and they are the platform operator's, as every other write to that catalog is.
9+
Metadata-protocol: none
10+
11+
<!-- adr-0087: not-required (no-migration-prescription) No key of any schema is added, removed, renamed or re-shaped and nothing at rest changes: which caller an import door or the catalog refresh admits under a walled tenancy posture is all that moves, and every object already imported and every catalog snapshot already stored stays as it is. -->
12+
13+
**BREAKING** accept-set narrowing at both import doors and at the catalog refresh (`@objectstack/rest`, `@objectstack/runtime`) under a walled tenancy posture.
14+
15+
- **The rule.** Under `isolated` and `group` every organization shares the environment's metadata catalog, and a write to it is admitted only for the platform operator and for the engine's own writes (ADR-0131 D3, D12 items 7–8, D11). An import saves an object into that catalog, and a catalog refresh saves the datasource's `external_catalog` snapshot there, so each of those doors asks the walled-catalog verdict the metadata save door asks (`walledCatalogWriteVerdict`, `@objectstack/metadata-core`) for the type it writes, after the federation capability rule and before the federation service is looked up.
16+
- **The answer.** A refused call answers `403 PERMISSION_DENIED` with the verdict's sentence for the type written, the one the metadata save door answers, naming the posture. Nothing is introspected and nothing is saved. A `tenancy` service that is registered and cannot answer refuses the call `503 SERVICE_UNAVAILABLE`.
17+
- **Unchanged.** The platform operator imports and refreshes under a wall as before. Under `single`, and on a deployment with no `tenancy` service, every caller holding both federation capabilities imports and refreshes as before. Listing remote tables, drafting an object and validating are unchanged, and the federation capability rule answers first, as before.
18+
- **Who moves.** On a walled deployment, an import or a catalog refresh by a caller below the platform operator is refused; the platform operator performs it.

‎content/docs/data-modeling/odata-datasources.mdx‎

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -145,7 +145,11 @@ What each piece does on this driver:
145145
datasource page lists its entity sets on the **Tables** tab. **Import** saves one,
146146
and **Import selected** saves several, referenced entity sets first. An MCP client
147147
whose token carries `metadata:write` reaches the same import through
148-
`list_remote_tables` and `import_object`. The draft maps each EDM type to a field
148+
`list_remote_tables` and `import_object`. An import saves an object into the
149+
environment's metadata, so under the `group` and `isolated` tenancy postures only
150+
the platform operator imports, as only the platform operator saves any other
151+
metadata there ([tenancy modes](/docs/deployment/tenancy-modes)). A refresh of the
152+
datasource's catalog saves its snapshot there too, so the same holds for it. The draft maps each EDM type to a field
149153
type, makes the key the `id`, and writes `columnMap` for every property whose name is
150154
not a snake_case field name (`CategoryID` becomes `category_id`). EDM types with no
151155
field type, such as `Edm.Duration` or a service's own enum, are listed for review. An

‎content/docs/deployment/tenancy-modes.mdx‎

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -67,8 +67,12 @@ and the engine's own writes (seeders, installers, migrations) write it. Holding
6767
`manage_metadata` inside an organization does not admit a write: the `/meta`
6868
write doors (save, reset, publish, rollback), the install-wide stored rewrite
6969
(`POST /meta/_migrate-stored`), every `/packages` write,
70-
`install-local` install, upgrade and uninstall, and the `/automation`
71-
definition writes refuse such a caller `403`, naming the posture.
70+
`install-local` install, upgrade and uninstall, the `/automation`
71+
definition writes, the import of a remote table as an object
72+
(`POST /datasources/:name/external/tables/:remote/import` and the MCP
73+
`import_object` tool), and the refresh of a federated datasource's cached
74+
catalog (`POST /datasources/:name/external/refresh-catalog`) refuse such a
75+
caller `403`, naming the posture.
7276

7377
- **Headquarters IT** authors the environment's metadata as the platform
7478
operator: declare its administrators in `OS_PLATFORM_OWNER_EMAIL`. A

‎content/docs/permissions/system-context.mdx‎

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -204,7 +204,7 @@ One of the largest consumers of the flag;
204204
| 54 | Package REST route capability gate bypassed | rest | Get: a marketplace publish over REST (`POST /packages/publish`, the one route the REST registrar mounts since #14503) without `manage_metadata`; the package read cohort (`studio.access` / `setup.access`) is enforced by the dispatcher `/packages` domain's own read gate, where the reads are served | `refusePackageRequest` |
205205
| 55 | Package domain capability gates bypassed | runtime | Get: package management and package-inventory reads without the capability | `requireManageMetadata`, `requireReadCapability` |
206206
| 56 | Activation write / authoring refusals do not fire | runtime | Get: activation artifacts writable and authorable without the activation-authoring capability | `refuseUngrantedActivationWrite`, `refuseUngrantedActivationAuthoring` |
207-
| 55b | Walled environment-metadata refusal does not fire | metadata-core, rest, runtime | Get: under `isolated` / `group`, a write of the environment's metadata, of any type, at the `/meta` write doors (save, reset, publish, rollback) and `POST /meta/_migrate-stored` on both transports, at every `/packages` write door (create, the manifest `PATCH`, enable, disable, publish, `publish-drafts`, `discard-drafts`, `revert`, `rollback`, `commits/:commitId/revert`, `duplicate`, `adopt-orphans`, delete), at the `/automation` definition writes (create, update, delete, clone), and at the permission-set clone door. Every other caller below the platform-operator rung is refused `403` there (ADR-0131 D3, D12 items 7–8, D11), because one organization's write would change what every organization resolves. The seeders, the package door and migrations write the environment's metadata this way. Under `single` the refusal is inert for every caller | `walledCatalogWriteVerdict`, `walledCatalogRefusal`, `refuseWalledCatalogPackageWrite`, `refuseWalledFlowDefinitionWrite` |
207+
| 55b | Walled environment-metadata refusal does not fire | metadata-core, rest, runtime | Get: under `isolated` / `group`, a write of the environment's metadata, of any type, at the `/meta` write doors (save, reset, publish, rollback) and `POST /meta/_migrate-stored` on both transports, at every `/packages` write door (create, the manifest `PATCH`, enable, disable, publish, `publish-drafts`, `discard-drafts`, `revert`, `rollback`, `commits/:commitId/revert`, `duplicate`, `adopt-orphans`, delete), at the `/automation` definition writes (create, update, delete, clone), at the permission-set clone door, at the import of a remote table as an object (the REST import route and the MCP `import_object` tool), and at the refresh of a federated datasource's cached catalog. Every other caller below the platform-operator rung is refused `403` there (ADR-0131 D3, D12 items 7–8, D11), because one organization's write would change what every organization resolves. The seeders, the package door and migrations write the environment's metadata this way. Under `single` the refusal is inert for every caller | `walledCatalogWriteVerdict`, `walledCatalogRefusal`, `refuseWalledCatalogPackageWrite`, `refuseWalledFlowDefinitionWrite`, `registerExternalDatasourceRoutes`, `federationService` |
208208
| 57 | Automation run-state read, flow-authoring write, the paused-run caller gate (screen read and resume), the two operator run-lifecycle writes, and the elevated self-triggered start and the start-record check at every door that starts a flow by name all pass | runtime | Get: run state, flow writes, a paused run's screen and its resume with no grant and without being the run's starter — and cancelling or restoring a suspension without the platform-operator rung. The screen read and the resume ask one predicate, so this is one bypass for both. The lifecycle bypass is the in-process owner's door: plugin-approvals' revise-window recall (ADR-0044) cancels on behalf of a decision it already authorized and recorded. And starting a flow declared `runAs: 'system'` whose type is self-triggered (`autolaunched`, `record_change`, `schedule`) through any door that starts a flow by name — the trigger route, an action of `type: 'flow'` (REST `/actions` and the MCP `run_action` bridge) and a declared endpoint of `type: 'flow'` — which every other caller, the guest principal included, is refused `403 PERMISSION_DENIED` (the maintainer's rulings, letters B and A) — and starting, through the same doors, a flow whose `requiredPermissions` the caller does not hold (ruling record 6377848497, letter A). And starting, through the same doors, a flow on a record key the caller's own scope does not deliver, which every other caller is refused with the action door's `404 RECORD_NOT_FOUND` before the run exists (and `400 VALIDATION_FAILED` for a key that is not one id, or for an elevated start that names no object). The three doors ask one check, so this is one bypass for all of them, and it is the in-process caller's, such as a job: inbound HTTP never carries the flag | `mayReadRunState`, `refuseUngrantedFlowWrite`, `isRunStarterOrRunStateReader`, `refuseUngrantedRunLifecycleWrite`, `flowStartRefusal`, `refuseUnreadableFlowStartRecords` |
209209
| 57b | Connector credential door's capability and posture gates pass | runtime | Get: listing, setting and clearing the environment's stored connector credentials (`/automation/connectors/credential-refs`) without `manage_metadata`, and under the `group` / `isolated` postures, where the door refuses every other caller `403 PERMISSION_DENIED` naming the posture, the platform operator included, because the store is environment-level. The bypass is the in-process caller's: inbound HTTP never carries the flag. Get nothing more about the value: no answer of this door carries it | `refuseUngrantedConnectorCredentialAccess`, `refuseWalledConnectorCredentialAccess` |
210210
| 57c | Connector connection door's capability gate passes | runtime | Get: reading, beginning, completing (the OAuth callback) and revoking an organization's connection of an `oauth2` `authorization_code` connector instance (`/automation/connectors/:name/connection`, `/automation/connectors/oauth/callback`, ADR-0143 D5) without `manage_metadata`. No posture refuses it: a connection belongs to its organization. The bypass is the in-process caller's: inbound HTTP never carries the flag. Get nothing more: no answer of this door carries a token, and the callback's `state` is still bound to the user and organization that began the connect | `refuseUngrantedConnectorConnectionAccess` |
@@ -618,7 +618,7 @@ and CI refuses one that differs from what the tree produces.
618618
| `packages/plugins/plugin-webhooks/src/declared-webhooks.ts#bindDeclaredWebhookNameGuard` | 1 | 59 |
619619
| `packages/plugins/plugin-webhooks/src/webhook-provenance.ts#bindWebhookProvenanceStamp` | 1 | 59 |
620620
| `packages/rest/src/data-action-routes.ts#importUndoWriteContext` | 1 | 30c |
621-
| `packages/rest/src/external-datasource-routes.ts#registerExternalDatasourceRoutes` | 1 | 52 |
621+
| `packages/rest/src/external-datasource-routes.ts#registerExternalDatasourceRoutes` | 2 | 52, 55b |
622622
| `packages/rest/src/import-prepare.ts#importControlOptOutRefusal` | 1 | 30b |
623623
| `packages/rest/src/meta-collection-routes.ts#registerMetaCollectionRoutes` | 1 | 50 |
624624
| `packages/rest/src/meta-item-history-routes.ts#registerMetaItemHistoryRoutes` | 2 | 50 |
@@ -648,6 +648,7 @@ and CI refuses one that differs from what the tree produces.
648648
| `packages/runtime/src/domains/automation.ts#refuseWalledConnectorCredentialAccess` | 1 | 57b |
649649
| `packages/runtime/src/domains/automation.ts#refuseWalledFlowDefinitionWrite` | 1 | 55b |
650650
| `packages/runtime/src/domains/i18n.ts#handleI18nRequest` | 1 | 52 |
651+
| `packages/runtime/src/domains/mcp.ts#federationService` | 1 | 55b |
651652
| `packages/runtime/src/domains/mcp.ts#handleMcpRequest` | 1 | 53 |
652653
| `packages/runtime/src/domains/meta.ts#handleMetadataRequest` | 5 | 50, 52 |
653654
| `packages/runtime/src/domains/packages.ts#handlePackagesRequest` | 1 | 52 |

‎packages/metadata-core/src/meta-write-capability.ts‎

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,9 @@
4545
* Under a wall, the doors that write the environment's metadata ask a second
4646
* predicate after their capability gate, {@link walledCatalogWriteVerdict}:
4747
* the `/meta` item write doors, `POST /meta/_migrate-stored`, the `/packages`
48-
* write doors, the `/automation` definition writes and `install-local`.
48+
* write doors, the `/automation` definition writes, `install-local`, the
49+
* external-table import (the REST route and the MCP `import_object` tool) and
50+
* the federated catalog refresh.
4951
*
5052
* ── Refusal messages (#7450) ──────────────────────────────────────────────
5153
*
@@ -181,8 +183,11 @@ export const PLATFORM_OPERATOR_POSTURE = 'PLATFORM_ADMIN';
181183
* `/meta` item write doors on both transports (save, reset, publish,
182184
* rollback), `POST /meta/_migrate-stored` on both transports, the `/packages`
183185
* write doors, the `/automation` definition writes,
184-
* `install-local` (install, upgrade, uninstall) and the permission-set clone
185-
* door. (The security plugin's data door on the catalog objects asked it too,
186+
* `install-local` (install, upgrade, uninstall), the permission-set clone
187+
* door, the external-table import on both of its doors (the REST route and
188+
* the MCP `import_object` tool, which save an `object` into the catalog), and
189+
* the federated catalog refresh (which saves an `external_catalog`). (The
190+
* security plugin's data door on the catalog objects asked it too,
186191
* until those objects retired — ADR-0131 D3, C3 stage 8: no `sys_position`,
187192
* `sys_permission_set` or `sys_capability` write reaches the engine, so a
188193
* position, set or capability is written through the `/meta` doors above.)

0 commit comments

Comments
 (0)