Skip to content

[APPS-2961] Update: run each local backend function call in a fresh process - #608

Open
tyffical wants to merge 1 commit into
tiffany.trinh/apps-2961-shared-execution-helpersfrom
tiffany.trinh/apps-2961-child-process-execution
Open

tyffical wants to merge 1 commit into
tiffany.trinh/apps-2961-shared-execution-helpersfrom
tiffany.trinh/apps-2961-child-process-execution

Conversation

@tyffical

@tyffical tyffical commented Oct 21, 2026 •

Copy link
Copy Markdown
Contributor

Motivation

Architecture

sequenceDiagram
    participant B as "Browser"
    participant D as "Vite dev server"
    participant C as "Call process"
    participant API as "Datadog API"
    B->>D: "POST /__dd/executeAction"
    D->>API: "Hydrate $ (preview-async)"
    D->>C: "Fork a fresh process: stripped env, 256 MB heap"
    loop "Each module the call imports"
        C->>D: "fetchModule over IPC"
        D-->>C: "Module code"
    end
    C->>D: "$.Actions call"
    D->>D: "Allowlist check, add credentials"
    D->>API: "Single-action query"
    D-->>C: "action-result"
    C-->>D: "stdout and stderr lines"
    C->>D: "result or error"
    D->>C: "SIGKILL"
    D-->>B: "Response"
Loading
  • The dev server keeps the credentials and the connection allowlist.

Changes

7 changes across local-execution-process.ts, local-execution-child.ts, index.ts, the README and tests
What changed File
Forks one process per call, one at a time, with a 256 MB heap, its own temp dir and no developer variables. local-execution-process.ts
Serves modules and runs $.Actions for the call over IPC. local-execution-process.ts
Streams output by line, and kills calls on timeout, close, restart and exit. local-execution-process.ts, index.ts
The child runs a Vite ModuleRunner under the in-process guards. local-execution-child.ts, local-execution-protocol.ts
Uses the child on Vite 6.0.4+, except in Yarn Plug'n'Play projects. index.ts, dev-server.ts, dev-server-module-graph.ts
Builds the child entry and documents local execution. package.json, local-execution.ts, README.md
Tests. local-execution-process.integration.test.ts, local-execution-process.test.ts, dev-server.integration.test.ts, index.test.ts

QA Instructions

yarn typecheck:all
# Expected: exit 0 ✅ VERIFIED
yarn lint
# Expected: exit 0 ✅ VERIFIED
yarn cli integrity
# Expected: exit 0, no changes ✅ VERIFIED
yarn test:unit packages/plugins/apps
# Expected: Tests: 944 passed; only the 15 manifest-tags and parameters tests fail, under macOS's /var TMPDIR ✅ VERIFIED
TMPDIR=<a dir outside /var> yarn test:unit packages/plugins/apps/src/vite/manifest-tags.integration.test.ts packages/plugins/apps/src/vite/parameters.integration.test.ts
# Expected: Tests: 33 passed ✅ VERIFIED
Manual QA in a Vite 8 app against a Datadog staging org
Check Result on this head
Module state over 3 calls 1, 1, 1
Four 1000 ms calls at once 7074 ms, four pids
Hung call, synchronous loop 10 s timeout errors
Outside write, subprocess guard refusals
DD_* variables a dependency sees none
$.Actions unscoped, unlisted connection 200, refused
dev:verify runs in the cloud
Yarn Plug'n'Play app warning; in-process, Vite 8 fails as on the base branch
  • Node 20.19.4, Vite 8.3.3, a scaffolded app with this branch's plugin and these fixtures:
// src/qaState.backend.ts
let calls = 0;

export async function countAndSleep(delayMs: number) {
    const startedAt = Date.now();
    calls += 1;
    await new Promise((resolve) => setTimeout(resolve, delayMs));
    return { calls, pid: process.pid, startedAt, endedAt: Date.now() };
}

export async function waitForever() {
    await new Promise(() => {});
}

export function spinForever() {
    // eslint-disable-next-line no-empty
    for (;;) {}
}

export async function logSlowly(delayMs: number) {
    console.log('qa first line');
    await new Promise((resolve) => setTimeout(resolve, delayMs));
    console.error('qa second line');
    return 'logged';
}
// src/qaSandbox.backend.ts
import { probeSandbox } from 'qa-sandbox-probes';

export async function runSandboxProbes(projectFile: string) {
    return probeSandbox(projectFile);
}
// src/qaActions.backend.ts
declare const $: { Actions: { http: { request(call: unknown): Promise<unknown> } } };

export async function httpWithoutConnection() {
    const response = await $.Actions.http.request({
        inputs: { url: 'https://httpbin.org/status/204', verb: 'GET' },
    });
    return { type: typeof response, keys: Object.keys((response as object) ?? {}).sort() };
}

export async function httpWithUnlistedConnection() {
    return $.Actions.http.request({
        inputs: { url: 'https://example.com', verb: 'GET' },
        connectionId: 'qa-unlisted-connection',
    });
}
// node_modules/qa-sandbox-probes/index.js, with { "name": "qa-sandbox-probes", "main": "index.js" }
// A dependency, since the dev server's static checks refuse these imports in app code.
const { spawnSync } = require('node:child_process');
const fs = require('node:fs');
const os = require('node:os');
const path = require('node:path');
const attempt = (run) => {
    try {
        return 'ok: ' + run();
    } catch (error) {
        return 'refused: ' + (error.code || error.message);
    }
};
exports.probeSandbox = (projectFile) => ({
    projectWrite: attempt(() => (fs.writeFileSync(projectFile, 'x'), 'written')),
    tempWrite: attempt(() => {
        const file = path.join(os.tmpdir(), 'qa-probe.txt');
        fs.writeFileSync(file, 'x');
        return fs.readFileSync(file, 'utf8');
    }),
    spawn: attempt(() => {
        const spawned = spawnSync('echo', ['hi']);
        if (spawned.error) throw spawned.error;
        return String(spawned.stdout).trim();
    }),
    envKeys: Object.keys(process.env).filter((name) => /DD_|DATADOG/.test(name)).join(',') || 'none',
});
// qa-run.mjs
// Starts a QA app's Vite dev server and drives /__dd/executeAction through each APPS-2961 check.
// Usage: DD_SITE=<staging site> DD_API_KEY=... DD_APP_KEY=... node qa-run.mjs <appDir> <port> <before|after> [vite args...]
import { spawn } from 'node:child_process';
import fs from 'node:fs';
import path from 'node:path';

const [appDir, port, label, ...viteArgs] = process.argv.slice(2);
const base = `http://localhost:${port}`;
const lines = [];
const vite = spawn(
    path.join(appDir, 'node_modules/.bin/vite'),
    ['--port', port, '--strictPort', ...viteArgs],
    { cwd: appDir, env: process.env, stdio: ['ignore', 'pipe', 'pipe'] },
);
for (const stream of [vite.stdout, vite.stderr]) {
    let buffered = '';
    stream.on('data', (chunk) => {
        buffered += chunk;
        const parts = buffered.split('\n');
        buffered = parts.pop();
        for (const line of parts) lines.push({ at: Date.now(), line: line.replace(/\x1b\[[0-9;]*m/g, '') });
    });
}

const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
async function waitForServer() {
    for (let i = 0; i < 120; i++) {
        try {
            await fetch(base);
            return;
        } catch {
            await sleep(500);
        }
    }
    throw new Error('dev server never came up');
}
const names = {};
async function functionName(file, exportName) {
    names[file] ??= await (await fetch(`${base}/src/${file}`)).text();
    const match = names[file].match(new RegExp(`executeBackendFunction\\("([^"]+\\.${exportName})"`));
    if (!match) throw new Error(`no proxy for ${exportName}: ${names[file].slice(0, 300)}`);
    return match[1];
}
async function call(file, exportName, args = []) {
    const started = Date.now();
    const response = await fetch(`${base}/__dd/executeAction`, {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ functionName: await functionName(file, exportName), args }),
    });
    const body = await response.json();
    return { status: response.status, ms: Date.now() - started, body };
}

