Skip to content

fix(runtime): refuse to disable or delete a read-only package on the /packages lifecycle routes (#7560) - #7599

Merged
os-help merged 1 commit into
mainfrom
claude/issue-7560-package-lifecycle-readonly-gate
Aug 11, 2026
Merged

fix(runtime): refuse to disable or delete a read-only package on the /packages lifecycle routes (#7560)#7599
os-help merged 1 commit into
mainfrom
claude/issue-7560-package-lifecycle-readonly-gate

Conversation

@os-help

@os-help os-help commented Aug 11, 2026

Copy link
Copy Markdown
Collaborator

Fixes #7560

The defect

PATCH /packages/<id>/disable and DELETE /packages/<id> answered 200 on a platform package, and the DELETE really removed it from the running process's registry listing. One authorized API call took platform functionality out of a live deployment. Reproduced on two platform packages in the QA run behind #7514.

Blast radius — one correction to the card

The card states the packages come back after a restart. That is true for DELETE (they are code-loaded, so nothing is permanently destroyed). It is not true for disable: setPackageDisabled (packages/runtime/src/package-state-store.ts) persists the choice to <OS_HOME>/package-state/<env>.json, and SchemaRegistry replays it at boot via initialDisabledPackageIds (packages/objectql/src/registry.ts:2202). A disabled platform package stayed disabled across restarts — so the disable half was the more durable of the two, not the lesser.

Two axes, not one

#7033 / PR #7083 gave the whole /packages domain caller authorization — manage_metadata on writes, the ADR-0106 D4 read set, plus a domain-wide anonymous floor. That decides who may call the route.

This is the second, missing check on the same routes: what the route may do once the caller is allowed. The caller gate is untouched. In every new test the caller holds manage_metadata and is refused anyway, because read-only is a property of the package, not of the caller. Tightening the caller gate would not have fixed this and would have broken legitimate admins.

The refusal — ADR-0070's existing vocabulary, not a new one

422 / WRITABLE_PACKAGE_REQUIRED — the code saveMetaItem already throws when asked to author into a read-only package (packages/metadata-protocol/src/protocol.ts, ADR-0070 D1/D2). Same status, same code, same details.packageId / details.docs. The sentence is lifecycle-specific only because the authoring one ("switch to a writable package in the package selector") names a remedy that makes no sense for a delete.

The predicate behind it moved out of ObjectStackProtocolImplementation's private isWritablePackage into an exported isWritablePackage(engine, packageId) in @objectstack/metadata-protocol, and both callers now reference it. A second hand-kept copy of "which packages are read-only" is exactly the drift that let DELETE remove a platform package while saveMetaItem was refusing to add one field to it. Both read-only signals are covered:

  • a booted code package (engine.manifests, populated by registerApp) — the shape the repro used;
  • a platform-delivered manifest scope of system or cloud.

Deliberately not caller-sensitive: no isSystem bypass, unlike requireManageMetadata. Internal teardown calls registry.uninstallPackage directly and never passes through a gate.

Not an outage

A gate that refuses everything is not a fix. Packages an org owns (project-scoped bases, ADR-0048 authoring workspaces) still disable, re-enable and delete exactly as before, and an unknown package id still gets its 404 rather than being re-labelled 422 — so the gate never becomes an existence oracle of its own.

What the tests assert

packages/runtime/src/domains/packages-readonly-gate.test.ts (15 tests) drives a real SchemaRegistry, not vi.fn() doubles, and asserts the registry listinggetAllPackages() / pkg.enabled — in both directions. The original defect's harm was that the package left the listing of the running process; a mock uninstallPackage cannot tell you whether it survived, and the status code alone would not have caught it.

Mutation table — every new test proven able to fail

Each mutation applied to the merged tree, suite run (packages-readonly-gate + the pre-existing packages-capability-gate, 91 tests), then reverted. Predicate mutations required a metadata-protocol rebuild, since @objectstack/runtime resolves the built dist.

# Mutation Result Tests that turned red
M1 DELETE gate removed (pre-fix state) 6 failed / 85 all 3 DELETE-refusal cases, isSystem, envelope shape, verb naming
M2 disable gate removed (pre-fix state) 5 failed / 86 all 3 disable-refusal cases, "not the #7033 gate", verb naming
M3 Predicate always false — the outage shape 8 failed / 83 writable disable + delete, both 404 cases, and 4 pre-existing #7033 tests
M4 Code-loaded signal (engine.manifests) dropped 2 failed / 89 both code-loaded cases only
M5 Manifest-scope signal dropped 8 failed / 83 both system + both cloud cases, isSystem, axis, envelope, verb
M6 DELETE gate moved after uninstallPackage 6 failed / 85 the listing assertions — "delete first, refuse second"
M7 disable gate moved after disablePackage 3 failed / 88 the enabled/status assertions
M8 Refusal restated as PACKAGE_READ_ONLY 7 failed / 84 all 6 refusal cases + the envelope-shape case
M9 Unknown id treated as read-only (existence oracle) 2 failed / 89 both 404 cases only
M10 Blanket caller bypass re-added to the gate 10 failed / 81 all 6 refusal cases, isSystem, axis, envelope, verb

M3 is the load-bearing one for the accept bar: refusing everything breaks not only the new writable-package cases but four of #7033's own allow-path tests.

Gates

  • pnpm lint — clean
  • pnpm typecheck (spec, runtime; metadata-protocol has no typecheck script) — clean
  • @objectstack/runtime 2005 ✓ · @objectstack/spec 9805 ✓ · @objectstack/metadata-protocol 1051 ✓ · @objectstack/objectql 3153 ✓ · @objectstack/rest 1344 ✓
  • Changeset included. No content/docs/releases/** edits.

Scope notes


Generated by Claude Code

`PATCH /packages/<id>/disable` and `DELETE /packages/<id>` answered 200 on a
platform package, and the DELETE really removed it from the running process's
registry listing. One authorized API call took platform functionality out of a
live deployment. Reproduced on two platform packages in the QA run behind #7514.

Blast radius, measured: the card said the packages come back after a restart —
true for DELETE (they are code-loaded), but NOT for disable. `setPackageDisabled`
persists the choice to `<OS_HOME>/package-state/<env>.json`, which SchemaRegistry
replays at boot, so a disabled platform package stayed disabled across restarts.

Two axes, not one. #7033 / PR #7083 gave the domain CALLER authorization — who
may call the route. This is the second, missing check on the same routes: what
the route may do once the caller is allowed. The caller gate is untouched.

No new vocabulary: the refusal is ADR-0070's existing 422 /
WRITABLE_PACKAGE_REQUIRED, the code `saveMetaItem` already throws when asked to
author INTO a read-only package. The predicate moved out of
ObjectStackProtocolImplementation's private method into
`@objectstack/metadata-protocol`'s exported `isWritablePackage(engine, id)` and
is now referenced by both callers, so "which packages are read-only" has one
definition rather than two that drift. Both signals are covered: a booted code
package (`engine.manifests`) and a `system`/`cloud` manifest scope.

Deliberately not caller-sensitive — no isSystem bypass, unlike the write gate.
Read-only is a property of the package. Internal teardown calls
`registry.uninstallPackage` directly and never passes through a gate.

Packages an org owns still disable, re-enable and delete exactly as before. The
new suite drives a REAL SchemaRegistry and asserts the registry LISTING in both
directions — the status code alone would not have caught the original harm.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Q1pL2j4cMGRFc7jAYYmR8U
@vercel

vercel Bot commented Aug 11, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
Project Deployment Actions Updated (UTC)
objectstack Ignored Ignored Aug 11, 2026 6:53am

Request Review

@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 3 package(s): @objectstack/metadata-protocol, @objectstack/runtime, @objectstack/spec.

112 hand-written doc(s) reference the affected code and may need an implementation-accuracy re-verification:

  • content/docs/ai/agents.mdx (via @objectstack/spec)
  • content/docs/ai/skills-reference.mdx (via @objectstack/spec)
  • content/docs/ai/skills.mdx (via @objectstack/spec)
  • content/docs/api/client-sdk.mdx (via packages/runtime, @objectstack/spec)
  • content/docs/api/environment-routing.mdx (via @objectstack/spec)
  • content/docs/api/error-catalog.mdx (via @objectstack/spec)
  • content/docs/api/error-handling-client.mdx (via @objectstack/spec)
  • content/docs/api/error-handling-server.mdx (via @objectstack/spec)
  • content/docs/api/index.mdx (via @objectstack/runtime, @objectstack/spec)
  • content/docs/api/wire-format.mdx (via @objectstack/runtime)
  • content/docs/automation/approvals.mdx (via @objectstack/spec)
  • content/docs/automation/connectors.mdx (via @objectstack/spec)
  • content/docs/automation/flows.mdx (via @objectstack/spec)
  • content/docs/automation/hook-bodies.mdx (via @objectstack/runtime, packages/spec)
  • content/docs/automation/hooks.mdx (via @objectstack/spec)
  • content/docs/automation/index.mdx (via @objectstack/spec)
  • content/docs/automation/webhooks.mdx (via @objectstack/spec)
  • content/docs/automation/workflows.mdx (via @objectstack/spec)
  • content/docs/concepts/architecture.mdx (via @objectstack/spec)
  • content/docs/concepts/design-principles.mdx (via packages/spec)
  • content/docs/concepts/index.mdx (via @objectstack/spec)
  • content/docs/concepts/metadata-driven.mdx (via @objectstack/spec)
  • content/docs/concepts/metadata-lifecycle.mdx (via @objectstack/metadata-protocol, @objectstack/runtime, packages/spec)
  • content/docs/concepts/north-star.mdx (via packages/runtime, @objectstack/spec)
  • content/docs/data-modeling/analytics.mdx (via @objectstack/spec)
  • content/docs/data-modeling/drivers.mdx (via @objectstack/runtime, @objectstack/spec)
  • content/docs/data-modeling/external-datasources.mdx (via @objectstack/spec)
  • content/docs/data-modeling/field-types.mdx (via @objectstack/spec)
  • content/docs/data-modeling/fields.mdx (via @objectstack/spec)
  • content/docs/data-modeling/formulas.mdx (via @objectstack/spec)
  • content/docs/data-modeling/index.mdx (via @objectstack/spec)
  • content/docs/data-modeling/objects.mdx (via @objectstack/spec)
  • content/docs/data-modeling/queries.mdx (via @objectstack/spec)
  • content/docs/data-modeling/schema-design.mdx (via @objectstack/spec)
  • content/docs/data-modeling/seed-data.mdx (via @objectstack/spec)
  • content/docs/data-modeling/validation-rules.mdx (via @objectstack/spec)
  • content/docs/data-modeling/validation.mdx (via @objectstack/spec)
  • content/docs/deployment/cli.mdx (via @objectstack/spec)
  • content/docs/deployment/index.mdx (via @objectstack/runtime)
  • content/docs/deployment/production-readiness.mdx (via @objectstack/runtime)
  • content/docs/deployment/single-project-mode.mdx (via @objectstack/runtime)
  • content/docs/deployment/tenancy-modes.mdx (via @objectstack/spec)
  • content/docs/deployment/troubleshooting.mdx (via @objectstack/spec)
  • content/docs/deployment/validating-metadata.mdx (via @objectstack/spec)
  • content/docs/deployment/vercel.mdx (via @objectstack/runtime)
  • content/docs/getting-started/build-with-claude-code.mdx (via @objectstack/spec)
  • content/docs/getting-started/common-patterns.mdx (via @objectstack/spec)
  • content/docs/getting-started/examples.mdx (via @objectstack/spec)
  • content/docs/getting-started/quick-reference.mdx (via @objectstack/spec)
  • content/docs/getting-started/quick-start.mdx (via @objectstack/spec)
  • content/docs/getting-started/your-first-project.mdx (via @objectstack/runtime, @objectstack/spec)
  • content/docs/kernel/cluster.mdx (via @objectstack/runtime, @objectstack/spec)
  • content/docs/kernel/contracts/auth-service.mdx (via packages/spec)
  • content/docs/kernel/contracts/cache-service.mdx (via packages/spec)
  • content/docs/kernel/contracts/data-engine.mdx (via @objectstack/spec)
  • content/docs/kernel/contracts/index.mdx (via @objectstack/spec)
  • content/docs/kernel/contracts/metadata-service.mdx (via packages/spec)
  • content/docs/kernel/contracts/storage-service.mdx (via @objectstack/spec)
  • content/docs/kernel/index.mdx (via packages/spec)
  • content/docs/kernel/runtime-services/data-service.mdx (via @objectstack/spec)
  • content/docs/kernel/runtime-services/email-service.mdx (via packages/spec)
  • content/docs/kernel/runtime-services/examples.mdx (via @objectstack/spec)
  • content/docs/kernel/runtime-services/index.mdx (via packages/spec)
  • content/docs/kernel/runtime-services/queue-service.mdx (via packages/spec)
  • content/docs/kernel/runtime-services/sharing-service.mdx (via @objectstack/spec)
  • content/docs/kernel/runtime-services/sms-service.mdx (via packages/spec)
  • content/docs/kernel/runtime-services/storage-service.mdx (via @objectstack/spec)
  • content/docs/kernel/services-checklist.mdx (via @objectstack/metadata-protocol, @objectstack/spec)
  • content/docs/kernel/services.mdx (via @objectstack/spec)
  • content/docs/permissions/authentication.mdx (via @objectstack/runtime)
  • content/docs/permissions/authorization.mdx (via packages/runtime, @objectstack/spec)
  • content/docs/permissions/permission-sets.mdx (via @objectstack/spec)
  • content/docs/permissions/permissions-matrix.mdx (via @objectstack/spec)
  • content/docs/permissions/positions.mdx (via @objectstack/spec)
  • content/docs/permissions/rls.mdx (via @objectstack/spec)
  • content/docs/permissions/sharing-rules.mdx (via @objectstack/spec)
  • content/docs/permissions/system-context.mdx (via packages/runtime, packages/spec)
  • content/docs/plugins/adding-a-metadata-type.mdx (via @objectstack/spec)
  • content/docs/plugins/development.mdx (via @objectstack/spec)
  • content/docs/plugins/index.mdx (via @objectstack/spec)
  • content/docs/plugins/packages.mdx (via @objectstack/runtime, @objectstack/spec)
  • content/docs/protocol/backward-compatibility.mdx (via @objectstack/spec)
  • content/docs/protocol/diagram.mdx (via packages/spec)
  • content/docs/protocol/kernel/config-resolution.mdx (via @objectstack/spec)
  • content/docs/protocol/kernel/http-protocol.mdx (via @objectstack/metadata-protocol, @objectstack/runtime, @objectstack/spec)
  • content/docs/protocol/kernel/i18n-standard.mdx (via @objectstack/spec)
  • content/docs/protocol/kernel/index.mdx (via @objectstack/runtime, @objectstack/spec)
  • content/docs/protocol/kernel/lifecycle.mdx (via @objectstack/runtime, @objectstack/spec)
  • content/docs/protocol/kernel/plugin-spec.mdx (via @objectstack/spec)
  • content/docs/protocol/knowledge.mdx (via @objectstack/spec)
  • content/docs/protocol/objectql/index.mdx (via @objectstack/spec)
  • content/docs/protocol/objectql/query-syntax.mdx (via @objectstack/spec)
  • content/docs/protocol/objectql/schema.mdx (via @objectstack/spec)
  • content/docs/protocol/objectql/security.mdx (via packages/spec)
  • content/docs/protocol/objectql/state-machine.mdx (via @objectstack/spec)
  • content/docs/protocol/objectui/actions.mdx (via @objectstack/spec)
  • content/docs/protocol/objectui/concept.mdx (via @objectstack/spec)
  • content/docs/protocol/objectui/index.mdx (via @objectstack/spec)
  • content/docs/protocol/objectui/layout-dsl.mdx (via @objectstack/spec)
  • content/docs/protocol/objectui/record-alert.mdx (via @objectstack/spec)
  • content/docs/protocol/objectui/widget-contract.mdx (via @objectstack/spec)
  • content/docs/ui/actions.mdx (via @objectstack/spec)
  • content/docs/ui/apps.mdx (via @objectstack/spec)
  • content/docs/ui/create-vs-edit-form.mdx (via @objectstack/spec)
  • content/docs/ui/dashboards.mdx (via @objectstack/spec)
  • content/docs/ui/field-grouping-and-order.mdx (via @objectstack/spec)
  • content/docs/ui/forms.mdx (via @objectstack/spec)
  • content/docs/ui/index.mdx (via @objectstack/spec)
  • content/docs/ui/public-data-collection.mdx (via @objectstack/spec)
  • content/docs/ui/setup-app.mdx (via @objectstack/spec)
  • content/docs/ui/translations.mdx (via @objectstack/spec)
  • content/docs/ui/views.mdx (via @objectstack/spec)

7 release-owned page(s) also reference the affected code. These are read-only:

  • content/docs/releases/implementation-status.mdx (via @objectstack/runtime, @objectstack/spec)
  • content/docs/releases/index.mdx (via @objectstack/spec)
  • content/docs/releases/v12.mdx (via @objectstack/spec)
  • content/docs/releases/v13.mdx (via @objectstack/spec)
  • content/docs/releases/v16.mdx (via @objectstack/spec)
  • content/docs/releases/v17.mdx (via @objectstack/runtime, @objectstack/spec)
  • content/docs/releases/v9.mdx (via @objectstack/metadata-protocol, @objectstack/spec)

content/docs/releases/ is RELEASE-OWNED (AGENTS.md "Documentation Guardrails"): release
notes are written centrally at release time, and a code PR that edits them is the exact PR
that guardrail exists to stop. They are still audited — read-only. If one of them is actually
wrong, file an issue or open a dedicated docs-only PR; do not edit it here.

Advisory only. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs origin/main → pass the list as args.docs.

@github-actions github-actions Bot added documentation Improvements or additions to documentation tests tooling labels Aug 11, 2026
@os-help
os-help marked this pull request as ready for review August 11, 2026 08:06
@os-help
os-help added this pull request to the merge queue Aug 11, 2026
Merged via the queue into main with commit 61ea810 Aug 11, 2026
27 checks passed
@os-help
os-help deleted the claude/issue-7560-package-lifecycle-readonly-gate branch August 11, 2026 08:22
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation size/l tests tooling

Projects

None yet

2 participants