Skip to content

feat(wsl): deploy and launch orcad inside a WSL distro (WSL-via-orcad C4) - #27819

Merged
nwparker merged 7 commits into
mainfrom
nwparker/wsl-c4-deploy-launch
Oct 13, 2026
Merged

nwparker merged 7 commits into
mainfrom
nwparker/wsl-c4-deploy-launch

Conversation

@nwparker

@nwparker nwparker commented Oct 12, 2026 •

Copy link
Copy Markdown
Contributor
Files Added Deleted Net
Test 11 $\color{#1a7f37}{\Huge{\mathbf{+}}}$​899 $\color{#cf222e}{\Huge{\mathbf{−}}}$​5 $\color{#1a7f37}{\Huge{\mathbf{+}}}$​894
Prod 17 $\color{#1a7f37}{\Huge{\mathbf{+}}}$​720 $\color{#cf222e}{\Huge{\mathbf{−}}}$​46 $\color{#1a7f37}{\Huge{\mathbf{+}}}$​674

Stacked on #27800 (C3, the distro holder). Do not merge.

ELI5

Orca can already install and start its server (orcad) on an SSH machine. This change lets it do the same inside a WSL distro on Windows. It copies the server in, starts it so that it keeps running after the command that started it ends, and makes sure the server finds the user's Linux tools (claude, codex) rather than the Windows copies that WSL also puts on PATH. Nothing in the UI changes yet. Step C5 will connect the desktop to this server.

What Changed

  • One entry point for C5: deployAndLaunchWslOrcad(distro, options?) in src/main/wsl/wsl-orcad-deploy.ts. It returns { outcome: 'ready', port, fullVersion, pid, distroId, readiness, holder }, or { outcome: 'deferred', code, reason, forceable } when live terminals would block an update. In order, it:
    1. refuses WSL 1 (OrcadHostUnsupportedError, the same refusal class SSH uses);
    2. takes the C3 holder (ensureHeld) before anything is launched;
    3. resolves the host context: platform, home, and libc/glibc target. An unsupported libc gets the same refusal as SSH, because the SSH resolver now runs here too;
    4. reuses the SSH flow: recovery, deployOrcad (install and activation fence/journal), the pinned Node store, the same census and downgrade admission, readiness, and version GC;
    5. starts the active slot again when the distro stop had ended it ("already active" but dead).
  • Context widening: OrcadRemoteContext<Target = SshTarget> and resolveOrcadRemoteContext are now generic over the host target. Every SSH call site still gets SshTarget with no edits. The SSH-only platform memo is skipped for WSL. The census and downgrade-admission policy moved into managedOrcadDeployPolicy (shared, same text), so SSH and WSL cannot drift.
  • Launch: orcadLaunchCommand(host, spec, detach) takes a new 'setsid' mode, picked by the transport's optional orcadLaunchDetach (the WSL transport sets it; SSH leaves it unset, so its command is byte-for-byte unchanged). The forked child writes its own PID because the setsid -f parent exits at once. busybox setsid has no -f, so on busybox it falls back to a backgrounded setsid. Readiness still comes from .orcad-readiness.
  • Upload: the WSL transport now defaults to tar over stdin, which C1 measured as faster. The /mnt copy stays as an option (uploadSource: 'guest-mount').
  • PATH (D8): orcad already hydrates PATH from $SHELL -ilc (it reports isPackaged), and getPreflightWslTarget is null on Linux, so orcad inside the guest is never treated as a WSL preflight context. Both are now pinned by tests. New: inside a WSL guest, local agent detection skips the Windows-drive PATH entries (drvfs/9p/virtiofs mounts, the same rule as the existing guest lookup script). This covers the preflight walk, the install-dir fallback, and the agent-hook CLI presence check.
  • Distro GUID for C2: readWslDistroRegistryId(distro) reads HKCU\…\Lxss\{guid}\DistributionName with a single reg.exe query through process-host, the existing convention (no PowerShell). The native registry addon cannot list subkeys.
  • C3 carry-over: adoptHolders now reports the distro name with its original casing (taken from the record), not the lowercase file name.

Why

Reusing the SSH install and activation code keeps one fence, journal, and admission policy for every orcad host. The context is generic, with SSH as the default type argument, rather than a wider union. A union would have meant editing about 26 SSH call sites that read SSH-only fields. The detach mode is a transport capability, not a target check, so existing transport fakes and SSH connections need no changes. We measured that nohup … & launched through wsl.exe --exec is killed when that wsl.exe exits on a distro without systemd. setsid -f puts the server in its own session, and it survives.

Linked Issue

Internal WSL-via-orcad tracker, step C4.

Visual Proof

N/A: no UI. This step has no user-visible surface; C7 adds the UI.

Testing

  • I manually tested these changes locally
  • Automated tests added/updated, or explained why not below

Real host (win-lowspec, Windows 11, WSL 2.7.10). I used a throwaway Ubuntu 24.04 rootfs imported as orca-c4-probe with systemd=false and a non-root user. That user had a fake ~/.local/bin/claude, and the Windows host had npm shims for claude, codex, and opencode on /mnt/c. I ran the bundled deployAndLaunchWslOrcad with Orca.exe as Node (ELECTRON_RUN_AS_NODE), using a template that carries this branch's orcad JS.

  • Cold deploy (install, pinned Node, activate): 22 s, outcome ready, holder: spawned, GUID read.
  • The same orcad PID was still alive on the next call (holder: adopted), after the launching Orca.exe had exited and the distro had sat idle for more than 20 s. Its session id equals its PID, which confirms setsid worked.
  • After a kill, the wake relaunch took 2.6–2.9 s from process start to the readiness line (three runs). The full call took about 11 s, mostly about 40 sequential wsl.exe round trips at roughly 180 ms each.
  • Detection under orcad's environment (SHELL=/bin/bash, WSL_DISTRO_NAME set):
    • Before hydration, the unfiltered PATH resolves claude, codex, and opencode to /mnt/c/.../npm/*.
    • After hydration with the filter: claude → /home/u/.local/bin/claude, and codex and opencode are not detected.
    • The first run caught the install-dir fallback re-admitting the shims; that is fixed in the second commit and covered by a regression test.
  • Cleanup: the distro is unregistered, the temp dir and terminal are removed, and the host was left as found.

Automated. New tests cover target refusals (WSL 1, libc), holder-before-launch ordering, the deferral path, the wake of a dead active slot, the setsid launch (a nohup regression test, plus a Linux-only test that really detaches), the hydration gate, /mnt filtering, the registry parse, and the holder casing. src/main/ssh, src/main/wsl, src/main/orcad, src/main/ipc, and src/main/agent-hooks pass under config/vitest.config.ts. A few real-shell timing tests are flaky under full parallel load and pass when run alone. pnpm tc, check:code-quality:changed, and the static-analysis ratchets also pass. I ran the generated launch command in ubuntu:20.04 (util-linux setsid -f) and alpine:3.20 (busybox fallback) containers.

AI Disclosure

Review

Agent skill upstream boundary

  • Not applicable, or this change copies or mechanically translates no upstream skill-installer source, tests, fixtures, registry entries, path tables, comments, or documentation.

Notes

  • SSH: the launch command, context resolution, and admission behavior are unchanged.
  • Wire: no protocol or RPC change.
  • Windows: the registry is read with one reg.exe through process-host. The PATH filter only applies inside a WSL guest (detected by WSL_DISTRO_NAME, WSL_INTEROP, or the interop binfmt).
  • Open: orcad startup is about 2.7 s on this low-spec host, above the 2 s target. Repeated wsl.exe probes dominate the wake path.

Checklist

  • This PR is small and focused
  • I explained what changed and why (ELI5, the user-facing before/after, the mechanism, and why over the alternatives)
  • Before/after screenshots or videos attached for UI changes, or N/A with reason
  • Self-reviewed for correctness, security, and performance
  • Cross-platform, SSH/remote, and path/shortcut impact considered (or N/A)
  • pnpm lint, pnpm typecheck, pnpm test, and pnpm build pass (or CI will cover; local preferred)

…as a WSL host

SSH-managed orcad inside a distro and Orca under WSLg kept detecting
drive-mounted CLIs before this stack; the setsid launch now marks itself
with ORCA_WSL_ORCAD_HOST=1 and only that process filters.
… WSL host orcad

On Windows the preflight env merges the persisted registry Path, which
the fallback never searched before; only the marked WSL host runtime
now passes a filtered PATH.
@nwparker
nwparker force-pushed the nwparker/wsl-c4-deploy-launch branch from b7ce5f1 to deaf720 Compare October 13, 2026 07:00
@nwparker
nwparker changed the base branch from nwparker/wsl-c3-distro-holder to main October 13, 2026 07:00
@nwparker
nwparker marked this pull request as ready for review October 13, 2026 07:01
@nwparker

Copy link
Copy Markdown
Contributor Author

Merge-train review (blast radius outside WSL): the /mnt/ PATH filter in agent detection/preflight was active in any WSL guest process (SSH-managed orcad inside a distro, Orca under WSLg). It now applies only to an orcad started by the WSL-host setsid launch (ORCA_WSL_ORCAD_HOST=1); the SSH nohup launch stays byte-identical. The install-dir fallback also keeps searching the process PATH on Windows (it had started receiving the registry-merged Path). Regression tests added.

@coderabbitai

coderabbitai Bot commented Oct 13, 2026 •

Copy link
Copy Markdown
Contributor

Review in Change Stack →

📝 Walkthrough

The changes add WSL guest Windows-drive mount detection and filter those mounts from selected CLI searches and preflight PATH values. They also add WSL Orcad deployment and instance adoption, support setsid launches for WSL hosts, preserve distro-name casing in holder results, and add distro registry-ID lookup.

Priority: ➖ Normal

Merge Risk: 🔵 Low · up to deaf7

Deployment failures may report a less useful error, and a transient mount-read failure can make later CLI checks search Windows-drive paths. These bounded issues merit correction or owner acceptance before merging.

Pre-merge checks · Passed 4 · Failed 1

Failed Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage Warning Docstring coverage is 38.30% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 47 functions across 27 files. (1 skipped:… Write docstrings for the functions missing them to satisfy the coverage threshold.
Passed Passed checks (4 passed)
Check name Status Explanation
Linked Issues check Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check Passed Check skipped because no linked issues were found for this pull request.
Title check Passed The title clearly identifies the primary change: deploying and launching Orcad inside a WSL distro.
Description check Passed The description is comprehensive and covers the required sections, including scope, rationale, testing, platform impact, visual proof, and checklist status. It identifies an internal tracker item but …
Full details: Docstring Coverage

Explanation

Docstring coverage is 38.30% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 47 functions across 27 files. (1 skipped: 1 unsupported.)

  • Fix all pre-merge checks with AI
✨ Finishing Touches 💡 2
📝 Generate docstrings 💡
  • Commit to this branch
  • Create a new PR
🛠️ Fix failing CI checks 💡
  • Commit to this branch
  • Create a new PR
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2


ℹ️ Review info
⚙️ Run configuration
  • Configuration used: Repository UI
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: 454c2521-9232-46d8-82f6-7004fb11067c
📥 Commits

Reviewing files that changed from the base of the PR and between 099bb76 and deaf720.

📒 Files selected for processing (28)
  • config/tsconfig.cli.json
  • src/main/agent-hooks/local-agent-cli-presence.test.ts
  • src/main/agent-hooks/local-agent-cli-presence.ts
  • src/main/ipc/preflight-local-env.ts
  • src/main/ipc/wsl-guest-windows-drive-path.test.ts
  • src/main/ipc/wsl-guest-windows-drive-path.ts
  • src/main/orcad/orcad-wsl-guest-shell-path.test.ts
  • src/main/preflight/agent-command-detection.test.ts
  • src/main/preflight/agent-command-detection.ts
  • src/main/ssh/orcad-host-transport.ts
  • src/main/ssh/orcad-managed-runtime-context.ts
  • src/main/ssh/orcad-remote-context.ts
  • src/main/ssh/orcad-remote-launch-setsid.test.ts
  • src/main/ssh/orcad-remote-launch.ts
  • src/main/ssh/orcad-remote-runtime-control.ts
  • src/main/ssh/orcad-runtime-deployment.ts
  • src/main/wsl/wsl-distro-holder.test.ts
  • src/main/wsl/wsl-distro-holder.ts
  • src/main/wsl/wsl-distro-registry-id.test.ts
  • src/main/wsl/wsl-distro-registry-id.ts
  • src/main/wsl/wsl-orcad-deploy.test.ts
  • src/main/wsl/wsl-orcad-deploy.ts
  • src/main/wsl/wsl-orcad-host-transport.test.ts
  • src/main/wsl/wsl-orcad-host-transport.ts
  • src/main/wsl/wsl-orcad-instance-adoption.test.ts
  • src/main/wsl/wsl-orcad-instance-adoption.ts
  • src/shared/agent-cli-install-dir-fallback.test.ts
  • src/shared/local-agent-install-dir-detection.ts

Included review availability: This review used your included allowance. Your plan provides up to 10 included reviews per hour; 7 remain after this review.

// Unreadable: keep PATH as is rather than guess which entries are Windows drives.
}
}
cachedGuestMounts = mounts

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

🔎 Supported by static analysis

🏁 Script executed:

sed -n '154,200p' src/main/agent-hooks/local-agent-cli-presence.ts
sed -n '15,48p' src/main/ipc/preflight-local-env.ts
sed -n '20,53p' src/main/preflight/agent-command-detection.ts
sed -n '69,91p' src/main/ipc/wsl-guest-windows-drive-path.ts

Repository: stablyai/orca

Length of output: 4439


🏁 Script executed:

set -o pipefail
printf '%s\n' '--- local-agent-cli-presence imports and caller guards ---'
sed -n '1,80p' src/main/agent-hooks/local-agent-cli-presence.ts
sed -n '120,250p' src/main/agent-hooks/local-agent-cli-presence.ts
printf '%s\n' '--- preflight command detection complete relevant flow ---'
sed -n '1,130p' src/main/preflight/agent-command-detection.ts
printf '%s\n' '--- preflight local environment callers ---'
rg -n -F --glob '*.ts' -e 'buildLocalPreflightEnv' -e 'detectLocalManagedAgentCliPresence' -e 'detectCommandsInInstallDirs' -e 'isCommandOnPath' src/main
printf '%s\n' '--- definitions of path detection helpers ---'
rg -n -F --glob '*.ts' -e 'function isCommandOnPath' -e 'export async function isCommandOnPath' -e 'function detectCommandsInInstallDirs' -e 'export function detectCommandsInInstallDirs' src/main

Repository: stablyai/orca

Length of output: 15275


🏁 Script executed:

set -o pipefail
printf '%s\n' '--- managed-agent hook caller ---'
sed -n '120,185p' src/main/agent-hooks/managed-agent-hook-controls.ts
printf '%s\n' '--- preflight command execution helpers ---'
sed -n '1,95p' src/main/ipc/preflight-command-exec.ts
sed -n '220,270p' src/main/ipc/preflight-command-exec.ts
sed -n '145,185p' src/main/ipc/preflight-command-exec.ts
printf '%s\n' '--- install-directory fallback ---'
sed -n '1,220p' src/shared/local-agent-install-dir-detection.ts
printf '%s\n' '--- preflight WSL target resolution ---'
sed -n '1,180p' src/main/ipc/preflight-runtime-target.ts

Repository: stablyai/orca

Length of output: 11902


Do not cache failed mount discovery.

When /proc/mounts cannot be read, readGuestWindowsDriveMounts() caches []. Later CLI detection and generic preflight then search an unfiltered PATH, so /mnt/<drive> entries can be treated as valid candidates. Skip caching on the failure path.

Suggested fix
--- "a/src/main/ipc/wsl-guest-windows-drive-path.ts"
+++ "b/src/main/ipc/wsl-guest-windows-drive-path.ts"
@@ -72,16 +72,20 @@
   if (cachedGuestMounts) {
     return cachedGuestMounts
   }
   let mounts: string[] = []
+  let cacheable = true
   if (isWslHostOrcadProcess()) {
     try {
       mounts = parseWindowsDriveMountPoints(readFileSync('/proc/mounts', 'utf8'))
     } catch {
-      // Unreadable: keep PATH as is rather than guess which entries are Windows drives.
+      // Unreadable: keep PATH as is rather than guess; retry on the next call.
+      cacheable = false
     }
   }
-  cachedGuestMounts = mounts
+  if (cacheable) {
+    cachedGuestMounts = mounts
+  }
   return mounts
 }
 
 /** @internal - tests need a fresh mount read. */

Comment on lines +144 to +150
if (deployed.outcome === 'already-active') {
// A stopped distro ended the active slot with it; start it again only on proven exit.
const wake = await wakeStoppedManagedOrcad(slot)
if (wake.outcome === 'recovery-refused') {
throw new Error(wake.reason)
}
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

🔎 Supported by static analysis

🏁 Script executed:

#!/bin/bash
rg -nP -C3 "outcome:\s*'" src/main/ssh/orcad-managed-wake.ts

Repository: stablyai/orca

Length of output: 2279


🏁 Script executed:

sed -n '110,180p' src/main/wsl/wsl-orcad-deploy.ts
rg -n -F -- 'probeManagedOrcadReadiness' src/main
rg -n -F -- 'type ServeReadiness' src/main

Repository: stablyai/orca

Length of output: 3562


🏁 Script executed:

sed -n '80,145p' src/main/ssh/orcad-managed-runtime-context.ts
sed -n '115,175p' src/main/ssh/orcad-runtime-deployment.ts
sed -n '120,165p' src/main/ssh/orcad-runtime-maintenance.ts

Repository: stablyai/orca

Length of output: 6222


🏁 Script executed:

rg -n -F -- 'function probeActiveOrcadReadiness' src/main
rg -n -F -- 'probeActiveOrcadReadiness' src/main
rg -n -F -- 'wakeStoppedManagedOrcad' src/main/**/*.test.ts
sed -n '1,180p' src/main/ssh/orcad-managed-wake.ts

Repository: stablyai/orca

Length of output: 14178


🏁 Script executed:

sed -n '1,150p' src/main/ssh/orcad-active-readiness.ts
sed -n '1,190p' src/main/wsl/wsl-orcad-deploy.test.ts
sed -n '90,145p' src/main/ssh/orcad-managed-serving.test.ts

Repository: stablyai/orca

Length of output: 13725


🏁 Script executed:

rg -n -F -- 'function ensureManagedOrcadServing' src/main/ssh
sed -n '1,180p' src/main/ssh/orcad-managed-serving.ts
sed -n '180,380p' src/main/wsl/wsl-orcad-deploy.test.ts

Repository: stablyai/orca

Length of output: 10764


Handle non-serving wake outcomes before probing readiness.

When deployed.outcome === 'already-active', wakeStoppedManagedOrcad(slot) can return fenced, unverifiable, or not-activated. The branch ignores these outcomes and calls probeManagedOrcadReadiness again. That probe can throw an OrcadActiveReadinessError based only on the second liveness check, so the caller can lose the wake outcome that explains why the slot was not started. Map each non-serving, non-started outcome to an explicit error before the readiness probe. Keep serving and started on the normal readiness path.

@nwparker
nwparker marked this pull request as draft October 13, 2026 07:15
@nwparker
nwparker marked this pull request as ready for review October 13, 2026 07:15
@nwparker
nwparker merged commit 8cd5bb2 into main Oct 13, 2026
133 of 176 checks passed
trs-80 added a commit to trs-80/orca that referenced this pull request Oct 21, 2026
…returning detection

Main (stablyai#27713) replaced wrapWindowsCmdHookCommand with wrapWindowsBatchHookCommand, which
also sends UNC homes to the encoded launcher; Bob registers through it like ZCode, Kiro and
Droid. Main's install-dir fallback tests (stablyai#27819) mock the fallback detection calls, which
on this branch is resolveCommandsInInstallDirs (paths kept for the identity probe), so the
mocks follow the rename; the PATH assertions are unchanged.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant