Repository navigation
fix(runtime)!: POST /packages/:id/adopt-orphans refuses a read-only package with 422 WRITABLE_PACKAGE_REQUIRED - #24861
Conversation
…main) POST /packages/:id/adopt-orphans binds package-less sys_metadata rows into any id it is handed. These pins read the rows back over a real store: a code-booted, marketplace, system-scoped or cloud-scoped target answers 200 and the orphans are rebound into it today. Committed ahead of the gate so the reproduction is a commit. Claude-Session: https://claude.ai/code/session_01LmjU2x1faLZ39DVTFyVca6 Co-authored-by: Claude <noreply@anthropic.com>
…TABLE_PACKAGE_REQUIRED POST /packages/:id/adopt-orphans now asks requireWritablePackage (the ADR-0070 D2 predicate isWritablePackage) after the capability gate and the walled refusal and before the protocol is resolved, the order its sibling write arms use. reassignOrphanedMetadata asks no writability question of its own, so the door bound the environment's package-less items into a code-booted, marketplace, system- or cloud-scoped package at 200. Claude-Session: https://claude.ai/code/session_01LmjU2x1faLZ39DVTFyVca6 Co-authored-by: Claude <noreply@anthropic.com>
…ntime Claude-Session: https://claude.ai/code/session_01LmjU2x1faLZ39DVTFyVca6 Co-authored-by: Claude <noreply@anthropic.com>
📓 Docs Drift CheckThis PR changes 1 package(s): 1 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:
What this run could not see
Coarse fallback — 26 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 08803b8dcaeeee3868a04a536316984f2ecdfccd && git checkout 08803b8dcaeeee3868a04a536316984f2ecdfccd
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 7cacad5348d0f2665f1c069eccb5131bd955d768 e0844d5eff86bd4529648e907cf7e24068ae5a34 && git checkout -B drift-repro 7cacad5348d0f2665f1c069eccb5131bd955d768 && git merge --no-ff e0844d5eff86bd4529648e907cf7e24068ae5a34
node scripts/docs-audit/affected-docs.mjs --json 7cacad5348d0f2665f1c069eccb5131bd955d768
|
Contract reviewServed-tier: ① Derived judgmentsNet diff against
② Semver level
③ Boundary flags
Implemented-by: VERDICT: PASS Generated by Claude Code |
席位核对 · ACCEPT
Accepted deviations:
Out-of-scope dispositions (one line each):
Landing: through the merge queue by this seat once every check on this head reads Generated by Claude Code |
Fixes #24813
Clause-②: no (narrowing)
What changed
POST /api/v1/packages/:id/adopt-orphansnow asksrequireWritablePackage(that is,isWritablePackage, the ADR-0070 D2 read-only rule) for the path's id, with the verbadopt items into. It asks afterrequireManageMetadataand the walled refusal, and before the protocol is resolved: the order disable, publish, the manifestPATCHand delete already use (manage, then walled, then writable, then the service that writes). Adopting into a package the deployment ships is refused422 WRITABLE_PACKAGE_REQUIRED: booted by code, installed from the marketplace, or a stored row whose manifestscopeissystem/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 realsys_metadata*objects, a realObjectStackProtocolImplementationover a realObjectQL). The orphans come from the real producer, a package-lesssaveMetaItem, and every refusal reads the rows back..changeset/24813-adopt-orphans-readonly-package.md:@objectstack/runtimeminorunder the launch-window convention for a breaking narrowing, with FROM / TO for an integrator, theClause-②line, and the ADR-0087 dispositionnot-required (no-migration-prescription).A
CONTRACT_REVIEW_TIERreview is owed before the queue (narrowing); the seat runs it.Contract
Every input whose answer changes, old to new. A
manage_metadatacaller, not refused by the wall (thesingleposture, notenancyservice, the operator rung, orisSystem):POST /packages/ID/adopt-orphanswhere ID is read-only (code-booted, marketplace-installed,scope: system,scope: cloud), and theprotocolservice hasreassignOrphanedMetadata200with{ success, reassignedCount, reassigned, targetPackageId }; every package-lesssys_metadatarow rebound to ID422,error.code: WRITABLE_PACKAGE_REQUIRED,error.details.packageId: ID,error.details.docs: docs/adr/0070-package-first-authoring.md; nothing rebound,reassignOrphanedMetadatanever calledprotocolservice is absent or has noreassignOrphanedMetadata501Orphan adoption not supported422 WRITABLE_PACKAGE_REQUIRED(the gate precedes the protocol)Unchanged, each pinned or already pinned:
401, the domain's anonymous floormanage_metadata, read-only ID403, the capability gate answers first (pinned here)isolated/group, a holder below the operator rung, read-only ID403 PERMISSION_DENIEDnaming the posture, the walled refusal answers first (pinned here)scope: project)200, every orphan bound to it (pinned here, rows read back)501(pinned here)200, the orphans bound to that ID: the predicate treats an unregistered id as writable (pinned here, rows read back)503The response shape of the
200arm and the request body (actor) are untouched.Measured
Premise.
reassignOrphanedMetadata(packages/metadata-protocol/src/protocol.ts,origin/main41646a578) refuses an organization-scoped request first and then rebinds every package-less row (package_idnull, empty or thesys_metadatasentinel, minus artifact-backed overlays) torequest.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 answered200 {"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,systemandcloudids. With a protocol that cannot adopt, a read-only id answered501 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 withconst readOnly = null;plus a marker: anchor 1 to 0, marker 0 to 1, blob3bc96afefdd0to4908b0439001. The test reachespackages.tsthroughhttp-dispatcher.ts's relative import, sosrcis 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 HEAD3bc96afefdd0,git diff HEADis empty,git status --porcelainis clean, marker count 0.The dispatch's mechanism assumptions
reassignOrphanedMetadata: confirmed by reading (above), and reproduced at the door on the base.PATCHand delete ask manage, then walled, thenrequireWritablePackage, 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 floor401; nomanage_metadata403; the walled403 PERMISSION_DENIED; read-only422; no adopting protocol501; then the protocol's own answer.200and 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:isWritablePackagedocuments an unregistered id as a writable ADR-0048 authoring-workspace id, and the authoring door (saveMetaItemnaming 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 nomanifestsand noscope, so writable.packages-vetted-org-source.test.ts:scope: 'project', writable.packages-protocol-handle-typing.test.ts(/crm/adopt-orphans, expects501):getPackageanswersundefinedand the double has nomanifests, so writable; it still reaches its501.wall-package-doors-refusal.dogfood.test.ts,wall-environment-metadata-refusal.dogfood.test.ts: every target is created throughPOST /packagesin the same test (authored), and the walled403precedes 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-lockVERDICT command-exit 0.pnpm --filter @objectstack/runtime typecheck:VERDICT command-exit 0, withcheck:test-typecheck: OK(27 files / 190 errors / 68 pinned signatures held, unchanged). The new test file is in thetsconfig.test.jsonprogram (--listFilescount 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-wideeslint . --no-inline-config): exit 0.node scripts/pm/dispatch-gates.mjs --commands(67 families, from merge base41646a578): 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, andcheck:dual-build-cjs-loads, which needs every package'sdist/. Re-run:check:type-check-debtunder the lock,VERDICT command-exit 0(--re-measure: OK, none above its recorded number); thencheck:dual-build-cjs-loadsexit 0 (112 require entry points across 69 packages load).--ranreconciliation:67 derived, 67 run, 0 NOT-MEASURED, 0 UNRUN.check-adr-0087-registration --base origin/mainexit 0 (one declared-breaking changeset carryingnot-required (no-migration-prescription));check-changeset-no-major --base origin/mainexit 0;check:nul-bytesexit 0;check:cross-package-test-inputsexit 0.test:repowas not run locally: the diff touches no test in that project and the new test reads nothing outside its package. CI owns it, withLint & Repo Gates,TypeScript Type Check,Test CoreandBuild Coreon this PR.origin/mainwas not merged before opening; CI runs on the merge ref.Acceptance notes
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.requireWritablePackage's doc block inpackages.tslists 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 ofpackages/client'spackages.adoptOrphansdoes not mention the read-only refusal.Generated by Claude Code