Repository navigation
fix(rest,runtime)!: the import doors and the catalog refresh read the walled-catalog verdict the metadata save door reads - #30702
objectstack-fleet[bot] wants to merge 10 commits into
Conversation
… verdict the metadata save door reads Pins for the import route and the MCP import tool under the walled tenancy postures: a caller below the platform operator is refused with 403 PERMISSION_DENIED and the save door's sentence for an object, before the federation service is looked up; the operator and the open posture import as before; a tenancy service that cannot answer refuses 503. Co-authored-by: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01L6bhgVfjk2i8T2D4RmxsKg
… the metadata save door reads Under the isolated and group tenancy postures every organization shares the environment's metadata catalog, and a write to it is the platform operator's (ADR-0131 D3, D12 items 7-8, D11). Importing a remote table saves an object into that catalog, so the REST import route and the MCP import_object tool now ask walledCatalogWriteVerdict, the verdict the metadata save door asks, after the federation capability rule and before the federation service is looked up. A refusal answers 403 PERMISSION_DENIED with the verdict's sentence for an object; a tenancy service that cannot answer refuses 503. The REST route reads the posture off its own kernel, the MCP tool through the dispatcher's one posture read. Co-authored-by: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01L6bhgVfjk2i8T2D4RmxsKg
…adata:write and actions:execute The MCP federation pair is registered only for an OAuth agent granted `metadata:write`, and an agent holds its user's system capabilities only under `actions:execute`. Each MCP call in the composed-stack pin now presents a real access token with both scopes, consented from a session active in the organization the agent acts in, so the refusal and both controls run the `import_object` tool itself. Co-authored-by: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01L6bhgVfjk2i8T2D4RmxsKg
…ort doors Row 55b of the system-context census now lists the import of a remote table as an object, on the REST import route and on the MCP `import_object` tool, among the writes the walled refusal judges, and anchors the two symbols that read the caller's elevation for it. The site register is regenerated. Co-authored-by: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01L6bhgVfjk2i8T2D4RmxsKg
…o tenancy service The ledger's context answers every service with a stand-in whose methods all reject, and a stand-in for `tenancy` reads as a walled posture. The import door now asks the walled-catalog verdict, so under that stand-in the import was answered before the federation service the ledger reads. The context now registers no `tenancy` service, as on a deployment that is not walled, and the import reaches the federation service as each other row does. Co-authored-by: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01L6bhgVfjk2i8T2D4RmxsKg
📓 Docs Drift CheckThis PR changes 3 package(s): 3 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:
⛔ 1 release-owned page(s) also name something this change touched. These are read-only:
What this run could not see
Coarse fallback — 39 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): Which tree this was computed onThis run read A worktree cut from an older # while this PR is open — GitHub drops the merge commit once it closes
git fetch origin f3f09692589e7fab8bcb646468ec256e1468b365 && git checkout f3f09692589e7fab8bcb646468ec256e1468b365
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 9b24f3ca8734b5e0ea77cc3cb282c3f8f55a862b b5648f2fb463a1d4c70e8198d98fe3093711c5a4 && git checkout -B drift-repro 9b24f3ca8734b5e0ea77cc3cb282c3f8f55a862b && git merge --no-ff b5648f2fb463a1d4c70e8198d98fe3093711c5a4
node scripts/docs-audit/affected-docs.mjs --json 9b24f3ca8734b5e0ea77cc3cb282c3f8f55a862b
|
Co-authored-by: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01L6bhgVfjk2i8T2D4RmxsKg
…ict the metadata save door reads Under `isolated` and `group`, a catalog refresh rewrites the datasource's cached `external_catalog` snapshot, an item of the environment's shared metadata catalog, so it is the platform operator's. The pins: an organization administrator holding both federation capabilities is refused 403 PERMISSION_DENIED with the save door's sentence for that type, before the federation service is looked up, and nothing is stored; the capability rule answers first; a tenancy service that cannot answer refuses 503. Controls: the platform operator refreshes under a wall, and under `single` and with no tenancy service the same administrator refreshes as before. Co-authored-by: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01L6bhgVfjk2i8T2D4RmxsKg
…nvironment holds it A refresh persists its snapshot through `metadata.register`, which writes the metadata service's registry and each writable database loader the deployment has. The pin now reads "stored" as either, so the controls do not depend on which loaders a boot registers, and the refusal pin still requires neither. Co-authored-by: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01L6bhgVfjk2i8T2D4RmxsKg
…metadata save door reads Under `isolated` and `group` every organization shares the environment's metadata catalog, and a write to it is the platform operator's (ADR-0131 D3, D12 items 7–8, D11). A refresh of a federated datasource's catalog saves its `external_catalog` snapshot into that catalog, so `POST /datasources/:name/external/refresh-catalog` now asks the walled-catalog verdict for that type, as the import doors ask it for an object: after the federation capability rule, before the federation service is looked up, `403 PERMISSION_DENIED` with the save door's sentence, and `503 SERVICE_UNAVAILABLE` when a registered tenancy service cannot answer. The operator, `single` and a deployment with no tenancy service refresh as before. The changeset, the tenancy-modes and OData datasource pages, census row 55b and the verdict's door lists name the door. Co-authored-by: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01L6bhgVfjk2i8T2D4RmxsKg
Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01L9qibvynRbCtMndW8EHNGc
|
security#1 · os-tesla · session_01L9qibvynRbCtMndW8EHNGc Contract reviewServed-tier: Face: narrowing. Check-runs on the head: 54, all completed, none queued. The ten required contexts: The rule reviewed against, verbatim from the seat post: "under the walled tenancy postures, importing a remote table as an object and refreshing a datasource's external catalog are catalog writes reserved to the platform operator, at REST and MCP." Governing text read: ADR-0131 D3 (the catalog is environment-level; under ① Derived judgments
② Semver level
③ Boundary flags
④ Design judgment
No must-fix. Two should-fixes, named above. Implemented-by: VERDICT: PASS Generated by Claude Code |
|
security#1 · os-tesla · session_01L9qibvynRbCtMndW8EHNGc 席位核对ACCEPT · 2026-11-06T07:27Z · head
Generated by Claude Code |
Clause-②: no (narrowing) — under the
isolatedandgrouptenancy postures, importing a remote table as an object (POST /api/v1/datasources/:name/external/tables/:remote/import, and the MCPimport_objecttool) 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,refreshCatalogandimport_objectunderskills/;datasourceitself 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).维护者速读
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
isolatedandgroupevery 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 MCPimport_objecttool, saves anobject; the catalog refresh at the REST route saves the datasource'sexternal_catalogsnapshot. 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.403 PERMISSION_DENIEDwith 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. Atenancyservice that is registered and cannot answer refuses the call503 SERVICE_UNAVAILABLE, never a permit.single, and on a deployment with notenancyservice, 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.deployment/tenancy-modeslists the import and the catalog refresh among the walled writes;data-modeling/odata-datasourcesstates who imports and refreshes under a wall; row 55b of the system-context census names the doors, with its register regenerated. ThewalledCatalogWriteVerdictdoc 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, undergroupandisolatedan administrator holding both federation capabilities is refused403 PERMISSION_DENIEDbefore 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 thesingle/ no-tenancy controls are served; a failingtenancyservice answers503 SERVICE_UNAVAILABLE.packages/runtime/src/domains/mcp-federation-bridge.test.ts: the same rule, refusals and controls at the MCPimport_objectbridge.packages/rest/src/rest-write-route-hook-refusal-sentence.ledger.test.ts: the ledger's context registers notenancyservice, 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 undergroupwith 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 atimport_object(a real OAuth agent token grantedmetadata:writeandactions: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; undersinglethe same kind of administrator does each as before.Acceptance notes
saveMetaItembehindimportObject, the catalog persistence behindrefreshCatalog) 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.PERMISSION_DENIED, the standard-catalog member this family already answers for a 403; the REST/metadoor answers its grandfathered synonym (STANDARD_SYNONYM_WAIVERS, ADR-0112 D3), which keeps a wire value and is not endorsed for new code.not-required (no-migration-prescription)). The changeset names all three doors.Verification
All readings below are at head
b5648f2fb463a1d4c70e8198d98fe3093711c5a4, afterorigin/mainwas merged in (merge commit, no conflicts;mainhad movedrest,runtimeandservice-datasource), the closure rebuilt (pnpm --filter '@objectstack/dogfood^...' build) and every build and test run under the shared verify lock.Pins at the head
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).Ablations
Each through
scripts/ablation-replace.mjs(anchor hit once, blob changed, restored blob-equal to HEAD withgit diff HEADempty), on the committed tree. Where the dogfood pin was read, the package was rebuilt andscripts/ablation-dist-preflight.mjsproved the mutation indist/first; after each restore the package was rebuilt and the preflight proveddist/back at the head with the tree clean.group,isolatedand the verdict sentence, for the import and for the refreshfederationServicegroup,isolated, verdict sentence503pins503pin there)503pingroup,isolated, verdict-sentence and503pins; every import pin greenAfter the last restore:
git status --porcelainempty andgit diff HEADempty.Package tests and typecheck
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.typecheckfor rest, runtime, metadata-core, service-datasource and dogfood: exit 0, 0error TSlines each.Derived gates
node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack(no paths) atb5648f2fb4: 99 commands, each run as printed with stdin from/dev/nulland its exit code recorded.--ran: 99 derived, 99 run, 0 NOT-MEASURED, 0 UNRUN. Two first answered exit 3 (check:skill-examplesandcheck: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:generatedafter the merge (mainmovedpackages/spec): all 14 generated artifacts up to date.Full dogfood battery at the head
b5648f2fb463a1d4c70e8198d98fe3093711c5a4, the closure built at that head, then under the lockpnpm --filter @objectstack/dogfood testwith no shard and no path:os-verify-lock: VERDICT command-exit 0git status --porcelainempty after the runThis body was rewritten for head
b5648f2fb4in sessionsession_01L9qibvynRbCtMndW8EHNGc, after the catalog refresh joined the two import doors.