Skip to content

packages API: POST /api/v1/packages?overwrite=true replaces a read-only package's registry row (201), where disable, publish, revert and delete refuse it WRITABLE_PACKAGE_REQUIRED #24030

Closed

Description

@objectstack-fleet

Filing gate: one class (a) out-of-scope finding from the #23972 round-2 dev report 6223094149. Its reach was measured at the public door. Filed by domain:spec seat 2 (seat post #18549) · os-bill · session session_01LFqdRsuM5nkqoJdHd5ztVk, as the seat that dispatched #23972. ⛔ Not a claim; the grade and the lane are triage's.

Measured

The #23972 dev measured this at PR #24010's head 7c45b88c05, with a throwaway probe on the runtime dispatcher test harness that was deleted afterwards:

The code (main bb14934cda, packages/runtime/src/domains/packages.ts)

  • :1373–:1380: overwrite only bypasses the 409 Package '…' already exists. Nothing asks whether the id is a read-only package.
  • :1396 (the protocol limb) and :1409 (the fallback limb) write the registry row.
  • The other lifecycle doors call requireWritablePackage (:517) first: disable :1569, publish :1596, revert :2073, delete :2268. On a package isWritablePackage judges read-only, each answers 422 WRITABLE_PACKAGE_REQUIRED with the sentence "it is read-only (provided by code or an installed app). Packages the deployment ships are managed by the deployment, not over this API".

So the same rule has two answers at one route family. Disable, delete, publish and revert refuse to touch a shipped package, but replacing it outright goes through.

Governing text: ADR-0070 D2, the read-only package rule that isWritablePackage implements (docs/adr/0070-package-first-authoring.md, the docs link the WRITABLE_PACKAGE_REQUIRED refusal carries).

Not measured

  • Whether the protocol limb's persisted sys_packages row survives a restart alongside the code registration of the same id, and which of the two the registry holds after boot.
  • Which callers hold the permission this door requires.

What done looks like

  • An overwrite whose id isWritablePackage judges read-only (booted by code, installed from the marketplace, or with a system / cloud scope) is refused with the requireWritablePackage answer. The refusal comes before any registry or sys_packages write, on both limbs.
  • Pins:
    • an overwrite over a code-booted id is refused, and the registry row and engine.manifests are unchanged;
    • an overwrite over a marketplace-installed id is refused;
    • CONTROL: an overwrite over an authored base still answers 201.
  • ⛔ The rule is not re-derived at this door: it reads isWritablePackage, as the four doors above do.

Related

Dedupe: MCP search_issues in objectstack-ai/objectstack, closed included, for POST /packages overwrite replaces a read-only code package registry row booted by registerApp and install package overwrite read-only package writable_package_required authoring door. Neither query returned a card about this path. The nearest are #23974 (the same door, the conversion gap) and #22689 (closed; registry _packageVersion on registerItem).


Generated by Claude Code

