Repository navigation
fix(runtime)!: PATCH /packages/:id refuses a read-only package with 422 WRITABLE_PACKAGE_REQUIRED - #24719
Conversation
The pins for the manifest-edit door's ADR-0070 D2 read-only refusal, ahead of the gate itself: four read-only kinds x four bodies on both limbs, plus the writable, body-check and unknown-id controls. Claude-Session: https://claude.ai/code/session_01LmjU2x1faLZ39DVTFyVca6 Co-authored-by: Claude <noreply@anthropic.com>
…22 WRITABLE_PACKAGE_REQUIRED The manifest-edit arm now asks requireWritablePackage (isWritablePackage, the ADR-0070 D2 rule) after the manage_metadata and walled gates and before the body is read, in the order install-overwrite, disable, publish, revert and delete already use. Claude-Session: https://claude.ai/code/session_01LmjU2x1faLZ39DVTFyVca6 Co-authored-by: Claude <noreply@anthropic.com>
… refusal Breaking narrowing of a REST door, shipped minor under the launch-window convention, with FROM / TO, the Clause-② line and its ADR-0087 disposition. 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 2cd17fe7e2f0cbb489a68d560f2442af3441fcf3 && git checkout 2cd17fe7e2f0cbb489a68d560f2442af3441fcf3
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 54159fd742e5a79fc10ac41854dba3395e1a1ffe 5697a4d225f3130e5b18f05d7a20489a4101bbe7 && git checkout -B drift-repro 54159fd742e5a79fc10ac41854dba3395e1a1ffe && git merge --no-ff 5697a4d225f3130e5b18f05d7a20489a4101bbe7
node scripts/docs-audit/affected-docs.mjs --json 54159fd742e5a79fc10ac41854dba3395e1a1ffe
|
Contract reviewServed-tier: ① Derived judgmentsNet diff against Check-runs on the head (the gate verdicts). 39 runs, latest per name: 29
② Semver level
③ Boundary flags
Implemented-by: VERDICT: PASS Generated by Claude Code |
席位核对 · ACCEPT
The review of record. An isolated reviewer at
Out-of-scope dispositions (one line each):
Landing: held while main-red #24700 is open (its fix is PR #24724, ACCEPTed). Then it goes through the merge queue by this seat, ahead of PR #24703. Generated by Claude Code |
Fixes #24705
Clause-②: no (narrowing)
What changed
PATCH /api/v1/packages/:idnow asksrequireWritablePackage(that is,isWritablePackage, the ADR-0070 D2 read-only rule) for the path's id. It asks afterrequireManageMetadataand the walled refusal, and before the body is read, in the order install-overwrite, disable, publish, revert and delete already use (manage, then walled, then writable). A manifest edit of 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 thePATCH /packages/:idarm only. Verbedit, so the refusal reads "Cannot edit package 'ID': it is read-only (provided by code or an installed app)...".packages/runtime/src/domains/packages-manifest-patch-readonly.test.ts: the pins, 38 cases on both limbs (theprotocolservice'supdatePackage, and the no-protocol registry fallback)..changeset/24705-patch-manifest-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.Answers, path by path
PATCHon a read-only id, withname/description/version(flat, or undermanifest)200, manifest edited in the registry (andsys_packageswritten on the protocol limb)422 WRITABLE_PACKAGE_REQUIRED, nothing writtenPATCHon a read-only id, with a body the arm itself refuses ({}, a non-SemVerversion, adependencies-only body on today'smain)400422 WRITABLE_PACKAGE_REQUIRED: the gate precedes the bodyPATCHon a package an organization owns200/400per the bodyPATCHon an id nothing installed404404, unchangedSerial with PR #24703
PR #24703 (#24670) edits the body handling of this same arm a few lines below the gate, adding
dependencies. This PR is built onorigin/mainwithout it, does not touch the body handling, and lands first; PR #24703 then mergesmain. Because the gate is asked before the body is read, the{ dependencies: {} }refusal rows hold on both trees: on today'smainthat body answered400before this gate, and with PR #24703 it would answer200; both become422for a read-only id.Measured
Reproduction at base
1c1144fcd. The new pin file, committed ahead of the fix (ade20c179), against the unchanged arm:Tests 32 failed | 6 passed (38). Every refusal row is red, withexpected 200 to be 422for the editable bodies andexpected 400 to be 422for the{ dependencies }and{}bodies. Every CONTROL row is green.At head
5697a4d22: the new pin file run together with the seven suites that exercise the/packagesarms (packages-readonly-gate,packages-install-overwrite-readonly,packages-capability-gate,packages-walled-write-refusal,error-passthrough,packages-single-door,http-dispatcher):Test Files 8 passed (8),Tests 459 passed (459).Ablation, on the committed fix (
5697a4d22).scripts/ablation-replace.mjsreplaced the gate callrequireWritablePackage(deps, qlService, id, 'edit')withnullplus a marker: anchor 1 to 0, marker 0 to 1, blobd00fe003b219to90eadd49957b. The test reachespackages.tsthroughhttp-dispatcher.ts's relative import, sosrcis what runs and no rebuild was needed. Result:Tests 32 failed | 6 passed (38). The 32 refusal rows went red; the 6 CONTROL rows stayed green. Restore: blob equals HEADd00fe003b219,git diff HEADis empty, andgit status --porcelainis clean.The dispatch's mechanism assumptions
requireManageMetadataandrefuseWalledCatalogPackageWriteand nothing else. Disable (:1602), publish (:1629) and delete (:2309) ask manage, then walled, thenrequireWritablePackage; delete reads itskeepDataarity between the second and third, and revert (:2114) asks the gate after the protocol's stored-row answer. This arm uses manage, walled, writable.isWritablePackageanswers writable for a non-empty id that no booted manifest and no registry row name, so the gate falls through and the arm's own404stands. Pinned on both limbs: the fallback'sregistry.updatePackageManifestanswersundefined, and the protocol'supdatePackagethrows its404. The predicate's empty-id branch (not writable) is unreachable here, because the domain splits the path withfilter(Boolean).62f959dd6(PR fix(runtime)!: POST /packages refuses an overwrite over a read-only package with 422 WRITABLE_PACKAGE_REQUIRED #24055): the same changeset form, ADR-0087 marker and pin shape (a realObjectQLengine,registerAppfor the code and marketplace kinds, a stored row for the scope-only kinds, both limbs, spies on every write, CONTROL rows on the same spies)..objectui-shapin12dc7b47c(read withgit showat that commit, nothing checked out): the one caller isPackageFormDialogin edit mode, opened byEditPackageDialogfrom the Packages page's detail sheet (packages/app-shell/src/views/metadata-admin/PackagesPage.tsx). It always sends{ name, description, version }.isKernelis false, andisKernelismanifest.scope === 'system' || manifest.scope === 'cloud'. So asystem/cloudpackage is never offered Edit. A code-booted or marketplace-installed package whose manifestscopeisprojector absent is offered Edit; that is the case that now answers422, as Disable, Publish, Revert and Delete on the same sheet already do for it. The dialog's fall-through error arm renders the envelope's message, so the person reads the refusal sentence (static read; not measured in a browser).packages/client:packages.update(id, patch)sends this PATCH; only its own tests call it. The CLI has no caller.200PATCH on a read-only package (sweep below), so no pin flips.Full-repo pin sweep
Searched for every test, dogfood and conformance case that sends
PATCHto/packages/:id(not/disableor/enable), and forWRITABLE_PACKAGE_REQUIREDacross the repo. Each hit names a writable or unknown package, so none flips:http-dispatcher.test.ts(5 cases),error-passthrough.test.ts,packages-capability-gate.test.ts: engine doubles with nomanifestsand noscope, so writable.dispatcher-body-after-anonymous-floor.hono.integration.test.ts(pkg_x): unknown, so writable; it still reaches the body.packages-walled-write-refusal.test.tsandwall-environment-metadata-refusal.dogfood.test.ts: the walled403precedes the gate; the CONTROL operator edit there targets a package the same test created throughPOST /packages(authored).docs/qa/platform-checklist/areas/api-backend.json: the manifest-edit step uses a scratch package the item installs itself.Tests and gates (local, at
5697a4d22)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. The new test file is in thetsconfig.test.jsonprogram (--listFilescount 1) and carries no ledger entry, so it has no tsc error.pnpm --filter @objectstack/runtime exec vitest run --project local --maxWorkers=2:Test Files 427 passed (427),Tests 6759 passed | 19 skipped (6778),VERDICT command-exit 0.pnpm lint(repo-wideeslint . --no-inline-config): exit 0.node scripts/pm/dispatch-gates.mjs --commands(66 families, from merge base1c1144fcd): 66 exit 0.check:dual-build-cjs-loadsfirst exited 3 (PREREQUISITE NOT MET: not every package had adist/); re-run aftercheck:type-check-debtbuilt every package: exit 0, 111 require entry points across 69 packages load.--ranreconciliation: 66 derived, 66 run, 0 NOT-MEASURED, 0 UNRUN.check-adr-0087-registration --base origin/main: exit 0, one declared-breaking changeset carryingnot-required (no-migration-prescription).check-changeset-no-major --base origin/main --eventwith this body as the payload: exit 0, level axisno (narrowing)with the one moved package gradedminor.test:repowas not run locally: the new test reads nothing outside its package (check:cross-package-test-inputsexit 0). CI owns it.Lint & Repo Gates,TypeScript Type Check,Test CoreandBuild Core: CI on this PR.origin/mainwas not merged before opening. Main has gained six commits since the base (73d4cc739to54159fd74). None touchespackages/runtime,package-writability.ts,protocol.updatePackageor the registry's package methods; theobjectqlandmetadata-protocolchanges are in hooks, validation and the seed loader. CI runs on the merge ref.Acceptance notes
manifest.scope(isKernel), while its Scope badge reads the servedwritableverdict. So a code-booted or marketplace package whosescopeisprojector absent is offered Edit, Disable, Publish, Revert and Delete, and each of those now answers422. Read at the pin12dc7b47c,packages/app-shell/src/views/metadata-admin/PackagesPage.tsx.requireWritablePackage's doc block inpackages.tslists the doors that ask it and does not name this one; the arm's own comment does. The JSDoc ofpackages/client'spackages.updatedoes not mention the read-only refusal.sys_packagesrow does on the next boot after an edit. This PR stops new edits; rows an earlier edit wrote are not touched.Generated by Claude Code