Repository navigation
fix(runtime)!: POST /packages refuses an overwrite over a read-only package with 422 WRITABLE_PACKAGE_REQUIRED - #24055
Conversation
…ackage with 422 WRITABLE_PACKAGE_REQUIRED The install door's `overwrite` only bypassed the 409 for an existing id and never asked the ADR-0070 D2 read-only rule, so an overwrite over a code-booted or marketplace-installed id answered 201 and replaced its registry row (and, on the protocol limb, wrote its sys_packages row) while engine.manifests kept judging the package read-only. The door now asks requireWritablePackage (isWritablePackage) for the manifest's id after the 409 and before either install limb writes, the answer disable, publish, revert and delete give. Claude-Session: https://claude.ai/code/session_018t6a5rvqPRvEDMCVVW9kYT 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 82f6a80c4300e1c4748abb0ab49d04e6ccaeeb61 && git checkout 82f6a80c4300e1c4748abb0ab49d04e6ccaeeb61
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 5c2ac83a230d2eec038502a23a8142153cad814d 8dd02793f0f6b14c9d09098f278993944f350b9b && git checkout -B drift-repro 5c2ac83a230d2eec038502a23a8142153cad814d && git merge --no-ff 8dd02793f0f6b14c9d09098f278993944f350b9b
node scripts/docs-audit/affected-docs.mjs --json 5c2ac83a230d2eec038502a23a8142153cad814d
|
Contract reviewServed-tier: Inputs: card #24030 (body and all three comments: the triage grade ① Derived judgmentsThe diff is one guard call at one door (
② Semver level
Clause-②: no (narrowing) — on the PR body and in the changeset, matching the claim's line ( ③ Boundary flags
Out-of-scope findings, each escalated:
Check-runs on the head, re-read 2026-10-16T22:09Z after the Implemented-by: VERDICT: PASS Generated by Claude Code |
席位核对ACCEPT · #24030 ·
Escalated by the dev and the review, filed by this seat in this window: an authored install may declare Noted, not filed: the spec's Landing: Generated by Claude Code |
Fixes #24030
Clause-②: no (narrowing)
What changed
POST /api/v1/packagesnow asksrequireWritablePackage(that is,isWritablePackage, the ADR-0070 D2 read-only rule) for the manifest's id. It asks after the409 … already existscheck and before either install limb writes. An overwrite over a package the deployment ships is refused422 WRITABLE_PACKAGE_REQUIRED: booted by code, installed from the marketplace, or a stored row whose manifestscopeissystem/cloud. Disable, publish, revert and delete already give that answer. The rule is not re-derived at this door.packages/runtime/src/domains/packages.ts: the guard, one call plus its comment, on thePOST /packagesinstall path only.packages/runtime/src/domains/packages-install-overwrite-readonly.test.ts: the pins, 16 cases over both limbs (theprotocolservice'sinstallPackage, and the no-protocol fallback)..changeset/24030-install-overwrite-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
overwrite: truein the body, or?overwrite=true)201, registry row replaced (andsys_packageswritten on the protocol limb)422 WRITABLE_PACKAGE_REQUIRED, nothing writtenoverwrite(read-only or not)409409, unchanged: the guard sits after itoverwrite201201201201Measured
Reproduction at base
b74434c58. A throwaway probe on the dispatcher harness (realObjectQLwithregisterApp, realObjectStackProtocolImplementation), deleted afterwards:The new pin file at base:
8 failed | 8 passed. Every refusal row is red withexpected 201 to be 422; every CONTROL row and the 409 row are green.At head
8dd02793f:16 passed (16).Ablation, on the committed fix.
scripts/ablation-replace.mjsreplaced the guard call withnull: anchor 1 to 0, marker 0 to 1, blobb0bb67c44296to4e5996f62b61. The test reachespackages.tsby relative import, so no rebuild was needed. Result:8 failed | 8 passed. The 8 refusal rows went red; the 6 CONTROL rows and the two 409 rows stayed green. Restore: blob equals HEADb0bb67c44296, andgit diff HEADis empty.The dispatch's mechanism assumptions
sys_packagesrow: thepackageservice'spublishwas called once.isWritablePackageanswers writable for an id nothing installed. An install of a new id withoverwrite: trueanswers201, pinned on both limbs. So the guard runs on every path that reaches a write without refusing a fresh install.409(pinned on both limbs); an overwrite gets the422.overwrite: no first-party caller does.packages/clientexposespackages.install(m, { overwrite }); only its own tests send it..objectui-shapinf8267df93sends nooverwrite:git grepforoverwrite: true/overwrite=truefinds 0 hits. Its package create posts{ manifest }and expects the 409 on a duplicate.Tests and gates (local, at
8dd02793f)turbo run build --filter='@objectstack/runtime...' --concurrency=2: 31 of 31 tasks successful, verify-lockVERDICT command-exit 0.pnpm --filter @objectstack/runtime typecheck: exit 0, withcheck:test-typecheck: OK. The new test file is in thetsconfig.test.jsonprogram (--listFilescount 1) and has no tsc error.pnpm --filter @objectstack/runtime exec vitest run --project local --maxWorkers=2:405 passed (405)files,6157 passed | 19 skippedtests, exit 0.pnpm exec eslint --no-inline-configon the two changed.tsfiles: exit 0.node scripts/pm/dispatch-gates.mjs --commands(64 families, from merge baseb74434c58): 62 exit 0. The other 2 are NOT MEASURED, each exit 3 (PREREQUISITE NOT MET):check:dual-build-cjs-loadsneeds every package'sdist/, and only the runtime closure was built locally.check:type-check-debt: its closure build never acquired the shared verify lock (queue-timeout).--ranreconciliation: 64 accounted, 0 UNRUN.Lint & Repo Gates(the repo-wide eslint run),TypeScript Type Check,Test CoreandBuild Core: CI on this PR.test:repowas not run locally; CI owns it.Acceptance notes
POST /api/v1/packageswithmanifest.scope: 'cloud'(or'system') answers201and stores anauthoredrow.isWritablePackagejudges that row read-only, soDELETEandPATCH …/disablethen answer422 WRITABLE_PACKAGE_REQUIRED.scope: 'project'was the one API exit. After it, that overwrite is refused too, as the card's Done-when requires for the scope arm.create=201 source=authored storedScope=cloud delete=422 disable=422 overwriteToProject=422.ManifestSchema.scope) admits, so the remedy is a contract decision.overwritedescription ("Overwrite an already-installed package id instead of answering 409 Conflict") does not mention the read-only refusal. This is a note on the doc surface only.origin/mainwas not merged before opening. Main has gained two commits since the base (c4111c676,d731b89a6); they touch no file inpackages/runtime,packages/metadata-protocolorpackages/objectql. CI runs on the merge ref.Generated by Claude Code