const report = { label };
try {
    await waitForServer();
    report.freshState = [];
    for (let i = 0; i < 3; i++) {
        const { body } = await call('qaState.backend.ts', 'countAndSleep', [0]);
        report.freshState.push(body.result?.data?.calls ?? body);
    }
    const sequential = Date.now();
    await call('qaState.backend.ts', 'countAndSleep', [1000]);
    report.oneCallMs = Date.now() - sequential;
    const parallel = Date.now();
    const runs = await Promise.all([1, 2, 3, 4].map(() => call('qaState.backend.ts', 'countAndSleep', [1000])));
    report.fourParallelCallsMs = Date.now() - parallel;
    report.parallelPids = runs.map((run) => run.body.result?.data?.pid);
    report.timeout = await call('qaState.backend.ts', 'waitForever');
    if (label === 'after') {
        report.syncLoopTimeout = await call('qaState.backend.ts', 'spinForever');
    }
    // This app lives under /tmp, which a call may write to, so the probe targets a folder outside it.
    const projectFile = process.env.QA_OUTSIDE_TMP_FILE;
    report.sandbox = (await call('qaSandbox.backend.ts', 'runSandboxProbes', [projectFile])).body;
    report.projectFileExists = fs.existsSync(projectFile);
    fs.rmSync(projectFile, { force: true });
    const logCall = Date.now();
    const logged = await call('qaState.backend.ts', 'logSlowly', [1500]);
    const logLines = lines.filter(({ line }) => line.includes('qa first line') || line.includes('qa second line'));
    report.logs = {
        result: logged.body,
        lines: logLines.map(({ at, line }) => ({ msAfterCallStart: at - logCall, line: line.trim() })),
        callMs: logged.ms,
    };
    report.actionWithoutConnection = await call('qaActions.backend.ts', 'httpWithoutConnection');
    report.actionWithUnlistedConnection = await call('qaActions.backend.ts', 'httpWithUnlistedConnection');
} catch (error) {
    report.error = String(error);
} finally {
    report.serverLines = lines.filter(({ line }) => /datadog-apps-plugin|error|Executing/i.test(line)).slice(-15).map(({ line }) => line);
    vite.kill('SIGTERM');
}
console.log(JSON.stringify(report, null, 2));
export QA_OUTSIDE_TMP_FILE=<a path outside /tmp and the app>
DD_SITE=<staging site> node qa-run.mjs ./qa-app 5961 after > after.json
{
 "label": "after",
 "freshState": [
  1,
  1,
  1
 ],
 "oneCallMs": 1940,
 "fourParallelCallsMs": 7074,
 "parallelPids": [
  992,
  1023,
  1029,
  1038
 ],
 "timeout": {
  "status": 500,
  "ms": 10797,
  "body": {
   "success": false,
   "error": "Local execution of \"waitForever\" timed out after 10000ms"
  }
 },
 "syncLoopTimeout": {
  "status": 500,
  "ms": 10813,
  "body": {
   "success": false,
   "error": "Local execution of \"spinForever\" timed out after 10000ms"
  }
 },
 "sandbox": {
  "success": true,
  "result": {
   "data": {
    "projectWrite": "refused: EROFS",
    "tempWrite": "ok: x",
    "spawn": "refused: Spawning a subprocess is not allowed in backend functions.",
    "envKeys": "none"
   }
  }
 },
 "projectFileExists": false,
 "logs": {
  "result": {
   "success": true,
   "result": {
    "data": "logged"
   }
  },
  "lines": [
   {
    "msAfterCallStart": 738,
    "line": "qa first line"
   },
   {
    "msAfterCallStart": 2240,
    "line": "qa second line"
   }
  ],
  "callMs": 2243
 },
 "actionWithoutConnection": {
  "status": 200,
  "ms": 1204,
  "body": {
   "success": true,
   "result": {
    "data": {
     "type": "object",
     "keys": [
      "body",
      "headers",
      "status"
     ]
    }
   }
  }
 },
 "actionWithUnlistedConnection": {
  "status": 500,
  "ms": 825,
  "body": {
   "success": false,
   "error": "Action $.Actions.http.request used connection \"qa-unlisted-connection\", which is not in this function's allowed connections: []"
  }
 }
}
// dev-verify-run.mjs
// Starts a QA app's dev server in dev-verify mode and runs one backend function through it.
// Usage: DD_SITE=<staging site> DD_API_KEY=... DD_APP_KEY=... node dev-verify-run.mjs <appDir> <port>
import { spawn } from 'node:child_process';
import path from 'node:path';