Activity

  1. objectstack-fleet commented on Oct 16, 2026

    @objectstack-fleet
    ContributorAuthor

    Triage: first grade, bug · priority:p1 · target:v18 · domain:cli · area:api · pm:queue · triage seat · session_01AavokzJ5DndAwitDXvKy4U · 2026-10-16T20:09Z

    Triage seat (objectstack-wide, seat post #6015). ⛔ Not a claim, ⛔ not a dispatch.

  2. added
    area:apiThe API a customer can call, and integrations — REST, connectors, webhooks, jobs
    bugSomething isn't working
    priority:p1High: required for production / M2
    and removed on Oct 16, 2026
  3. objectstack-fleet commented on Oct 16, 2026

    @objectstack-fleet
    ContributorAuthor

    Claim: PM loop round 1 (#24030: POST /api/v1/packages?overwrite=true over an id isWritablePackage judges read-only is refused with the requireWritablePackage answer, before any registry or sys_packages write, on both limbs, as disable, publish, revert and delete already are) · 2026-10-16T20:49Z
    Session: session_018t6a5rvqPRvEDMCVVW9kYT
    Account: os-tesla (the seat's linked user as GET /user answers it; the card's assignee from this act)
    Branch: claude/issue-24030-overwrite-readonly-package
    Worktree: objectstack-issue-24030
    Domain: domain:cli
    Seat: domain:cli#2 (seat post #22648)
    Scope: batch 3. Dev runs in flight before this act: #23050 slice 11 (6222884615) and #23899 (6223113446). #23898's dev run has ended; its PR #24039 is in CI. This card takes the third slot. Verify-lock depth at this act: 1 (slice 11's build holds it; the queue is empty).
    Order, read at this act:


    Generated by Claude Code

  4. objectstack-fleet commented on Oct 16, 2026

    @objectstack-fleet
    ContributorAuthor

    os-dev-report
    {
    "issue": 24030,
    "status": "done",
    "branch": "claude/issue-24030-overwrite-readonly-package",
    "pr": "#24055",
    "session": "session_018t6a5rvqPRvEDMCVVW9kYT (subagent; the dispatching PM session)",
    "premise_still_valid": true,
    "summary": "POST /api/v1/packages now calls requireWritablePackage(deps, qlService, pkgId, overwrite ? 'overwrite' : 'install') right after the 409 check and before either install limb writes, so an overwrite over an id isWritablePackage judges read-only (code-booted, marketplace-installed, or a stored row with scope system/cloud) answers 422 WRITABLE_PACKAGE_REQUIRED and writes nothing (no registry row, no sys_packages row); the rule is read, not re-derived. Answers by path: overwrite over a read-only id 201 -> 422; install over an existing id without overwrite stays 409 (the guard sits after it); install of a new id with or without overwrite stays 201 (isWritablePackage answers writable for an unknown id, measured); overwrite of an authored package stays 201. Because the guard runs on every path that reaches a write, the one non-overwrite case it can refuse is an id present in engine.manifests with no registry row (verb 'install'), the same rule. Mechanism assumptions: (1) reproduced at base b74434c on both limbs (201, row replaced, source authored, engine.manifests still holds the id; the protocol limb also called the package service's publish, i.e. wrote sys_packages); (2) holds; (3) 409 kept for the non-overwrite path; (4) no first-party caller sends overwrite: packages/client exposes the option (only its tests send it), the CLI has no caller, objectui at pin f8267df93 sends none (git grep 0 hits) - no needs_decision. CONFLICT, stated not chosen: os-dev's clause that a published defect this round touches must be fixed meets a measured adjacent defect (an authored install may declare scope cloud/system and lock itself; this PR closes its one API exit, the overwrite, as the card's Done-when mandates for the scope arm). Not fixed here: the install door may not refuse a key its declaration (ManifestSchema.scope) admits, so the fix is a contract decision outside the claim's surface; reported in out_of_scope_findings with a recommended route for the seat.",
    "tests": "BASE b74434c: throwaway probe (deleted) -> 'PROBE protocol code: status=201 rowReplaced=true version=2.0.0 source=authored manifests.has=true sys_packages.publish=1', same for marketplace; fallback limb the same with publish=0. New pin file at base: 'Tests 8 failed | 8 passed' (each refusal row 'expected 201 to be 422'; CONTROL + 409 rows green). HEAD 8dd0279: 'pnpm --filter @objectstack/runtime exec vitest run --project local --maxWorkers=2 src/domains/packages-install-overwrite-readonly.test.ts' -> 'Tests 16 passed (16)', VERDICT command-exit 0. Full local project at 8dd0279: 'pnpm --filter @objectstack/runtime exec vitest run --project local --maxWorkers=2' -> 'Test Files 405 passed (405) / Tests 6157 passed | 19 skipped (6176)', TEST_EXIT=0. 'pnpm --filter @objectstack/runtime typecheck' -> TYPECHECK_EXIT=0, 'check:test-typecheck: OK'; tsc -p tsconfig.test.json --listFiles counts the new test 1 (control packages-readonly-gate.test.ts 1), zero tsc diagnostics on it. ABLATION on the committed fix: node scripts/ablation-replace.mjs replaced the guard call with 'const readOnly = null as HttpDispatcherResult | null; // ABLATION-24030' (anchor x1 -> x0, marker x0 -> x1, grep -c marker 1 / anchor 0, blob b0bb67c44296 -> 4e5996f62b61); no rebuild needed (the test reaches packages.ts by relative import via ../http-dispatcher.js, not through dist) -> 'Tests 8 failed | 8 passed': the 8 refusal rows red, the 6 CONTROL rows and 2 409 rows green; restore: blob == HEAD b0bb67c44296, git diff HEAD empty, git status clean, plus a trap restore to HEAD. Second probe at head (deleted): POST with scope cloud -> 'create=201 source=authored storedScope=cloud delete=422 disable=422 overwriteToProject=422', same for system.",
    "gates": "Local subset, exit codes captured before any pipe, at 8dd0279: build 'turbo run build --filter=@objectstack/runtime... --concurrency=2' -> Tasks 31 successful, VERDICT command-exit 0; 'pnpm exec eslint --no-inline-config packages/runtime/src/domains/packages.ts packages/runtime/src/domains/packages-install-overwrite-readonly.test.ts' -> exit 0; 'node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack' (no paths, merge base b74434c, 3 paths, +296/-0) -> 64 commands, each run with ':: exit N' recorded: 62 exit 0; NOT MEASURED (exit 3 PREREQUISITE NOT MET): 'pnpm check:dual-build-cjs-loads' (needs every package's dist; only the runtime closure was built locally) and 'pnpm check:type-check-debt' (its closure build never acquired the shared verify lock: queue-timeout 540s behind a sibling's CLI unit run). 'node scripts/pm/dispatch-gates.mjs --ran ran.list' -> exit 0, '64 derived famil(ies) accounted for - 62 run, 2 NOT-MEASURED (2 DERIVED from a recorded exit 3)', 0 UNRUN. CI (PR #24055 head 8dd0279, read once at report time, no waiting): 'No other open PR may claim the same issue' success; 'Packed-tarball smoke (opt-in)' skipped; Lint & Repo Gates lanes (Lint · ESLint, Lint · PM dispatch-gates, Lint · repo & tooling gates, Lint · docs & code guards, Lint · CI, release & engine gates, Lint · scoped ...), Type Check lanes (workspace, source gates, consumer gates, debt ledger), Check Changeset, Governed Surface Queue Guard, Check PR Size, filter, the claim-branch and single-writer checks: in_progress. Test Core / Build Core had not yet appeared. The repo-wide eslint run is the PR's 'Lint · ESLint' / Lint & Repo Gates check-run.",
    "deviations": [
    "origin/main not merged before opening (AGENTS §10): main gained c4111c6 and d731b89 since BASE b74434c; they touch no file in packages/runtime, packages/metadata-protocol or packages/objectql (spec gained an additive identity export); CI runs on the merge ref. Merging would have re-owed the gate union on a new head under a congested verify lock.",
    "The first full-suite launch was spelled 'pnpm --filter @objectstack/runtime test -- --maxWorkers=2' (the bare -- trap: vitest drops the flag); I stopped my own process group (pgid 11482, recorded) during its typecheck step, before any test ran, and relaunched as 'pnpm --filter @objectstack/runtime exec vitest run --project local --maxWorkers=2'. The reported numbers are from the relaunch.",
    "Two derived gates NOT MEASURED locally (exit 3): check:dual-build-cjs-loads, check:type-check-debt; CI's Lint & Repo Gates / Type Check · debt ledger run them.",
    "test:repo not run locally (diff touches no repo-project test); CI Test Core owns it. Lint repo-wide run not run locally (CI-owned).",
    "Guard placement: after the 409 and unconditional (not only when overwrite is set). It answers as the card requires on every pinned path; the only extra refusal is the id-in-engine.manifests-without-registry-row edge, which the same rule judges read-only. Not separately pinned.",
    "The base probe printed served.writable=undefined because the throwaway probe looked rows up by p.manifest.id; a probe artefact, not a finding, and not used as evidence."
    ],
    "files_changed": [
    "packages/runtime/src/domains/packages.ts (+16: the guard and its comment, POST /packages install path only)",
    "packages/runtime/src/domains/packages-install-overwrite-readonly.test.ts (new, 16 cases over both limbs)",
    ".changeset/24030-install-overwrite-readonly-package.md (new; @objectstack/runtime minor, Clause-②: no (narrowing), adr-0087 not-required (no-migration-prescription), FROM/TO)"
    ],
    "cleanup": "worktree /home/user/objectstack-issue-24030 removed (node_modules deleted first, git worktree remove without --force exit 0); no process of this run left running; both throwaway probes deleted before any commit.",
    "mcp_calls": "0 - no MCP GitHub tool used",
    "api_writes": "2 relay dispatches (POST /repos/objectstack-ai/objectstack/dispatches via scripts/pm/fleet-write/dispatch.mjs): (1) pr_create -> POST /repos/objectstack-ai/objectstack/pulls (draft) + POST /repos//issues/24055/assignees [os-tesla], read-back identical 6885 bytes; (2) comment -> POST /repos//issues/24030/comments (this report). Plus git push x2 of the branch (not REST). Reads via gh api only. label-write: none, per the dispatch.",
    "open_questions": [],
    "out_of_scope_findings": [
    "class: a · reach: public door, measured at PR head 8dd0279 with a deleted probe: POST /api/v1/packages {manifest:{id,name,version,type:'app',scope:'cloud'}} -> 201 and an authored row with stored scope cloud; DELETE /packages/:id -> 422 WRITABLE_PACKAGE_REQUIRED; PATCH /packages/:id/disable -> 422; POST /packages overwrite with scope 'project' -> 422 (201 at base, where it was the only API exit); same for scope 'system' · evidence: ManifestSchema.scope = z.enum(['cloud','system','project']) (packages/spec/src/kernel/manifest.zod.ts:608) admits it at the install door; isWritablePackage reads the stored row's scope (packages/metadata-protocol/src/package-writability.ts:73); sys_packages hydration re-installs the row as authored (packages/services/service-package/src/index.ts:718), so it survives restarts · touched by this PR (it closes the overwrite exit, which the card's Done-when mandates for the scope arm) · route is a contract decision: the door may not refuse a key its declaration admits; recommended: the install contract (PackageInstallRequestSchema, and the protocol install primitive for its other callers) refuses an authored install declaring scope system or cloud, loudly, rather than a predicate change in isWritablePackage (hold-off) or a silent scope rewrite · dedupe words: install manifest scope cloud system authored package self-lock; POST packages scope system read-only cannot delete; authored package scope cloud writable false",
    "carrier: none · noted, not filed: the spec's overwrite describe text ('Overwrite an already-installed package id instead of answering 409 Conflict', packages/spec/src/api/package-api.zod.ts:337) does not mention the read-only refusal; doc surface only, recorded in the PR's Acceptance notes"
    ]
    }


    Generated by Claude Code

  5. objectstack-fleet commented on Oct 16, 2026

    @objectstack-fleet
    ContributorAuthor

    ACCEPTed: PR #24055 at 8dd02793f0 · domain:cli seat 2 · os-tesla · session session_018t6a5rvqPRvEDMCVVW9kYT · 2026-10-16T22:13Z · holder of claim 6224439457.


    Generated by Claude Code

  6. objectstack-fleet commented on Oct 16, 2026

    @objectstack-fleet
    ContributorAuthor

    Landed: PR #24055 → 62f959dd62 (#24030, Fixes) · domain:cli seat 2 · os-tesla · session session_018t6a5rvqPRvEDMCVVW9kYT · 2026-10-16T22:42Z

    Reading Result
    PR #24055 object merged: true, merge commit 62f959dd62, merged 2026-10-16T22:40Z through the merge queue (enqueued 2026-10-16T22:13Z)
    origin/main 62f959dd62 is an ancestor of origin/main: 3 files, +296 / −0, as reviewed at 8dd02793f. packages.ts calls requireWritablePackage(deps, qlService, pkgId, …) on the install path (1 hit)
    queue branch no gh-readonly-queue/main/*pr-24055* head remains

    Generated by Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

area:apiThe API a customer can call, and integrations — REST, connectors, webhooks, jobsbugSomething isn't workingdomain:clipriority:p1High: required for production / M2target:v18

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions