feat(mcp): connect the daemon to its engine over IPC - #304
Merged
Conversation
StefanSteiner
force-pushed
the
feat/daemon-engine-ipc
branch
2 times, most recently
from
September 7, 2026 05:23
1ad38f1 to
c1bd1c3
Compare
The single-instance daemon now reaches its hyperd engine over a local IPC channel — a Unix domain socket on Unix/macOS, a named pipe on Windows — instead of a loopback TCP port. Scope (PR A of the daemon IPC plan): only the daemon's *engine* connection moves. The health/control channel and daemon discovery keep their loopback TCP port (daemon.json still carries a numeric health_port); collapsing discovery is deferred to PR B. The per-start callback "dead man's switch" stays TCP. Clean break: no dual-transport and no TCP fallback for the engine connection. - build_params sets TransportMode::Ipc and, on Unix, creates an owner-only (0700) socket directory under the state dir via the existing state_perms::ensure_owner_only_dir, passed through the domain_socket_directory override so hyperd binds inside an already-locked directory (no client-side chmod race). On Windows the named pipe carries hyperd's default owner-only DACL. - The daemon publishes connection_endpoint() rather than the raw endpoint() string: for a Unix socket the raw descriptor reconstructs a non-connectable "<dir>/domain/hyper" path (a tab.domain:// scheme artifact) while hyperd actually binds "<dir>/hyper". - Engine::is_running()'s daemon-mode probe now connects over the endpoint's transport (UDS / named pipe / TCP) instead of TCP only. - hyperdb-api: the Windows named-pipe name gains a monotonic per-process suffix so several HyperProcess instances in one process (a daemon restart, or the test harness) never reuse a pipe name and fail to bind. Verified end-to-end: the daemon spawns, publishes a socket-path endpoint, and a client connects and runs queries over it; the crash-restart and idle-timeout integration tests pass over IPC.
StefanSteiner
force-pushed
the
feat/daemon-engine-ipc
branch
from
September 7, 2026 05:48
c1bd1c3 to
6e2291e
Compare
…tableau#310) find_hyperd_pid_for_endpoint located hyperd over a Unix domain socket with `lsof -nP -t -c hyperd <path>`. lsof ORs separately-stated selectors unless `-a` is given, so `-c hyperd <path>` matched *every* running hyperd process, not just the one bound to <path>. Under the parallel test binary that OR union made `.find(first)` return an unrelated concurrent test's hyperd; the restart test then SIGKILLed the wrong process, its own hyperd stayed alive, the daemon's liveness monitor correctly never restarted, and wait_for_live_hyperd_after_kill timed out. This reproduced only on the loaded ubuntu-latest runner (two of three restart tests, both attempts) and never in the serial or macOS runs; the TCP path on main was immune because `-iTCP:{port} -sTCP:LISTEN` already names a unique listener. Add `-a` so the command-name and path selectors are ANDed and only the hyperd actually bound to the socket path is returned. Test-only; no production or public-API change.
StefanSteiner
force-pushed
the
feat/daemon-engine-ipc
branch
from
September 7, 2026 08:37
6de9cf7 to
0516aa5
Compare
This was referenced Sep 7, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What & why
The single-instance MCP daemon now reaches its
hyperdengine over a local IPC channel — a Unix domain socket on Unix/macOS, a named pipe on Windows — instead of a loopback TCP port. This is PR A of the daemon IPC transport plan.Scope (deliberately narrow)
In scope — only the daemon's engine connection moves to IPC:
daemon/run.rs::build_paramsnow setsTransportMode::Ipc.0700) socket directory under the state dir (~/.hyperdb/sockets) via the existingstate_perms::ensure_owner_only_dir, and hands it toHyperProcessthrough thedomain_socket_directoryoverride sohyperdbinds inside an already-locked directory — there is no window for a client-sidechmodrace. On Windows the named pipe carrieshyperd's default owner-only DACL (we never setnamed_pipes_allow_all_users), so there is no directory to place.Explicitly deferred to PR B (NOT touched here):
TcpListener;daemon.jsonkeeps its numerichealth_port.resolve_port_scan,probe_port,ProbeResult, the port scan, and port-0 handling all remain and still govern the health port.TcpListeneris untouched.Clean break: no dual-transport and no TCP fallback for the engine connection.
Notable discovery (verified, not assumed)
The daemon previously published
HyperProcess::endpoint()— the raw callback descriptor. For a Unix socket that string reconstructs the path as<dir>/domain/hyper(atab.domain://scheme artifact), buthyperdactually binds<dir>/hyper, so a client dialing the published string gotENOENT. Fix: publishconnection_endpoint(), which carries the path a client can actually connect to (TCP and Windows named pipes were already consistent). That in turn requiredEngine::is_running()'s daemon-mode probe to connect over the endpoint's transport (UDS / named pipe / TCP) instead of TCP only.Review fixes applied (adversarial review — "With fixes", no Critical)
hyper-<pid>collision the Windows pipe name did — two concurrently-live IPCHyperProcessinstances in one process collided (the sequential case only worked becauseDropremoved the per-pid dir first). Both now carry a monotonic per-process suffix (hyper-<pid>-<seq>) from one sharedIPC_INSTANCE_SEQcounter.Drop's cleanup keys onstarts_with("hyper-"), which still matches; the socket filename is alwayshyper, so nothing parses the directory basename. New unit test asserts twodefault_socket_dir()calls produce distinct,hyper--prefixed names. Thehyperdb-apichangelog bullet was rewritten to state the truth for both platforms.build_paramsnow triesUnixStream::connect(<socket_dir>/hyper)before spawning; a successful connect means anotherhyperdis genuinely bound → return a clearAddrInUseerror naming the path, instead of a 60 s callback-timeout. A refused/missing socket (stale file, dead owner) proceeds. This cannot wedge the crash-restart path:try_restart_hyperdreaps the SIGKILLed child (guard.hyper = None) beforebuild_params, so the connect is refused and the respawn rebinds. That drop-before-respawn ordering is now documented as load-bearing.probe_endpoint_alive's comment no longer claims "TCP connect"; it describes the three-transport connect.lsofbranch now uses-c hyperdso a client sharing the socket can't be the PID the test kills.hyper-, sinceHyperProcess::dropremove_dir_alls caller-supplied dirs whose basenamestarts_with("hyper-").Known / deferred (noted, not fixed here)
ERROR_PIPE_BUSY. Bounded to thestatusJSON field; it cannot cause a spurious restart (restarts come fromconnection_lost, not this probe). Deferred.hyperdb-api'stransport_type()reports"TCP"for a UDS connection (pre-existing;describe_endpointis correct). Out of scope.HYPERDB_STATE_DIRcould push the socket path past thesun_path104/108-byte limit. Flagged as a PR B validation item; no validation added now.Evidence it works over IPC
Verified locally against the pinned engine (
0.0.26479):…/sockets/hyper→statusreportstransport: "unix_domain_socket"→ engine connects and runsCREATE/INSERT/SELECTover the socket.hyperdSIGKILLed → monitor restarts it → client reconnects to the re-bound socket path) passes 4/4 locally with the Fix 2 pre-flight in place (isolated + 3× parallel).0700; newdefault_socket_diruniqueness test guards Fix 1.Gate (local, real numbers)
cargo fmt --all -- --check→ exit 0cargo clippy --workspace --all-targets --all-features -- -D warnings→ exit 0cargo test -p hyperdb-api→ 633 passed, 0 failed (incl. the newdefault_socket_dirtest)cargo test -p hyperdb-mcp --no-fail-fast→ 663 passed, 1 failed, 17 ignored — the single failure issmoke_demo_and_changelog_contract(see CI caveat)RUSTDOCFLAGS="-D warnings" cargo doc --no-deps→ exit 0npx markdownlint-cli2→ 0 issues in 68 filesCI caveat (both failures are pre-existing / main-side, not from this PR's fixes)
smoke_demo_and_changelog_contractfails on every platform. #307 ("roll over per-crate changelogs for 1.0.0-rc.3") moved the mcp## [Unreleased]bullets under## [1.0.0-rc.3]without updating this contract test, which still hard-codes rc.3-era tokens (doctor,bar_orientation, …) as required under## [Unreleased].mainitself fails this test today (itstestCI onb5fb14chas not run). Reproduced locally: droppingmain's ownhyperdb-mcp/CHANGELOG.mdinto the test fails identically. This PR keeps its new IPC bullet under## [Unreleased]and leaves the rc.3 history intact; the token list is a main-side fix.engine_recovers_after_hyperd_killed,hyperd_monitor_detects_killed_hyperd_and_restarts) time out onubuntu-latest. They use a fixed UDS path + the daemon's ~5 s monitor tick within a tight 12 s budget, so a loaded CI runner occasionally misses the window (the third restart test,client_report_triggers_restart_after_kill, passes on the same run, and all three pass 4/4 locally). This timing fragility predates the five review fixes (already#[ignore]d on macOS for a related startup-timeout reason) and is unrelated to the Fix 2 pre-flight, which adds negligible latency and does not error on the restart path.The Windows named-pipe path is not exercisable on the macOS dev host; it rests on the
test (windows-latest)andclippy (windows-latest)CI legs.Rebase note
Rebased onto current
maintipb5fb14c(#307 — the rc.3 per-crate changelog rollover). The changelog resolution is take-both:main's rollover (empty## [Unreleased]+ populated## [1.0.0-rc.3]) is preserved verbatim, and this PR's new entries are added under## [Unreleased]only (### Fixedforhyperdb-api,### Changedforhyperdb-mcp). No rc.3 history is rewritten; no MD024 duplicate headings.