fix(mcp): bound ShellTool process wait so hung commands cannot block forever - #298
fix(mcp): bound ShellTool process wait so hung commands cannot block forever#298SebTardif wants to merge 4 commits into
Conversation
…forever After concurrent pipe drain (openclaw#292), ShellTool still used Process.waitUntilExit() with no deadline. A hung child (sleep, blocked network tool, stuck interactive prompt) never closes stdout/stderr, so pipe EOF never arrives and the MCP tool blocks the agent loop indefinitely. Add an optional timeout argument (default 30s, max 3600s). On deadline, send SIGTERM then SIGKILL, surface a timed-out ToolResponse, and let pipe drains complete. Mirrors the capture action process deadline pattern from openclaw#215 without duplicating the CLI runner into AgentRuntime. Include regression tests for hang timeout and success-with-timeout, plus a standalone prove-shell-tool-timeout.swift for live terminal proof.
|
Codex review: needs maintainer review before merge. Reviewed July 30, 2026, 4:14 PM ET / 20:14 UTC. ClawSweeper reviewWhat this changesThis PR adds an optional timeout to the MCP shell tool that terminates the shell process tree and bounds post-timeout output draining, while retaining unlimited execution when callers omit the timeout. Merge readinessKeep this PR open for maintainer review. The contributor supplied credible terminal proof that explicit timeouts return promptly, terminate a recorded descendant, and preserve the unlimited legacy path when Priority: P1 Review scores
Verification
How this fits togetherPeekaboo’s MCP shell tool runs commands on behalf of agent workflows and returns their stdout, stderr, and exit result to the MCP client. The proposed timeout path changes how that tool waits for child processes, drains inherited pipes, and recovers the agent loop from a hung command. flowchart LR
A[MCP shell request] --> B[Shell tool]
B --> C[Shell process and output pipes]
C --> D{Timeout supplied?}
D -->|No| E[Wait for normal completion]
D -->|Yes| F[Enumerate and stop descendants]
F --> G[Bound remaining pipe drain]
E --> H[MCP tool response]
G --> H
Decision needed
Why: The functional behavior is proven, but choosing whether a low-probability PID-reuse path may signal an unrelated local process is a safety boundary and contract decision rather than a mechanical test gap. Before merge
Findings
Agent review detailsSecurityNeeds attention: The patch does not add dependencies or broaden credentials, but its PID-snapshot termination strategy can cross the intended local-process boundary if PID reuse occurs. Review metrics
Root-cause clusterRelationship: Members:
Proposal only: this assessment does not dispatch repair, suppress jobs, mutate sibling items, close, or merge anything. Merge-risk optionsMaintainer options:
Technical reviewBest possible solution: Use a process-group or otherwise identity-safe containment strategy for timeout termination, or obtain an explicit maintainer decision that the bounded PID-reuse risk is acceptable for this privileged local shell tool. Do we have a high-confidence way to reproduce the issue? Yes for the intended timeout behavior: the PR gives a concrete macOS terminal scenario using a hung command and a recorded descendant PID. The PID-reuse concern is source-reproducible as a race analysis, but not deterministically demonstrated by the submitted test. Is this the best way to solve the issue? Unclear. The opt-in contract, descendant cleanup, and bounded drain are well targeted, but recursive signaling of a numeric PID snapshot is not the safest process-identity boundary. AGENTS.md: found and applied where relevant. Codex review notes: model internal, reasoning high; reviewed against 8448599ab899. LabelsLabel justifications:
EvidenceSecurity concerns:
What I checked:
Likely related people:
Rank-up movesOptional improvements that raise the rating; they are not merge blockers.
Rating scale
Overall follows the weaker of proof and patch quality. Workflow
HistoryReview history (9 earlier review cycles; latest 8 shown)
|
ClawSweeper P2 on openclaw#298: terminating only the direct bash process left descendants holding inherited stdout/stderr open, so readDataToEndOfFile could still block the MCP response past the advertised deadline. - On timeout, recursively signal the process tree via proc_listchildpids (SIGTERM then SIGKILL), not only the root pid - Drain pipes with non-blocking reads; after timeout race a short bound and close FDs so a surviving writer cannot hang the agent forever - Regression: background sleep + forever loop with timeout=1 returns <5s - Refresh prove-shell-tool-timeout.swift with the process-tree case
|
@clawsweeper re-review Addressed the P2 process-tree / pipe-drain finding:
|
|
🦞👀 Command router queued. I will update this comment with the next step. |
ClawSweeper P1 on openclaw#298: a silent 30s default changed the legacy MCP contract for clients that omit timeout. Preserve unlimited wait when timeout is absent; apply process-tree kill and bounded drain only for explicit timeout values. - resolveTimeout(nil/invalid) -> nil (no invented deadline) - schema/docs: optional timeout, omit for unlimited - tests: resolveTimeout contract + sleep 2 without timeout succeeds
|
@clawsweeper re-review Addressed the P1 omitted-timeout compatibility finding:
|
|
🦞👀 Command router queued. I will update this comment with the next step. |
ClawSweeper P1 on openclaw#298: proc_listchildpids returns the number of child PIDs written, not a byte length. Dividing by pid_t stride zeroed the walk for normal 1-3 child trees, so timeout only signaled the root and could leave descendants running after the MCP response returned. - Iterate Int(returned) directly (buffersize arg remains bytes) - Regression: record background sleep pid, assert gone after timeout - Prove script verifies childAliveAfterTimeout=false
|
@clawsweeper re-review Fixed the P1
|
|
🦞🧹 I asked ClawSweeper to review this item again. Re-review progress:
|
What Problem This Solves
ShellTool.execute()drains stdout/stderr concurrently (fixed in #292) but previously waited with no kill path for hung children and no opt-in deadline. Hung commands can block the MCP agent loop forever.This PR adds an opt-in
timeoutthat kills the shell process tree and bounds pipe draining. Omittingtimeoutpreserves unlimited execution (legacy MCP contract).A follow-up fix corrects process-tree enumeration:
proc_listchildpidsreturns a PID count, not a byte length. Mis-dividing bypid_tstride skipped children, so only the root was signaled while descendants could keep running.Call chain
Evidence
Live terminal proof from the patched branch:
Key line:
childAliveAfterTimeout=falsewith a recorded backgroundsleepPID after timeout=1.Summary
timeout: omit for unlimited (legacy); pass to opt into bounded execution.Real behavior proof
/tmp/pr-peekaboo-shell-timeouton branchfix/shell-tool-process-timeoutat head8ec5568a.swift scripts/prove-shell-tool-timeout.swiftswift test --package-path Core/PeekabooCore --filter 'Shell tool'Related
Test plan
swift scripts/prove-shell-tool-timeout.swift→ PROOF_OK (descendant gone)swift test --package-path Core/PeekabooCore --filter 'Shell tool'→ 10 passed