Repository navigation
Run the daemon as the app's child for another NODESPACE_HOME, never touching the user's registration (held) - #3637
mstomar125 wants to merge 3 commits into
Conversation
…3616) DaemonHome resolves NODESPACE_HOME by the daemon's rule: unset, or naming the user's home directory however spelled, is the user's home; anything else is another home. Every path the app derives for its daemon (bin, logs, database dir, socket fallback, UI pid file, incompatible-database marker) is now under that home, so the user's home produces exactly the paths and launchd plist it did before. For another home, start_daemon starts the daemon as this app's own headless child (daemon_home::spawn_child) with NODESPACE_HOME and NODESPACED_SOCKET set, instead of register_user_service, which holds the unchanged launchd/systemd/Windows registration. The socket is NODESPACED_SOCKET or <home>/.nodespace/daemon[-dev].sock, refused up front when too long to bind; on Windows the default pipe gets a hash of the home. The product check boots nothing out for another home, Windows stops that home's daemon by the pid it started rather than by image name, and the Windows log-rotation restart skips it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Pragmatic Code Review: PR #3637 (round 1)Review Type: Initial Review (head The PR is held for the design discussion on nodespace-docs#111. That is expected and is not a finding here. Requirements Check (#3616)
What I verified1. Default home: unchanged from
2. Another home never reaches the user's registration or daemon.
3. Child lifecycle.
4. Windows and Linux lines compiled by reading. I found no compile or
No issue numbers in comments, no lint suppression, no Pro wording, no compatibility code. Code Review Findings🟡 1. [Improvement] Windows: another home's daemon can only be stopped through an in-memory pid, so after an app crash it can never be stopped again📁
This contradicts the docs' "Stop" row ("Quitting the app stops the daemon, as it does for the user's home") and the "Updates" row on Windows. Unix has no such gap, because it finds the daemon by socket holder plus argv0 rather than by a remembered pid. Recommendation: persist the child's pid under the home, for example Engineering principle: process ownership that has to survive a crash of the owner cannot live only in the owner's memory. Make the handle durable, or make the limitation explicit. 🟡 2. [Improvement] A home path containing whitespace makes the app unable to stop or restart another home's daemon (Unix)📁 args.split_whitespace().next() == Some(expected_path)The child is spawned with
This parser predates the PR. It was harmless while argv0 was always under the user's home directory, which in practice never contains spaces. This PR makes it reachable through a path the developer picks freely. Recommendation: match the prefix instead. The trimmed Engineering principle: a pid-ownership check is a safety guard. Its parser should not silently turn "this is ours" into "not ours" for valid inputs. 🟢 3. [Suggestion]
|
mstomar125
left a comment
There was a problem hiding this comment.
Automated pragmatic-code-review: APPROVE (posted as a comment — GitHub blocks self-approval). Full findings in the review comment above.
…ommand line process_argv0_matches compared only the first whitespace-separated word of `ps -o args=`, so a daemon under a NODESPACE_HOME whose path holds a space was never recognised: quit, the stale-binary kill and the update restart all skipped it. command_line_runs now matches the whole program path, followed by whitespace or the end of the line. For a path without spaces this is the same answer as before, so the user's home is unaffected. The directory-creation errors in ensure_daemon_running name the actual directory instead of a fixed ~/.nodespace path, and CHILD_PID's doc says it is Windows-only bookkeeping that does not survive an app crash. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Address review, round 1 (head
|
Pragmatic Code Review: PR #3637 (round 2)Review Type: Re-Review. It covers only Previous Review Summary
Requirements Check (#3616)
What I verified1. Old:
So the user's-home SIGTERM and SIGKILL paths ( 2. A path with spaces cannot be mistaken for another process in any practical case.
3. The
4. The docs Stop row and the 🟡1 decision are reasonable and honest (📁
The reasoning against a persisted pid also holds. Code Review FindingsNo 🔴 or 🟡 findings. Nitpicks
Summary
Only two optional nits remain. Recommendation: APPROVE. The two nits are optional. The PR stays held for the design discussion on nodespace-docs#111, as intended. |
mstomar125
left a comment
There was a problem hiding this comment.
Automated pragmatic-code-review (round 2): APPROVE (posted as a comment — GitHub blocks self-approval). Full findings in the review comment above.
The empty-program assertion passed with or without the guard; it now checks blank `ps` output, which only the guard rejects. The doc on process_argv0_matches describes the whole-path match it now does. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Address review, round 2 (head
|
|
@malibio held for the design discussion. Decision: NodeSpaceAI/nodespace-docs#111. Summary on #3616. 🤖 Generated with Claude Code |
Independent review for the design discussion (head
|
malibio
left a comment
There was a problem hiding this comment.
Independent read for the design discussion, on behalf of the owner. Verdict: COMMENT. The diff matches the decision table in nodespace-docs#111 and leaves the user's home unchanged; no blocker inside the diff. Three points (agents reaching the user's daemon, the child's lifetime, service_env) should be settled with the design. Full findings are in the comment above.
| ) -> Option<Self> { | ||
| match (nodespace_home.map(PathBuf::from), user_home) { | ||
| (Some(home), Some(user)) if same_directory(&home, &user) => Some(Self::User(user)), | ||
| (Some(home), _) => Some(Self::Other(home)), |
There was a problem hiding this comment.
🟢 The other home is kept as typed. A relative or empty NODESPACE_HOME makes every derived path relative to the working directory, and on Unix lsof matches the bound string, so a daemon started as /tmp/x is not recognised by a run with /private/tmp/x. Require an absolute path and canonicalise once the directory exists.
| let shared = nodespace_proto::socket::DAEMON_PIPE_NAME; | ||
| match home { | ||
| Some(DaemonHome::Other(home)) => { | ||
| let digest = Sha256::digest(home.as_os_str().as_encoded_bytes()); |
There was a problem hiding this comment.
🟢 Hash of the raw spelling: C:\s\a and c:\s\a\ give two pipes, so two daemons on one database directory, with no lock file on Windows to stop the second. Hash the normalised home.
| command | ||
| .env("NODESPACE_HOME", home) | ||
| .env(nodespace_proto::socket::SOCKET_ENV_VAR, endpoint) | ||
| .stdin(std::process::Stdio::null()); |
There was a problem hiding this comment.
🟡 Two things in child_command.
service_env:write_plistrendersdaemon_profile::active().service_envinto the registration; the child gets none of it. Apply it here and assert it in the test below, or say in the doc that the child does not carry it.- Lifetime: with
stdinnull nothing links the child to the app, so a crashed app leaves a daemon with the model loaded. A pipe here, with the daemon shutting down on end-of-file, would replaceCHILD_PIDand the Windows pid arm with one rule.
| tracing::warn!("could not tell which daemon is running; leaving it in place"); | ||
| false | ||
| } | ||
| ProductCheck::Mismatch if home.is_other() => { |
There was a problem hiding this comment.
🟢 After this arm the caller goes on to use the mismatching daemon when the binary is unchanged. That is a fair choice, but the doc's Product check row should say it, and it interacts with the call hold proposed in nodespace-docs PR 112.
| write(&other_marker, "{}"); | ||
|
|
||
| let _env = EnvGuard::set(&[ | ||
| ("HOME", Some(user.path())), |
There was a problem hiding this comment.
🟢 This sets HOME for the whole test process and relies on nextest's process-per-test. Related: grpc_client's resolve_socket_path_honors_env_override_then_falls_back still asserts the default is under dirs::home_dir(), which now fails when NODESPACE_HOME is set in the runner's environment; clear it there.
|
Not merging this. Please close it, and close #3616 as not planned. The design it implements (NodeSpaceAI/nodespace-docs#111) is not accepted; the reasoning is in the comment there: https://github.com/NodeSpaceAI/nodespace-docs/pull/111 In short: One change in the other direction is wanted: the app's existing read of the variable in |
Held for design discussion: proposes how the app's daemon management follows NODESPACE_HOME. Do not merge before it is agreed.
Closes #3616
Problem
An app started with
NODESPACE_HOMEset managed the user's real daemon. Every path indaemon_setupcame fromdirs::home_dir().NODESPACE_HOME.~/.nodespace/daemon*.sock.So an "isolated" app read and wrote the user's databases.
What changes
Implements the decision proposed in the docs PR below. A new
daemon_homemodule resolvesDaemonHome:NODESPACE_HOMEunset, or naming the user's home however spelled.User home: nothing changes. Every path is still
dirs::home_dir()'s. The registration code is moved verbatim intoregister_user_service, and a new golden test pins the plist byte for byte (run againstorigin/main's code too, below).Another home: a headless child process, no service manager.
<home>/.nodespace/. This usesdaemon_setup::home_dir(),incompatible_database::marker_path()andgrpc_client::default_socket_path().NODESPACED_SOCKET, else<home>/.nodespace/daemon[-dev].sock, passed explicitly to the child (managed_socket_path). On Windows the default pipe is\\.\pipe\nodespace-daemon-<sha256 prefix of the home>.NODESPACED_SOCKET.start_daemondispatches on the home and takes the registration and the child spawn as closures, so tests stand in for the service manager.evict_if_other_producttakes the home).taskkill /IM. The log-rotation restart is skipped for another home.Tests
All run with scratch homes. None runs
launchctl,systemctlorreg: the service manager is a counting closure, and the child is/usr/bin/env, which prints its environment into the daemon log.Each new test was mutation-checked on its own: one mutation applied, that test run, the file restored. Every mutation was caught by the assertion it targets.
macos_plist_keepalive_tests::the_user_home_plist_is_unchanged_byte_for_byte(golden, full text)RunAtLoadtrue→falseinwrite_plistorigin/main's code (main's four source files restored, only the test added)daemon_home::tests::unset_or_the_users_own_directory_is_the_users_home$HOMEbecomes "another home")daemon_home::tests::any_other_directory_is_another_homeNODESPACE_HOMEresolves toUserdaemon_home::tests::another_home_gets_its_own_pipedaemon_home::tests::another_homes_daemon_is_stopped_by_process_id_never_by_nametaskkill /IMdaemon_home::tests::the_child_runs_headless_on_the_homes_endpoint--tray)daemon_home::tests::a_socket_path_too_long_to_bind_is_refused_before_startingproduct_eviction_tests::another_homes_mismatch_never_boots_outstart_daemon_tests::only_the_users_home_is_registered_with_the_service_managerstart_daemonregisters another homeother_home_tests::another_home_never_touches_the_users_home(end to end:HOME/NODESPACE_HOMEscratch dirs, counting registration,/usr/bin/envas the child)home_dir()back todirs::home_dir()dirs::home_dir()NODESPACED_SOCKET$HOME$HOMEformulaunix_quit_signal_tests::a_program_path_with_spaces_is_matched_whole(review round 1: pid ownership under a home path with spaces)Also run, all in the machine slot:
cargo clippy -p nodespace-app-lib --all-targets -- -D warnings -D clippy::unused-async: clean.cargo nextest run -p nodespace-app-lib --lib: 349 of 349 pass (head3ac18333).bun test scripts/: 1448 pass.bun run scripts/check-pro-boundary.ts: clean.No frontend files changed. Windows-only lines (
resolve_pipe_name, the twotaskkillcall sites,CREATE_NO_WINDOW, the rotation early return) are not compiled on this Mac. Their logic lives in the pure functions tested above.Coordination
ensure_daemon_runningintorecord_outcome(bring_up_daemon(..)). This PR changes the first line of that body and the registration block, and adds no newreturn, so a rebase either way is a mechanical conflict.Documentation
architecture/daemon-cli-lifecycle.md, plus a correction todaemon-cli-protocol.md's socket claim.🤖 Generated with Claude Code