[GH-ISSUE #6208] [Bug] Second-argument utilityProcess.fork options lose the service name #3778

Closed
opened 2026-10-31 00:43:58 +00:00 by bsakaguchi · 0 comments
Owner

Originally created by @coygeek on GitHub (Oct 22, 2026).
Original GitHub issue: https://github.com/morluto/rea/issues/6208

Originally assigned to: @kaoru0822-kitauji on GitHub.

Before submitting

  • I searched the open and closed issues for a related report.
  • I removed credentials, tokens, private paths, and sensitive target data.

Area

Evidence, artifacts, or reconstruction

Observed behavior

analyze_javascript_application succeeds on a local Electron source fixture containing utilityProcess.fork("./worker.js", { serviceName: "worker-gpu" }), but its electron-utility node's utility-fork observation has service_name: null, exact confidence and complete coverage. Passing [] before the same options returns the literal name. This was observed through the registered MCP tool; the source was parsed as inert data and Electron was not launched.

Reproduction

After building the baseline checkout once with npm run build:cached, save the test below as tests/boundary/mcp/utility-options.repro.test.ts. It creates inert application directories and sends real analyze_javascript_application requests through the registered server using the SDK in-memory transport. The native-provider factory throws if called. The verifier ran this test with a temporary filename; the portable filename below replaces it.

import { mkdir, rm, writeFile } from "node:fs/promises";
import { join } from "node:path";
import { Client, InMemoryTransport } from "@modelcontextprotocol/client";
import { expect, it } from "vitest";
import { createServer } from "../../../src/server/createServer.js";
import { analyzeJavaScriptStaticSource } from "../../../src/domain/javascript/javascriptStaticAnalysis.js";
import { createTestBinarySession } from "../../fixtures/binarySession.js";

it("compares second-argument utility options with explicit args and omitted options", async () => {
  const base = process.env.VERIFY_SCRATCH!;
  const session = createTestBinarySession(() => { throw new Error("unexpected native provider"); });
  const server = createServer({ kind: "session", session });
  const client = new Client({ name: "utility-options-repro", version: "1" });
  const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair();
  const forms = [
    ["second", '{ serviceName: "worker-gpu" }'],
    ["third", '[], { serviceName: "worker-gpu" }'],
    ["omitted", ""],
  ];
  try {
    await server.connect(serverTransport);
    await client.connect(clientTransport);
    const reports = [];
    for (const [name, args] of forms) {
      const root = join(base, name);
      await mkdir(root, { recursive: true });
      const source = `const { app, utilityProcess } = require("electron");
app.whenReady().then(() => { utilityProcess.fork("./worker.js"${args ? `, ${args}` : ""}); });`;
      await writeFile(join(root, "main.js"), source);
      await writeFile(join(root, "worker.js"), "module.exports = {};\n");
      await writeFile(join(root, "package.json"), JSON.stringify({ name: "utility-fixture", main: "main.js" }));
      const response = await client.callTool({ name: "analyze_javascript_application", arguments: { input_path: root } });
      expect(response.isError, JSON.stringify(response)).not.toBe(true);
      const result = (response.structuredContent as any).normalized_result;
      const utilities = result.graph.nodes.filter((n: any) => n.kind === "electron-utility");
      expect(utilities).toHaveLength(1);
      const direct = analyzeJavaScriptStaticSource(source);
      reports.push({ form: name, utilities, parse_status: direct.parse_status, findings: direct.electron.utility_processes });
    }
    console.log(JSON.stringify(reports));
    expect(reports[1].findings[0].service_name).toBe("worker-gpu");
    expect(reports[2].findings[0].service_name).toBe(null);
    expect(reports[0].findings[0].service_name).toBe("worker-gpu");
  } finally {
    await client.close();
    await server.close();
    await session.close();
    for (const [name] of forms) await rm(join(base, name), { recursive: true, force: true });
  }
});

Run from the repository root with isolated configuration:

proof_root=$(mktemp -d)
mkdir -p "$proof_root/home" "$proof_root/dsh" "$proof_root/config" "$proof_root/cache"
HOME="$proof_root/home" DSH_HOME="$proof_root/dsh" XDG_CONFIG_HOME="$proof_root/config" XDG_CACHE_HOME="$proof_root/cache" VERIFY_SCRATCH="$proof_root" node node_modules/vitest/vitest.mjs run tests/boundary/mcp/utility-options.repro.test.ts --maxWorkers=1

One completed post-build run reproduced the problem: the second-argument case returned a null service name, while the explicit-argument-array and omitted-options controls behaved as expected. The test prints the MCP utility-node observations and separately checks source-analysis parse status before failing its expected service-name assertion. The first pre-build attempt stopped before analysis because the compiled worker was missing; building resolved that prerequisite.

Expected behavior

Preserve the declared service name when the optional argument array is omitted, as with the explicit-array control. Electron's v43.0.0 implementation treats a non-array object in argument two as options. REA's unknowns contract requires unresolved options to remain unknown with partial coverage; if unsupported, this syntax should carry that limitation instead of complete exact coverage.

Impact and scope

P2: a bounded loss of declared process-name evidence can mislead static reconstruction. The node, entrypoint and source range remain available, so manually inspecting the call site is a safe workaround. One completed post-build run reproduced this case; the explicit-array and omitted-options controls behaved as expected. Incidence in real applications and downstream use are NOT_ENOUGH_INFO: no application corpus or analyst workflow was measured. No security boundary crossing or runtime process effect was established.

REA version

Source commit c465ff5a81 (main)

Runtime and platform

Node.js v26.12.0; macOS 27.0.1; arm64. The repository pins Node 24.18.0; that pinned runtime was not rerun.

Target and artifact context

Authored local JavaScript application directory containing package.json, main.js and worker.js; recognized Electron import and fork inside app.whenReady(). No live Electron process or binary-analysis provider.

Interface

MCP

Earliest failure stage

Native analysis or reconstruction

Relevant output or evidence

The MCP response's graph.nodes[] utility node contains an observation with extractor operation observe-utility-process-fork. Selecting that observation, the three requests returned:

{"form":"second","label":"./worker.js","module_path":"./worker.js","service_name":null,"confidence":"exact","coverage":"complete","limitations":[]}
{"form":"third","label":"worker-gpu","module_path":"./worker.js","service_name":"worker-gpu","confidence":"exact","coverage":"complete","limitations":[]}
{"form":"omitted","label":"./worker.js","module_path":"./worker.js","service_name":null,"confidence":"exact","coverage":"complete","limitations":[]}

These are compact projections of the printed MCP node observations. Separate direct source analysis returned parse_status: "complete" for all three. The focused regression failed with expected "worker-gpu", received null. The single build completed successfully before this run.

Typed outcome or recovery expected

Return the literal service name for the recognized options form, or explicitly retain unknown/partial coverage if the form cannot be interpreted. Genuinely omitted options should continue to produce no declared name.

Analysis or suspected cause

Source-derived explanation: inspectUtilityProcess reads only the third argument, so the second-position object is skipped. staticObjectProperty classifies the absent third argument as missing rather than dynamic. That leaves the unknown counter unchanged, and parse-status finalization reports complete parsing. The utility graph projection publishes the null service name unchanged. No implementation choice is prescribed.

Additional context

Related #710 handles computed option keys and overrides; #2742 handles runtime entrypoint paths; #3480 handles exact static property keys; #1391 and #1673 handle Electron binding/call identity; #1237 handles stale preload associations; #375 requests runtime lifecycle capture. None describes second-argument options losing the service name. Searches included open and closed issues and PRs with utilityProcess fork, service_name serviceName, electronStaticAnalysisBrowser, "utilityProcess", "serviceName", and "utility" "second".

Fetched upstream main at 719d56ebfeff78acf560d759326d5ba9d20e273e: no later commits modify the cited extractor, graph projection, source parse-status finalization or option tests, and the extractor still reads only the third argument. This freshness check is source-derived; the current-main runtime was not run. Last known good version is NOT_ENOUGH_INFO: no historical regression boundary was established.

Originally created by @coygeek on GitHub (Oct 22, 2026). Original GitHub issue: https://github.com/morluto/rea/issues/6208 Originally assigned to: @kaoru0822-kitauji on GitHub. ### Before submitting - [x] I searched the open and closed issues for a related report. - [x] I removed credentials, tokens, private paths, and sensitive target data. ### Area Evidence, artifacts, or reconstruction ### Observed behavior `analyze_javascript_application` succeeds on a local Electron source fixture containing `utilityProcess.fork("./worker.js", { serviceName: "worker-gpu" })`, but its `electron-utility` node's utility-fork observation has `service_name: null`, exact confidence and complete coverage. Passing `[]` before the same options returns the literal name. This was observed through the registered MCP tool; the source was parsed as inert data and Electron was not launched. ### Reproduction After building the baseline checkout once with `npm run build:cached`, save the test below as `tests/boundary/mcp/utility-options.repro.test.ts`. It creates inert application directories and sends real `analyze_javascript_application` requests through the registered server using the SDK in-memory transport. The native-provider factory throws if called. The verifier ran this test with a temporary filename; the portable filename below replaces it. ```ts import { mkdir, rm, writeFile } from "node:fs/promises"; import { join } from "node:path"; import { Client, InMemoryTransport } from "@modelcontextprotocol/client"; import { expect, it } from "vitest"; import { createServer } from "../../../src/server/createServer.js"; import { analyzeJavaScriptStaticSource } from "../../../src/domain/javascript/javascriptStaticAnalysis.js"; import { createTestBinarySession } from "../../fixtures/binarySession.js"; it("compares second-argument utility options with explicit args and omitted options", async () => { const base = process.env.VERIFY_SCRATCH!; const session = createTestBinarySession(() => { throw new Error("unexpected native provider"); }); const server = createServer({ kind: "session", session }); const client = new Client({ name: "utility-options-repro", version: "1" }); const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair(); const forms = [ ["second", '{ serviceName: "worker-gpu" }'], ["third", '[], { serviceName: "worker-gpu" }'], ["omitted", ""], ]; try { await server.connect(serverTransport); await client.connect(clientTransport); const reports = []; for (const [name, args] of forms) { const root = join(base, name); await mkdir(root, { recursive: true }); const source = `const { app, utilityProcess } = require("electron"); app.whenReady().then(() => { utilityProcess.fork("./worker.js"${args ? `, ${args}` : ""}); });`; await writeFile(join(root, "main.js"), source); await writeFile(join(root, "worker.js"), "module.exports = {};\n"); await writeFile(join(root, "package.json"), JSON.stringify({ name: "utility-fixture", main: "main.js" })); const response = await client.callTool({ name: "analyze_javascript_application", arguments: { input_path: root } }); expect(response.isError, JSON.stringify(response)).not.toBe(true); const result = (response.structuredContent as any).normalized_result; const utilities = result.graph.nodes.filter((n: any) => n.kind === "electron-utility"); expect(utilities).toHaveLength(1); const direct = analyzeJavaScriptStaticSource(source); reports.push({ form: name, utilities, parse_status: direct.parse_status, findings: direct.electron.utility_processes }); } console.log(JSON.stringify(reports)); expect(reports[1].findings[0].service_name).toBe("worker-gpu"); expect(reports[2].findings[0].service_name).toBe(null); expect(reports[0].findings[0].service_name).toBe("worker-gpu"); } finally { await client.close(); await server.close(); await session.close(); for (const [name] of forms) await rm(join(base, name), { recursive: true, force: true }); } }); ``` Run from the repository root with isolated configuration: ```sh proof_root=$(mktemp -d) mkdir -p "$proof_root/home" "$proof_root/dsh" "$proof_root/config" "$proof_root/cache" HOME="$proof_root/home" DSH_HOME="$proof_root/dsh" XDG_CONFIG_HOME="$proof_root/config" XDG_CACHE_HOME="$proof_root/cache" VERIFY_SCRATCH="$proof_root" node node_modules/vitest/vitest.mjs run tests/boundary/mcp/utility-options.repro.test.ts --maxWorkers=1 ``` One completed post-build run reproduced the problem: the second-argument case returned a null service name, while the explicit-argument-array and omitted-options controls behaved as expected. The test prints the MCP utility-node observations and separately checks source-analysis parse status before failing its expected service-name assertion. The first pre-build attempt stopped before analysis because the compiled worker was missing; building resolved that prerequisite. ### Expected behavior Preserve the declared service name when the optional argument array is omitted, as with the explicit-array control. [Electron's v43.0.0 implementation](https://github.com/electron/electron/blob/v43.0.0/lib/browser/api/utility-process.ts#L18-L23) treats a non-array object in argument two as options. REA's [unknowns contract](https://github.com/morluto/rea/blob/c465ff5a814b8e1a386fc547cbcd94df5c35f086/docs/javascript-artifact-reconstruction.md#L353-L367) requires unresolved options to remain unknown with partial coverage; if unsupported, this syntax should carry that limitation instead of complete exact coverage. ### Impact and scope P2: a bounded loss of declared process-name evidence can mislead static reconstruction. The node, entrypoint and source range remain available, so manually inspecting the call site is a safe workaround. One completed post-build run reproduced this case; the explicit-array and omitted-options controls behaved as expected. Incidence in real applications and downstream use are NOT_ENOUGH_INFO: no application corpus or analyst workflow was measured. No security boundary crossing or runtime process effect was established. ### REA version Source commit c465ff5a814b8e1a386fc547cbcd94df5c35f086 (main) ### Runtime and platform Node.js v26.12.0; macOS 27.0.1; arm64. The repository pins Node 24.18.0; that pinned runtime was not rerun. ### Target and artifact context Authored local JavaScript application directory containing package.json, main.js and worker.js; recognized Electron import and fork inside app.whenReady(). No live Electron process or binary-analysis provider. ### Interface MCP ### Earliest failure stage Native analysis or reconstruction ### Relevant output or evidence The MCP response's `graph.nodes[]` utility node contains an observation with extractor operation `observe-utility-process-fork`. Selecting that observation, the three requests returned: ```json {"form":"second","label":"./worker.js","module_path":"./worker.js","service_name":null,"confidence":"exact","coverage":"complete","limitations":[]} {"form":"third","label":"worker-gpu","module_path":"./worker.js","service_name":"worker-gpu","confidence":"exact","coverage":"complete","limitations":[]} {"form":"omitted","label":"./worker.js","module_path":"./worker.js","service_name":null,"confidence":"exact","coverage":"complete","limitations":[]} ``` These are compact projections of the printed MCP node observations. Separate direct source analysis returned `parse_status: "complete"` for all three. The focused regression failed with expected `"worker-gpu"`, received `null`. The single build completed successfully before this run. ### Typed outcome or recovery expected Return the literal service name for the recognized options form, or explicitly retain unknown/partial coverage if the form cannot be interpreted. Genuinely omitted options should continue to produce no declared name. ### Analysis or suspected cause Source-derived explanation: [inspectUtilityProcess](https://github.com/morluto/rea/blob/c465ff5a814b8e1a386fc547cbcd94df5c35f086/src/domain/javascript/electronStaticAnalysisBrowser.ts#L307-L345) reads only the third argument, so the second-position object is skipped. [staticObjectProperty](https://github.com/morluto/rea/blob/c465ff5a814b8e1a386fc547cbcd94df5c35f086/src/domain/javascript/javascriptAstValues.ts#L207-L212) classifies the absent third argument as missing rather than dynamic. That leaves the unknown counter unchanged, and [parse-status finalization](https://github.com/morluto/rea/blob/c465ff5a814b8e1a386fc547cbcd94df5c35f086/src/domain/javascript/javascriptStaticAnalysis.ts#L201-L206) reports complete parsing. The [utility graph projection](https://github.com/morluto/rea/blob/c465ff5a814b8e1a386fc547cbcd94df5c35f086/src/application/javascript/ElectronBoundaryGraphWindows.ts#L270-L290) publishes the null service name unchanged. No implementation choice is prescribed. ### Additional context Related #710 handles computed option keys and overrides; #2742 handles runtime entrypoint paths; #3480 handles exact static property keys; #1391 and #1673 handle Electron binding/call identity; #1237 handles stale preload associations; #375 requests runtime lifecycle capture. None describes second-argument options losing the service name. Searches included open and closed issues and PRs with `utilityProcess fork`, `service_name serviceName`, `electronStaticAnalysisBrowser`, `"utilityProcess"`, `"serviceName"`, and `"utility" "second"`. Fetched upstream main at `719d56ebfeff78acf560d759326d5ba9d20e273e`: no later commits modify the cited extractor, graph projection, source parse-status finalization or option tests, and the extractor still reads only the third argument. This freshness check is source-derived; the current-main runtime was not run. Last known good version is NOT_ENOUGH_INFO: no historical regression boundary was established.
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
morluto/rea#3778
No description provided.