Skip to content

Clear package-owned durable state on delete and key app values by appId only - #1026

Merged
kody-bot merged 4 commits into
mainfrom
cursor/orchistrate-issue-231-package-state-model-e462
Jul 29, 2026
Merged

kody-bot merged 4 commits into
mainfrom
cursor/orchistrate-issue-231-package-state-model-e462

Conversation

@kentcdodds

Copy link
Copy Markdown
Owner

Summary

Brings the two items deferred from #1022 into scope (follow-up requested on #231's conductor run; overlaps item 4 of #1024):

  • Package delete now clears package-owned durable state. deleteSavedPackageProjection previously removed only D1 rows, orphaning every package-owned StorageRunner bucket (package bucket, legacy raw-id app root, facet/internal-DO buckets, job scratch, service scratch) and leaving app-scoped values behind while package secrets were deleted. It now collects the package-owned storage id set (deterministic ids plus the user_storage_buckets inventory), clears each bucket with per-bucket error tolerance — inventory rows survive failed clears so account deletion can still enumerate them — and deletes app-scoped values alongside secrets.
  • App-scoped values resolve from appId only. The app value scope no longer falls back to the run's storageId, so non-package contexts (ad hoc execute or job_schedule runs with a bound storage id) can no longer mint "app"-scoped value buckets keyed by arbitrary storage ids. Every package surface sets appId to the saved package id, so package code is unaffected. Migration 0109 deletes the now-unreachable fallback-keyed buckets (job:/exec: binding keys), guarded so any binding key matching a real saved package id survives.

An independent review of the initial implementation found — and empirically verified — an over-deletion hole: saved package ids are caller-suppliable (not guaranteed UUIDs), so raw SQL LIKE prefix matching let a package id of 'job' match every job:* bucket, and %/_ survived into bound prefixes. Fixed before merge: inventory matching is now exact JS-side comparison, with prefix arms (facets, service/job scratch) applied only to UUID-shaped package ids where they are provably unambiguous; non-UUID packages clear the deterministic id set only.

The #mcp/values import from package-registry gets the same lint boundary exception (with the same extraction TODO) as the existing #mcp/secrets one.

Testing

  • npm run validate (full local gate)
  • New unit coverage: bucket-set collection and clearing (including failure tolerance, cross-user/cross-package isolation, and adversarial package ids 'job'/'%'), pure filter tests for the UUID gate, migration 0109 guard behavior, app-scope save rejection without appId, and the read-path regression showing fallback-keyed buckets are unreachable
  • Independent review of the diff (data-loss focus); its BLOCKER finding is fixed in 3aca213e

Refs #1024 (delivers the package-delete cleanup portion; the legacy app-bucket removal remains tracked there)

System recap — extends existing primitives (medium risk)

Mode: recap · Base: main @ 356426e6 · Head: d8544633

Classification: extends — saved-packages delete behavior now destroys package-owned durable state; mcp-server value binding contract narrows; d1-app-db gains cleanup migration 0109. No new primitives; primitives.yaml unchanged.

Primitives touched

Primitive Group Impact
saved-packages assistant extends — package delete clears StorageRunner buckets + app-scoped values
mcp-server surfaces extends — app value scope resolves from appId only (storageId fallback removed)
d1-app-db storage extends — migration 0109 deletes stranded fallback-keyed app value buckets
durable-storage assistant composes — clearStorage() / inventory helpers reused as-is

System map

Package delete flows from the delete capability through the registry service, which enumerates buckets from the D1 inventory and clears each StorageRunner DO; the values service loses its storage-id fallback and the migration removes rows that fallback created.

Legend: green = composes (wiring only) · amber = extended by this PR · red = new primitive · gray = context (unchanged, included only when an edge crosses it).

flowchart LR
	savedPackages["saved-packages<br/>Saved packages"]:::extended
	durableStorage["durable-storage<br/>StorageRunner buckets"]:::touched
	d1AppDb["d1-app-db<br/>D1 app database"]:::extended
	mcpServer["mcp-server<br/>MCP endpoint"]:::extended
	savedPackages -->|"clearStorage() per package-owned bucket (UUID-gated prefix match)"| durableStorage
	savedPackages -->|"user_storage_buckets enumeration + row removal; deleteAllAppScopedValues"| d1AppDb
	mcpServer -->|"app value scope: appId only, no storageId fallback"| d1AppDb
	d1AppDb -->|"migration 0109: delete job:/exec:-keyed app value buckets"| d1AppDb
	classDef touched fill:#1a7f37,color:#fff
	classDef extended fill:#9a6700,color:#fff
	classDef added fill:#cf222e,color:#fff
	classDef untouched fill:#57606a,color:#fff
