Repository navigation
fix(service-datasource)!: the datasource admin door asks the walled-catalog verdict on every row-writing verb, as the /meta door does - #31005
Conversation
…talog verdict on every row-writing verb Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01L9qibvynRbCtMndW8EHNGc
…ource admin door's row-writing verbs Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01L9qibvynRbCtMndW8EHNGc
…d tenancy posture Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01L9qibvynRbCtMndW8EHNGc
… walled rule Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01L9qibvynRbCtMndW8EHNGc
… rule to ADR-0131 Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01L9qibvynRbCtMndW8EHNGc
… datasource admin routes Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01L9qibvynRbCtMndW8EHNGc
📓 Docs Drift CheckThis PR changes 2 package(s): 3 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:
⛔ 3 release-owned page(s) also name something this change touched. These are read-only:
What this run could not see
Coarse fallback — 8 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 3a36dac89f869e9bc72c972dbaf205c3007edbe2 && git checkout 3a36dac89f869e9bc72c972dbaf205c3007edbe2
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 8ae0ab9b0d77d56473e89dae9fe437778172dda8 e8ccebeca528cbacef26dbd96e663f190cf2c59d && git checkout -B drift-repro 8ae0ab9b0d77d56473e89dae9fe437778172dda8 && git merge --no-ff e8ccebeca528cbacef26dbd96e663f190cf2c59d
node scripts/docs-audit/affected-docs.mjs --json 8ae0ab9b0d77d56473e89dae9fe437778172dda8
|
…t module top in the two walled-catalog suites check:test-source-alias: both suites reach a dynamic driver import in the source under test inside a clocked case; the warm import moves that load to collection, as the gate's remedy and the package's other suites do. Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01L9qibvynRbCtMndW8EHNGc
…with a ledgered mock, not a load The kept-pool suite boots the admin plugin with its own driver factory, so the default factory's memory arm never runs; a warm import of the driver was a new binding check:driver-memory-census refuses. A vi.mock replacement satisfies check:test-source-alias without loading the driver, and is ledgered under the census's mock-replacement axis. Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01L9qibvynRbCtMndW8EHNGc
|
security#1 · os-tesla · session_01L9qibvynRbCtMndW8EHNGc Contract reviewServed-tier: Read as the net diff of PR #31005 against ① Derived judgments
② Semver level
③ Boundary flags
④ Design judgment
Implemented-by: VERDICT: PASS Generated by Claude Code |
|
security#1 · os-tesla · session_01L9qibvynRbCtMndW8EHNGc 席位核对ACCEPT · 2026-11-06T17:33Z · head
Generated by Claude Code |
security#1 · os-tesla · session_01L9qibvynRbCtMndW8EHNGc
Clause-②: no (narrowing) — under the
isolatedandgrouptenancy postures, creating, changing or removing a datasource at the datasource admin routes (POST /api/v1/datasources,PATCHandDELETE /api/v1/datasources/:name,POST /api/v1/datasources/:name/migrate-credential) is a write to the environment's metadata catalog, and it is the platform operator's, as every other write to that catalog is.Metadata-protocol: none
Skills-docs: none — no
skills/**text teaches the datasource admin routes or who may call them (0 hits forapi/v1/datasourcesandmigrate-credentialunderskills/;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/external-datasources.mdx).维护者速读
isolated/group租户姿态下,与/meta写入门问同一个“围墙目录写入”裁决(walledCatalogWriteVerdict,类型datasource)。datasource条目,与PUT /meta/datasource/:name写的是同一条记录;围墙姿态下这个目录只归平台运营方(与引擎自身)写,所以这道门与/meta门读同一条裁决、给同一个状态码与同一句话。manage_platform_settings的调用方(包括组织内的admin_full_access持有人)写数据源改为403 PERMISSION_DENIED,什么都不存、不封存凭据、不重建连接池;single与未注册租户服务的部署不变;列表、读取、驱动目录、两种连接测试、远程表列表与对象草稿不变;回滚即还原本 PR,已存的数据源不受影响。packages/services/service-datasource/src/admin-routes.ts(requireDatasourceAdmin的catalogWrite段与walledVerdictTenancyPosture,以及四条写路由的调用)、packages/services/service-datasource/src/__tests__/admin-routes-tenancy-posture-admission.test.ts(§1 改用运营方作写入对照,§8 / §8b 新矩阵)、packages/services/service-datasource/src/__tests__/datasource-update-kept-pool-stores-nothing.test.ts(被拒的 PATCH 不动存储与连接池)。pnpm --filter @objectstack/service-datasource exec vitest run src/__tests__/admin-routes-tenancy-posture-admission.test.ts src/__tests__/datasource-update-kept-pool-stores-nothing.test.ts,预期 2 个文件 73 个用例全部通过。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). A datasource is adatasourceitem of that catalog, the recordPUT /api/v1/meta/datasource/:namewrites. The datasource admin door now asks the walled-catalog verdict on every row-writing verb, as the/metadoor does: create, update, remove and migrate-credential each askwalledCatalogWriteVerdict(@objectstack/metadata-core) for the typedatasource, after themanage_platform_settingscapability gate and before the datasource service is looked up. The verdict is reused, not restated.403 PERMISSION_DENIEDwith the verdict's sentence for adatasourceitem, the sentence the/metawrite doors answer for the same record, naming the posture. Nothing is stored, no secret is sealed or removed, and no pool is rebuilt or evicted.tenancyservice decides both. On a host without that accessor, where the admission does not read the posture, the verdict reads a registeredtenancyservice synchronously; a service that is not registered reads as the no-tenancycomposition, inert assingleis.single, and with notenancyservice, every caller holdingmanage_platform_settingswrites datasources as before. Listing, reading, the driver catalog, the draft probe (POST /datasources/test), the saved-datasource probe (POST /datasources/:name/test), the remote-table listing and the object draft write no datasource record and do not ask the verdict; the object draft hands back a definition the caller saves through the/metadoors, which ask it themselves. The capability gate still answers first.deployment/tenancy-modesanddata-modeling/external-datasourcesstate who writes a datasource under a wall. ThewalledCatalogWriteVerdictdoc comment and its ADR anchor name the datasource admin routes among the doors that ask it, and a new ADR-0131 anchor holds the rule onadmin-routes.ts.Tests
admin-routes-tenancy-posture-admission.test.ts(realObjectKernel, realresolveAuthzContext, API keys over the shipped permission-store shapes):isolatedare now the platform operator's key (declared inOS_PLATFORM_OWNER_EMAILover a verified row); they previously pinned a member's create and delete landing underisolated.isolatedand forgroup, each of the four verbs: refused for a current member holdingmanage_platform_settings(403,PERMISSION_DENIED, the message equal to the verdict's message fordatasourceon that posture, the service method never called, the store unchanged); refused for an organization'sadmin_full_accessholder; allowed for the platform operator, and the write lands. The capability refusal still answers first for a caller without the capability. The seven routes that write no record still answer200for the member.singleand with notenancyservice: each of the four verbs is served for the member and the write lands.datasource-update-kept-pool-stores-nothing.test.ts(the realDatasourceAdminServicePluginandExternalDatasourceServicePluginbehind the real routes, the real secret binder, a driver factory that records every pool it builds): underisolatedandgroupa PATCH carrying a new secret and a new host by the entitled caller is refused with the verdict's status, code and sentence; the stored record and thesys_secretids are byte-equal to before, no pool is built, and the serving pool still answers its test. Control: the same PATCH undersinglestores the new host and builds one new pool. This rig has nogetKernel, so it also covers the synchronous posture read.Acceptance notes
resolve(): the service below receives the record, not the caller's rung, and the door is where this family's admission already lives. One option on the shared floor (catalogWrite) is passed by exactly the four row-writing routes.putDatasourceRecord,deleteDatasourceRecord, the secret writes and the pool seams) are reached only fromcreateDatasource,updateDatasource,removeDatasourceandmigrateCredential.testConnection(draft and saved),listRemoteTables,generateObjectDraft,listDatasourcesandgetDatasourcereach none of them.PERMISSION_DENIED, the standard-catalog member this family already answers for a 403, and the code the dispatcher/metadoor answers; the REST/metadoor answers its grandfathered synonym (STANDARD_SYNONYM_WAIVERS, ADR-0112 D3), which is not endorsed for new code.isSystem. Not passed to the verdict: this family's resolution never carries it from inbound HTTP, so the verdict's engine admission is unreachable here, as before.not-required (no-migration-prescription)).Verification
All readings below are at head
b99ce15091cc324f14293a091741c641936cd413, afterorigin/mainwas merged in ate304cae63f(no conflicts;mainhad movedcore/security,metadata-core,tenancy-modesand aservice-datasourcetest), the closure rebuilt and every build and test run under the shared verify lock.mainhas moved since by 14 commits, none touchingservice-datasource,metadata-core, the authorization resolver or the two docs pages (onecore/securitypin test).Package tests and typecheck
pnpm --filter @objectstack/service-datasource exec vitest run: Test Files 72 passed (72), Tests 1279 passed (1279).pnpm --filter @objectstack/metadata-core exec vitest run: Test Files 23 passed (23), Tests 911 passed (911).typecheckforservice-datasource,metadata-coreand the direct consumers ofservice-datasource(rest,runtime,cli,verify,dogfood,example-showcase):VERDICT command-exit 0, every packageDone, 0error TSlines. The package's typecheck compiles both edited test files (tsc --listFiles).pnpm --filter @objectstack/spec check:generatedafter the merge: all 14 generated artifacts up to date.Ablations
Each through
scripts/ablation-replace.mjs(anchor hit once, blob changed, restored blob-equal to HEAD withgit diff HEADempty), on the committed tree. The pins import../admin-routes.jsfrom source, so no rebuild sits between the mutation and the run. The four route ablations were read at56a424acf3(blobc44ff35c97f1);admin-routes.tshas since changed only in a doc comment (blobbf51f83b0cbc). The posture-read ablation was read at the head.admin_full_accessrefusals, underisolatedandgroupPATCH /:nameisolatedandgroupDELETE /:nameadmin_full_accessrefusals, both posturesPOST /:name/migrate-credentialadmin_full_accessrefusals, both posturesisolatedandgroupAfter each restore the blob equals
HEAD's andgit diff HEADis empty;git status --porcelainempty after the last.Derived gates
node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack(no paths) atb99ce15091: 102 commands, each run as printed with stdin from/dev/nulland its exit code recorded.--ran: 102 derived, 102 run, 0 NOT-MEASURED, 0 UNRUN; every recorded exit is 0. Before the merge, atf2dc54d5ea, two first answered exit 3 (check:skill-examples,check:dual-build-cjs-loads: prerequisite packages outside this diff unbuilt) and answered 0 once those were built; they were built before the run at the head.pnpm check:adr-anchors: exit 0 with the new anchor.Full dogfood battery at the head
b99ce15091cc324f14293a091741c641936cd413, 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 0