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

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 realSchemaRegistry, 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.

#MutationResultTests that turned red
M1DELETE gate removed (pre-fix state)6 failed / 85all 3 DELETE-refusal cases, isSystem, envelope shape, verb naming
M2disable gate removed (pre-fix state)5 failed / 86all 3 disable-refusal cases, "not the #7033 gate", verb naming
M3Predicate always false — the outage shape8 failed / 83writable disable + delete, both 404 cases, and 4 pre-existing #7033 tests
M4Code-loaded signal (engine.manifests) dropped2 failed / 89both code-loaded cases only
M5Manifest-scope signal dropped8 failed / 83both system + both cloud cases, isSystem, axis, envelope, verb
M6DELETE gate moved afteruninstallPackage6 failed / 85the listing assertions — "delete first, refuse second"
M7disable gate moved afterdisablePackage3 failed / 88the enabled/status assertions
M8Refusal restated as PACKAGE_READ_ONLY7 failed / 84all 6 refusal cases + the envelope-shape case
M9Unknown id treated as read-only (existence oracle)2 failed / 89both 404 cases only
M10Blanket caller bypass re-added to the gate10 failed / 81all 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

vercelBot commented Aug 11, 2026

Copy link
Copy Markdown

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

1 Skipped Deployment
ProjectDeploymentActionsUpdated (UTC)
objectstackIgnoredIgnoredAug 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-actionsgithub-actionsBot 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 queueAug 11, 2026
Merged via the queue into main with commit 61ea810Aug 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 freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentationImprovements or additions to documentationsize/lteststooling

Projects

None yet

2 participants

@os-help@claude