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
130 lines
6.1 KiB
Markdown
130 lines
6.1 KiB
Markdown
# 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 1–5 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`.
|