Repository navigation
Conversation
…wned children A stop refused for active calls or an already-unverified transport set draining first and threw afterwards, so a daemon that had decided not to stop rejected every new call. Refuse before draining. Setuid helpers such as macOS ps appear under uid 0 in a server's process tree, which failed ownership capture. Exclude descendants with a verified foreign owner and start identity, keep walking through them so owned processes beneath are still tracked, and refuse any row with incomplete identity.
|
🦞👀 Pull request received. I will update this pull request when review starts. ClawSweeper review completeClawSweeper finished reviewing this revision. The review result is being finalized. |
Codex Review Summary: ✅ CompletedThis comment shows the latest Codex review activity on this pull request.
ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings. |
|
Codex review: needs maintainer review before merge. What this changesThis PR keeps the daemon serving after an upfront stop refusal and excludes verified foreign-owned descendants while retaining owned descendants beneath them. Example: Run daemon stop while plain.slow is executing a 15-second call.
Review scores
ProductKind: Bug fix · Worth it: Yes · Fix scope: Complete Merge readiness✅ Ready for maintainer review Keep open: this PR fixes current daemon failures with a focused patch, credible CLI evidence, and passing checks; no actionable introduced defect was found. Priority: P1 Before mergeNone. FindingsNone. Agent review detailsHow this fits togetherThe daemon broker accepts authenticated client operations, retains MCP transports, and uses process identities to verify retirement before shutdown or replacement. flowchart LR
A[CLI client] --> B[Authenticated daemon host]
B --> C[Daemon broker]
C --> D[MCP transport]
D --> E[Server process tree]
C --> F[Process retirement verifier]
E --> F
Technical reviewBest possible solution: Land the focused refusal-ordering and owned-tree traversal fixes while preserving existing retirement identity checks. Do we have a high-confidence way to reproduce the issue? The contributor supplies concrete current-main and 0.17.0 CLI reproductions with fixtures, configuration, environment, and before/after output; source inspection confirms both failure paths, without an independent execution during this review. Is this the best way to solve the issue? The patch fixes the existing owners directly: refuse before entering draining state, and separate traversal from the set of verified same-user processes to await. AGENTS.md: found and applied where relevant. Codex review notes: model internal, reasoning medium; reviewed against 21624e8cc823. Provenance checked
TestingProof path: shipped entry point. SecurityNone. EvidenceWhat I checked:
Likely related people:
Preflight sourceSource unknown. Source comparison at the observed PR snapshot only. Completion is self-reported; body, policy, tests, execution, and report bytes are not verified. Review metrics
LabelsLabel changes:
Label justifications:
Rating scale6/6 🦀 challenger crab · 5/6 🦞 diamond lobster · 4/6 🐚 platinum hermit · 3/6 🦐 gold shrimp · 2/6 🦪 silver shellfish · 1/6 🧂 unranked krab. Overall follows the weaker of proof and patch quality; ✨ marks media proof (a screenshot, video, or linked artifact) that directly shows the changed behavior. WorkflowClawSweeper edits this one comment on every review. Comment Reviewed November 7, 2026, 2:38 PM ET / 19:38 UTC. |
|
My agent got overzealous and added changelog entries here, but feel free to remove them. |
Summary
Three daemon bugs in 0.17.0, all reproduced with the real CLI on published
mcporter@0.17.0and onmain(21624e8):daemon stoporrestartrefused for active calls leaves the daemon permanently draining. It stays alive and rejects every call from every client.ps, includingxcodebuildmcp.(2) can set up (3): a server hitting (2) is left with unverified retirement, so the next
daemon stopwedges the whole daemon, and SIGTERM doesn't get it out either.What's included
src/daemon/broker.ts:close()makes both refusals before settingdraining.src/daemon/process-retirement.ts:ownedProcessTreeleaves out descendants that have a verified foreign owner and start identity, and keeps walking through them so owned processes beneath are still tracked. A row with no owner or no start identity is still refused, including a foreign one.Evidence
Environment: macOS 27.0.1 arm64, Node 25.9.0. Each scenario used a fresh daemon in its own
MCPORTER_DAEMON_DIRand a config with no OAuth servers. Output is trimmed to the relevant lines; stack traces are removed.releaseandmainbehaved the same in every scenario, so only one is shown where noted. The fixture and config are at the bottom.1. Stop refused for an active call
plainandotherare fixture stdio servers.plain.slowis a 15s tool, started in the background.Before (
main;releaseis the same):After:
2. Server with a setuid-root child
Real server,
xcodebuildmcp@2.7.0. Samplingpswhile it started found the uid 0 child on every build:Before (
main;releaseis the same):After:
Deterministic fixture: a server that spawns
/usr/bin/top(-r-sr-xr-x root), so the foreign child is alive at capture.Before:
After:
Owned process beneath a foreign parent: a real cross-user tree, using
crontab -e(setuid root) withEDITORset to a script that execssleep.crontab -lwas compared before and after: unchanged.3. Stop refused for unverified retirement, zero active calls
With (2) fixed, the patched build needs another way to fail capture. Here the server's root process is setuid:
command: /usr/bin/crontab -ewithEDITORset to a script that execs the fixture. MCP works over the inherited stdio, and the root process is uid 0, so capture fails on every build.crontab -lunchanged.Before (
main;releaseis the same):After:
The same wedge happens on
mainwith no contrived setup: after thesetuidfixture from (2) fails capture,daemon stopleavesother.pingfailing withDaemon is draining., and the daemon survives SIGTERM.Tests
New or extended regression tests, run against
mainsource. Four of the five fail there; 48 other tests in those files pass:tests/daemon-liveness.test.ts: a call succeeds after a refused stop.tests/daemon-broker-transport-capacity.test.ts: after a refused stop for unverified retirement, nothing further is retired and a call succeeds.tests/process-retirement.test.ts: foreign-owned descendant excluded; owned process beneath a foreign parent kept.tests/process-retirement.test.ts: foreign row with no start identity refused. This one passes onmain, which refuses all foreign rows. It stops the new exclusion from accepting incomplete identity.pnpm checkpasses.pnpm test: 2,367 passed, 8 failed. Onmain: 2,364 passed, 8 failed. The same 8 tests fail on both, all intests/generate-cli.test.tsandtests/cli-generate-cli.integration.test.ts; the error isBun 1.4.4 or newer is required for native builds; found 1.4.2, so they come from my local Bun version.Limits of this evidence
daemon stop. So that wait isn't demonstrated end to end.Not changed
close()loop, still leaves the daemon draining. Some transports may already be closed by then, so resuming needs recovery logic; a repeated stop can retry. I left that for separate work.close(). It now keeps serving rather than sitting wedged, but it still needs SIGKILL to replace it. That may deserve its own change.Fixture and config used
fixture.mjs:Config (all
"lifecycle": "keep-alive","imports": []):plain,othernode fixture.mjs plainsetuidnode fixture.mjs setuid-childgrandchildnode fixture.mjs owned-under-setuid,REPRO_EDITOR= script runningexec sleep 313setuidroot/usr/bin/crontab -e,EDITORandVISUAL= script runningexec node fixture.mjs plainxcodenpx -y xcodebuildmcp@latest mcpIsolation:
export MCPORTER_DAEMON_DIR=$(mktemp -d "$HOME/.mi.XXXX"), mode 0700. Keep the path short because of the socket length limit. OverridingHOMEdoesn't isolate the daemon.