[GH-ISSUE #3867] [Bug] analyze_javascript_application hides its worker process and temporary file writes #1486

Closed
opened 2026-10-16 21:06:05 +00:00 by bsakaguchi · 0 comments
Owner

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

Before submitting

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

Area

MCP or agent integration

Observed behavior

The MCP catalog and binary_session describe analyze_javascript_application as a session-only operation (openWorldHint: false; effects mutatesSession, idempotent). A successful analysis of a single JavaScript file actually launches REA's Node analysis worker and writes a staged .source file, so the advertised launchesProcess and writesFilesystem flags are wrong.

tools/list advertises openWorldHint: false, and binary_session lists only mutatesSession and idempotent for the tool. The same session's successful analysis of one inert JavaScript file launched the javascriptAnalysisWorker process and wrote a .source staging file.

Reproduction

Run from the root of a built source checkout. The preload only records calls to Node's child_process.spawn and fs/promises.writeFile and forwards them unchanged; the worker strips NODE_OPTIONS before launch (src/javascript/analysis/JavaScriptAnalysisWorker.ts:462), so the analysis itself runs without the preload.

repo="$PWD"
work="$(mktemp -d)"
mkdir "$work/fixture"
cat > "$work/fixture/main.js" <<'JS'
export function add(a, b) { return a + b; }
JS
cat > "$work/preload.cjs" <<'JS'
const cp = require('node:child_process');
const fs = require('node:fs');
const fsp = require('node:fs/promises');
const { syncBuiltinESMExports } = require('node:module');
const record = value => fs.appendFileSync(process.env.PROBE_LOG, JSON.stringify(value) + '\n');
const spawn = cp.spawn;
cp.spawn = function(command, args, options) { record({kind:'spawn', command, args}); return spawn.apply(this, arguments); };
const writeFile = fsp.writeFile;
fsp.writeFile = function(path, data, options) { record({kind:'write', path:String(path)}); return writeFile.apply(this, arguments); };
syncBuiltinESMExports();
JS
cat > "$work/probe.mjs" <<'JS'
import { spawn } from 'node:child_process';
import { readFileSync } from 'node:fs';
import { createInterface } from 'node:readline';
const [repo, work] = process.argv.slice(2);
const child = spawn(process.execPath, [repo+'/scripts/rea.mjs','mcp'], {
 env:{...process.env,HOME:work, NODE_OPTIONS:'--require '+work+'/preload.cjs',PROBE_LOG:work+'/events.jsonl'},stdio:['pipe','pipe','pipe']
});
const pending = new Map(); let id=0; let stderr='';
child.stderr.on('data',b=>stderr+=b);
createInterface({input:child.stdout}).on('line', line=>{try{const m=JSON.parse(line);if(m.id!==undefined){pending.get(m.id)?.(m);pending.delete(m.id)}}catch{}});
const request = (method,params={}) => new Promise((resolve)=>{const requestId=++id;pending.set(requestId,resolve);child.stdin.write(JSON.stringify({jsonrpc:'2.0',id:requestId,method,params})+'\n')});
const timer=setTimeout(()=>{console.error('timeout',stderr);child.kill('SIGTERM');process.exitCode=1},45000);
try {
 await request('initialize',{protocolVersion:'2025-06-18',capabilities:{},clientInfo:{name:'local-contract-probe',version:'1'}});
 child.stdin.write(JSON.stringify({jsonrpc:'2.0',method:'notifications/initialized'})+'\n');
 const list=await request('tools/list');
 const tool=list.result.tools.find(t=>t.name==='analyze_javascript_application');
 console.log('advertised annotations', JSON.stringify(tool.annotations));
 const status=await request('tools/call',{name:'binary_session',arguments:{}});
 const group=status.result?.structuredContent?.result?.tool_availability?.groups?.find(g=>g.names.includes(tool.name));
 console.log('advertised effect flags',JSON.stringify(group?.effects));
 const result=await request('tools/call',{name:tool.name,arguments:{input_path:work+'/fixture',detail:'summary'}});
 console.log('analysis isError', result.result?.isError ?? false);
 console.log('analysis operation', result.result?.structuredContent?.operation ?? result.error ?? result.result?.content);
 const events=readFileSync(work+'/events.jsonl','utf8').trim().split('\n').map(JSON.parse);
 console.log('worker spawn observed',events.some(e=>e.kind==='spawn' && e.args?.some(a=>String(a).includes('javascriptAnalysisWorker'))));
 console.log('source staging write observed',events.some(e=>e.kind==='write' && e.path.endsWith('.source')));
} finally { clearTimeout(timer); child.stdin.end(); child.kill('SIGTERM'); }
JS
node "$work/probe.mjs" "$repo" "$work"

Expected behavior

The advertised effects include launchesProcess: true and writesFilesystem: true, and the derived annotation is openWorldHint: true. src/contracts/toolEffects.ts:1 defines effect flags as possible effects across supported inputs, "including temporary work". docs/mcp-contracts.md:482-486 says binary_session carries each flag separately and that owned temporary processes and directories belong in this record. The annotation mapping at src/contracts/toolEffects.ts:734 derives openWorldHint from launchesProcess || accessesNetwork. The worker is REA-owned code, so executesForeignCode is not in question.

Impact and scope

MCP clients and agents that gate tools on advertised effects receive incorrect metadata for every JavaScript application analysis and cannot learn that it needs a child process and writable temporary storage; the analysis result itself is correct.

Workaround: available. Client authors can manually treat this tool as launching a process and writing temporary files; no MCP option corrects the published metadata.

Reporter's impact estimate: P2 (supported).

REA version

6.5.0, built from source at main commit 034342ba28 (latest npm release is 6.5.0)

Runtime and platform

Node.js v26.10.0; macOS 27.0.1 arm64

Target and artifact context

Single-file JavaScript source directory

Interface

MCP

Earliest failure stage

Transport or result projection

Relevant output or evidence

  • Expected source: src/contracts/toolEffects.ts:1 (effects include temporary work) and docs/mcp-contracts.md:482-486
  • Failure source: observed MCP probe output below; preset assignment analyze_javascript_application: evidence at src/contracts/toolEffects.ts:650
  • Evidence provenance: observed
  • Local verification: reproduced
  • Reproduction completeness: complete
advertised annotations {"readOnlyHint":false,"destructiveHint":false,"idempotentHint":true,"openWorldHint":false}
advertised effect flags ["mutatesSession","idempotent"]
analysis isError false
analysis operation inspect_analysis_view
worker spawn observed true
source staging write observed true

The inspect_analysis_view operation is expected for detail: "summary": the handler completes the analysis, then projects its summary. A direct import of the built contract also showed writesFilesystem: false and launchesProcess: false, so the error is in the canonical contract, not only the transport rendering. Normal cleanup removed the staged file.

Source-derived analysis: the evidence preset (src/contracts/toolEffects.ts:32-46) sets only mutatesSession: true. The production path reaches the worker through src/server/registerElectronTools.ts:99-115, src/application/javascript/JavaScriptApplicationService.ts:64, src/application/javascript/JavaScriptArtifactReconstruction.ts:150-168, and src/artifacts/javascript/JavaScriptSourceAnalysis.ts:8-12. The worker creates its private runtime directory at src/javascript/analysis/JavaScriptAnalysisWorker.ts:455, launches the process at line 463, and writes the staged source at line 197.

Typed outcome or recovery expected

tools/list and binary_session should advertise launchesProcess and writesFilesystem for analyze_javascript_application, with openWorldHint true.

Restoration check:

With the probe above, the advertised annotations line currently shows "openWorldHint":false and the effect flags omit launchesProcess and writesFilesystem; after a fix it should show "openWorldHint":true and the flags should include both, while worker spawn observed true and source staging write observed true remain unchanged.

Analysis or suspected cause

Source-derived: src/contracts/toolEffects.ts:650 assigns the evidence preset (lines 32-46, only mutatesSession); the worker creates a private runtime directory at src/javascript/analysis/JavaScriptAnalysisWorker.ts:455, launches at :463, and stages source at :197.

Additional context

Open #3322 reports a different effect omission for close_frida_session, and closed #1712 concerned attached IDA UI effects; neither covers this tool. No open issue reports this omission.

This report came from AI-assisted source review with local reproduction where stated above; the evidence labels distinguish observed runs from source-only reasoning.

Originally created by @coygeek on GitHub (Oct 15, 2026). Original GitHub issue: https://github.com/morluto/rea/issues/3867 ### 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 MCP or agent integration ### Observed behavior The MCP catalog and `binary_session` describe `analyze_javascript_application` as a session-only operation (`openWorldHint: false`; effects `mutatesSession`, `idempotent`). A successful analysis of a single JavaScript file actually launches REA's Node analysis worker and writes a staged `.source` file, so the advertised `launchesProcess` and `writesFilesystem` flags are wrong. `tools/list` advertises `openWorldHint: false`, and `binary_session` lists only `mutatesSession` and `idempotent` for the tool. The same session's successful analysis of one inert JavaScript file launched the `javascriptAnalysisWorker` process and wrote a `.source` staging file. ### Reproduction Run from the root of a built source checkout. The preload only records calls to Node's `child_process.spawn` and `fs/promises.writeFile` and forwards them unchanged; the worker strips `NODE_OPTIONS` before launch (`src/javascript/analysis/JavaScriptAnalysisWorker.ts:462`), so the analysis itself runs without the preload. ```sh repo="$PWD" work="$(mktemp -d)" mkdir "$work/fixture" cat > "$work/fixture/main.js" <<'JS' export function add(a, b) { return a + b; } JS cat > "$work/preload.cjs" <<'JS' const cp = require('node:child_process'); const fs = require('node:fs'); const fsp = require('node:fs/promises'); const { syncBuiltinESMExports } = require('node:module'); const record = value => fs.appendFileSync(process.env.PROBE_LOG, JSON.stringify(value) + '\n'); const spawn = cp.spawn; cp.spawn = function(command, args, options) { record({kind:'spawn', command, args}); return spawn.apply(this, arguments); }; const writeFile = fsp.writeFile; fsp.writeFile = function(path, data, options) { record({kind:'write', path:String(path)}); return writeFile.apply(this, arguments); }; syncBuiltinESMExports(); JS cat > "$work/probe.mjs" <<'JS' import { spawn } from 'node:child_process'; import { readFileSync } from 'node:fs'; import { createInterface } from 'node:readline'; const [repo, work] = process.argv.slice(2); const child = spawn(process.execPath, [repo+'/scripts/rea.mjs','mcp'], { env:{...process.env,HOME:work, NODE_OPTIONS:'--require '+work+'/preload.cjs',PROBE_LOG:work+'/events.jsonl'},stdio:['pipe','pipe','pipe'] }); const pending = new Map(); let id=0; let stderr=''; child.stderr.on('data',b=>stderr+=b); createInterface({input:child.stdout}).on('line', line=>{try{const m=JSON.parse(line);if(m.id!==undefined){pending.get(m.id)?.(m);pending.delete(m.id)}}catch{}}); const request = (method,params={}) => new Promise((resolve)=>{const requestId=++id;pending.set(requestId,resolve);child.stdin.write(JSON.stringify({jsonrpc:'2.0',id:requestId,method,params})+'\n')}); const timer=setTimeout(()=>{console.error('timeout',stderr);child.kill('SIGTERM');process.exitCode=1},45000); try { await request('initialize',{protocolVersion:'2025-06-18',capabilities:{},clientInfo:{name:'local-contract-probe',version:'1'}}); child.stdin.write(JSON.stringify({jsonrpc:'2.0',method:'notifications/initialized'})+'\n'); const list=await request('tools/list'); const tool=list.result.tools.find(t=>t.name==='analyze_javascript_application'); console.log('advertised annotations', JSON.stringify(tool.annotations)); const status=await request('tools/call',{name:'binary_session',arguments:{}}); const group=status.result?.structuredContent?.result?.tool_availability?.groups?.find(g=>g.names.includes(tool.name)); console.log('advertised effect flags',JSON.stringify(group?.effects)); const result=await request('tools/call',{name:tool.name,arguments:{input_path:work+'/fixture',detail:'summary'}}); console.log('analysis isError', result.result?.isError ?? false); console.log('analysis operation', result.result?.structuredContent?.operation ?? result.error ?? result.result?.content); const events=readFileSync(work+'/events.jsonl','utf8').trim().split('\n').map(JSON.parse); console.log('worker spawn observed',events.some(e=>e.kind==='spawn' && e.args?.some(a=>String(a).includes('javascriptAnalysisWorker')))); console.log('source staging write observed',events.some(e=>e.kind==='write' && e.path.endsWith('.source'))); } finally { clearTimeout(timer); child.stdin.end(); child.kill('SIGTERM'); } JS node "$work/probe.mjs" "$repo" "$work" ``` ### Expected behavior The advertised effects include `launchesProcess: true` and `writesFilesystem: true`, and the derived annotation is `openWorldHint: true`. `src/contracts/toolEffects.ts:1` defines effect flags as possible effects across supported inputs, "including temporary work". `docs/mcp-contracts.md:482-486` says `binary_session` carries each flag separately and that owned temporary processes and directories belong in this record. The annotation mapping at `src/contracts/toolEffects.ts:734` derives `openWorldHint` from `launchesProcess || accessesNetwork`. The worker is REA-owned code, so `executesForeignCode` is not in question. ### Impact and scope MCP clients and agents that gate tools on advertised effects receive incorrect metadata for every JavaScript application analysis and cannot learn that it needs a child process and writable temporary storage; the analysis result itself is correct. Workaround: available. Client authors can manually treat this tool as launching a process and writing temporary files; no MCP option corrects the published metadata. Reporter's impact estimate: P2 (supported). ### REA version 6.5.0, built from source at main commit 034342ba28c8e56c3746be92b78aa7299afa02bd (latest npm release is 6.5.0) ### Runtime and platform Node.js v26.10.0; macOS 27.0.1 arm64 ### Target and artifact context Single-file JavaScript source directory ### Interface MCP ### Earliest failure stage Transport or result projection ### Relevant output or evidence - Expected source: `src/contracts/toolEffects.ts:1` (effects include temporary work) and `docs/mcp-contracts.md:482-486` - Failure source: observed MCP probe output below; preset assignment `analyze_javascript_application: evidence` at `src/contracts/toolEffects.ts:650` - Evidence provenance: observed - Local verification: reproduced - Reproduction completeness: complete ```text advertised annotations {"readOnlyHint":false,"destructiveHint":false,"idempotentHint":true,"openWorldHint":false} advertised effect flags ["mutatesSession","idempotent"] analysis isError false analysis operation inspect_analysis_view worker spawn observed true source staging write observed true ``` The `inspect_analysis_view` operation is expected for `detail: "summary"`: the handler completes the analysis, then projects its summary. A direct import of the built contract also showed `writesFilesystem: false` and `launchesProcess: false`, so the error is in the canonical contract, not only the transport rendering. Normal cleanup removed the staged file. Source-derived analysis: the `evidence` preset (`src/contracts/toolEffects.ts:32-46`) sets only `mutatesSession: true`. The production path reaches the worker through `src/server/registerElectronTools.ts:99-115`, `src/application/javascript/JavaScriptApplicationService.ts:64`, `src/application/javascript/JavaScriptArtifactReconstruction.ts:150-168`, and `src/artifacts/javascript/JavaScriptSourceAnalysis.ts:8-12`. The worker creates its private runtime directory at `src/javascript/analysis/JavaScriptAnalysisWorker.ts:455`, launches the process at line 463, and writes the staged source at line 197. ### Typed outcome or recovery expected tools/list and binary_session should advertise launchesProcess and writesFilesystem for analyze_javascript_application, with openWorldHint true. Restoration check: With the probe above, the `advertised annotations` line currently shows `"openWorldHint":false` and the effect flags omit `launchesProcess` and `writesFilesystem`; after a fix it should show `"openWorldHint":true` and the flags should include both, while `worker spawn observed true` and `source staging write observed true` remain unchanged. ### Analysis or suspected cause Source-derived: src/contracts/toolEffects.ts:650 assigns the evidence preset (lines 32-46, only mutatesSession); the worker creates a private runtime directory at src/javascript/analysis/JavaScriptAnalysisWorker.ts:455, launches at :463, and stages source at :197. ### Additional context Open #3322 reports a different effect omission for `close_frida_session`, and closed #1712 concerned attached IDA UI effects; neither covers this tool. No open issue reports this omission. This report came from AI-assisted source review with local reproduction where stated above; the evidence labels distinguish observed runs from source-only reasoning.
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#1486
No description provided.