Repository navigation
Commit 8d9e8b9
fix(cloud-connection)!: refuse a cloud-fetched marketplace install whose catalog name an environment-authored item holds (#25435)
<!-- claude-projects-attribution: {"github_login":"os-sales"} -->
_Requested by **Sales** · [project
thread](https://claude.ai/code/project/chan_01D5bWFY3rFzgJcytD5gD4Uu?thread=cmsg_01D5bWFY3rFzgJcytD5gD4Uu6LTunNmT4vzpPoisQo1ZXX)_
Fixes #25366
Clause-②: no (narrowing)
Metadata-protocol: none
Claim 6364974343 (`domain:services` seat 1, #6021). The branch is the
harness-pinned `claude/project-thread-gzt6ro`. It names no issue, so the
claim is the identity record for it.
## What changed
**`cloud-connection`, step 3 of `POST
/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 refuses `422 PLUGIN_REGISTER_FAILED` before 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 answered `200` with
`hotLoaded: false`, wrote the ledger entry, and said the package would
load at the next restart.
- **One shared pre-write check.** The registry's package door
(`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. `refusesEnvironmentHeldCatalogName`
recognises it by `NAMESPACE_CONFLICT_CODE` plus a `conflicts` entry
whose holder is `environment`. The engine keeps the error class
unexported, so this is a code comparison and not `instanceof`.
- **Narrow on purpose.** Every other register failure on the cloud
branch keeps its lenient answer. A name another package or a built-in
holds is not judged here.
- ⛔ Engine lane untouched: the refusal, its message and the cold-boot
check in `packages/objectql` stay as they are.
## Every install door, measured
| door | before | after |
|---|---|---|
| marketplace install-local, inline `manifest` | 422, nothing written |
unchanged |
| marketplace install-local, cloud-fetched, fresh | 200
`hotLoaded:false`, ledger written | **422, nothing written** |
| marketplace install-local, cloud-fetched, upgrade | 200
`hotLoaded:false`, ledger overwritten | **422, earlier version stays** |
| `POST /api/v1/packages`, fresh and `overwrite` | 400
`VALIDATION_ERROR` at the body parse, naming `positions` /
`capabilities` / `permissions` | unchanged, now pinned |
| `protocol.installPackage` callers (duplicate, AI blueprint) | same
strict parse | unchanged |
| unmanaged install | the import verb's own collision refusal |
unchanged, out of scope |
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, logs `rehydrate
failed for com.acme.field-service` with 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 while `GET
/api/v1/marketplace/install-local` still lists the package. The fix is
the same either way.
## R1–R4
My reading of R1–R4, taken from #24446 U2 practice:
- **R1:** a dev-server run under `single` that drives the door.
- **R4:** a production `objectstack serve` run under walled `isolated`
and `group` that drives the door.
- **R2 and R3:** the package tests and the dispatch gates.
The test manifests are served by a local stub control plane
(`OS_CLOUD_URL` pointed at it, `OS_MARKETPLACE_PUBLIC_BASE_URL=off`).
Package `com.acme.field-service`:
- v1 ships `rone_dispatcher`;
- v2-collides ships `rone_dispatcher` and `rone_dispatcher_lite`;
- v2-free ships `rone_dispatcher` and `rone_auditor`.
The environment's clone is `PUT
/api/v1/meta/permission/rone_dispatcher_lite`, which answers "Saved
permission … (env-wide)".
**R1, `single` (`pnpm dev` on the showcase, persistent SQLite).**
The ablated leg (guard disabled with `ablation-replace`, dist rebuilt;
esbuild drops the dead branch, and the dist has no `existingHolder`):
1. v1 installs `200`.
2. Clone `200`.
3. v2-collides answers `200 hotLoaded:false` ("registers it again at its
next restart"), and the ledger reads 2.0.0.
4. A restart logs `rehydrate failed`. `rone_dispatcher` answers `404`,
and `/api/v1/packages` has no row for the package.
Then the two remedies, each across a real restart:
- **Re-install v1:** `200 hotLoaded:true`, and it loads again after a
restart.
- **Rename the clone:** author `rone_dispatcher_lite_custom`, delete
`rone_dispatcher_lite`, then restart. v2 loads, `rone_dispatcher_lite`
is 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:
1. v1 `200`, then clone `200`.
2. v2-collides answers `422 PLUGIN_REGISTER_FAILED` with the sentence.
v1 stays loaded and the ledger stays at 1.0.0.
3. A restart loads v1, with no `rehydrate failed`.
4. CONTROL: v2-free answers `200 hotLoaded:true, upgradedFrom` set, and
`rone_auditor` is at 2.0.0.
**R4, production `objectstack serve` of the built showcase.** Settings:
`NODE_ENV=production`, persistent SQLite, `OS_PLATFORM_OWNER_EMAIL` set
to the operator, `OS_AUTH_MEMBERSHIP_POLICY=invite-only`, and
`OS_SECRET_KEY`. Each banner prints `Tenancy: isolated` or `Tenancy:
group`.
The run needed temporary changes, all reverted afterwards:
`@objectstack/organizations` declared in the showcase's `package.json`
and linked into its `node_modules` (no lockfile change, compared byte
for byte), plus the test databases and ledgers under scratch. The tree
is clean.
Under both `isolated` and `group`, the operator saw the same sequence:
1. v1 `200`, then clone `200`.
2. v2-collides answers `422 PLUGIN_REGISTER_FAILED`. v1 stays and the
ledger stays at 1.0.0.
3. A restart loads v1, with no `rehydrate failed`.
4. CONTROL: v2-free `200`, and `rone_auditor` is 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 real `ObjectKernel` with `ObjectQLPlugin` and the
kernel's real `manifest` service. The cases cover permission set,
position and capability, on both the inline and the cloud-fetched
branch:
- a fresh install is refused with nothing registered and no ledger
entry;
- an upgrade is refused and v1 stays registered and ledgered;
- CONTROL: a non-colliding v2 installs.
- Each refusal is a valid `BaseResponseSchema` envelope naming the name
and "the environment catalog". The environment item is kept.
- Ablation (guard disabled) turns the 6 cloud-branch refusal cases red,
with main's `200 hotLoaded:false` answer. The inline cases stay green.
-
**`runtime/src/package-door-security-catalog-collection-refused.test.ts`**
(9). It runs the real `HttpDispatcher.handlePackages` over a real
`SchemaRegistry` holding the environment's item. For each type, a fresh
install and an overwrite carrying the collection are refused `400`
naming the key, and nothing is installed or the earlier version stands.
CONTROL: the same manifests without the collection install `201`. `POST
/api/v1/packages` gets no check of its own, because it cannot carry a
catalog name.
- The cloud-connection suite: 56 files and 722 tests pass. Typecheck
passes for cloud-connection and runtime. eslint on the touched files
passes.
## Acceptance notes
- **Out of scope, engine lane (not measured as a defect):**
`engine.registerApp` sets `this.manifests` and the datasource
definitions before `installPackage` runs its refusal. So a refused
install may leave a manifest entry in the engine's own map. Not touched
here.
- **Out of scope, observation:** `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.
- A name held by another package or a built-in still takes the lenient
cloud answer. That is outside this card.
- The changeset carries the operator remedy for a deployment that
already took such an upgrade. Both remedy steps were measured in R1
across a real restart.
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](https://claude.ai/code/session_01ENyzeNx2dXdHp37P4Zdp2d)_
🤖 Generated with [Claude Code](https://claude.com/claude-code)
https://claude.ai/code/session_01ENyzeNx2dXdHp37P4Zdp2d
---------
Co-authored-by: Claude <noreply@anthropic.com>1 parent b3f3298 commit 8d9e8b9
4 files changed
Lines changed: 479 additions & 1 deletion
File tree
- .changeset
- packages
- cloud-connection/src
- runtime/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 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 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 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
| 37 | + | |
| 38 | + | |
| 39 | + | |
| 40 | + | |
| 41 | + | |
| 42 | + | |
| 43 | + | |
| 44 | + | |
| 45 | + | |
| 46 | + | |
| 47 | + | |
| 48 | + | |
| 49 | + | |
| 50 | + | |
| 51 | + | |
| 52 | + | |
| 53 | + | |
| 54 | + | |
| 55 | + | |
| 56 | + | |
| 57 | + | |
| 58 | + | |
| 59 | + | |
| 60 | + | |
| 61 | + | |
| 62 | + | |
| 63 | + | |
| 64 | + | |
| 65 | + | |
| 66 | + | |
| 67 | + | |
| 68 | + | |
| 69 | + | |
| 70 | + | |
| 71 | + | |
| 72 | + | |
| 73 | + | |
| 74 | + | |
| 75 | + | |
| 76 | + | |
| 77 | + | |
| 78 | + | |
| 79 | + | |
| 80 | + | |
| 81 | + | |
| 82 | + | |
| 83 | + | |
| 84 | + | |
| 85 | + | |
| 86 | + | |
| 87 | + | |
| 88 | + | |
| 89 | + | |
| 90 | + | |
| 91 | + | |
| 92 | + | |
| 93 | + | |
| 94 | + | |
| 95 | + | |
| 96 | + | |
| 97 | + | |
| 98 | + | |
| 99 | + | |
| 100 | + | |
| 101 | + | |
| 102 | + | |
| 103 | + | |
| 104 | + | |
| 105 | + | |
| 106 | + | |
| 107 | + | |
| 108 | + | |
| 109 | + | |
| 110 | + | |
| 111 | + | |
| 112 | + | |
| 113 | + | |
| 114 | + | |
| 115 | + | |
| 116 | + | |
| 117 | + | |
| 118 | + | |
| 119 | + | |
| 120 | + | |
| 121 | + | |
| 122 | + | |
| 123 | + | |
| 124 | + | |
| 125 | + | |
| 126 | + | |
| 127 | + | |
| 128 | + | |
| 129 | + | |
| 130 | + | |
| 131 | + | |
| 132 | + | |
| 133 | + | |
| 134 | + | |
| 135 | + | |
| 136 | + | |
| 137 | + | |
| 138 | + | |
| 139 | + | |
| 140 | + | |
| 141 | + | |
| 142 | + | |
| 143 | + | |
| 144 | + | |
| 145 | + | |
| 146 | + | |
| 147 | + | |
| 148 | + | |
| 149 | + | |
| 150 | + | |
| 151 | + | |
| 152 | + | |
| 153 | + | |
| 154 | + | |
| 155 | + | |
| 156 | + | |
| 157 | + | |
| 158 | + | |
| 159 | + | |
| 160 | + | |
| 161 | + | |
| 162 | + | |
| 163 | + | |
| 164 | + | |
| 165 | + | |
| 166 | + | |
| 167 | + | |
| 168 | + | |
| 169 | + | |
| 170 | + | |
| 171 | + | |
| 172 | + | |
| 173 | + | |
| 174 | + | |
| 175 | + | |
| 176 | + | |
| 177 | + | |
| 178 | + | |
| 179 | + | |
| 180 | + | |
| 181 | + | |
| 182 | + | |
| 183 | + | |
| 184 | + | |
| 185 | + | |
| 186 | + | |
| 187 | + | |
| 188 | + | |
| 189 | + | |
| 190 | + | |
| 191 | + | |
| 192 | + | |
| 193 | + | |
| 194 | + | |
| 195 | + | |
| 196 | + | |
| 197 | + | |
| 198 | + | |
| 199 | + | |
| 200 | + | |
| 201 | + | |
| 202 | + | |
| 203 | + | |
| 204 | + | |
| 205 | + | |
| 206 | + | |
| 207 | + | |
| 208 | + | |
| 209 | + | |
| 210 | + | |
| 211 | + | |
| 212 | + | |
| 213 | + | |
| 214 | + | |
| 215 | + | |
| 216 | + | |
| 217 | + | |
| 218 | + | |
| 219 | + | |
| 220 | + | |
| 221 | + | |
| 222 | + | |
| 223 | + | |
| 224 | + | |
| 225 | + | |
| 226 | + | |
| 227 | + | |
| 228 | + | |
| 229 | + | |
| 230 | + | |
| 231 | + | |
| 232 | + | |
| 233 | + | |
| 234 | + | |
| 235 | + | |
| 236 | + | |
| 237 | + | |
| 238 | + | |
| 239 | + | |
| 240 | + | |
| 241 | + | |
| 242 | + | |
| 243 | + | |
| 244 | + | |
| 245 | + | |
| 246 | + | |
| 247 | + | |
| 248 | + | |
| 249 | + | |
| 250 | + | |
| 251 | + | |
| 252 | + | |
| 253 | + | |
| 254 | + | |
| 255 | + | |
| 256 | + | |
| 257 | + | |
| 258 | + | |
| 259 | + | |
| 260 | + | |
| 261 | + | |
| 262 | + | |
| 263 | + | |
| 264 | + | |
| 265 | + | |
| 266 | + | |
| 267 | + | |
| 268 | + | |
| 269 | + | |
| 270 | + | |
| 271 | + | |
| 272 | + | |
| 273 | + | |
| 274 | + | |
| 275 | + | |
| 276 | + | |
| 277 | + | |
| 278 | + | |
| 279 | + | |
| 280 | + | |
| 281 | + | |
| 282 | + | |
| 283 | + | |
| 284 | + | |
| 285 | + | |
| 286 | + | |
0 commit comments