Skip to content

fix: contain stdio process trees during aggregate teardown - #263

Merged
mohanagy merged 3 commits into
developmentfrom
fix/255-aggregate-suite-stability
Jul 27, 2026
Merged

fix: contain stdio process trees during aggregate teardown#263
mohanagy merged 3 commits into
developmentfrom
fix/255-aggregate-suite-stability

Conversation

@mohanagy

@mohanagy mohanagy commented Jul 27, 2026

Copy link
Copy Markdown
Owner

Summary

  • launch stdio upstreams inside a verified containment boundary: a dedicated POSIX process group or the checked Windows kill-on-close Job Object helper
  • withhold transport close and profile-capacity reuse until descendant containment is confirmed
  • bound the forced-close wait, then make containment verification the authoritative teardown gate
  • keep unrelated macOS OAuth local-lock work out of this PR; it is tracked separately in bug: avoid unrelated macOS OAuth local-lock collisions #264

Security impact

  • No command strings or shell execution were introduced: upstream processes use argument arrays with shell: false.
  • Windows process-tree containment remains enforced by the checked Job Object helper. If its helper-close proof is absent, teardown fails closed and capacity is not released.
  • POSIX only releases containment after the process group is observed gone; a bounded poll avoids treating normal asynchronous reaping as an escape.
  • No secrets, provider output, or sensitive errors are logged by these changes.

Root cause

A direct stdio child could exit while a descendant retained inherited pipes. Its public close never arrived, leaving teardown/capacity state behind for later aggregate tests. Windows also needed a launch-time Job Object boundary rather than post-exit tree cleanup.

Validation

  • Focused containment transport regressions (13 passed)
  • Upstream manager and OAuth lock tests (43 passed, 2 skipped)
  • npm test (1,716 passed, 27 skipped)
  • npm run test:core (415 passed, 22 skipped)
  • npm run test:coverage (1,716 passed, 27 skipped; containment transport 86.8% branches)
  • npm run lint, npm run typecheck, npm run build, npm run smoke:cli, npm run check:pack, npm run test:package
  • Current-head CI: Linux quality; Linux/macOS/Windows Node 20/22/24; Verify
  • All CodeRabbit inline threads resolved; its stale review was dismissed under the documented 23-minute rate-limit exception.

Fixes #255

@coderabbitai

coderabbitai Bot commented Jul 27, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

The PR adds deterministic macOS OAuth lock fallbacks and replaces upstream stdio handling with a platform-aware contained transport. Upstream startup, shutdown, descendant cleanup, Windows launching, and related tests are updated.

Changes

macOS OAuth lock coordination

Layer / File(s) Summary
macOS fallback lock acquisition
src/oauth/local-lock.ts, tests/oauth-local-lock.test.ts
macOS locks derive fallback TCP endpoints and use them when legacy candidates are unavailable, with regression coverage for collisions, occupied candidates, and concurrent acquisition.

Contained upstream stdio lifecycle

Layer / File(s) Summary
Contained transport launch configuration
src/secrets/windows-secret-command.ts, src/upstream/contained-stdio-transport.ts
Windows helper launches accept stdin and working-directory options, while transport creation composes environments and resolves platform-specific launchers.
Contained transport lifecycle
src/upstream/contained-stdio-transport.ts
The new transport wires JSON-RPC streams, tracks child processes, verifies containment, and performs staged graceful or forced shutdown.
Upstream manager integration
src/upstream/upstream-process-manager.ts
The manager creates contained transports, detects closure during startup, and bounds graceful shutdown before force termination.
Containment and manager lifecycle validation
tests/contained-stdio-transport.test.ts, tests/upstream-manager.test.ts, vitest.config.ts
Tests cover POSIX and Windows containment, descendant reaping, restart and shutdown sequencing, and targeted coverage thresholds.

Estimated code review effort: 5 (Critical) | ~120 minutes

Sequence Diagram(s)

sequenceDiagram
  participant UpstreamProcessManager
  participant ContainedStdioClientTransport
  participant ChildProcess
  participant ContainmentBoundary
  UpstreamProcessManager->>ContainedStdioClientTransport: close()
  ContainedStdioClientTransport->>ChildProcess: end stdin
  ContainedStdioClientTransport->>ContainmentBoundary: verify containment
  ContainedStdioClientTransport->>ChildProcess: force terminate after timeout
  ContainmentBoundary-->>ContainedStdioClientTransport: containment complete
  ContainedStdioClientTransport-->>UpstreamProcessManager: onclose
Loading

Possibly related PRs

Poem

A rabbit hops where lock ports meet,
And finds a fallback path so neat.
Contained streams now close with care,
While lost descendants vanish in air.
“Thump, thump!” says Bunny, “tests agree—
A safer burrow for every tree!”


Caution

Pre-merge checks failed

Please resolve all errors before merging. Addressing warnings is optional.

  • Ignore

❌ Failed checks (1 error)

Check name Status Explanation Resolution
Out of Scope Changes check ❌ Error The macOS OAuth local-lock fallback changes and tests are unrelated to #255's stdio containment objective. Split the OAuth local-lock changes and tests into a separate PR or explain how they are required by #255.
✅ Passed checks (4 passed)
Check name Status Explanation
Linked Issues check ✅ Passed The containment and teardown changes directly address the aggregate-suite startup instability in #255 without weakening isolation or increasing timeouts.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Title check ✅ Passed The title clearly summarizes the main change: containing stdio process trees during teardown.
Description check ✅ Passed It includes Summary, Security impact, Root cause, and Validation sections with concrete results, satisfying the template requirements.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/255-aggregate-suite-stability