const [appDir, port] = process.argv.slice(2);
const base = `http://localhost:${port}`;
const output = [];
const vite = spawn(path.join(appDir, 'node_modules/.bin/vite'), ['--port', port, '--strictPort', '--mode', 'dev-verify'], {
    cwd: appDir,
    env: process.env,
    stdio: ['ignore', 'pipe', 'pipe'],
});
vite.stdout.on('data', (chunk) => output.push(String(chunk)));
vite.stderr.on('data', (chunk) => output.push(String(chunk)));
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
try {
    for (let i = 0; ; i++) {
        try {
            await fetch(base);
            break;
        } catch {
            if (i > 120) throw new Error('dev server never came up');
            await sleep(500);
        }
    }
    const proxy = await (await fetch(`${base}/src/getGreeting.backend.ts`)).text();
    const functionName = proxy.match(/executeBackendFunction\("([^"]+\.getGreeting)"/)[1];
    const started = Date.now();
    const response = await fetch(`${base}/__dd/executeAction`, {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ functionName, args: ['dev-verify'] }),
    });
    console.log(JSON.stringify({ status: response.status, ms: Date.now() - started, body: await response.json() }));
    const log = output.join('').replace(/\x1b\[[0-9;]*m/g, '');
    console.log(log.split('\n').filter((line) => /Executing action|preview-async|child process/.test(line)).join('\n'));
} finally {
    vite.kill('SIGTERM');
}
{"status":200,"ms":987,"body":{"success":true,"result":{"data":"Hello, dev-verify! This greeting ran as a backend function."}}}
[Qa App|debug|vite|apps] Executing action via cloud: src/getGreeting/getGreeting with args
[Qa App|debug|vite|apps] Calling Datadog API: https://api.<staging site>/api/v2/app-builder/queries/preview-async
  • The Plug'n'Play app is the same app with nodeLinker: pnp, run with corepack yarn vite:
[Qa App|warn|vite|apps] Backend functions in a Yarn Plug'n'Play project run inside the dev server's process, one at a time, instead of each in a fresh process.
=== executeAction pnpProbe.plain
HTTP 500
{"success":false,"error":"Some options passed to require() aren't supported by PnP yet (conditions)"}

Blast Radius

  • Local development only: /__dd/executeAction on Vite 6.0.4+; builds, deploys and dev:verify are unchanged.
  • No feature flag. Medium risk: a function relying on module state or the developer's environment now fails locally, as in production; each call adds about 100-150 ms.

Out of Scope / Follow-ups

3 items deferred
Item Status Next step
Yarn Plug'n'Play projects stay in-process, where Vite 8's SSR loader fails as before this PR. Deferred Forward Yarn's hooks to the child; document the limit.
A call in a synchronous loop outlives a dev server killed without closing, such as by kill -9. Deferred Add a watcher process if this shows up.
Output isn't bounded: lines wait for a newline, and a slow terminal queues output. Deferred Split long lines and handle backpressure.

Documentation

🤖 Generated with Claude Code

@github-actions

github-actions Bot commented Oct 21, 2026 •

Copy link
Copy Markdown

Live Debugger Runtime Benchmark

SDK-loaded dormant-probe runtime overhead, measured against an uninstrumented bundle in the same browser session.

Browser Workload Quality Per-call overhead upper
chrome Hot clean <= 3.63 ns
chrome Tiny clean <= 0.01 ns
firefox Hot clean <= 7.64 ns
firefox Tiny clean <= 0.14 ns
safari Hot clean <= 4.08 ns
safari Tiny clean <= 0.76 ns

What do the Tiny and Hot workloads represent?

Browser Debugger SDK: 7.16.1 · built 2026-10-21 · S3 ETag 1426183a

Full diagnostics
browser  workload  quality  per-call overhead upper  overhead upper          95% CI         A/A diag        block CI  acf(1)   baseline  instrumented                         samples
-------  --------  -------  -----------------------  --------------  --------------  ---------------  --------------  ------  ---------  ------------  ------------------------------
chrome   Hot       clean                 <= 3.63 ns        <= 8.40%   3.58..3.61 ns   -0.01..0.00 ns   3.56..3.63 ns    0.37  50.965 ms     55.200 ms   102 (trim 20%, outliers 1.0%)
chrome   Tiny      clean                 <= 0.01 ns        <= 0.20%  -0.07..0.01 ns   -0.02..0.07 ns  -0.10..0.01 ns    0.06  50.883 ms     50.835 ms  102 (trim 20%, outliers 17.6%)
firefox  Hot       clean                 <= 7.64 ns       <= 35.84%   7.63..7.64 ns  -0.01..-0.00 ns   7.63..7.64 ns    0.14  40.760 ms     55.360 ms   102 (trim 20%, outliers 9.8%)
firefox  Tiny      clean                 <= 0.14 ns        <= 1.20%   0.14..0.14 ns   -0.00..0.00 ns   0.14..0.14 ns   -0.05  50.800 ms     51.400 ms   102 (trim 20%, outliers 6.9%)
safari   Hot       clean                 <= 4.08 ns       <= 19.35%   4.07..4.08 ns   -0.00..0.00 ns   4.07..4.08 ns   -0.17  46.380 ms     55.360 ms   102 (trim 20%, outliers 2.9%)
safari   Tiny      clean                 <= 0.76 ns       <= 23.13%   0.74..0.76 ns   -0.00..0.00 ns   0.74..0.76 ns    0.08  45.680 ms     56.110 ms   102 (trim 20%, outliers 2.0%)

