fix(ssh): fix reconnect issues and node binary path resolution#2504
fix(ssh): fix reconnect issues and node binary path resolution#2504juliusmarminge merged 7 commits intomainfrom
Conversation
|
Important Review skippedAuto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: Repository UI Review profile: CHILL Plan: Pro Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
✨ Finishing Touches🧪 Generate unit tests (beta)
Comment |
eed1673 to
1a50e27
Compare
1a50e27 to
4ab0d12
Compare
4ab0d12 to
9e3adf0
Compare
9e3adf0 to
2c9f59c
Compare
Pre-flight now reads `apps/server`'s `engines.node` and rejects WSL distros whose Node version is too old, naming the actual version and the required range. Without this we'd let an outdated Node fall through to node-gyp/runtime errors that don't mention version at all. The semver-range check is ported from the JS used by the SSH launcher's remote node check in PR pingdotgg#2504. When that PR merges (or this one does), whichever lands second extracts the helper into `packages/shared` so both call sites can drop their copy.
Co-authored-by: codex <codex@users.noreply.github.com>
Co-authored-by: codex <codex@users.noreply.github.com>
- Parse remote launch and pairing JSON from the last JSON suffix - Add regression coverage for pretty-printed pairing output after shell noise
ApprovabilityVerdict: Needs human review This PR introduces substantial SSH launch behavior changes including node version manager detection and reconnect handling. Three unresolved review comments identify potential bugs: JSON extraction may fail with certain shell output, misleading error messages, and risk of killing external servers. These substantive issues warrant human review. You can customize Macroscope's approvability policy. Learn more. |
- Move version comparison and JSON extraction into `@t3tools/shared` - Reuse shared semver checks in provider and SSH maintenance code - Add tests for semver, JSON extraction, and tunnel script generation
| return yield* Effect.fail(error); | ||
| }), | ||
| ), | ||
| ); |
There was a problem hiding this comment.
JSON extraction finds first brace, not last payload
Medium Severity
decodeRemoteJsonOutput falls back to extractJsonObject which finds the first { in stdout, not the last JSON payload. If shell startup noise from version managers contains any { character (e.g., nvm: {default} or asdf: update {notice}), the extraction grabs the wrong substring, decoding fails, and the valid JSON payload later in the output is never attempted — causing a spurious SSH launch or pairing failure.
Additional Locations (1)
Reviewed by Cursor Bugbot for commit 394dc29. Configure here.
| if ! ensure_remote_node_path; then | ||
| printf 'Remote host is missing node on PATH. Install Node or configure a supported version manager for non-interactive shells.\\n' >&2 | ||
| exit 1 | ||
| fi |
There was a problem hiding this comment.
Misleading error when node version fails range check
Low Severity
When ensure_remote_node_path fails because the discovered node doesn't satisfy the engine range (not because node is missing), the launch script prints "Remote host is missing node on PATH" which contradicts the earlier stderr message from remote_node_satisfies_engine saying the node version doesn't satisfy the required range. Users troubleshooting SSH launch failures would see conflicting guidance.
Reviewed by Cursor Bugbot for commit 394dc29. Configure here.
- Keep the default remote port available when clearing a managed tunnel - Update restart logic to rewrite port and managed markers instead of removing them - Adjust script test coverage for the new state handling
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 1 potential issue.
There are 3 total unresolved issues (including 2 from previous reviews).
Autofix Details
Bugbot Autofix prepared a fix for the issue found in the latest run.
- ✅ Fixed: Caret operator incorrect for semver major version zero
- Added special-case handling for the caret operator when major is zero: pins minor when major=0 and minor!=0, and pins patch when both major=0 and minor=0, matching the semver specification.
Or push these changes by commenting:
@cursor push 6d301562d5
Preview (6d301562d5)
diff --git a/packages/shared/src/semver.ts b/packages/shared/src/semver.ts
--- a/packages/shared/src/semver.ts
+++ b/packages/shared/src/semver.ts
@@ -188,7 +188,20 @@
const operator = match[1] || "=";
switch (operator) {
case "^":
- return version.major === target.major && compared >= 0;
+ if (target.major !== 0) {
+ return version.major === target.major && compared >= 0;
+ }
+ if (target.minor !== 0) {
+ return (
+ version.major === target.major && version.minor === target.minor && compared >= 0
+ );
+ }
+ return (
+ version.major === target.major &&
+ version.minor === target.minor &&
+ version.patch === target.patch &&
+ compared >= 0
+ );
case ">=":
return compared >= 0;
case ">":You can send follow-ups to the cloud agent here.
- Correct caret range handling for 0.x and 0.0.x versions - Reject non-positive runtime PIDs in the SSH tunnel script
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 1 potential issue.
There are 3 total unresolved issues (including 2 from previous reviews).
Bugbot Autofix prepared a fix for the issue found in the latest run.
- ✅ Fixed: Fallback PID may kill external server unintentionally
- Removed the ${REMOTE_PID:-$DEFAULT_RUNTIME_PID} fallback so PID_TO_STOP is set to $REMOTE_PID directly, preventing an empty REMOTE_PID from resolving to the external server's PID.
Or push these changes by commenting:
@cursor push a2607dec13
Preview (a2607dec13)
diff --git a/packages/ssh/src/tunnel.ts b/packages/ssh/src/tunnel.ts
--- a/packages/ssh/src/tunnel.ts
+++ b/packages/ssh/src/tunnel.ts
@@ -546,7 +546,7 @@
REMOTE_PORT="$DEFAULT_REMOTE_PORT"
if wait_ready "@@T3_REUSE_READY_TIMEOUT_MS@@"; then
if [ "$REMOTE_MANAGED" = "managed" ]; then
- PID_TO_STOP="\${REMOTE_PID:-$DEFAULT_RUNTIME_PID}"
+ PID_TO_STOP="$REMOTE_PID"
if [ -n "$PID_TO_STOP" ] && kill -0 "$PID_TO_STOP" 2>/dev/null; then
kill "$PID_TO_STOP" 2>/dev/null || true
wait_for_pid_exit "$PID_TO_STOP"You can send follow-ups to the cloud agent here.
Reviewed by Cursor Bugbot for commit c4be90c. Configure here.
| kill "$REMOTE_PID" 2>/dev/null || true | ||
| wait_for_pid_exit "$REMOTE_PID" | ||
| if [ "$REMOTE_MANAGED" = "managed" ]; then | ||
| PID_TO_STOP="\${REMOTE_PID:-$DEFAULT_RUNTIME_PID}" |
There was a problem hiding this comment.
Fallback PID may kill external server unintentionally
Medium Severity
When REMOTE_MANAGED is "managed" but REMOTE_PID is empty (e.g., PID file was deleted or corrupted), PID_TO_STOP falls back to DEFAULT_RUNTIME_PID via "${REMOTE_PID:-$DEFAULT_RUNTIME_PID}". DEFAULT_RUNTIME_PID comes from server-runtime.json and may represent an externally started server. Killing it defeats the purpose of adopting an external server, as the subsequent wait_ready on that port will fail, forcing an unnecessary full relaunch. The fallback should only kill the process we actually launched (i.e., only when REMOTE_PID is non-empty).
Reviewed by Cursor Bugbot for commit c4be90c. Configure here.



Summary
Fixes SSH launcher failure modes reported from remote Linux hosts:
sh -sSSH sessions before running remote Node helper scripts or the T3 runnerapps/server/package.json'sengines.noderange and the selected remote Node is validated before launchREMOTE.mdnow documents SSH launch troubleshooting, Node requirements, version-manager resolution, and reconnect cleanup behaviorCloses #2534
Root Cause
The remote bootstrap ran under non-interactive
sh, so hosts with Node exposed only through shell setup from a version manager could fail withnode: command not foundduring port picking/readiness. Even when a Node binary was found, the launcher did not check that it satisfied the server package's runtime engine range before starting helper scripts and the server. Separately, the shared remote runtime file could make a launcher-managed process look like an external server before runner-change restart logic ran, leaving users stuck after updates unless they manually killed remotet3codestate.Validation
bun fmtbun lint(passes with existing warnings)bun typecheckbun run --filter @t3tools/ssh testNote
Medium Risk
Medium risk: changes the SSH bootstrap/launch scripts and remote server reuse logic, which can affect connectivity and remote process lifecycle across diverse host environments. Also replaces version-comparison utilities used for provider/CLI gating, which could alter upgrade/advisory behavior if edge cases differ.
Overview
Hardens desktop-managed SSH launch/reconnect on remote hosts. The SSH bootstrap now tries to locate/activate a compatible remote
nodein non-interactiveshsessions (common paths + Volta/asdf/mise/fnm/nodenv/nvm), and can enforce a required Node engine range before running helper scripts or startingt3.Launch/pairing output decoding is made resilient to shell startup noise by extracting the JSON object from stdout before decoding. Reconnect behavior is adjusted to avoid treating stale launcher-managed servers as “external” by stopping them and clearing/refreshing saved PID/port state.
Separately, a shared
@t3tools/shared/semvermodule is introduced (with tests) and replaces the server’s customcliVersioncomparator;extractJsonObjectis moved into@t3tools/shared/schemaJsonand reused by text-generation and SSH parsing.REMOTE.mdgains detailed SSH launch troubleshooting and Node requirements.Reviewed by Cursor Bugbot for commit c4be90c. Bugbot is set up for automated code reviews on this repo. Configure here.
Note
Fix SSH reconnect issues and Node binary path resolution for remote launch
REMOTE_NODE_ENV_SCRIPTin tunnel.tssatisfiesSemverRangeimplementation (valid plain JS for remote execution) is stringified and sent to the remote host to validate the Node version before proceedingdecodeRemoteJsonOutputhelper@t3tools/shared/semverpackage, replacing localcompareCliVersionsinClaudeProvider,OpenCodeProvider, andproviderMaintenanceextractJsonObjectto@t3tools/shared/schemaJson, removing duplication fromTextGenerationUtilsMacroscope summarized c4be90c.