Files
wehub-resource-sync 3e076d4dd9
Deploy Worker / deploy (push) Failing after 1s
Shellcheck / Check shell scripts (push) Failing after 1s
CI / Build Packages (amd64 - appimage) (push) Has been skipped
CI / Build Packages (amd64 - deb) (push) Has been skipped
CI / Build Packages (amd64 - rpm) (push) Has been skipped
CI / Build Packages (arm64 - appimage) (push) Has been skipped
CI / Build Packages (arm64 - deb) (push) Has been skipped
CI / Test Flags Parsing (push) Failing after 1s
BATS Tests / BATS unit tests (push) Failing after 1s
CI / Build Packages (arm64 - rpm) (push) Has been skipped
CI / Test Build Artifacts (amd64) (push) Has been skipped
CI / Test Build Artifacts (arm64) (push) Has been skipped
CI / Update APT Repository (push) Has been cancelled
CI / Mirror Official .deb to Release (push) Has been cancelled
CI / Update DNF Repository (push) Has been cancelled
CI / Update AUR Package (push) Has been cancelled
CI / Create Release (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 12:29:21 +08:00

6.1 KiB
Raw Permalink Blame History

Official cowork-linux-helper socket protocol (1.18286.0)

The wire contract the official client speaks to its helper over $XDG_RUNTIME_DIR/claude-cowork-vm.sock, extracted from the beautified bundle (build-reference/app-extracted/.vite/build/index.js, line refs below drift between releases). cowork-vm-service.js implements the server side of this contract when COWORK_VM_BACKEND=bwrap swaps it in for the native helper.

Spawn contract

The client spawns the helper itself (~157177):

<helper> -socket $XDG_RUNTIME_DIR/claude-cowork-vm.sock
  • stdio is piped; stdout/stderr lines land in cowork_vm_node.log.
  • Restart backoff is client-owned (exponential 1 s → 60 s, reset after 30 s uptime). The helper just needs to bind the socket and serve.
  • On ENOENT/ECONNREFUSED during a request, the client respawns the helper and retries (5 attempts, 1 s apart).
  • The client connects via @ant/claude-native's connectUnixSocketSameUid — the server process must run as the same uid (a locally spawned child always does). The socket's parent dir must be 0700, same-uid, and the socket path must not be a symlink (client-side checks at ~157010/~157026).

Framing

Every connection, both directions: 4-byte big-endian length prefix + UTF-8 JSON body (~157249). Max body 10 MiB (~157259).

Connection modes

The client opens three kinds of connections to the same socket:

  1. One-shot request (~157268): {method, params?} (no id), one {success, result?|error?} reply, then the client half-closes. Used always when remote-config flag 770567414 is on, and as the fallback when the persistent pipe can't connect.

  2. Persistent RPC pipe (~157384): {method, id, params?} with an incrementing numeric id; replies {success, result?|error?, id} matched by id, out-of-order allowed. Client-side timeout 30 s (remote-configurable). Immediately after connecting, the client sends configure {userDataName, userDataRoot, sessionOnly: true} on the pipe.

  3. Event subscription (~157539): client sends {method: "subscribeEvents", params: {userDataName, userDataRoot}} and expects an ack frame {success: true} FIRST, then a stream of event frames on the same connection (server → client only):

    frame shape
    stdout/stderr {type, id, data}
    exit {type: "exit", id, exitCode?, signal?, oomKillCount?}
    error {type: "error", id, message, fatal?}
    networkStatus {type: "networkStatus", status}
    apiReachability {type: "apiReachability", status}
    startupStep {type: "startupStep", step, status}
    guest request {type: "request", id, method, params}

    Guest requests are answered out-of-band by the client via the sendGuestResponse RPC. If the subscription drops, the client resubscribes after 15 s.

userDataName/userDataRoot identify the Electron profile (multi- profile isolation); the server may scope per-profile state by them.

Methods

params / expected result (all optional fields may be absent):

method params result consumed by client
configure {userDataName, userDataRoot, networkDrives, memoryMB?, cpuCount?, smolBinPath, logDir, virtiofsdPath, firmwareCodePath, firmwareVarsTemplatePath} (also {…, sessionOnly: true} pipe-connect variant)
subscribeEvents {userDataName, userDataRoot} ack {success: true}, then events
createVM {bundlePath, diskSizeGB}
startVM {bundlePath, memoryGB?, cpuCount?, apiProbeURL?}
stopVM
isRunning {running}
isGuestConnected {connected}
spawn {id, name, command, args, isResume, cwd?, env?, additionalMounts?, allowedDomains?, oneShot?, mountSkeletonHome?, oauthToken?} {failedMounts?}
kill {id, signal}
writeStdin {id, data}
isProcessRunning {id} {running, exitCode?}
sendGuestResponse {id, resultJson?, error?}
setDebugLogging {enabled}
mountPath {processId, subpath, mountName, mode}
readFile {processName, filePath} {content}
getSessionsDiskInfo {lowWaterBytes} {totalBytes, freeBytes, sessions[]}
deleteSessionDirs {names[]} {deleted[], errors{}}
pruneSessionCaches {onlyIfFreeBytesBelow, includeSessionTmp, sessionTmpOlderThanSeconds} {prunedSessions[], skippedSessions[], freedBytes, errors{}}
installSdk {sdkSubpath, version}
addApprovedOauthToken {token}
getNetworkDrives {drives[]} (Linux client builds networkDrives as [])

isRunning, isGuestConnected, isProcessRunning, readFile are called through an idempotent-retry wrapper — they must be safe to repeat.

Boot sequence the server must satisfy

From the client's Linux boot orchestrator (~283525):

  1. configure (RPC) — "Linux VM helper configured" logged on success.
  2. startVM {bundlePath, memoryGB?, cpuCount?, apiProbeURL?} — must return success; the client treats a thrown error as vm_boot_error.
  3. The client then polls isGuestConnected (a few times per second, multi-minute timeout) until {connected: true}.
  4. installSdk {sdkSubpath, version} — the sdk_install step.
  5. Cowork sessions arrive as spawn calls; their stdout/stderr/exit flow back as events on the subscription connection.

Balloon-related methods (getMemoryTier, enableBalloonMonitoring, getHostMemoryInfo) are optional-chained client-side and absent from the Linux client — never called over the wire.

The client's heartbeat and "already connected" fast path both reduce to isGuestConnected, so the server must keep answering it truthfully for the daemon's lifetime.

Support gate context (not part of the wire protocol)

The client only dials the helper when the yukonSilver evaluator (~281877) reports supported, which requires qemu/OVMF/virtiofsd, /dev/kvm, and /dev/vhost-vsock. The COWORK_VM_BACKEND=bwrap asar patch short-circuits that evaluator and swaps the spawn target; see scripts/patches/cowork-bwrap.sh.