Raw samples are in the live-debugger-runtime-bench-results artifact.

@tyffical
tyffical force-pushed the tiffany.trinh/apps-2961-child-process-execution branch from b0d7fe4 to 5bb05d3 Compare October 21, 2026 16:04
@tyffical
tyffical changed the base branch from tiffany.trinh/apps-2961-vite-version-matrix to tiffany.trinh/apps-2961-shared-execution-helpers October 21, 2026 16:04
@tyffical
tyffical force-pushed the tiffany.trinh/apps-2961-child-process-execution branch from 5bb05d3 to 4c9b050 Compare October 21, 2026 19:40
@tyffical
tyffical requested a balanced review from Copilot October 21, 2026 20:07
@tyffical

Copy link
Copy Markdown
Contributor Author

@codex review

@tyffical
tyffical marked this pull request as ready for review October 21, 2026 20:07
@tyffical
tyffical requested review from a team as code owners October 21, 2026 20:07
@tyffical
tyffical requested review from sdkennedy2 and removed request for a team October 21, 2026 20:07
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Oct 21, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-10-22T18:52:17.972637Z 4d1fb34 Manual request
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@tyffical
tyffical added this pull request to stack #612 October 21, 2026 20:09
@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. 🎉

Reviewed commit: 4c9b050140

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Copilot AI 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.

🟡 Changes recommended

Shutdown cleanup can leave children running, and linked-package dependencies outside the app’s workspace can be incorrectly blocked.

2 open issues
What changed in this PR

Moves Vite 6+ local backend calls into fresh child processes, improving production parity while retaining Vite 5’s in-process fallback. Parallel execution remains a follow-up.

Changes:

  • Adds process isolation, permissions, timeouts, and streamed logs.
  • Routes module loading and authenticated actions through the dev server.
  • Adds integration coverage, fixtures, packaging, and documentation.
File Description
yarn.lock Locks new matching dependencies.
packages/​tests/​src/​_jest/​fixtures/​apps_backend_project/​childVirtual.backend.ts Tests virtual module imports.
packages/​tests/​src/​_jest/​fixtures/​apps_backend_project/​childTopLevelWriteHelper.ts Probes load-time writes.
packages/​tests/​src/​_jest/​fixtures/​apps_backend_project/​childTopLevelWrite.backend.ts Exercises load-time guards.
packages/​tests/​src/​_jest/​fixtures/​apps_backend_project/​childState.backend.ts Exercises isolation, limits, and logs.
packages/​tests/​src/​_jest/​fixtures/​apps_backend_project/​childSlowLoad.backend.ts Exercises loading timeouts.
packages/​tests/​src/​_jest/​fixtures/​apps_backend_project/​childSandboxProbes.ts Probes permissions and module requests.
packages/​tests/​src/​_jest/​fixtures/​apps_backend_project/​childSandbox.backend.ts Exposes sandbox test functions.
packages/​tests/​src/​_jest/​fixtures/​apps_backend_project/​childLoadTime.backend.ts Exercises runtime-context initialization.
packages/​tests/​src/​_jest/​fixtures/​apps_backend_project/​childLaterImport.backend.ts Exercises missing imports.
packages/​tests/​src/​_jest/​fixtures/​apps_backend_project/​childActions.backend.ts Exercises action validation.
packages/​plugins/​apps/​src/​vite/​local-execution.ts Exports the shared execution queue.
packages/​plugins/​apps/​src/​vite/​local-execution-protocol.ts Defines parent-child messages.
packages/​plugins/​apps/​src/​vite/​local-execution-process.ts Implements child orchestration and checks.
packages/​plugins/​apps/​src/​vite/​local-execution-process.test.ts Covers child execution behavior.
packages/​plugins/​apps/​src/​vite/​local-execution-process.fixtures.ts Builds the test child entry.
packages/​plugins/​apps/​src/​vite/​local-execution-child.ts Executes functions and forwards actions.
packages/​plugins/​apps/​src/​vite/​local-execution-child-path.ts Locates the bundled child entry.
packages/​plugins/​apps/​src/​vite/​index.ts Selects and configures the executor.
packages/​plugins/​apps/​src/​vite/​index.test.ts Tests executor selection and fallback.
packages/​plugins/​apps/​src/​vite/​dev-server.ts Delegates local calls to the executor.
packages/​plugins/​apps/​src/​vite/​dev-server.test.ts Tests executor delegation.
packages/​plugins/​apps/​src/​vite/​dev-server.integration.test.ts Verifies fresh processes through middleware.
packages/​plugins/​apps/​src/​vite/​dev-server-module-graph.ts Makes stale-module uncaching configurable.
packages/​plugins/​apps/​src/​vite/​dev-server-module-graph.test.ts Tests child-mode cache handling.
packages/​plugins/​apps/​README.md Documents isolation, limits, and debugging.
packages/​plugins/​apps/​package.json Adds child packaging and matching dependencies.

