Repository navigation
fix: 15 findings from the whole-repo code review - #130
Merged
Merged
Conversation
The API has no authentication and can create and run arbitrary shell commands, so listening on every interface let any host on the LAN run code as the developer. Only /mcp had a loopback guard; /api exposed the same surface without one. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Only /mcp checked that callers were on this machine. /api exposes the same command-running surface, so a DNS-rebinding page could drive it, and any cross-site page could fire bodyless POSTs (run a profile, restart a command, close the workspace) that skip the CORS preflight. The check now runs as an onRequest hook for all routes; /mcp keeps the same 403 behaviour. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
UNIQUE (scope, profile, key) never fires for global vars because SQLite treats NULL profiles as distinct, so each PUT inserted a new row and the API answered with the oldest value. Deleting one copy left another one active. Upserts now conflict on a COALESCE expression index, and opening an existing DB keeps only the newest row of each duplicated var. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The server always resolved a command's env through the first profile that listed it, so running "staging" started a command shared with "dev" using dev's env, while the .env files were compiled with staging's. The queue now passes its launch-profile tag to the env resolver, falling back to the first profile only when the command was started outside a known profile. This matches what `conductor run` already did. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
restartQueued awaited the in-flight start once and then registered its own restart. Two restarts waiting on the same promise resumed together, both stopped the old wrapper and both spawned a new one; `wrappers` kept the second and the first process ran on untracked, holding its port. It now waits until no start or restart is in flight before registering. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Starting a command that was already running force-kills its process first. forceKillAndWait never marked that as intentional, so the exit (code 137) recorded a "crashed" notification, scheduled an auto-restart under on_failure/always, and then a "recovered" one. It now flags the stop as intentional before killing, like stop() does. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A service that handles SIGTERM exits 0, so after the user stopped it the wrapper read "stopped" with exit code 0, and both isDependencyReady and waitForDependency accepted that as a satisfied dependency: restarting a dependent ran it against a service that was down. Only a one-shot that completed on its own with exit 0 counts now; anything else is started again. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A `command` probe had no timeout: one that hung (curl without --max-time, pg_isready on a dropped host) left the command "starting" forever, and the health monitor spawned another probe every interval on top of it. Each run is now killed (whole process group) after 10s or the healthcheck's timeout_ms, whichever is lower, and the monitor skips ticks while a probe is still in flight. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The move endpoint persisted the removal from the source profile before adding to the target, so a target that was unknown or already listed the command answered 400 with the command gone from the source. The sync endpoint applied its adds and removes one write at a time, with the same partial-failure problem. Both now go through store methods that validate everything first and write the config once. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
SpawnQueue.close() returned as soon as every process had exited, but the last lines could still be in the pipes, and the session closed its SQLite DB right after. Those lines then hit insertLog on a closed DB, the pump's catch handler threw again, and `drained`, documented as never rejecting, rejected unhandled. close() now waits for every wrapper to drain (bounded to 2s), and `drained` swallows handler errors as its doc says. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
stop_signal accepted any string. process.kill throws ERR_UNKNOWN_SIGNAL on a name like "TERM", and killTree swallows every error as "group already gone", so the process never got the graceful signal: every stop waited the full stop_timeout_ms and then SIGKILLed it. The schema now only accepts signal names the OS knows. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
interpolateEnv made one pass over the raw values, so with
API_URL=http://${HOST}:3000 and CALLBACK=${API_URL}/cb the process got the
literal "http://${HOST}:3000/cb". References are now resolved recursively.
A reference back into a value being resolved (PATH=${PATH}:/x) reads the
inherited process.env value instead of looping or doubling the suffix.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Clearing the description in the edit modal sent `description: undefined`,
which JSON drops, so the body was `{}` and the API answered 400. A rename
to the same name with no description also wiped it, and a rename could
never clear it. The UI now sends "" for a cleared field; the store treats
an omitted description as "keep" and "" as "remove" in every branch.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
On quit the shell gave the sidecar 5s after SIGTERM, but the core waits up to each command's stop_timeout_ms (5s by default) plus a group sweep before escalating, so the SIGKILL often landed mid-shutdown. Managed processes run in their own process groups, so they survived holding their ports. The grace period is now 10s, and on escalation the shell SIGKILLs each child's process group before the sidecar itself. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Every packaged launch downloaded and installed any newer release and then restarted the app on its own, even after the user had opened a workspace and started processes, all of which the restart stopped. Startup now only checks: the UI shows a notice when a release is newer, and installing stays the explicit button in Environment → About. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The profile-env test printed "$TARGET" through the default shell, which is cmd.exe on Windows and doesn't expand it; it now reads the env from a shell:false bun script like the queue tests. The duplicate-env-var test removed its temp dir with the SQLite file still open, which Windows refuses (EBUSY); it now closes the DB first. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
PhantomDave
enabled auto-merge (squash)
October 22, 2026 22:14
…drain The drain wait added to SpawnQueue.close() hit its 2s cap on every close on Windows (open(current) went from ~0.7s to ~2.2s in CI), and the workspace tests that close with live processes then failed to remove their temp dirs (EBUSY). close() no longer waits; instead the session ignores log lines that arrive after it closed its DB, which is what the wait was protecting against. `drained` still never rejects. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Windows releases a just-closed SQLite file a moment later, so the duplicate-env-var test's cleanup failed with EBUSY even after closing the DB. It now retries like the workspace tests do. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
On Bun 1.4.4 Windows takes 1.5-2s to release a just-stopped child's cwd or a just-closed SQLite file: "open(current) is a no-op" went from ~0.7s on 1.4.3 to ~2.1s, on main as well. The 3s cleanup retry then sometimes runs out and the workspace and MCP tests fail in afterEach with EBUSY. The three copies of that retry loop are now one helper which, on Windows, leaves the temp dir for the OS once retries are exhausted instead of failing; the tests already check that the processes are gone. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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.
Fixes the 15 findings from an xhigh code review of the whole repo, one commit per finding. Each commit message explains the bug and the fix; review commit by commit.
Security
127.0.0.1instead of0.0.0.0. The API has no auth and can create and run shell commands, so any host on the LAN could run code as the developer.checkMcpRequest(nowcheckLocalRequestinsrc/local-guard.ts) only protected/mcp. It now runs as anonRequesthook for all routes, which blocks DNS rebinding and cross-site bodyless POSTs that skip the CORS preflight (run profile, restart command, close workspace…).Core correctness
NULLprofiles as distinct inUNIQUE. Upserts now conflict on aCOALESCEexpression index, and opening an existing DB keeps the newest row of each duplicate.conductor run.restartQueuedre-checks the single-flight map until it's free).commandhealthchecks had no timeout. Each run is killed (whole group) after 10s ortimeout_ms, and the monitor no longer stacks probes.drainednever rejects, as its doc said. (A first version waited for the pipes to drain inclose(); on Windows that hit its 2s cap on every close and broke the workspace tests, so it was replaced.)${VAR}references are resolved;PATH=${PATH}:/xextends the inherited value.stop_signalnames are rejected by the schema. Previouslyprocess.killthrew, killTree swallowed it, and every stop silently waited for SIGKILL.UI
undefined, which JSON drops, and the API answered 400.Desktop (Tauri)
pgrep -P) before the sidecar.Reviewer notes
.conductor.ymlwith an invalidstop_signal(e.g.TERM) now fails to load;bun test, lint, typecheck, prettier and the run-desktop sidecar smoke. Two healthcheck port tests (probeOnce - type: port,waitForHealthy … retrying) fail on my machine before and after these changes, becauselocalhostresolves only to::1there. CI should be unaffected.cargo checkonly; please runbun run --cwd packages/desktop-tauri devand quit with a running service, and check the update notice;🤖 Generated with Claude Code