Comment @coderabbitai help to get the list of available commands.

coderabbitai[bot]
coderabbitai Bot previously requested changes Jul 27, 2026

@coderabbitai coderabbitai 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.

Actionable comments posted: 3

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
tests/oauth-local-lock.test.ts (1)

223-333: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Consider extracting a deferred() helper for the repeated hold/entered promise pattern.

The let releaseX!: () => void; const holdX = new Promise<void>((resolve) => { releaseX = resolve; }); pattern (and its markXEntered/xEntered counterpart) is repeated ~4 times across these two tests. A small shared helper would reduce duplication and make the tests easier to scan.

♻️ Proposed helper
+function deferred<T = void>(): { promise: Promise<T>; resolve: (value: T) => void } {
+  let resolve!: (value: T) => void;
+  const promise = new Promise<T>((res) => {
+    resolve = res;
+  });
+  return { promise, resolve };
+}

Usage example:

-    let releaseFirst!: () => void;
-    const holdFirst = new Promise<void>((resolve) => {
-      releaseFirst = resolve;
-    });
-    let markFirstEntered!: () => void;
-    const firstEntered = new Promise<void>((resolve) => {
-      markFirstEntered = resolve;
-    });
+    const { promise: holdFirst, resolve: releaseFirst } = deferred();
+    const { promise: firstEntered, resolve: markFirstEntered } = deferred();
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/oauth-local-lock.test.ts` around lines 223 - 333, Extract a shared
deferred-promise helper for the repeated hold/release and entered/marker pairs
in the macOS lock tests. Update the affected tests around withOAuthLocalLock to
use the helper for both waiting and resolving, while preserving the existing
synchronization and cleanup behavior.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/upstream/contained-stdio-transport.ts`:
- Around line 187-191: Bound the second await of childClose in the close flow
after forceTerminate(), using the existing gracefulShutdownDelayMs or an
equivalent timeout helper. Keep verifyContainment() as the authoritative step
that detects and throws for unconfirmed containment, while preserving the normal
graceful-close path.
- Around line 266-278: Replace the single post-SIGKILL probe in the termination
flow with bounded polling until containmentVerificationTimeoutMs (1,000 ms)
expires, rechecking isPosixProcessGroupRunning after short delays. Record and
throw the containment failure only if the process group remains running at the
deadline or verification throws; preserve the existing forceTerminate and
containmentFailure checks.

In `@tests/contained-stdio-transport.test.ts`:
- Around line 347-355: Guard the “rejects a second start rather than spawning a
second contained child” test with the same runIf condition used by the other
real-process tests, excluding win32. Keep the existing transport setup and
double-start assertion unchanged for supported platforms.

---

Outside diff comments:
In `@tests/oauth-local-lock.test.ts`:
- Around line 223-333: Extract a shared deferred-promise helper for the repeated
hold/release and entered/marker pairs in the macOS lock tests. Update the
affected tests around withOAuthLocalLock to use the helper for both waiting and
resolving, while preserving the existing synchronization and cleanup behavior.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 16327af8-3ec9-4903-9b90-22bdf072ee89

📥 Commits

Reviewing files that changed from the base of the PR and between 7e435c2 and 4679a82.

📒 Files selected for processing (8)
  • src/oauth/local-lock.ts
  • src/secrets/windows-secret-command.ts
  • src/upstream/contained-stdio-transport.ts
  • src/upstream/upstream-process-manager.ts
  • tests/contained-stdio-transport.test.ts
  • tests/oauth-local-lock.test.ts
  • tests/upstream-manager.test.ts
  • vitest.config.ts

Comment thread src/upstream/contained-stdio-transport.ts
Comment thread src/upstream/contained-stdio-transport.ts Outdated
Comment thread tests/contained-stdio-transport.test.ts Outdated
@mohanagy

Copy link
Copy Markdown
Owner Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jul 27, 2026

Copy link
Copy Markdown
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@mohanagy

Copy link
Copy Markdown
Owner Author

Review exception documented for the final head 5fe72d4:

  • CodeRabbit's requested incremental review is rate-limited for 23 minutes; its bot confirms it did not re-review already-reviewed commits.
  • All three inline CodeRabbit findings are resolved in 6a8c611; the unrelated macOS OAuth work was split to bug: avoid unrelated macOS OAuth local-lock collisions #264.
  • The final current-head CI matrix is fully green, including Linux quality and Windows Node 20/22/24.
  • A read-only local Claude Opus review was attempted with 1-, 4-, and 8-turn bounds. Each ended at its turn cap without a verdict or edits.

I manually re-reviewed the final containment diff and test coverage: no shell execution was introduced; Windows remains fail-closed on missing Job Object helper-close proof; POSIX only emits close after a verified-empty process group. Dismissing the stale automated request under the maintainer rate-limit exception.

@mohanagy
mohanagy dismissed coderabbitai[bot]’s stale review July 27, 2026 06:56

All inline findings are resolved. CodeRabbit incremental re-review is rate-limited and documented in the PR; final CI is green.

@mohanagy
mohanagy merged commit f846511 into development Jul 27, 2026
12 checks passed
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.

test: eliminate aggregate-suite upstream startup instability

1 participant