Repository navigation
fix(cloud-connection)!: refuse a cloud-fetched marketplace install whose catalog name an environment-authored item holds - #25435
Conversation
…se catalog name an environment-authored item holds The cloud-fetched branch's lenient register catch swallowed the registry's security-catalog refusal and wrote the ledger entry anyway, answering 200 with hotLoaded false. No restart heals that refusal: the environment's item is loaded at every boot, so the ledger entry can never load. Both branches now refuse it with 422 PLUGIN_REGISTER_FAILED before the ledger write. Claude-Session: https://claude.ai/code/session_01ENyzeNx2dXdHp37P4Zdp2d Co-authored-by: Claude <noreply@anthropic.com>
…ent-held catalog name on both branches Claude-Session: https://claude.ai/code/session_01ENyzeNx2dXdHp37P4Zdp2d Co-authored-by: Claude <noreply@anthropic.com>
…ction at its body parse Claude-Session: https://claude.ai/code/session_01ENyzeNx2dXdHp37P4Zdp2d Co-authored-by: Claude <noreply@anthropic.com>
…e environment-held catalog name refusal Claude-Session: https://claude.ai/code/session_01ENyzeNx2dXdHp37P4Zdp2d Co-authored-by: Claude <noreply@anthropic.com>
📓 Docs Drift Check3 anchor(s) derived from 1 changed package(s); no hand-written page names any of them, so this run has nothing to list — not a clean bill of health. This check sees only pages that NAME a derived anchor: one that documents this change in prose, or enumerates it in an authoring dialect, names none and stays invisible to it on every run. What this run could not see
Coarse fallback — 5 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 eb1bc47bc860a43f52920d031d14e28f83289148 && git checkout eb1bc47bc860a43f52920d031d14e28f83289148
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 77392e901cce58316e803144971f2741ef049fc4 6a16bf07b0e66885315645fa69bfda96ff26d270 && git checkout -B drift-repro 77392e901cce58316e803144971f2741ef049fc4 && git merge --no-ff 6a16bf07b0e66885315645fa69bfda96ff26d270
node scripts/docs-audit/affected-docs.mjs --json 77392e901cce58316e803144971f2741ef049fc4 |
Contract reviewServed-tier: Inputs: card #25366 (body, claim 6364974343, os-dev-report 6366332431), PR #25435 (body, 4 files, net diff ① Derived judgments
② Semver level
③ Boundary flags
Implemented-by: VERDICT: PASS |
席位核对 · ACCEPT · 2026-10-23T13:56Z
Generated by Claude Code |
Requested by Sales · project thread
Fixes #25366
Clause-②: no (narrowing)
Metadata-protocol: none
Claim 6364974343 (
domain:servicesseat 1, #6021). The branch is the harness-pinnedclaude/project-thread-gzt6ro. It names no issue, so the claim is the identity record for it.What changed
cloud-connection, step 3 ofPOST /api/v1/marketplace/install-local. When the registry refuses the package because an environment-authored item already holds a position, permission set or capability name the package ships, the cloud-fetched branch now refuses422 PLUGIN_REGISTER_FAILEDbefore the ledger write. The answer carries the registry's own sentence, which names each colliding name and its holder. The inline (file-import) branch already answered exactly this. Before, the cloud branch answered200withhotLoaded: false, wrote the ledger entry, and said the package would load at the next restart.SchemaRegistry.installPackage→refuseSecurityCatalogNameConflicts) already runs ahead of every registry mutation, for every door that installs a package. The door asks that refusal rather than keeping a second reading of who holds a name.refusesEnvironmentHeldCatalogNamerecognises it byNAMESPACE_CONFLICT_CODEplus aconflictsentry whose holder isenvironment. The engine keeps the error class unexported, so this is a code comparison and notinstanceof.packages/objectqlstay as they are.Every install door, measured
manifesthotLoaded:false, ledger writtenhotLoaded:false, ledger overwrittenPOST /api/v1/packages, fresh andoverwriteVALIDATION_ERRORat the body parse, namingpositions/capabilities/permissionsprotocol.installPackagecallers (duplicate, AI blueprint)Boot-time plugin registrations are not install doors.
The premise, measured
The card says the next cold boot is refused. A real restart shows something different. The boot runs. The marketplace rehydrate, at
kernel:ready, after the engine's own cold-boot check, logsrehydrate failed for com.acme.field-servicewith the security-catalog sentence. The package is then not loaded at all. On an upgrade its earlier version does not load either, because the ledger entry that named that version was overwritten. The deployment does not stop, but the package's apps, objects and permission sets disappear whileGET /api/v1/marketplace/install-localstill lists the package. The fix is the same either way.R1–R4
My reading of R1–R4, taken from #24446 U2 practice:
singlethat drives the door.objectstack serverun under walledisolatedandgroupthat drives the door.The test manifests are served by a local stub control plane (
OS_CLOUD_URLpointed at it,OS_MARKETPLACE_PUBLIC_BASE_URL=off). Packagecom.acme.field-service:rone_dispatcher;rone_dispatcherandrone_dispatcher_lite;rone_dispatcherandrone_auditor.The environment's clone is
PUT /api/v1/meta/permission/rone_dispatcher_lite, which answers "Saved permission … (env-wide)".R1,
single(pnpm devon the showcase, persistent SQLite).The ablated leg (guard disabled with
ablation-replace, dist rebuilt; esbuild drops the dead branch, and the dist has noexistingHolder):200.200.200 hotLoaded:false("registers it again at its next restart"), and the ledger reads 2.0.0.rehydrate failed.rone_dispatcheranswers404, and/api/v1/packageshas no row for the package.Then the two remedies, each across a real restart:
200 hotLoaded:true, and it loads again after a restart.rone_dispatcher_lite_custom, deleterone_dispatcher_lite, then restart. v2 loads,rone_dispatcher_liteis the vendor's at 2.0.0, and the clone lives on under its new name.The fixed leg (restored, blob == HEAD, dist rebuilt), on a fresh database:
200, then clone200.422 PLUGIN_REGISTER_FAILEDwith the sentence. v1 stays loaded and the ledger stays at 1.0.0.rehydrate failed.200 hotLoaded:true, upgradedFromset, androne_auditoris at 2.0.0.R4, production
objectstack serveof the built showcase. Settings:NODE_ENV=production, persistent SQLite,OS_PLATFORM_OWNER_EMAILset to the operator,OS_AUTH_MEMBERSHIP_POLICY=invite-only, andOS_SECRET_KEY. Each banner printsTenancy: isolatedorTenancy: group.The run needed temporary changes, all reverted afterwards:
@objectstack/organizationsdeclared in the showcase'spackage.jsonand linked into itsnode_modules(no lockfile change, compared byte for byte), plus the test databases and ledgers under scratch. The tree is clean.Under both
isolatedandgroup, the operator saw the same sequence:200, then clone200.422 PLUGIN_REGISTER_FAILED. v1 stays and the ledger stays at 1.0.0.rehydrate failed.200, androne_auditoris at 2.0.0.A walled tenant administrator is stopped earlier by the unchanged ADR-0131 D12 refusal (
403), so the operator is the only caller who reaches this step.Tests
cloud-connection/src/marketplace-install-local-catalog-name-collision.test.ts(18). It runs over a realObjectKernelwithObjectQLPluginand the kernel's realmanifestservice. The cases cover permission set, position and capability, on both the inline and the cloud-fetched branch:BaseResponseSchemaenvelope naming the name and "the environment catalog". The environment item is kept.200 hotLoaded:falseanswer. The inline cases stay green.runtime/src/package-door-security-catalog-collection-refused.test.ts(9). It runs the realHttpDispatcher.handlePackagesover a realSchemaRegistryholding the environment's item. For each type, a fresh install and an overwrite carrying the collection are refused400naming the key, and nothing is installed or the earlier version stands. CONTROL: the same manifests without the collection install201.POST /api/v1/packagesgets no check of its own, because it cannot carry a catalog name.Acceptance notes
engine.registerAppsetsthis.manifestsand the datasource definitions beforeinstallPackageruns its refusal. So a refused install may leave a manifest entry in the engine's own map. Not touched here.os migrate security-catalog-overlays, named by the engine's cold-boot remedy, boots the stack composition, which likely does not include the marketplace ledger. It may therefore list nothing for a marketplace package. Not measured.The proof readings are in the os-dev-report on #25366: the dispatch gates,
check-governed-merges --branch(0 of 4 paths governed, 480 lines), R1, R4 and the dogfood battery.Generated by Claude Code
🤖 Generated with Claude Code
https://claude.ai/code/session_01ENyzeNx2dXdHp37P4Zdp2d