Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,14 @@ file is the human-readable summary of what changed and why.

### Desktop / Web UI

- **Chat now works inside Windows sandboxes** — every model request from a WSL
sandbox failed with `Cannot connect to API: other side closed` and retried
forever. The guest-side bridge that relays model traffic to the desktop app
got its target address through `wsl.exe`'s command line, which corrupts
quoted spaces, so the bridge dialed an empty host. The sandbox boot script
now reaches the guest over stdin instead of the command line, and the bridge
waits for the guest network on a cold start instead of grabbing whatever the
route table says first.
- **Code and Work** — the sidebar now opens on two tabs. **Code** is your
machine: the local server, plus any WSL, remote or SSH servers you have
added. **Work** is your sandboxes. Each side keeps its own projects, pinned
Expand Down
9 changes: 6 additions & 3 deletions packages/codev-sandbox/src/wsl/provider.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,9 @@ import type { SandboxConfig } from "../provider"
import { decodeWslOutput, parseDistroList, wslEnvSpec } from "./cli"
import { createWslProvider, wslDistroName, type WslProviderOptions } from "./provider"

const config: SandboxConfig = { id: "sandbox:default", name: "default", backend: "wsl", projectDir: "/tmp" }
// tmpdir() rather than a literal /tmp so the suite also runs on the Windows
// machines this backend actually targets.
const config: SandboxConfig = { id: "sandbox:default", name: "default", backend: "wsl", projectDir: tmpdir() }
const ASSET_SHA = "a".repeat(64)

// A fake wsl.exe: tracks registered distros in a state file, answers wslpath,
Expand Down Expand Up @@ -35,8 +37,9 @@ if (args[0] === "--unregister") {
}
if (args.includes("wslpath")) { process.stdout.write("/mnt/hostpath\\n"); process.exit(0) }

const script = args[args.length - 1]
// Import-time distro configuration runs through the same -- /bin/sh -c shape.
// The boot script arrives on stdin (never argv — wsl.exe corrupts quoted
// spaces there); import-time configuration still uses -- /bin/sh -c.
const script = args[args.length - 1] === "/bin/sh" ? readFileSync(0, "utf8") : args[args.length - 1]
if (script.includes("/etc/wsl.conf")) {
const value = read(); value.wslConf = script; write(value)
process.exit(0)
Expand Down
34 changes: 22 additions & 12 deletions packages/codev-sandbox/src/wsl/provider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -213,10 +213,17 @@ export function createWslProvider(options: WslProviderOptions): SandboxProvider
`chown -R codev:codev "$HOME" 2>/dev/null || true`,
`if [ -f /etc/codev-guest.env ]; then . /etc/codev-guest.env; fi`,
// Guest loopback 8788 is the broker stub on every backend; on WSL it
// relays to the host's TCP broker at the NAT gateway.
// relays to the host's TCP broker at the NAT gateway. The gateway can
// lag the distro on a cold VM boot, so poll briefly instead of
// capturing whatever the route table says first.
...(brokerPort !== null
? [
`HOST_IP=$(ip route show default | cut -d" " -f3)`,
`HOST_IP=""`,
`for _ in $(seq 1 50); do`,
` HOST_IP=$(ip route show default | awk '{print $3; exit}')`,
` [ -n "$HOST_IP" ] && break`,
` sleep 0.2`,
`done`,
`socat TCP-LISTEN:8788,bind=127.0.0.1,fork,reuseaddr TCP:$HOST_IP:${brokerPort} &`,
]
: []),
Expand All @@ -227,16 +234,19 @@ export function createWslProvider(options: WslProviderOptions): SandboxProvider
`exec setpriv --reuid=codev --regid=codev --init-groups node ${SANDBOX_GUEST_LAUNCHER}`,
].join("\n")

const child = spawnImpl(
options.wslCommand ?? "wsl.exe",
["-d", distro, "-u", "root", "--", "/bin/sh", "-c", bootScript],
{
stdio: ["ignore", "ignore", "pipe"],
// WSLENV forwards these into the distro without exposing the password
// on a command line that any host process could read.
env: { ...process.env, ...env, WSLENV: wslEnvSpec(Object.keys(env)) },
},
)
// The script goes to /bin/sh over stdin, never through wsl.exe's argv:
// wsl.exe reassembles its command line from Windows argv and drops the
// backslash-escaped quotes node's spawn produces, which silently
// corrupted any quoted space (observed live: `cut -d" " -f3` reached the
// guest as a broken delimiter and the broker bridge dialed an empty
// host). Stdin delivery has no quoting layer to corrupt.
const child = spawnImpl(options.wslCommand ?? "wsl.exe", ["-d", distro, "-u", "root", "--", "/bin/sh"], {
stdio: ["pipe", "ignore", "pipe"],
// WSLENV forwards these into the distro without exposing the password
// on a command line that any host process could read.
env: { ...process.env, ...env, WSLENV: wslEnvSpec(Object.keys(env)) },
})
child.stdin?.end(`${bootScript}\n`)
let stderrTail = ""
child.stderr?.on("data", (chunk) => {
stderrTail = `${stderrTail}${chunk}`.slice(-8000)
Expand Down
Loading