Skip to content

[APPS-2792] Add: harden the in-process local execution path - #480

Draft
tyffical wants to merge 2 commits into
tiffany.trinh/apps-2792-in-process-executionfrom
tiffany.trinh/apps-2792-harden-local-execution-v2
Draft

[APPS-2792] Add: harden the in-process local execution path#480
tyffical wants to merge 2 commits into
tiffany.trinh/apps-2792-in-process-executionfrom
tiffany.trinh/apps-2792-harden-local-execution-v2

Conversation

@tyffical

@tyffical tyffical commented Aug 7, 2026

Copy link
Copy Markdown
Contributor

Motivation

  • Part of APPS-2792 — local Node execution for App Builder backend functions. Milestone 1 in the Kickoff doc, stacked on Milestone 0 ([APPS-2792] Add: in-process local execution for backend functions #479).
  • [APPS-2792] Add: in-process local execution for backend functions #479 shipped the direct-import in-process execution mechanism itself but explicitly deferred hardening (both tracked as follow-ups in its own Out of Scope table). This PR adds it.
  • The single biggest correctness risk of running backend functions in-process (vs. production's fresh-Deno-subprocess-per-execution model): @datadog/action-catalog's setExecuteActionImplementation and @datadog/apps-backend's setBackend both register runtime context via a shared, module-level setter. Without serialization, a second concurrent execution's registration could silently redirect the first's still-in-flight typed-import calls to the wrong identity — with no error at all. See the RFC's Decisions and Trade-Offs.

Changes

What changed File
Local executions are now serialized via a promise-chain queue (enqueue) — never run concurrently. A rejected execution doesn't wedge the queue for whatever's next. local-execution.ts
A returned result is now checked for JSON-serializability before being handed back — a circular reference or BigInt gets a clear, attributed error instead of an opaque downstream JSON.stringify failure; a bare function/Symbol (which JSON.stringify silently drops instead of throwing) is also caught explicitly. local-execution.ts
New tests: two concurrent executions never interleave (proven via a shared globalThis order marker, not a mock); the queue keeps flowing after an earlier execution rejects; a loadModule rejection (simulating a native-module load failure) rejects cleanly; all three non-serializable-result shapes; the no-token-exposure and $.Source invariants from #479 are re-verified against the queued path. local-execution.test.ts

QA Instructions

yarn install
yarn test:unit packages/plugins/apps/src/vite/local-execution.test.ts
# Expected: Test Suites: 1 passed / Tests: 20 passed ✅ VERIFIED
yarn test:unit packages/plugins/apps
# Expected: Test Suites: 23 passed / Tests: 305 passed ✅ VERIFIED
yarn workspace @dd/apps-plugin run typecheck
# Expected: no output, clean exit ✅ VERIFIED
npx eslint packages/plugins/apps/src/vite/local-execution.ts packages/plugins/apps/src/vite/local-execution.test.ts --quiet
# Expected: no output, clean exit ✅ VERIFIED

No manual local/staging QA for this PR specifically, same reasoning as #479: this module still isn't wired into createDevServerMiddleware (that's Milestone 2, stacked next as #481), so there's no npm run dev request path that reaches it yet.

Blast Radius

  • No behavior change for any currently-shipping code path: local-execution.ts still isn't called from anywhere in the existing dev server.
  • Risk: low. All changes are additive/internal to a module with no external callers yet; full existing test suite (305 tests) passes.

Out of Scope / Follow-ups

Item Status Next step
Wiring into the real dev server (handleExecuteAction, threading a real LoadModule, /__dd/executeActionViaCloud split, real preview-async calls) Not started Milestone 2, stacked on this PR (#481)
Real auth token / closure-scoping for real $.Actions execution Blocked Same as #479 — needs the single-action execution endpoint (Action Platform team)
Runtime network/subprocess guard: block net.Socket.prototype.connect, fetch, and child_process's spawn/exec/execSync for the duration of a local execution, exempted only around the internal $.ActionsexecuteAction call Done Shipped in #484, stacked on this PR

Documentation

@datadog-datadog-us1-prod

datadog-datadog-us1-prod Bot commented Aug 7, 2026

Copy link
Copy Markdown

Tests

🎉 All green!

🧪 All tests passed
❄️ No new flaky tests detected

This comment will be updated automatically if new data arrives.
🔗 Commit SHA: 8292a6a | Docs | Datadog PR Page | Give us feedback!

@tyffical
tyffical force-pushed the tiffany.trinh/apps-2792-harden-local-execution-v2 branch from 6e85225 to 64c7a61 Compare August 7, 2026 15:17
Serializes local backend-function executions via a promise-chain queue,
since @datadog/action-catalog and @datadog/apps-backend both register
runtime context via a shared, module-level setter that isn't safe under
concurrent in-process execution. Also populates $.Source with a synthetic
local-dev identity (deferred from Milestone 0), and adds edge-case
coverage: non-serializable results, a top-level module throw, and a real
concurrent-execution test against a genuine @datadog/apps-backend typed
import confirming no cross-execution state leakage.
Runs the readOwnArgsAfterDelay concurrency check through the real,
serialized executeScriptLocally entrypoint (its test.skip counterpart
against PR #479's un-serialized base fails with cross-contaminated
args). Passing here confirms the enqueue/queueTail promise-chain mutex
actually closes the globalThis.$ race, not just reorders interleaved
work.
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.

1 participant