Repository navigation
Commit 4cef6e2
fix(runtime)!: POST /packages/:id/adopt-orphans refuses a read-only package with 422 WRITABLE_PACKAGE_REQUIRED (#24861)
Fixes #24813
Clause-②: no (narrowing)
## What changed
`POST /api/v1/packages/:id/adopt-orphans` now asks
`requireWritablePackage` (that is, `isWritablePackage`, the ADR-0070 D2
read-only rule) for the path's id, with the verb `adopt items into`. It
asks after `requireManageMetadata` and the walled refusal, and before
the protocol is resolved: the order disable, publish, the manifest
`PATCH` and delete already use (manage, then walled, then writable, then
the service that writes). Adopting into a package the deployment ships
is refused `422 WRITABLE_PACKAGE_REQUIRED`: booted by code, installed
from the marketplace, or a stored row whose manifest `scope` is `system`
/ `cloud`. The rule is not re-derived at this door.
- `packages/runtime/src/domains/packages.ts`: the gate, one call plus
its comment, on the adopt-orphans arm only.
-
`packages/runtime/src/domains/packages-adopt-orphans-readonly.test.ts`:
the pins, 9 cases over a real store (better-sqlite3 `:memory:`, the real
`sys_metadata*` objects, a real `ObjectStackProtocolImplementation` over
a real `ObjectQL`). The orphans come from the real producer, a
package-less `saveMetaItem`, and every refusal reads the rows back.
- `.changeset/24813-adopt-orphans-readonly-package.md`:
`@objectstack/runtime` `minor` under the launch-window convention for a
breaking narrowing, with FROM / TO for an integrator, the `Clause-②`
line, and the ADR-0087 disposition `not-required
(no-migration-prescription)`.
A `CONTRACT_REVIEW_TIER` review is owed before the queue (narrowing);
the seat runs it.
## Contract
Every input whose answer changes, old to new. A `manage_metadata`
caller, not refused by the wall (the `single` posture, no `tenancy`
service, the operator rung, or `isSystem`):
| Request | Before | After |
|---|---|---|
| `POST /packages/ID/adopt-orphans` where ID is read-only (code-booted,
marketplace-installed, `scope: system`, `scope: cloud`), and the
`protocol` service has `reassignOrphanedMetadata` | `200` with `{
success, reassignedCount, reassigned, targetPackageId }`; every
package-less `sys_metadata` row rebound to ID | `422`, `error.code:
WRITABLE_PACKAGE_REQUIRED`, `error.details.packageId: ID`,
`error.details.docs: docs/adr/0070-package-first-authoring.md`; nothing
rebound, `reassignOrphanedMetadata` never called |
| The same read-only ID where the `protocol` service is absent or has no
`reassignOrphanedMetadata` | `501` `Orphan adoption not supported` |
`422 WRITABLE_PACKAGE_REQUIRED` (the gate precedes the protocol) |
Unchanged, each pinned or already pinned:
| Request | Answer (before and after) |
|---|---|
| Anonymous caller, any ID | `401`, the domain's anonymous floor |
| A caller without `manage_metadata`, read-only ID | `403`, the
capability gate answers first (pinned here) |
| Under `isolated` / `group`, a holder below the operator rung,
read-only ID | `403 PERMISSION_DENIED` naming the posture, the walled
refusal answers first (pinned here) |
| A writable package (an org's own, `scope: project`) | `200`, every
orphan bound to it (pinned here, rows read back) |
| A writable package, no adopting protocol | `501` (pinned here) |
| An ID nothing installed | `200`, the orphans bound to that ID: the
predicate treats an unregistered id as writable (pinned here, rows read
back) |
| No ObjectQL registry | `503` |
The response shape of the `200` arm and the request body (`actor`) are
untouched.
## Measured
**Premise.** `reassignOrphanedMetadata`
(`packages/metadata-protocol/src/protocol.ts`, `origin/main`
`41646a578`) refuses an organization-scoped request first and then
rebinds every package-less row (`package_id` null, empty or the
`sys_metadata` sentinel, minus artifact-backed overlays) to
`request.targetPackageId`. It asks no writability question and no
existence question. So the premise holds and the producer to fix is the
door, as the card's Wanted says.
**Reproduction at base `41646a578`** (a throwaway probe, not committed;
same rig as the pins): adopting into each read-only id answered `200
{"success":true,"reassignedCount":2,...}` and both orphans were bound to
it:
`[["loose_grid","com.acme.codepkg"],["loose_board","com.acme.codepkg"]]`,
and the same for the marketplace, `system` and `cloud` ids. With a
protocol that cannot adopt, a read-only id answered `501
NOT_IMPLEMENTED`.
**The pins at `9c77155aa`** (committed ahead of the fix, against the
unchanged arm): `Tests 5 failed | 4 passed (9)`. The four read-only rows
and the gate-precedes-protocol row are red (`expected 200 to be 422`,
`expected 501 to be 422`); the two order rows and the two CONTROL rows
are green.
**At `6d9e82dc4`** (the fix): the new pin file with the seven suites
that exercise the adopt-orphans arm or the sibling gates
(`packages-capability-gate`, `packages-walled-write-refusal`,
`packages-vetted-org-source`, `packages-protocol-handle-typing`,
`packages-readonly-gate`, `packages-manifest-patch-readonly`,
`package-duplicate-adopt-org-scope.integration`): `Test Files 8 passed
(8)`, `Tests 295 passed (295)`.
**Ablation**, on the committed fix (`6d9e82dc4`).
`scripts/ablation-replace.mjs` (WRAP mode) replaced the gate call with
`const readOnly = null;` plus a marker: anchor 1 to 0, marker 0 to 1,
blob `3bc96afefdd0` to `4908b0439001`. The test reaches `packages.ts`
through `http-dispatcher.ts`'s relative import, so `src` is what runs
and no rebuild was needed. Result: `Tests 5 failed | 4 passed (9)`, the
same five rows red as on the base. Restore: blob equals HEAD
`3bc96afefdd0`, `git diff HEAD` is empty, `git status --porcelain` is
clean, marker count 0.
## The dispatch's mechanism assumptions
1. **No writability check in `reassignOrphanedMetadata`:** confirmed by
reading (above), and reproduced at the door on the base.
2. **Order:** confirmed and pinned. Disable, publish, the manifest
`PATCH` and delete ask manage, then walled, then
`requireWritablePackage`, each before it resolves the service that
writes. This arm now does the same. When several apply to one request
the answer is, first to last: the anonymous floor `401`; no
`manage_metadata` `403`; the walled `403 PERMISSION_DENIED`; read-only
`422`; no adopting protocol `501`; then the protocol's own answer.
3. **Unknown id:** measured, unchanged. Before and after, adopting into
an id nothing installed answers `200` and binds the orphans to that id
(base probe:
`[["loose_grid","com.acme.nopkg"],["loose_board","com.acme.nopkg"]]`;
after: the CONTROL pin). It is not the same defect: `isWritablePackage`
documents an unregistered id as a writable ADR-0048 authoring-workspace
id, and the authoring door (`saveMetaItem` naming a package) accepts the
same id. Noted below, not changed.
## Pin sweep
Every test and dogfood case that sends `POST .../adopt-orphans`, read
for its target:
- `packages-capability-gate.test.ts`,
`packages-walled-write-refusal.test.ts`: registry doubles with no
`manifests` and no `scope`, so writable.
- `packages-vetted-org-source.test.ts`: `scope: 'project'`, writable.
- `packages-protocol-handle-typing.test.ts` (`/crm/adopt-orphans`,
expects `501`): `getPackage` answers `undefined` and the double has no
`manifests`, so writable; it still reaches its `501`.
- `wall-package-doors-refusal.dogfood.test.ts`,
`wall-environment-metadata-refusal.dogfood.test.ts`: every target is
created through `POST /packages` in the same test (authored), and the
walled `403` precedes the gate.
- `packages/client` (`packages.adoptOrphans`): only its own
request-shape tests call it.
None names a read-only package, so no pin flips.
## Tests and gates
All local readings below are at head `e0844d5ef` (`git rev-parse --short
HEAD`), the final commit.
- `pnpm --filter '@objectstack/runtime^...' build` (the dependency
closure; this diff changes no package in it): verify-lock `VERDICT
command-exit 0`.
- `pnpm --filter @objectstack/runtime typecheck`: `VERDICT command-exit
0`, with `check:test-typecheck: OK` (27 files / 190 errors / 68 pinned
signatures held, unchanged). The new test file is in the
`tsconfig.test.json` program (`--listFiles` count 1) and has zero tsc
errors, so it needs no ledger entry.
- `pnpm --filter @objectstack/runtime exec vitest run --project local
--maxWorkers=2`: `Test Files 432 passed (432)`, `Tests 6791 passed | 19
skipped (6810)`, `VERDICT command-exit 0`.
- `pnpm lint` (repo-wide `eslint . --no-inline-config`): exit 0.
- `node scripts/pm/dispatch-gates.mjs --commands` (67 families, from
merge base `41646a578`): 67 exit 0. Two first exited 3 (`PREREQUISITE
NOT MET`): `check:type-check-debt`, whose closure build never got a turn
on the shared verify lock, and `check:dual-build-cjs-loads`, which needs
every package's `dist/`. Re-run: `check:type-check-debt` under the lock,
`VERDICT command-exit 0` (`--re-measure: OK`, none above its recorded
number); then `check:dual-build-cjs-loads` exit 0 (112 require entry
points across 69 packages load). `--ran` reconciliation: `67 derived, 67
run, 0 NOT-MEASURED, 0 UNRUN`.
- Among them: `check-adr-0087-registration --base origin/main` exit 0
(one declared-breaking changeset carrying `not-required
(no-migration-prescription)`); `check-changeset-no-major --base
origin/main` exit 0; `check:nul-bytes` exit 0;
`check:cross-package-test-inputs` exit 0.
- `test:repo` was not run locally: the diff touches no test in that
project and the new test reads nothing outside its package. CI owns it,
with `Lint & Repo Gates`, `TypeScript Type Check`, `Test Core` and
`Build Core` on this PR.
- `origin/main` was not merged before opening; CI runs on the merge ref.
## Acceptance notes
- **Observation, not filed:** adopting into an id nothing installed
binds the environment's orphans to that id at `200` (measured above). By
the predicate's documented rule an unregistered id is a writable
authoring workspace, so this is the stated contract, not a violation;
whether an orphan adoption should require an installed base is a product
question this PR does not open.
- **Rows already bound:** this PR stops new adoptions into a read-only
package. Rows an earlier adoption already bound to one are not touched,
and what the next boot does with them was not measured.
- **Doc surfaces left as they are (outside the claimed file surface):**
`requireWritablePackage`'s doc block in `packages.ts` lists the doors
that ask it and does not name this one (the arm's own comment does); the
block is on the dispatch's hold-off list. The JSDoc of
`packages/client`'s `packages.adoptOrphans` does not mention the
read-only refusal.
- objectui already hides "Adopt loose items" for a read-only package
(objectstack-ai/objectui#13181); this PR refuses at the door itself.
---
_Generated by [Claude
Code](https://claude.ai/code/session_01LmjU2x1faLZ39DVTFyVca6)_
---------
Co-authored-by: Claude <noreply@anthropic.com>1 parent 28ff8a9 commit 4cef6e2
3 files changed
Lines changed: 320 additions & 0 deletions
File tree
- .changeset
- packages/runtime/src/domains
| 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 | |
|---|---|---|---|
| |||
| 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 | + | |
| 287 | + | |
| 288 | + | |
| 289 | + | |
| 290 | + | |
| 291 | + | |
| 292 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
2141 | 2141 | | |
2142 | 2142 | | |
2143 | 2143 | | |
| 2144 | + | |
| 2145 | + | |
| 2146 | + | |
| 2147 | + | |
| 2148 | + | |
| 2149 | + | |
| 2150 | + | |
| 2151 | + | |
| 2152 | + | |
| 2153 | + | |
2144 | 2154 | | |
2145 | 2155 | | |
2146 | 2156 | | |
| |||
0 commit comments