sorcar-cloud — correctness review and live end-to-end test

Target host ksen@34.42.88.157 (Ubuntu 24.04, 32 vCPU GCP VM) · script version reviewed: 924 lines · 4 deployments executed · 4 defects found and fixed · commit 936fcf2f

What the script is supposed to do

sorcar-cloud takes the checkout it lives in — including uncommitted edits — pushes it to a Linux box over SSH, installs KISS Sorcar there, starts code-server (VS Code in the browser) plus the kiss-web webapp, tunnels both back to the laptop, and opens the browser. Its sibling sorcar-linux curls a fresh clone from GitHub instead; the whole point of sorcar-cloud is that the remote runs your working directory.

1SSH reachability check, resolve remote $HOME, derive the project name (a git worktree maps to the main repo's basename, e.g. kiss)local
2Delete ~/kiss on the remote, then stream the tree there with tar | ssh tar (or rsync for --keep-remote)transport
3Mirror ~/.kiss/ (models, tokens, config) minus host-local state; rewrite work_dir; copy ~/.ssh/ without authorized_keysstate
4Distill the laptop's API-key rc file into api_keys.env + a systemd EnvironmentFilesecrets
5Install code-server + a code shim, give the copy a real git repo, drop a foreign .venv, run the full install.shremote
6Relaunch code-server on 127.0.0.1:$PORT, open the SSH tunnels, open the browser (which activates the extension, which starts kiss-web), then poll the webapplaunch

Verdict

The script's architecture is sound and unusually well reasoned — the exclusion lists, the “browser must come before the webapp check” ordering, and the foreign-.venv guard all address real failure modes. Static analysis was clean (bash -n passes; shellcheck reports only intentional SC2029 client-side expansions, cosmetic SC2088 tildes inside message strings, and one SC2015 that is correct as written).

Four real defects were found. One of them was the direct cause of the “Could not request local forwarding / is local port 8080 already in use?” failure that started this thread, and — importantly — it was introduced by an earlier well-intentioned patch in this same session. All four are fixed and verified against the live VM.

Defect 1 — the tunnel PID was never found, so tunnels leaked

bug fixed

The script backgrounded the tunnel with ssh -f and then tried to recover its PID by pattern-matching the process table:

TUNNEL_PID="$(pgrep -f "ssh -f -N.*-L ${LOCAL_PORT}:127.0.0.1:${PORT}.*${TARGET}" | head -1 || true)"

The pattern assumed that when ssh forks for -f it rewrites its own argv into -f -N order. I tested that assumption directly:

$ ssh -N -f -o ExitOnForwardFailure=yes -L 19099:127.0.0.1:8080 ksen@34.42.88.157
$ ps -Ao pid,command | grep 19099
16570 ssh -N -f -o ExitOnForwardFailure=yes -L 19099:127.0.0.1:8080 ksen@34.42.88.157

$ pgrep -f "ssh -f -N.*19099..."   →  NO MATCH
$ pgrep -f "ssh -N -f.*19099..."   →  16570

ssh does not reorder its argv. The flags stay exactly as typed, so the pattern could never match, TUNNEL_PID was always empty, the if [[ -n "$TUNNEL_PID" ]] block at the end was skipped, the Ctrl+C trap was never installed — and the script returned immediately after printing its banner, leaving a detached ssh -f holding local port 8080 forever. The next run then died with ExitOnForwardFailure. (The stale tunnel seen earlier looked like -f -N simply because an older version of the script had invoked it in that order.)

The fix: own the process instead of guessing

TUNNEL_ARGS=(-N -n -o ExitOnForwardFailure=yes -o ServerAliveInterval=30
             -L "${LOCAL_PORT}:127.0.0.1:${PORT}")
...
ssh "${TUNNEL_ARGS[@]}" "$TARGET" &
TUNNEL_PID=$!
for _ in $(seq 1 10); do
    kill -0 "$TUNNEL_PID" 2>/dev/null || break
    sleep 0.5
done
kill -0 "$TUNNEL_PID" 2>/dev/null \
    || die "Could not open the SSH tunnels (is local port $LOCAL_PORT already in use?)."

An interesting near-miss: why not probe the port

My first version confirmed readiness with nc -z 127.0.0.1 $LOCAL_PORT. Adversarial testing killed that idea: with a foreign listener already on 8080, nc succeeds on the first iteration, the loop breaks instantly, and kill -0 still sees our ssh in the sub-second before ExitOnForwardFailure terminates it — a false success.

# nc-based check, busy port 8080
[INFO] Tunnels are up (ssh pid 18192)          ← wrong
./t.sh: line 22: 18192 Terminated: 15 ssh ...
PORT STILL BOUND (bad)

# final check, busy port 8080
[ERR] Could not open the SSH tunnels (is local port 8080 already in use?).   exit=1   ← right

A port probe answers “someone is listening”, never “my forward is listening”. The probe was removed.

Defect 2 — install.sh could pull GitHub's main over the code you just shipped

bug fixed

Step 7g runs the full install.sh on the remote, whose update_repo() does git stash push --include-untrackedgit fetch origingit pull --ff-only, and on divergence git reset --hard '@{upstream}'.

When the laptop checkout is a git worktree, step 7e replaces the dangling .git pointer with a fresh local repo that has no origin, so the fetch fails harmlessly (that is the WARNING: git fetch failed (offline?) line in the log). But when sorcar-cloud is run from an ordinary clone, .git — and therefore origin and the upstream tracking branch — travels verbatim. The remote then quietly fast-forwards to origin/main, contradicting the script's headline promise to deploy this exact working directory; and if the auto-stash of your uncommitted edits fails to re-apply, those edits are stranded in a stash on a machine you don't think of as a checkout.

Fix — a new opt-out in install.sh, matching the existing KISS_SKIP_LAUNCH idiom, which sorcar-cloud now sets:

# install.sh, update_repo()
if [ -n "${KISS_SKIP_UPDATE:-}" ]; then
    echo "   KISS_SKIP_UPDATE set — installing this checkout as-is, no pull."
    return 0
fi

# sorcar-cloud, step 7g
KISS_CODE_CLI=code-server KISS_SKIP_LAUNCH=1 KISS_SKIP_UPDATE=1 \
    bash "$WORKSPACE/install.sh"

Confirmed in the live log: >>> Updating kiss_ai repository... is now followed by KISS_SKIP_UPDATE set — installing this checkout as-is, no pull.

Defect 3 — the ~/.kiss mirror could delete the remote's API keys

bug fixed

Section 5a mirrors ~/.kiss/ with --delete so retired local overrides can't linger on the remote. But api_keys.env and api_keys.systemd.env exist only on the remote — step 6 creates them there. Since the mirror runs before step 6, every run deleted the remote's credential files and step 6 recreated them… unless the laptop has no rc file containing API_KEY, in which case step 6 is skipped entirely and the remote is left with no keys, a dead kiss-web unit (EnvironmentFile gone) and an extension that prompts for keys. Both names are now excluded from the mirror, so the copy can only ever add credentials, never remove them.

Defect 4 — deploying from the main repo shipped every agent worktree

wasteful fixed

Agent tasks run in <repo>/.kiss-worktrees/kiss_wt-*/. Each is a full second checkout (node_modules included) whose .git is a one-line file pointing at an absolute laptop path that does not exist on the remote. Copying them multiplies the transfer for zero benefit and drags dead git pointers into the snapshot commit step 7e creates. .kiss-worktrees is now excluded in both transports (the tar stream and the rsync path), next to the existing .venv filter.

Live end-to-end test

Four deployments were run against the VM, covering both transports, both tunnel outcomes, and a real agent task.

RunModeWhat it provedResult
1full wipe + tunnel Webapp port auto-probe (8787 busy → 18787), worktree → project name kiss, 208 MB copy, install.sh to completion, kiss-web live through the tunnel, Tunnels are up (ssh pid …) + hold loop pass
2full wipe, SORCAR_NO_TUNNEL=1 Final script incl. KISS_SKIP_UPDATE; exit 0; correct manual-tunnel hint printed pass
3--keep-remote rsync delta path: 1462 files scanned, 5 transferred; .venv kept (“native and complete on this host”); .git left alone; IDE port auto-probe fired for realLocal port 8080 is busy — tunneling the remote IDE to local port 18080 instead pass
4full wipe + tunnel, killed with SIGTERM Trap teardown: Closing the SSH tunnels..., after which nothing listens on local 8080 and no ssh tunnel process remains — the leak that caused the original bug report pass

Running a real task in the remote IDE

The tunneled IDE was opened in a browser, which redirected to http://localhost:8080/?folder=/home/ksen/kiss — confirming step 7h's coder.json query.folder rewrite. The workbench rendered the pushed tree (including a .venv that exists only because the remote built it from uv.lock), the KISS Sorcar panel activated, and the status bar advertised Forwarded Ports: 8787, … — i.e. the extension had started kiss-web.

A 257-byte task file was created on the VM, opened in the editor, selected with ⌘A (Ln 2, Col 1 (257 selected)), and submitted with ⌘E (kissSorcar.runSelection). The panel reported Done (7s) · Tokens 18,076 · Cost $0.0668 · Steps 2. The VM's own task database tells the rest:

-- ~/.kiss/sorcar.db, newest task_history row
task     : Run `uname -a` and `hostname` and `python3 -c "print(2**16)"` …
result   : <p>ksen-vm-32.c.r2eg-441800.internal</p>  <p>65536</p>
model    : claude-opus-5      work_dir : /home/ksen/kiss
version  : 2026.7.32          tokens   : 18076     cost : 0.066848
is_worktree : 1               auto_commit_mode : 1

-- events (event_json)
tool_call     Bash: mkdir -p tmp && { uname -a; hostname; python3 -c "print(2**16)"; } | tee tmp/remote_proof.txt
system_output Linux ksen-vm-32.c.r2eg-441800.internal 6.17.0-1021-gcp #24~24.04.1-Ubuntu … x86_64 GNU/Linux
system_output ksen-vm-32.c.r2eg-441800.internal / 65536
tool_call     finish(success=True)

The GCP kernel string and the VM's internal hostname could not have come from the laptop, so the task genuinely ran on the remote machine — and the successful claude-opus-5 call independently proves the whole API-key pipeline (~/.zshrcapi_keys.env → code-server → agent) works there.

One thing that looks like a failure but isn't: tmp/remote_proof.txt is absent from ~/kiss. The task ran with worktree isolation (is_worktree: 1) inside .kiss-worktrees/kiss_wt-*/, and .gitignore line 29 is tmp/ — an ignored file is never committed, so it was discarded with the worktree. Normal KISS behaviour, not a deployment problem.

Observations left as-is (no change made)

Change summary

FileChangeWhy
sorcar-cloud §8/§11ssh -N -n … & + $!, alive-after-grace readiness check, unconditional trapDefect 1 — orphaned tunnel held local $PORT
sorcar-cloud §7gpass KISS_SKIP_UPDATE=1Defect 2 — pull overwrote the shipped tree
install.sh update_repo()honour KISS_SKIP_UPDATEDefect 2
sorcar-cloud §5a--exclude='api_keys.env', --exclude='api_keys.systemd.env'Defect 3 — --delete erased remote credentials
sorcar-cloud copy_tree()--exclude='.kiss-worktrees' in both transportsDefect 4 — shipped sibling worktrees

bash -n passes on both files; shellcheck shows no new findings. Committed as 936fcf2f.