Review effort: Balanced

Comment thread packages/plugins/apps/src/vite/local-execution-process.ts
Comment thread packages/plugins/apps/src/vite/local-execution-process.ts Outdated
@tyffical
tyffical requested a review from Scott-Meyer October 21, 2026 20:17
@tyffical
tyffical force-pushed the tiffany.trinh/apps-2961-child-process-execution branch 2 times, most recently from aa5b6d7 to ba6b653 Compare October 22, 2026 02:01
@tyffical
tyffical requested a balanced review from Copilot October 22, 2026 06:08
@tyffical

Copy link
Copy Markdown
Contributor Author

@codex review

Copilot AI 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.

🔵 Needs a closer look

The sandbox and process lifecycle changes warrant human review, and unbounded output buffering remains an unresolved dev-server reliability risk.

1 open issue
2 resolved since last review

Review effort: Balanced

Comment thread packages/plugins/apps/src/vite/local-execution-process.ts Outdated

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: ba6b65359a

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread packages/plugins/apps/src/vite/local-execution-process.ts
@tyffical

Copy link
Copy Markdown
Contributor Author

@codex review

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. Delightful!

Reviewed commit: 4d1fb347dc

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Copilot AI 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.

🔵 Needs a closer look

Unresolved lifecycle and output-buffering defects, together with the security-sensitive process boundary, require fixes and final human validation.

2 open issues
1 resolved since last review

Review effort: Balanced

Comment on lines +139 to +140
env,
stdio: ['ignore', 'ignore', 'ignore', 'ipc'],

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

The reaper process is no longer in this PR, so this doesn't apply. A call stuck in a synchronous loop after its dev server dies without closing is listed under Out of Scope.

}

const writeToTerminal = (stream: OutputStream, line: string) => {
process[stream].write(`${line}\n`);

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

The 64 KiB cap is no longer in this PR, but forwarding still doesn't apply backpressure. That's tracked under Out of Scope instead of fixed here.

On Vite 6.0.4+, /__dd/executeAction forks a Node process per call, running
a Vite module runner that loads modules from the dev server over IPC. Each
call starts with fresh module state. Calls still run one at a time.

Each process runs under the in-process guards, which refuse writes outside
its temp dir and /tmp, subprocesses and worker threads. Its heap is capped
at 256 MB and its environment holds no developer variables or keys.
$.Actions calls go back to the dev server, which keeps the credentials and
checks each call's connection. Output streams to the terminal line by line.
A call past its time budget is killed, even in a synchronous loop, and the
dev server kills its calls when it closes, restarts or exits.

Vite 5, Vite 6.0.0 to 6.0.3, whose dev server answers the module runner in
an older shape, and Yarn Plug'n'Play projects, whose packages load through
hooks the call process doesn't get, keep the in-process path.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@tyffical
tyffical force-pushed the tiffany.trinh/apps-2961-child-process-execution branch from 4d1fb34 to 4384bc9 Compare October 23, 2026 13:04
@tyffical
tyffical removed this pull request from stack #612 October 23, 2026 13:10
@tyffical
tyffical added this pull request to stack #618 October 23, 2026 13:11
@tyffical tyffical changed the title [APPS-2961] Update: run each local backend function call in a fresh sandboxed process [APPS-2961] Update: run each local backend function call in a fresh process Oct 23, 2026
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