Files
aaddrick--claude-desktop-de…/scripts/cowork-fallback/PROTOCOL.md
T
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

130 lines
6.1 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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`.