[PR #7046] fix(capture): run without a PTY where the native preload cannot fork #7604

Open
opened 2026-11-03 13:39:03 +00:00 by bsakaguchi · 0 comments
Owner

📋 Pull Request Information

Original PR: https://github.com/morluto/rea/pull/7046
Author: @innotelinc
Created: 10/24/2026
Status: 🔄 Open

Base: main ← Head: fix/capture-no-pty-musl


📝 Commits (3)

  • fa3117f fix(capture): run without a PTY where the native preload cannot fork
  • 84c239d test(capture): prove the pipe backend never imports node-pty on musl
  • 935d82b fix(capture): own the pipe child, its stdin errors, and cancellation

📊 Changes

6 files changed (+556 additions, -8 deletions)

View changed files

📝 src/process/capture/ProcessCaptureCapability.ts (+11 -0)
📝 src/process/capture/ProcessCaptureLifecycle.ts (+17 -0)
📝 src/process/capture/ProcessHarness.ts (+23 -8)
➕ src/process/capture/ProcessTerminalBackend.test.ts (+222 -0)
➕ src/process/capture/ProcessTerminalBackend.ts (+239 -0)
➕ src/process/capture/ProcessTerminalCancellation.test.ts (+44 -0)

📄 Description

Summary

A captured command crashes the MCP server on a musl host. @lydell/node-pty's
Linux prebuild is linked against glibc; under Alpine a gcompat shim lets the
addon load, and then forkpty faults the process that called it — which is
rea --mcp itself, so one capture_process_scenario took the server down and
every tool after it failed.

This picks the terminal backend before the native module is imported: node-pty
where the runtime reports glibc, ordinary pipes where it does not. A capture on
a musl host now completes, and the result says the terminal was not observed
instead of claiming one.

Problem and expected behavior

Smallest trigger, on Alpine (musl), with rea --mcp serving tools:

  1. tools/call → capture_process_scenario with a minimal command.
  2. The server process dies with SIGSEGV, leaving a core dump; the MCP client
    reports MCP server "rea" exited (SIGSEGV).

Invariant violated: observing a target must not kill the observer. Pipe capture
still records output, exit, process samples and filesystem effects; what it
cannot observe is a TTY, which the evidence now reports as a limitation and a
terminal unknown.

Change and scope

  • src/process/capture/ProcessTerminalBackend.ts (new): backend choice
    (nativePtyUsable() reads process.report's glibcVersionRuntime, Linux
    only), a PipeTerminal implementing the subset of IPty the harness uses
    (data, exit, write, resize, kill, pause/resume, flow control), and
    spawnProcessTerminal() which imports node-pty only on the native path.
  • ProcessHarness.ts: spawns through spawnProcessTerminal() and records the
    backend it got.
  • ProcessCaptureLifecycle.ts / ProcessCaptureCapability.ts: carry the
    backend and the "terminal not observed" limitation into the capture result.
  • ProcessTerminalBackend.test.ts: pipe output/exit, scripted stdin, signal
    reporting, backend choice on a report with no glibc, and a guard that the
    native module is never imported on that path.

Not included: a PTY emulation for musl (a pure-JS terminal would still not be a
kernel TTY), and any change to the node-pty path on glibc/macOS/Windows, which
is unchanged.

Contract and boundary impact

  • Semantic owner and earliest changed stage: process capture adapter; the
    spawn stage inside ProcessHarness.
  • Contribution scope fit and linked maintainer decision: ordinary fix inside
    existing capture boundaries; not applicable.
  • CLI and MCP/tool-catalog contract: no new tools and no argument changes.
    capture_process_scenario output gains the limitation and a terminal unknown
    on hosts without a usable PTY.
  • Provider, bridge, target-format, or platform compatibility: adds musl Linux
    support where the capture previously crashed. glibc Linux, macOS and Windows
    keep the node-pty backend.
  • Evidence, artifact, provenance, or reconstruction contract: the capture
    records which backend ran and marks the terminal as not observed rather than
    inferring one.
  • Target selection, authority, upstream behavior, and cleanup details:
    unchanged; the captured command runs with the same cwd, environment and
    arguments, and is killed the same way.
  • Generated metadata (docs/public/product-catalog.json), package, or
    installation impact: none; no generated catalogs change.

Expected effects (known effects):

  • Launch or attach to a process.
  • Read existing files or captured data.
  • Inject instrumentation or run scripts/code.
  • Change process memory, registers, files, or configuration.
  • Contact network services.
  • Recover protected code or work around protections.
  • Documentation or usability only; no target interaction.
  • Not sure yet; investigation or maintainer guidance needed.

Evidence and regression coverage

  • Tests added or updated: ProcessTerminalBackend.test.ts — five tests,
    including "runs on pipes where the runtime reports no glibc, without
    importing node-pty", which makes @lydell/node-pty unimportable and runs a
    command through the default backend with a report that carries no glibc.

  • Base reproduction or other evidence: on Alpine, rea --mcp died with
    SIGSEGV on the first capture_process_scenario call, leaving core.<pid> in
    the server's working directory; the same call under the pipe backend returns
    output and exit status. Reverting the backend choice to unconditional makes
    the new test fail (observed).

  • User-visible CLI/MCP output (if applicable): the capture result reports the
    backend and a terminal unknown instead of terminal observations.

  • Remaining proof gaps: the full npm run typecheck could not be completed on
    the 2 GB machine used for this work (the compiler is OOM-killed); a scoped
    tsc over the changed files reported no errors in them. No real Hopper,
    Ghidra, browser or OS coverage was run — the change is in process spawning.

  • Observed, derived, and inferred claims remain distinguishable.

  • Artifact identity, source provenance, and failed attempts remain preserved.

  • Unsupported, incomplete, unavailable, or uncertain outcomes remain visible.

Validation performed

  • npx vitest run src/process/capture/ProcessTerminalBackend.test.ts — 5 tests
    passed.
  • npx vitest run src/process/capture — 18 files, 189 passed, 11 skipped.
  • Regression check with nativePtyUsable() forced to true — 2 tests failed,
    as intended; source restored.
  • npm run typecheck — not completed: the process was OOM-killed on a 2 GB host.

Compatibility, safety, and release

  • Breaking changes or migration steps: none. A musl host that previously
    crashed now runs with pipes and says the terminal was not observed; callers
    that require terminal observations see the limitation rather than a silent
    change.
  • Real Hopper/Ghidra, browser, or OS coverage: not applicable to a spawn-path
    change; not run.
  • Package or release metadata impact: none.
  • Security, privacy, process, or containment review: the pipe backend spawns
    with the same arguments, cwd and environment through node:child_process, and
    keeps the existing kill and flow-control behavior. No new privileges or
    network paths.

Author readiness

Human owner: @innotelinc

  • I reviewed the final diff and relevant existing code, understand the change and its edge cases, and take responsibility for the complete PR, including AI-assisted work.
  • I checked the evidence above, confirmed it supports the current change, and recorded meaningful validation gaps.
  • I declare this PR ready for initial maintainer review.

Before merge

  • This PR addresses a concrete problem or an agreed enhancement.
  • Material scope and authority questions are resolved, and any required maintainer scope decision covers the final diff.
  • The PR has one focused outcome and the title follows type(scope): outcome.
  • Related issue is linked, or the reason for not linking one is stated above.
  • Tests cover changed observable behavior and meaningful failure paths.
  • Owning docs, contracts, and generated metadata are updated where needed.
  • User-visible CLI/MCP changes include representative output.
  • I checked the final diff for secrets, unrelated cleanup, and unsupported claims.

🔄 This issue represents a GitHub Pull Request. It cannot be merged through Gitea due to API limitations.

## 📋 Pull Request Information **Original PR:** https://github.com/morluto/rea/pull/7046 **Author:** [@innotelinc](https://github.com/innotelinc) **Created:** 10/24/2026 **Status:** 🔄 Open **Base:** `main` ← **Head:** `fix/capture-no-pty-musl` --- ### 📝 Commits (3) - [`fa3117f`](https://github.com/morluto/rea/commit/fa3117fb270983c50d410b3a5bb1018f7f172bca) fix(capture): run without a PTY where the native preload cannot fork - [`84c239d`](https://github.com/morluto/rea/commit/84c239d3f66cbed929e2ace50666e331e49adf28) test(capture): prove the pipe backend never imports node-pty on musl - [`935d82b`](https://github.com/morluto/rea/commit/935d82b3653bc06c403107f3d4b5d732386782ed) fix(capture): own the pipe child, its stdin errors, and cancellation ### 📊 Changes **6 files changed** (+556 additions, -8 deletions) <details> <summary>View changed files</summary> 📝 `src/process/capture/ProcessCaptureCapability.ts` (+11 -0) 📝 `src/process/capture/ProcessCaptureLifecycle.ts` (+17 -0) 📝 `src/process/capture/ProcessHarness.ts` (+23 -8) ➕ `src/process/capture/ProcessTerminalBackend.test.ts` (+222 -0) ➕ `src/process/capture/ProcessTerminalBackend.ts` (+239 -0) ➕ `src/process/capture/ProcessTerminalCancellation.test.ts` (+44 -0) </details> ### 📄 Description ## Summary A captured command crashes the MCP server on a musl host. `@lydell/node-pty`'s Linux prebuild is linked against glibc; under Alpine a gcompat shim lets the addon load, and then `forkpty` faults the process that called it — which is `rea --mcp` itself, so one `capture_process_scenario` took the server down and every tool after it failed. This picks the terminal backend before the native module is imported: node-pty where the runtime reports glibc, ordinary pipes where it does not. A capture on a musl host now completes, and the result says the terminal was not observed instead of claiming one. ## Problem and expected behavior Smallest trigger, on Alpine (musl), with `rea --mcp` serving tools: 1. `tools/call` → `capture_process_scenario` with a minimal command. 2. The server process dies with SIGSEGV, leaving a core dump; the MCP client reports `MCP server "rea" exited (SIGSEGV)`. Invariant violated: observing a target must not kill the observer. Pipe capture still records output, exit, process samples and filesystem effects; what it cannot observe is a TTY, which the evidence now reports as a limitation and a terminal unknown. ## Change and scope - `src/process/capture/ProcessTerminalBackend.ts` (new): backend choice (`nativePtyUsable()` reads `process.report`'s `glibcVersionRuntime`, Linux only), a `PipeTerminal` implementing the subset of `IPty` the harness uses (data, exit, write, resize, kill, pause/resume, flow control), and `spawnProcessTerminal()` which imports node-pty only on the native path. - `ProcessHarness.ts`: spawns through `spawnProcessTerminal()` and records the backend it got. - `ProcessCaptureLifecycle.ts` / `ProcessCaptureCapability.ts`: carry the backend and the "terminal not observed" limitation into the capture result. - `ProcessTerminalBackend.test.ts`: pipe output/exit, scripted stdin, signal reporting, backend choice on a report with no glibc, and a guard that the native module is never imported on that path. Not included: a PTY emulation for musl (a pure-JS terminal would still not be a kernel TTY), and any change to the node-pty path on glibc/macOS/Windows, which is unchanged. ## Contract and boundary impact - Semantic owner and earliest changed stage: process capture adapter; the spawn stage inside `ProcessHarness`. - Contribution scope fit and linked maintainer decision: ordinary fix inside existing capture boundaries; not applicable. - CLI and MCP/tool-catalog contract: no new tools and no argument changes. `capture_process_scenario` output gains the limitation and a terminal unknown on hosts without a usable PTY. - Provider, bridge, target-format, or platform compatibility: adds musl Linux support where the capture previously crashed. glibc Linux, macOS and Windows keep the node-pty backend. - Evidence, artifact, provenance, or reconstruction contract: the capture records which backend ran and marks the terminal as not observed rather than inferring one. - Target selection, authority, upstream behavior, and cleanup details: unchanged; the captured command runs with the same cwd, environment and arguments, and is killed the same way. - Generated metadata (`docs/public/product-catalog.json`), package, or installation impact: none; no generated catalogs change. Expected effects (known effects): - [x] Launch or attach to a process. - [ ] Read existing files or captured data. - [ ] Inject instrumentation or run scripts/code. - [ ] Change process memory, registers, files, or configuration. - [ ] Contact network services. - [ ] Recover protected code or work around protections. - [ ] Documentation or usability only; no target interaction. - [ ] Not sure yet; investigation or maintainer guidance needed. ## Evidence and regression coverage - Tests added or updated: `ProcessTerminalBackend.test.ts` — five tests, including "runs on pipes where the runtime reports no glibc, without importing node-pty", which makes `@lydell/node-pty` unimportable and runs a command through the default backend with a report that carries no glibc. - Base reproduction or other evidence: on Alpine, `rea --mcp` died with SIGSEGV on the first `capture_process_scenario` call, leaving `core.<pid>` in the server's working directory; the same call under the pipe backend returns output and exit status. Reverting the backend choice to unconditional makes the new test fail (observed). - User-visible CLI/MCP output (if applicable): the capture result reports the backend and a terminal unknown instead of terminal observations. - Remaining proof gaps: the full `npm run typecheck` could not be completed on the 2 GB machine used for this work (the compiler is OOM-killed); a scoped `tsc` over the changed files reported no errors in them. No real Hopper, Ghidra, browser or OS coverage was run — the change is in process spawning. - [x] Observed, derived, and inferred claims remain distinguishable. - [x] Artifact identity, source provenance, and failed attempts remain preserved. - [x] Unsupported, incomplete, unavailable, or uncertain outcomes remain visible. ## Validation performed - `npx vitest run src/process/capture/ProcessTerminalBackend.test.ts` — 5 tests passed. - `npx vitest run src/process/capture` — 18 files, 189 passed, 11 skipped. - Regression check with `nativePtyUsable()` forced to `true` — 2 tests failed, as intended; source restored. - `npm run typecheck` — not completed: the process was OOM-killed on a 2 GB host. ## Compatibility, safety, and release - Breaking changes or migration steps: none. A musl host that previously crashed now runs with pipes and says the terminal was not observed; callers that require terminal observations see the limitation rather than a silent change. - Real Hopper/Ghidra, browser, or OS coverage: not applicable to a spawn-path change; not run. - Package or release metadata impact: none. - Security, privacy, process, or containment review: the pipe backend spawns with the same arguments, cwd and environment through `node:child_process`, and keeps the existing kill and flow-control behavior. No new privileges or network paths. ## Author readiness Human owner: @innotelinc - [x] I reviewed the final diff and relevant existing code, understand the change and its edge cases, and take responsibility for the complete PR, including AI-assisted work. - [x] I checked the evidence above, confirmed it supports the current change, and recorded meaningful validation gaps. - [x] I declare this PR ready for initial maintainer review. ## Before merge - [ ] This PR addresses a concrete problem or an agreed enhancement. - [ ] Material scope and authority questions are resolved, and any required maintainer scope decision covers the final diff. - [ ] The PR has one focused outcome and the title follows `type(scope): outcome`. - [ ] Related issue is linked, or the reason for not linking one is stated above. - [ ] Tests cover changed observable behavior and meaningful failure paths. - [ ] Owning docs, contracts, and generated metadata are updated where needed. - [ ] User-visible CLI/MCP changes include representative output. - [ ] I checked the final diff for secrets, unrelated cleanup, and unsupported claims. --- <sub>🔄 This issue represents a GitHub Pull Request. It cannot be merged through Gitea due to API limitations.</sub>
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#7604
No description provided.