Skip to content

Fix saved app facet exec and partial update semantics - #165

Merged
kentcdodds merged 4 commits into
mainfrom
cursor/saved-app-facets-fixes-22a0
Apr 14, 2026
Merged

kentcdodds merged 4 commits into
mainfrom
cursor/saved-app-facets-fixes-22a0

Conversation

@kentcdodds

@kentcdodds kentcdodds commented Apr 13, 2026 •

Copy link
Copy Markdown
Owner

Summary

  • move app_server_exec off in-facet string eval and into a throwaway Dynamic Worker loaded via APP_LOADER
  • preserve existing saved-app fields on ui_save_app updates unless the caller explicitly changes them
  • harden exec method dispatch and add saved-app regression coverage/docs for the stricter contract

Design note

app_server_exec no longer attempts to evaluate arbitrary source inside the running saved-app facet. The supervisor wraps the snippet in a synthetic WorkerEntrypoint module, loads it through the Dynamic Worker Loader API, and exposes an explicit app.call(methodName, ...args) bridge. That bridge now routes through a dedicated facet RPC wrapper that rejects reserved names such as fetch and __kody_* before dispatching to user-defined methods on the saved app's exported App class. For raw storage inspection, users should use app_storage_export.

Testing

  • repeated targeted MCP E2E runs for packages/worker/src/mcp/mcp-server.mcp-e2e.test.ts
  • covered app_server_exec trivial execution, explicit RPC calls, and reserved-method rejection
  • covered ui_save_app preserve / clear / rotate backend semantics across fresh MCP sessions
  • covered that omitted parameters do not overwrite existing saved parameter definitions
  • captured passing output artifact: saved-app-regression-test-output.txt
Open in Web Open in Cursor 

Summary by CodeRabbit

  • New Features

    • Saved apps support partial updates—omitted fields preserve existing values.
    • Backend can be explicitly cleared with serverCode: null.
    • Backend exec runs in a throwaway worker and exposes app.call(methodName, ...args) for validated RPC.
  • Documentation

    • Clarified saved-app update semantics, serverCode handling, and app_server_exec execution model and restrictions (reserved method names).
  • Tests

    • Added end-to-end tests for exec worker RPCs and partial app update behaviors.

Co-authored-by: Kent C. Dodds <me+github@kentcdodds.com>
@coderabbitai

coderabbitai Bot commented Apr 13, 2026 •

Copy link
Copy Markdown
📝 Walkthrough

Walkthrough

Refactors saved-app backend execution to compile one-off code into a throwaway Dynamic Worker with an explicit RPC bridge to saved app facets. Implements partial update semantics for saved apps (omitted fields preserve existing values; serverCode: null clears backend). Adds docs and e2e tests.

Changes