Loading

Before / after

package delete:            D1 rows only, buckets/values orphaned  →  buckets cleared + inventory rows removed + app-scoped values deleted
app value scope binding:   appId, else fallback to storageId      →  appId or unavailable
fallback-keyed buckets:    reachable via bound storageId          →  deleted by migration 0109 (saved-package ids protected)

Invariants

Per-user isolation unchanged: bucket enumeration is WHERE user_id = ?, every clearStorage() goes through storageRunnerRpc({ userId }), and the review confirmed cross-user deletion is structurally impossible. Deletion is destructive by design but reachable only through the intent-checked package_delete capability; the UUID gate prevents same-user cross-namespace over-deletion for adversarial package ids.

Open in Web Open in Cursor 

deleteSavedPackageProjection removed D1 rows but orphaned every
package-owned StorageRunner bucket (package bucket, legacy app root,
facet/internal-DO buckets, job scratch, service scratch) and left
app-scoped values behind while package secrets were deleted.

Collect the package-owned storage id set from deterministic ids plus the
user_storage_buckets inventory (user-scoped, bound-param prefix matches),
clear each bucket with per-bucket error tolerance (inventory rows survive
failed clears so account deletion can still enumerate them), and delete
app-scoped values alongside package secrets. The values-service import
gets the same package-registry boundary exception (with the same
extraction TODO) as the secrets service.
The app value scope fell back to the run's storageId when appId was
absent, letting non-package contexts (ad hoc execute or job runs with a
bound storageId) mint app-scoped value buckets keyed by arbitrary storage
ids. Package config is keyed by the saved package id, and every package
surface sets appId, so the fallback only served the contradiction. App
scope now resolves from appId or is unavailable.
Review found the inventory LIKE matching over-deleted for non-UUID
package ids: package_save accepts arbitrary ids, so a package id of
'job' matched every job:* bucket and LIKE metacharacters ('%', '_')
survived into the bound prefixes. Replace the SQL LIKE query with exact
JS-side matching over listUserStorageBucketIds; prefix arms (facets,
service scratch, job scratch) apply only to UUID-shaped package ids,
where they are provably unambiguous. Non-UUID packages still clear the
deterministic id set.
Value buckets minted under the removed app-scope storageId fallback
(binding keys shaped job:/exec:) are unreachable by any read path now
that app scope resolves from appId only. Migration 0109 deletes them,
guarded so binding keys matching a real saved package id survive;
value_entries cascade. Adds the read-path regression test showing
fallback-keyed buckets are invisible with only a storageId bound.
@coderabbitai

coderabbitai Bot commented Jul 29, 2026 •

Copy link
Copy Markdown

Warning

Review limit reached

@kody-bot, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 28 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: d747c6d7-cf4b-4886-a9ed-67f64e2d4c26

📥 Commits

Reviewing files that changed from the base of the PR and between b83c984 and d854463.

📒 Files selected for processing (8)
  • packages/worker/migrations/0109-delete-stranded-app-value-buckets.sql
  • packages/worker/src/mcp/storage-bindings.ts
  • packages/worker/src/mcp/values/migration.node.test.ts
  • packages/worker/src/mcp/values/service.node.test.ts
  • packages/worker/src/package-registry/service.node.test.ts
  • packages/worker/src/package-registry/service.ts
  • tools/migration-ledger.json
  • tools/oxlint/local-plugin.js

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@kody-bot
kody-bot marked this pull request as ready for review July 29, 2026 21:15
@github-actions

Copy link
Copy Markdown
Contributor

🔎 Preview deployed: https://kody-pr-1026.kody-a99.workers.dev

Worker: kody-pr-1026
D1: kody-pr-1026-db
KV: kody-pr-1026-oauth-kv

Mocks:

@kody-bot
kody-bot merged commit 8a8f594 into main Jul 29, 2026
17 checks passed
@kody-bot
kody-bot deleted the cursor/orchistrate-issue-231-package-state-model-e462 branch July 29, 2026 21:28
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants