Repository navigation
Commit 595311b
<!-- claude-projects-attribution: {"github_login":"os-project-manager"}
-->
_Requested by **Leehom** · [project
thread](https://claude.ai/code/project/chan_01TenzsRopZj4jMbvjX8uz1X?thread=cmsg_01TenzsRopZj4jMbvjX8uz1XTVkGrf2CjQTF79hBERgqiJ)_
domain:server#5 · os-project-manager · session_01FEkQ5ktJb5rQ5n2GUXSQMR
Fixes #31392
Clause-②: no (narrowing) — a cloud-branch install that answered success
(`hotLoaded: false`) for a held namespace or a held security catalog
name is now refused 422; one contract-review-tier review owed before the
queue
Metadata-protocol: none
## 维护者速读
- 改了什么:`POST /api/v1/marketplace/install-local`
的云端分支(从控制面下载的包)现在拒绝两类注册失败,和内联分支一样回 `422
PLUGIN_REGISTER_FAILED`。一类是包的命名空间已被另一个已装包占用(`NamespaceConflictError`);另一类是包带的岗位、权限集或能力名已被另一个包或平台内置名占用(`SecurityCatalogNameConflictError`,持有者为包或内置)。消息点名两个持有者,不写安装记录,也不注册。
- 为什么改:以前这两类都走宽容路径:写安装记录,回 200 和 `hotLoaded:
false`,声称“下次重启加载”。但这两项检查在每次加载时都会重新判定,而且没有封印,所以重启时同一个持有者还在,注册还是会被拒。
- 风险与回滚:原来的两个判定函数合并成一个 `refusesNameHeldElsewhere`,它按错误自身的 code 加 `name`
识别注册表的三种“名字已被占用”拒绝(命名空间、安全目录、裸名),不复制任何持有者或类型规则。其它注册失败照旧走宽容路径。`OS_METADATA_COLLISION=warn`
下命名空间闸门本来就不抛错,行为不变。回滚即还原本 PR。
- 先看:`packages/cloud-connection/src/marketplace-install-local-plugin.ts`
里的 `NAME_HELD_ELSEWHERE_REFUSALS`、`refusesNameHeldElsewhere`,以及 catch
分支里的那一行 `if`。
- 两分钟验证:`pnpm --filter @objectstack/cloud-connection exec vitest run
src/marketplace-install-local-catalog-name-collision.test.ts
src/marketplace-install-local-bare-name-conflict.test.ts`,预期 28 个用例全绿。
- 需要你做的:无,席位落地。
## What changes
`packages/cloud-connection/src/marketplace-install-local-plugin.ts`, the
cloud-fetched branch of `POST /api/v1/marketplace/install-local` (a
`packageId` + `versionId` body):
1. **One predicate in place of two.** `refusesNameHeldElsewhere(err)`
replaces `refusesEnvironmentHeldCatalogName` (#25366) and
`refusesBareNameHeldElsewhere` (#31296). It is #31296's code + name
recognition, widened to the registry's three held-name refusals, which
share `NAMESPACE_CONFLICT_CODE`:
- `NamespaceConflictError`
- `SecurityCatalogNameConflictError`
- `BareNameConflictError`
The names are listed once, in `NAME_HELD_ELSEWHERE_REFUSALS`. The
predicate never reads the error's holders or types. #25366's
environment-only filter goes away: the environment-held refusal is one
case of `SecurityCatalogNameConflictError`, and package and built-in
holders are now refused as well.
2. **The catch asks it.** When it answers yes, the cloud branch answers
`422 PLUGIN_REGISTER_FAILED` with the registry's sentence. That happens
before the ledger write and before anything else this door writes. The
registry's package door had already refused before any of its own
writes.
3. **Doc comments.** These now say which refusals are refused and why no
restart heals any of them:
- the module header's install paragraph;
- the import comment;
- the catch comment;
- step 1f's note, which used to read "a namespace held by another code
package keeps the lenient branch's existing behaviour, whose fate is a
separate decision".
What does not change:
- Any other register failure on the cloud branch keeps #22695's lenient
answer.
- The inline branch is unchanged.
- Step 1f's `409 RESOURCE_CONFLICT` for a namespace a writable base
holds still answers first.
- Under `OS_METADATA_COLLISION=warn`, the namespace gate warns and
throws nothing.
## Tests
Both harnesses use a real `ObjectKernel` + `ObjectQLPlugin`, booted. The
holder is installed through the same door, and the cloud leg is a
stubbed `fetch`. Cases are added to the two existing harnesses; there is
no new test file.
- `marketplace-install-local-catalog-name-collision.test.ts`: one new
describe on the cloud-fetched branch.
- A permission set name another installed package holds. On a fresh
install and on an upgrade, the answer is 422 in the declared envelope,
naming `package "com.acme.dispatch-roles"`. Nothing is registered or
ledgered, the installed version 1 is kept, and the holder's item and
ledger entry are unchanged.
- The built-in position `everyone`: 422 naming the platform, nothing
registered or ledgered.
- CONTROL: beside the holder, an upgrade shipping a name nobody holds
installs. The answer is `200` with `hotLoaded: true`, and the package is
registered and ledgered.
- The existing #25366 matrix stays as it is, and it is the CONTROL that
an environment-held catalog name keeps its 422.
- `marketplace-install-local-bare-name-conflict.test.ts`: one new
describe.
- The incoming package declares the namespace the holder owns
(`dispatch`), with no dependency on it. On a fresh install and on an
upgrade, the answer is 422 with the registry's namespace sentence, and
the registry and the ledger are the same as before the call.
- The harness's `objectql` service now hands the door the engine's
`manifests`, read live, as the real service does. Without that, the
holder this door installed reads as a writable base, and step 1f answers
409 first. That 409 was measured, and it is not this card's refusal.
- The existing CONTROL that a fresh flow name installs stays as it is.
<details><summary>Red on main (the new cases run against the unfixed
plugin at base 1ff9944, before the fix was written)</summary>
```
× a fresh install is refused: 422 naming the namespace and its owner, nothing registered or ledgered
× an upgrade into the held namespace is refused: 422, the installed version still registered and ledgered
× a fresh install of a name another package holds is refused: 422 naming the holder, nothing registered or ledgered
× an upgrade to a name another package holds is refused: 422, the installed version still registered and ledgered
× a fresh install of a built-in name is refused: 422 naming the platform, nothing registered or ledgered
AssertionError: {"success":true,"data":{"manifestId":"com.acme.routing",…,"hotLoaded":false,"hotLoadError":"Namespace conflict: namespace \"dispatch\" is already owned by package \"com.acme.dispatch\", so package \"com.acme.routing\" cannot be installed alongside it. …"}}: expected 200 to be 422
AssertionError: {"success":true,"data":{"manifestId":"com.acme.field-service",…,"hotLoaded":false,"hotLoadError":"Security catalog name conflict: … permission set \"acme_dispatcher_lite\" is already held by package \"com.acme.dispatch-roles\". …"}}: expected 200 to be 422
AssertionError: {"success":true,"data":{"manifestId":"com.acme.field-service",…,"hotLoaded":false,"hotLoadError":"Security catalog name conflict: … position \"everyone\" is already held by the platform, as a built-in name no package can declare. …"}}: expected 200 to be 422
Tests 5 failed | 23 passed (28)
```
The namespace half was measured before it was pinned. Its first run
against the harness without `manifests` answered `409 RESOURCE_CONFLICT`
from step 1f, because the holder read as a writable base. With the
engine's `manifests` handed over, it answered `200`, `hotLoaded: false`,
and the package was ledgered.
</details>
<details><summary>Ablations (fix committed at 2f6e606, each
restored)</summary>
Both were run through `node scripts/ablation-replace.mjs` on
`marketplace-install-local-plugin.ts`, against the two suites. The
suites import the plugin from `src/` relatively, so no build applies.
1. The `'NamespaceConflictError'` entry was commented out of
`NAME_HELD_ELSEWHERE_REFUSALS`:
```
ok mutation landed: anchor 1 -> 0, blob 6d5c60f -> adfacdf83da1
× a fresh install is refused: 422 naming the namespace and its owner, nothing registered or ledgered
× an upgrade into the held namespace is refused: 422, the installed version still registered and ledgered
Tests 2 failed | 26 passed (28)
ok restored: blob == HEAD (6d5c60f) and `git diff HEAD` is empty
```
2. The `'SecurityCatalogNameConflictError'` entry was commented out. The
three new cases red, and so do the six cloud-branch cases of the #25366
matrix plus its CONTROL in the bare-name file, because the
environment-held refusal is now recognised by the same entry:
```
ok mutation landed: anchor 1 -> 0, blob 6d5c60f -> 89a012028652
× CONTROL — an environment-held security catalog name keeps its answer: 422 naming the environment catalog
× a fresh install is refused: 422, nothing registered, no ledger entry (×3, cloud-fetched · permission / position / capability)
× an upgrade is refused: 422, the installed version still registered and ledgered (×3)
× a fresh install of a name another package holds is refused: 422 naming the holder, nothing registered or ledgered
× an upgrade to a name another package holds is refused: 422, the installed version still registered and ledgered
× a fresh install of a built-in name is refused: 422 naming the platform, nothing registered or ledgered
Tests 10 failed | 18 passed (28)
ok restored: blob == HEAD (6d5c60f) and `git diff HEAD` is empty
```
</details>
## Verification
All checks ran on head `2f6e606d`: the fix, the tests and the changeset.
`origin/main` was fetched before verification and was still at its base,
1ff9944, so there is no merge. Each exit code was captured before any
pipe.
- **Full build**, `pnpm turbo run build --concurrency=2`, under the
verify lock: VERDICT command-exit 0 (120 of 120 tasks).
- **Derived gate union**, `node scripts/pm/dispatch-gates.mjs --commands
--repo objectstack-ai/objectstack`: 69 families, every one exit 0 on its
first run. `--ran` reports "69 derived famil(ies) accounted for — 69
run, 0 NOT-MEASURED (a DERIVED zero — all 69 recorded an exit code and
none of them is 3)". `check:dts-closure` and
`check:dual-build-cjs-loads` ran after the full build.
`check:type-check-debt` ran after the dogfood run released the lock.
- **Facade gate**, `pnpm check:facade-delegation-only`, run by hand
until devx #31444 lands: exit 0. This diff touches no facade file.
- **Package**: `pnpm --filter @objectstack/cloud-connection test`: Test
Files 75 passed, Tests 1000 passed (exit 0). `pnpm --filter
@objectstack/cloud-connection typecheck` (src and `tsconfig.test.json`):
exit 0.
- **Lint**: `eslint --no-inline-config` on the three touched `.ts`
files: exit 0.
- **Full dogfood battery**, the `(narrowing)` arm, `pnpm --filter
@objectstack/dogfood test` after the full build, under the verify lock:
VERDICT command-exit 0. Test Files 439 passed / 3 skipped (442), Tests
3628 passed / 41 skipped (3669).
- CI-only families (the Test Core shards, the type-check lanes, Build
Docs) are read from this PR's check-runs, not re-run here.
## Acceptance notes
- **Why one predicate.** The seat asked for #31296's code + name
recognition with no copy of any holder rule. With package and built-in
holders refused too, #25366's environment-only filter would have been
the only holder reading left in this door, and the three classes' names
already say everything the door needs to know.
- **Namespace and step 1f.** Step 1f, the writable-base check, answers
`409` before the register for a namespace a writable base holds. This PR
is about the namespace a code package holds, which step 1f's comment had
left as a separate decision. That decision is now this card's 422.
- **Old ledger entries.** An entry an earlier build ledgered this way is
read at boot as before. This PR refuses new ones only.
- **Governed surface.** None. The diff is one source file, two test
files and one changeset.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
https://claude.ai/code/session_01FEkQ5ktJb5rQ5n2GUXSQMR
---
_Generated by [Claude
Code](https://claude.ai/code/session_01FEkQ5ktJb5rQ5n2GUXSQMR)_
Co-authored-by: Claude <noreply@anthropic.com>
1 parent a1d243e commit 595311b
4 files changed
Lines changed: 273 additions & 74 deletions
File tree
- .changeset
- packages/cloud-connection/src
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
40 | 40 | | |
41 | 41 | | |
42 | 42 | | |
43 | | - | |
| 43 | + | |
| 44 | + | |
| 45 | + | |
| 46 | + | |
| 47 | + | |
| 48 | + | |
| 49 | + | |
| 50 | + | |
44 | 51 | | |
45 | 52 | | |
46 | 53 | | |
| |||
154 | 161 | | |
155 | 162 | | |
156 | 163 | | |
| 164 | + | |
| 165 | + | |
| 166 | + | |
| 167 | + | |
157 | 168 | | |
158 | 169 | | |
159 | 170 | | |
| |||
298 | 309 | | |
299 | 310 | | |
300 | 311 | | |
| 312 | + | |
| 313 | + | |
| 314 | + | |
| 315 | + | |
| 316 | + | |
| 317 | + | |
| 318 | + | |
| 319 | + | |
| 320 | + | |
| 321 | + | |
| 322 | + | |
| 323 | + | |
| 324 | + | |
| 325 | + | |
| 326 | + | |
| 327 | + | |
| 328 | + | |
| 329 | + | |
| 330 | + | |
| 331 | + | |
| 332 | + | |
| 333 | + | |
| 334 | + | |
| 335 | + | |
| 336 | + | |
| 337 | + | |
| 338 | + | |
| 339 | + | |
| 340 | + | |
| 341 | + | |
| 342 | + | |
| 343 | + | |
| 344 | + | |
| 345 | + | |
| 346 | + | |
| 347 | + | |
| 348 | + | |
| 349 | + | |
| 350 | + | |
| 351 | + | |
| 352 | + | |
| 353 | + | |
| 354 | + | |
| 355 | + | |
| 356 | + | |
| 357 | + | |
| 358 | + | |
| 359 | + | |
| 360 | + | |
| 361 | + | |
| 362 | + | |
| 363 | + | |
| 364 | + | |
| 365 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
36 | 36 | | |
37 | 37 | | |
38 | 38 | | |
39 | | - | |
| 39 | + | |
| 40 | + | |
| 41 | + | |
| 42 | + | |
| 43 | + | |
| 44 | + | |
| 45 | + | |
40 | 46 | | |
41 | 47 | | |
42 | 48 | | |
| |||
48 | 54 | | |
49 | 55 | | |
50 | 56 | | |
| 57 | + | |
51 | 58 | | |
52 | 59 | | |
53 | 60 | | |
| |||
284 | 291 | | |
285 | 292 | | |
286 | 293 | | |
| 294 | + | |
| 295 | + | |
| 296 | + | |
| 297 | + | |
| 298 | + | |
| 299 | + | |
| 300 | + | |
| 301 | + | |
| 302 | + | |
| 303 | + | |
| 304 | + | |
| 305 | + | |
| 306 | + | |
| 307 | + | |
| 308 | + | |
| 309 | + | |
| 310 | + | |
| 311 | + | |
| 312 | + | |
| 313 | + | |
| 314 | + | |
| 315 | + | |
| 316 | + | |
| 317 | + | |
| 318 | + | |
| 319 | + | |
| 320 | + | |
| 321 | + | |
| 322 | + | |
| 323 | + | |
| 324 | + | |
| 325 | + | |
| 326 | + | |
| 327 | + | |
| 328 | + | |
| 329 | + | |
| 330 | + | |
| 331 | + | |
| 332 | + | |
| 333 | + | |
| 334 | + | |
| 335 | + | |
| 336 | + | |
| 337 | + | |
| 338 | + | |
| 339 | + | |
| 340 | + | |
| 341 | + | |
| 342 | + | |
| 343 | + | |
| 344 | + | |
| 345 | + | |
| 346 | + | |
| 347 | + | |
| 348 | + | |
| 349 | + | |
| 350 | + | |
| 351 | + | |
| 352 | + | |
| 353 | + | |
| 354 | + | |
| 355 | + | |
| 356 | + | |
| 357 | + | |
| 358 | + | |
| 359 | + | |
| 360 | + | |
| 361 | + | |
| 362 | + | |
| 363 | + | |
| 364 | + | |
| 365 | + | |
| 366 | + | |
| 367 | + | |
| 368 | + | |
| 369 | + | |
| 370 | + | |
| 371 | + | |
| 372 | + | |
| 373 | + | |
| 374 | + | |
| 375 | + | |
| 376 | + | |
| 377 | + | |
| 378 | + | |
| 379 | + | |
| 380 | + | |
| 381 | + | |
| 382 | + | |
| 383 | + | |
| 384 | + | |
| 385 | + | |
| 386 | + | |
| 387 | + | |
| 388 | + | |
| 389 | + | |
| 390 | + | |
| 391 | + | |
| 392 | + | |
| 393 | + | |
| 394 | + | |
| 395 | + | |
| 396 | + | |
| 397 | + | |
| 398 | + | |
| 399 | + | |
0 commit comments