Cohort / File(s) Summary
Docs & Capability Text
docs/use/saved-app-backends.md, docs/use/skills-and-apps.md, packages/worker/src/mcp/capabilities/apps/app-server-exec.ts
Clarified ui_save_app contract and updated app_server_exec description to reflect the throwaway Dynamic Worker model, RPC bridge (app.call(...)), params passing, and reserved method-name restrictions.
Saved App Execution Core
packages/worker/src/mcp/app-runner.ts
Reworked facet wrapper generation to not inline serverCode. Replaced direct __kody_exec evaluation with SavedAppExecWorker loaded via APP_LOADER. Added SavedAppExecWorker.run, AppFacetBridge.callAppRpc, DurableObject helper callFacetRpc, and extended appRunnerRpc to expose callFacetRpc. Method-invocation now validates/denies reserved names and invokes via Reflect.get(...).call(...).
Save/Update Capability
packages/worker/src/mcp/capabilities/apps/ui-save-app.ts
Changed ui_save_app from create-or-replace to create-or-partial-update: title, description, clientCode required only on create; serverCode is nullable (`string
E2E Tests
packages/worker/src/mcp/mcp-server.mcp-e2e.test.ts
Added tests covering app_server_exec (throwaway worker execution, app.call RPC, reserved-name denial, params passing, storage export) and ui_save_app partial update flows (preserve, clear, and replace backend; server_code_id rotation).

Sequence Diagram

sequenceDiagram
    participant Client
    participant AppRunner as AppRunner (RPC)
    participant Compiler as CodeCompiler
    participant ExecWorker as SavedAppExecWorker (Throwaway)
    participant AppFacet as SavedApp Facet

    Client->>AppRunner: codemode.app_server_exec(code, params)
    AppRunner->>Compiler: compile worker module from `code`
    Compiler-->>AppRunner: worker entrypoint module
    AppRunner->>ExecWorker: load & run(params)
    ExecWorker->>ExecWorker: evaluate provided function body
    ExecWorker->>AppRunner: app.call(methodName, args)  (RPC)
    AppRunner->>AppRunner: callFacetRpc(appId, facetName, methodName, args)
    AppRunner->>AppFacet: invoke __kody_invokeUserMethod(methodName, args)
    AppFacet-->>AppRunner: method result
    AppRunner-->>ExecWorker: return result to snippet
    ExecWorker-->>Client: snippet result
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~50 minutes

Possibly related PRs

Poem

🐰 I stitched a worker from a transient thread,
it hops, it calls, then tucks its head to bed,
app.call hops over to the saved facet's door,
updates kept safe unless null says remove—encore!
a tiny rabbit cheers: code runs, then is shed.

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely summarizes the two main changes: fixing app_server_exec semantics (moving from in-facet evaluation to Dynamic Worker) and partial update semantics for saved apps (preserving fields unless explicitly changed).

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch cursor/saved-app-facets-fixes-22a0

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 and usage tips.

Co-authored-by: Kent C. Dodds <me+github@kentcdodds.com>
@kentcdodds
kentcdodds marked this pull request as ready for review April 14, 2026 01:00
@github-actions

github-actions Bot commented Apr 14, 2026 •

Copy link
Copy Markdown
Contributor

🔎 Preview deployed: https://kody-pr-165.kentcdodds.workers.dev

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

Mocks:

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@packages/worker/src/mcp/app-runner.ts`:
- Around line 158-170: The injected worker module created by
createSavedAppExecWorkerModule (class ${savedAppExecEntrypointName}) currently
allows injected code to access this.env.APP directly and call any facet methods;
instead, prevent direct access to this.env.APP by only exposing a local app
helper whose call method routes to a new, dedicated RPC on the generated facet
module (e.g., facet.invokeUserMethod) that performs validation: accept only
allowed user-exported method names and/or a whitelist and explicitly reject
reserved names/prefixes like fetch and __kody_*; update
createSavedAppExecWorkerModule and the corresponding facet RPC implementation
(also apply same change to the other injection site around the existing
app_server_exec logic) so app.call(...) invokes the wrapper RPC which enforces
the allowed-methods policy before dispatching to the real facet.

In `@packages/worker/src/mcp/capabilities/apps/ui-save-app.ts`:
- Around line 264-279: The updates object currently always sets parameters from
serializedParameters (which falls back to existingApp.parameters), causing
client-only updates to overwrite parameters; change the logic so
serializedParameters does NOT default to existingApp.parameters and ensure
updates.parameters is only set when args.parameters !== undefined (i.e., set
parameters to serializedParameters when args.parameters is provided, otherwise
leave parameters undefined/omit the property) so updateUiArtifact(...) can treat
undefined as “leave unchanged”; adjust the code around serializedParameters,
args.parameters, existingApp.parameters and the updates object accordingly.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 16bdad33-d790-460a-80c1-0115a872df6f

📥 Commits

Reviewing files that changed from the base of the PR and between 7c85464 and 61a71de.

📒 Files selected for processing (6)
  • docs/use/saved-app-backends.md
  • docs/use/skills-and-apps.md
  • packages/worker/src/mcp/app-runner.ts
  • packages/worker/src/mcp/capabilities/apps/app-server-exec.ts
  • packages/worker/src/mcp/capabilities/apps/ui-save-app.ts
  • packages/worker/src/mcp/mcp-server.mcp-e2e.test.ts

Comment thread packages/worker/src/mcp/app-runner.ts
Comment thread packages/worker/src/mcp/capabilities/apps/ui-save-app.ts

@cursor cursor Bot left a comment •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 2 potential issues.

Fix All in Cursor

Bugbot Autofix prepared fixes for both issues found in the latest run.

  • ✅ Fixed: Exec worker params defaults to undefined instead of {}
    • Guarded the exec worker invocation by defaulting missing params to an empty object to preserve documented behavior.
  • ✅ Fixed: Unused getFacetRpcStub method is dead code
    • Removed the unused getFacetRpcStub method and its RPC type entry since no callers exist.

You can send follow-ups to the cloud agent here.

Reviewed by Cursor Bugbot for commit 61a71de. Configure here.

Comment thread packages/worker/src/mcp/app-runner.ts Outdated
Comment thread packages/worker/src/mcp/app-runner.ts Outdated
cursoragent and others added 2 commits April 14, 2026 01:10
Co-authored-by: Kent C. Dodds <me+github@kentcdodds.com>
Co-authored-by: Kent C. Dodds <me+github@kentcdodds.com>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
packages/worker/src/mcp/app-runner.ts (1)

540-541: Consider documenting the validation-only getFacetStub call.

Line 540 calls getFacetStub but discards the result. This appears intentional—to validate the facet exists and configuration is valid before loading the exec worker. A brief comment would clarify this is for early validation rather than an oversight.

📝 Suggested clarification
 	async execServer(input: {
 		appId: string
 		facetName?: string | null
 		code: string
 		params?: Record<string, unknown>
 	}) {
 		const facetName = buildFacetName(input.facetName)
+		// Validate facet exists and config is valid before loading exec worker
 		await this.getFacetStub(facetName)
 		const config = await this.readConfig(this.ctx.id.toString())
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/worker/src/mcp/app-runner.ts` around lines 540 - 541, The call to
this.getFacetStub(facetName) is intentionally made for validation only (its
return value is discarded) before loading the exec worker; add a concise inline
comment above or next to the call explaining that getFacetStub is invoked solely
to validate the facet exists and its configuration before proceeding (so the
reader knows the discarded result is intentional), referencing the call site
this.getFacetStub(facetName) and the subsequent
readConfig(this.ctx.id.toString()) context to make the intent clear.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@packages/worker/src/mcp/app-runner.ts`:
- Around line 540-541: The call to this.getFacetStub(facetName) is intentionally
made for validation only (its return value is discarded) before loading the exec
worker; add a concise inline comment above or next to the call explaining that
getFacetStub is invoked solely to validate the facet exists and its
configuration before proceeding (so the reader knows the discarded result is
intentional), referencing the call site this.getFacetStub(facetName) and the
subsequent readConfig(this.ctx.id.toString()) context to make the intent clear.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: ec964701-a4ab-4e9c-b708-1a66d63e9edf

📥 Commits

Reviewing files that changed from the base of the PR and between 61a71de and d4e5b99.

📒 Files selected for processing (5)
  • docs/use/saved-app-backends.md
  • packages/worker/src/mcp/app-runner.ts
  • packages/worker/src/mcp/capabilities/apps/app-server-exec.ts
  • packages/worker/src/mcp/capabilities/apps/ui-save-app.ts
  • packages/worker/src/mcp/mcp-server.mcp-e2e.test.ts
✅ Files skipped from review due to trivial changes (1)
  • packages/worker/src/mcp/capabilities/apps/app-server-exec.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • packages/worker/src/mcp/mcp-server.mcp-e2e.test.ts

@kentcdodds
kentcdodds merged commit d1fba9f into main Apr 14, 2026
9 checks passed
@kentcdodds
kentcdodds deleted the cursor/saved-app-facets-fixes-22a0 branch April 14, 2026 02:29
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.

2 participants