[GH-ISSUE #3431] [Feature] Model node:fs writes and synchronous child processes as semantic effects #1276

Open
opened 2026-10-16 21:03:40 +00:00 by bsakaguchi · 2 comments
Owner

Originally created by @MartOcd1709 on GitHub (Oct 14, 2026).
Original GitHub issue: https://github.com/morluto/rea/issues/3431

Before submitting

  • I searched the open and closed issues and pull requests for a related proposal.
  • I understand the contribution scope must be reviewed before integration; open questions are welcome here.
  • This request does not include credentials, private samples, or sensitive target data.

Area

Evidence, artifacts, or reconstruction (JavaScript application graph)

Current limitation

The semantic graph models node:fs reads (readFile/readFileSync → config-source + reads-config) and asynchronous child processes (spawn/exec/execFile/fork → child-process + spawns), but has no effect for:

  1. File writes: writeFile, writeFileSync, appendFile, appendFileSync, createWriteStream.
  2. Synchronous child processes: execSync, spawnSync, execFileSync.

Both produce only generic unknowns (dynamic-call, ambiguous-target) that look the same as any unresolved call. So "does this package write files or run commands?" can't be answered from the graph. For writes this matches the documented limitation ("Configuration extraction covers … node:fs reads"). I'm proposing the missing half.

Shortest reproduction (rea-agents 6.4.0, Linux x64, Node 22.22.1). package.json is {"name":"t","type":"module"}; index.js:

import { writeFileSync } from "node:fs";
import fs from "node:fs";
import { execSync } from "node:child_process";
writeFileSync("/tmp/a.txt", "x");
fs.writeFileSync("/tmp/b.txt", "x");
fs.appendFileSync(process.env.HOME + "/.bashrc", "x");
execSync("id");

rea analyze-javascript-application "$PWD" --json: the only effect node is config-source (HOME). The write and execSync calls appear only as unknowns, for example:

  • "Call target writeFileSync is imported from outside this source."
  • "Unsupported MemberExpression call target."
  • "Static read property /appendFileSync has unknown-coverage presence…"

Source:

  • configurationDetails() in javascriptSemanticDataEffects.ts maps only readFile/readFileSync.
  • CHILD_PROCESS_METHODS in javascriptSemanticChildProcesses.ts is async-only.
  • javascriptSemanticGraphSchemas.ts has no filesystem-mutation relation (writes is data-flow).

Related: open PR #2041 adds the sync methods and the fs write/append methods to BUILTIN_RECEIVER_METHODS for receiver validity. Its test retains asynchronous child effects alongside native %s asserts that cp.execSync(...) produces no child-process spawn. So after #2041, both behaviors stay intentionally effect-less. I'd like a maintainer decision on whether that should change.

Use cases

Observed on popular MCP servers. Static only: 35 npm MCP server packages, npm pack, never executed. Counts are regex call sites in shipped JS, spot-checked as real calls (not comments) versus semantic-graph effects.

  • File writes: 16 of 35 packages contain them (109 call sites), and none are recorded. Examples:
    • @modelcontextprotocol/server-filesystem 2025.8.21 (≈584k weekly downloads): the write_file implementation fs.writeFile(filePath, content, { flag: "wx" }) at dist/lib.js:185. The only related facts are acquires for fs.open/createReadStream handles.
    • @modelcontextprotocol/server-gdrive dist/index.js:154: fs.writeFileSync(credentialsPath, …).
  • Sync child processes: 15 standalone files from 6 packages, each analyzed alone because whole-package runs on large packages hit semantic_graph_file_node_ceiling. 0 of 15 are recovered on 6.4.0. Examples: chrome-devtools-mcp build/src/BrowserManager.js, @heroku/mcp-server dist/tools/deploy-to-heroku.js.

Supply-chain triage. The Nx "s1ngularity" compromise (Aug 2025) used fs.appendFileSync to append sudo shutdown -h 0 to ~/.bashrc/~/.zshrc, and used spawnSync to run local CLIs (StepSecurity write-up). On that pattern, today's graph shows the reads but not the write or the commands.

Desired outcome and success criteria

For the reproduction above:

  • One write-effect fact per fs write call: method, literal path when static (/tmp/a.txt, /tmp/b.txt), and partial resolution for the computed .bashrc path, linked by data flow to the HOME read.
  • One child-process fact for execSync("id") with method: "execSync" and command: "id", with no listener/signal linking, since sync calls return output rather than a handle.
  • Unknown or dynamic forms stay unknown. No secrecy inference.

Expected interaction with targets

Read existing files or captured data (static analysis only; nothing is executed).

Target and scope details

Scope questions:

  1. Writes: where should a file-mutation operation live? Options: a new filesystem family, or resource-lifecycle. Should rm/unlink/rename/chmod share it as a mutation kind, or stay out of scope?
  2. Sync processes: should they be child-process facts (as in the linked draft PR), or something separate, given #2041's current assertion?

Non-goals: runtime execution, obfuscation recovery, bundler __require resolution (a separate gap I can file).

Verification available so far

I have a prototype or observed results

Verification details

Draft PR (sync child processes only), linked below:

  • New test fails on main and passes on the branch.
  • src/domain/javascript + src/application/javascript: 1931/1931 pass on current main.
  • Per-file sync recovery on the 15 real files: 0 → 11 of 15. The 4 remaining use await import("child_process") or class-field aliases, which async methods miss too.

There is no write prototype yet. I'll wait for the scope decision. Measurement scripts and per-package TSV are available on request.

Alternatives considered

  • acquires covers fs.open handles only, not whole-file writes, and doesn't say whether a handle reads or writes.
  • Generic unknowns can't separate a known Node built-in write from any unresolved call.
  • Text grep works, but loses REA's provenance and data flow.

Possible direction

  • Writes: a JavaScriptSemanticFileMutation { method, path | null, flag | null, resolution } operation next to configuration operations, plus a relation (e.g. writes-file) with data-flow edges from path/content arguments.
  • Sync processes: extend CHILD_PROCESS_METHODS, and exclude sync results from interaction linking (as in the draft).

References

Originally created by @MartOcd1709 on GitHub (Oct 14, 2026). Original GitHub issue: https://github.com/morluto/rea/issues/3431 ### Before submitting - [x] I searched the open and closed issues and pull requests for a related proposal. - [x] I understand the contribution scope must be reviewed before integration; open questions are welcome here. - [x] This request does not include credentials, private samples, or sensitive target data. ### Area Evidence, artifacts, or reconstruction (JavaScript application graph) ### Current limitation The semantic graph models `node:fs` **reads** (`readFile`/`readFileSync` → `config-source` + `reads-config`) and **asynchronous** child processes (`spawn`/`exec`/`execFile`/`fork` → `child-process` + `spawns`), but has no effect for: 1. **File writes**: `writeFile`, `writeFileSync`, `appendFile`, `appendFileSync`, `createWriteStream`. 2. **Synchronous child processes**: `execSync`, `spawnSync`, `execFileSync`. Both produce only generic unknowns (`dynamic-call`, `ambiguous-target`) that look the same as any unresolved call. So "does this package write files or run commands?" can't be answered from the graph. For writes this matches the documented limitation ("Configuration extraction covers … node:fs reads"). I'm proposing the missing half. **Shortest reproduction** (rea-agents 6.4.0, Linux x64, Node 22.22.1). `package.json` is `{"name":"t","type":"module"}`; `index.js`: ```js import { writeFileSync } from "node:fs"; import fs from "node:fs"; import { execSync } from "node:child_process"; writeFileSync("/tmp/a.txt", "x"); fs.writeFileSync("/tmp/b.txt", "x"); fs.appendFileSync(process.env.HOME + "/.bashrc", "x"); execSync("id"); ``` `rea analyze-javascript-application "$PWD" --json`: the only effect node is `config-source` (`HOME`). The write and `execSync` calls appear only as unknowns, for example: - "Call target writeFileSync is imported from outside this source." - "Unsupported MemberExpression call target." - "Static read property /appendFileSync has unknown-coverage presence…" Source: - `configurationDetails()` in `javascriptSemanticDataEffects.ts` maps only `readFile`/`readFileSync`. - `CHILD_PROCESS_METHODS` in `javascriptSemanticChildProcesses.ts` is async-only. - `javascriptSemanticGraphSchemas.ts` has no filesystem-mutation relation (`writes` is data-flow). **Related:** open PR #2041 adds the sync methods and the fs write/append methods to `BUILTIN_RECEIVER_METHODS` for receiver validity. Its test `retains asynchronous child effects alongside native %s` asserts that `cp.execSync(...)` produces **no** child-process spawn. So after #2041, both behaviors stay intentionally effect-less. I'd like a maintainer decision on whether that should change. ### Use cases **Observed on popular MCP servers.** Static only: 35 npm MCP server packages, `npm pack`, never executed. Counts are regex call sites in shipped JS, spot-checked as real calls (not comments) versus semantic-graph effects. - **File writes:** 16 of 35 packages contain them (109 call sites), and none are recorded. Examples: - `@modelcontextprotocol/server-filesystem` 2025.8.21 (≈584k weekly downloads): the `write_file` implementation `fs.writeFile(filePath, content, { flag: "wx" })` at `dist/lib.js:185`. The only related facts are `acquires` for `fs.open`/`createReadStream` handles. - `@modelcontextprotocol/server-gdrive` `dist/index.js:154`: `fs.writeFileSync(credentialsPath, …)`. - **Sync child processes:** 15 standalone files from 6 packages, each analyzed alone because whole-package runs on large packages hit `semantic_graph_file_node_ceiling`. 0 of 15 are recovered on 6.4.0. Examples: `chrome-devtools-mcp` `build/src/BrowserManager.js`, `@heroku/mcp-server` `dist/tools/deploy-to-heroku.js`. **Supply-chain triage.** The Nx "s1ngularity" compromise (Aug 2025) used `fs.appendFileSync` to append `sudo shutdown -h 0` to `~/.bashrc`/`~/.zshrc`, and used `spawnSync` to run local CLIs ([StepSecurity write-up](https://www.stepsecurity.io/blog/supply-chain-security-alert-popular-nx-build-system-package-compromised-with-data-stealing-malware)). On that pattern, today's graph shows the reads but not the write or the commands. ### Desired outcome and success criteria For the reproduction above: - One write-effect fact per fs write call: method, literal path when static (`/tmp/a.txt`, `/tmp/b.txt`), and `partial` resolution for the computed `.bashrc` path, linked by data flow to the `HOME` read. - One `child-process` fact for `execSync("id")` with `method: "execSync"` and `command: "id"`, with no listener/signal linking, since sync calls return output rather than a handle. - Unknown or dynamic forms stay unknown. No secrecy inference. ### Expected interaction with targets Read existing files or captured data (static analysis only; nothing is executed). ### Target and scope details Scope questions: 1. **Writes:** where should a file-mutation operation live? Options: a new `filesystem` family, or `resource-lifecycle`. Should `rm`/`unlink`/`rename`/`chmod` share it as a mutation kind, or stay out of scope? 2. **Sync processes:** should they be `child-process` facts (as in the linked draft PR), or something separate, given #2041's current assertion? Non-goals: runtime execution, obfuscation recovery, bundler `__require` resolution (a separate gap I can file). ### Verification available so far I have a prototype or observed results ### Verification details Draft PR (sync child processes only), linked below: - New test fails on main and passes on the branch. - `src/domain/javascript` + `src/application/javascript`: 1931/1931 pass on current main. - Per-file sync recovery on the 15 real files: **0 → 11 of 15**. The 4 remaining use `await import("child_process")` or class-field aliases, which async methods miss too. There is no write prototype yet. I'll wait for the scope decision. Measurement scripts and per-package TSV are available on request. ### Alternatives considered - `acquires` covers `fs.open` handles only, not whole-file writes, and doesn't say whether a handle reads or writes. - Generic unknowns can't separate a known Node built-in write from any unresolved call. - Text grep works, but loses REA's provenance and data flow. ### Possible direction - **Writes:** a `JavaScriptSemanticFileMutation { method, path | null, flag | null, resolution }` operation next to configuration operations, plus a relation (e.g. `writes-file`) with data-flow edges from path/content arguments. - **Sync processes:** extend `CHILD_PROCESS_METHODS`, and exclude sync results from interaction linking (as in the draft). ### References - Nx s1ngularity: https://www.stepsecurity.io/blog/supply-chain-security-alert-popular-nx-build-system-package-compromised-with-data-stealing-malware - Related PR: #2041
Author
Owner

@MartOcd1709 commented on GitHub (Oct 14, 2026):

Draft PR for the synchronous child-process half: #3433 (test fails on main, passes on the branch; 0 → 11 of 15 real call sites recovered). It stays a draft until the scope question above is decided, especially given #2041's current expectation. No fs-write prototype until the family placement is agreed.

<!-- gh-comment-id:6166630203 --> @MartOcd1709 commented on GitHub (Oct 14, 2026): Draft PR for the synchronous child-process half: #3433 (test fails on main, passes on the branch; 0 → 11 of 15 real call sites recovered). It stays a draft until the scope question above is decided, especially given #2041's current expectation. No fs-write prototype until the family placement is agreed.
Author
Owner

@MartOcd1709 commented on GitHub (Oct 31, 2026):

Gentle ping on the scope question here — PR #3433 (the synchronous child-process half) is ready, and @CassieFox23 independently reproduced it (both new tests fail on main, pass with the patch, 150 nearby tests green). Happy to rebase onto #2041 or land it independently — whatever fits the roadmap. I just need a steer on whether this effect family is in scope and where it should live. No rush, and thanks for taking a look.

<!-- gh-comment-id:6540079878 --> @MartOcd1709 commented on GitHub (Oct 31, 2026): Gentle ping on the scope question here — PR #3433 (the synchronous child-process half) is ready, and @CassieFox23 independently reproduced it (both new tests fail on `main`, pass with the patch, 150 nearby tests green). Happy to rebase onto #2041 or land it independently — whatever fits the roadmap. I just need a steer on whether this effect family is in scope and where it should live. No rush, and thanks for taking a look.
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#1276
No description provided.