Files
wehub-resource-sync 98e40dac97
CLI Smoke Test / smoke-test-linux (20) (push) Has been cancelled
CLI Smoke Test / smoke-test-linux (24) (push) Has been cancelled
CLI Smoke Test / smoke-test-windows (20) (push) Has been cancelled
CLI Smoke Test / smoke-test-windows (24) (push) Has been cancelled
Expo App TypeScript typecheck / typecheck (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 12:40:49 +08:00

205 lines
8.9 KiB
Markdown

# Protocol
This document describes the Happy wire protocol as implemented in `packages/happy-server`. The protocol is intentionally small: JSON over HTTP for reads/actions and Socket.IO for real-time sync. Most payloads are end-to-end encrypted client-side; see `encryption.md` for the encryption boundaries and encoding details. For the full HTTP surface and auth flows, see `api.md`.
## Transport and versioning
- HTTP API: JSON requests/responses on `/v1` and `/v2` routes.
- WebSocket: Socket.IO server at path `/v1/updates` (transports: websocket, polling).
- CORS: `*` (server-side).
## Protocol design motivations
The protocol is designed to stay minimal, explicit, and resilient under intermittent connectivity. A few guiding principles shape naming, payloads, and versioning:
- **Small surface area over completeness.** Routes and events exist only when they provide a clear sync primitive (e.g., sessions, artifacts, KV). If a capability can be expressed as data within an existing primitive, it should be.
- **Explicit event types and short keys.** Update payloads use `t` for the event type and concise field names (`sid`, `id`, `seq`) to keep message size down without hiding meaning. These names are stable because they are used across clients.
- **Separation of persistent vs. ephemeral.** Anything that must be recoverable after reconnect is an `update` event with a sequence number. Presence and usage are `ephemeral` to avoid state confusion and minimize storage.
- **Monotonic ordering at the user level.** `UpdatePayload.seq` is a single per-user counter. This makes client reconciliation simple: apply updates in order and you are consistent for that user.
- **Optimistic concurrency by default.** Versioned fields (metadata, agent state, artifact parts, access keys, KV) require `expectedVersion`. This prevents silent overwrites and keeps conflict resolution client-driven.
- **Client-side encryption boundaries.** The server never needs to understand plaintext. The protocol therefore treats most payloads as opaque strings or base64 blobs, which keeps server logic simple and privacy guarantees strong.
- **Backward compatibility over breaking changes.** New routes/events are added rather than mutating existing shapes in incompatible ways. When dual behavior is needed (e.g., machines), the server emits both old and new updates.
- **Avoid full REST verbs.** Reads are primarily `GET`, while writes/actions are primarily `POST`, with `DELETE` used when the intent is unambiguous. We avoid the full REST palette because many mutations are not cleanly tied to a single entity or involve more than CRUD logic. Keeping to `GET` + `POST` (plus occasional `DELETE`) makes the client simpler and the protocol clearer.
If a new protocol field or event is proposed, it should answer: does this create a durable sync primitive, or can it be encoded inside existing encrypted payloads without expanding the API surface?
## Authentication
Most endpoints require `Authorization: Bearer <token>`. The same token is also used in the Socket.IO handshake. Full auth flows and endpoints are documented in `api.md`.
## WebSocket connection
### Handshake
Connect with Socket.IO using:
```
path: "/v1/updates"
auth: {
token: "<bearer token>",
clientType: "user-scoped" | "session-scoped" | "machine-scoped",
sessionId?: "<session id>",
machineId?: "<machine id>"
}
```
Rules enforced server-side:
- `token` is required.
- `session-scoped` requires `sessionId`.
- `machine-scoped` requires `machineId`.
### Connection types
- `user-scoped`: receives account-wide updates.
- `session-scoped`: receives updates for a specific session only.
- `machine-scoped`: used by daemons; receives machine updates and emits machine state.
### Server -> client events
The server emits two event types:
#### `update`
Persistent sync events. Payload shape:
```
{
id: string,
seq: number,
body: { t: string, ... },
createdAt: number
}
```
#### `ephemeral`
Transient presence/usage events. Payload shape:
```
{
type: string,
...
}
```
### Update event types
Field names below match on-wire payloads.
- `new-session`
- `body`: `{ t: "new-session", id, seq, metadata, metadataVersion, agentState, agentStateVersion, dataEncryptionKey, active, activeAt, createdAt, updatedAt }`
- `update-session`
- `body`: `{ t: "update-session", id, metadata?, agentState? }`
- `metadata`: `{ value, version }` or null
- `agentState`: `{ value, version }` or null
- `delete-session`
- `body`: `{ t: "delete-session", sid }`
- `new-message`
- `body`: `{ t: "new-message", sid, message: { id, seq, content, localId, createdAt, updatedAt } }`
- `update-account`
- `body`: `{ t: "update-account", id, settings?, github? }`
- `new-machine`
- `body`: `{ t: "new-machine", machineId, seq, metadata, metadataVersion, daemonState, daemonStateVersion, dataEncryptionKey, active, activeAt, createdAt, updatedAt }`
- `update-machine`
- `body`: `{ t: "update-machine", machineId, metadata?, daemonState?, activeAt? }`
- `new-artifact`
- `body`: `{ t: "new-artifact", artifactId, seq, header, headerVersion, body, bodyVersion, dataEncryptionKey, createdAt, updatedAt }`
- `update-artifact`
- `body`: `{ t: "update-artifact", artifactId, header?, body? }`
- `delete-artifact`
- `body`: `{ t: "delete-artifact", artifactId }`
- `relationship-updated`
- `body`: `{ t: "relationship-updated", uid, status, timestamp }`
- `new-feed-post`
- `body`: `{ t: "new-feed-post", id, body, cursor, createdAt }`
- `kv-batch-update`
- `body`: `{ t: "kv-batch-update", changes: [{ key, value, version }] }`
### Ephemeral event types
- `activity`: `{ type: "activity", id: sessionId, active, activeAt, thinking? }`
- `machine-activity`: `{ type: "machine-activity", id: machineId, active, activeAt }`
- `usage`: `{ type: "usage", id: sessionId, key, tokens, cost, timestamp }`
- `machine-status`: `{ type: "machine-status", machineId, online, timestamp }`
### Client -> server WebSocket events
- `ping` -> callback `{}`
- `update-metadata`
- `{ sid, metadata, expectedVersion }`
- Response: `{ result: "success", version, metadata }` or `{ result: "version-mismatch", version, metadata }`
- `update-state`
- `{ sid, agentState, expectedVersion }`
- Response: `{ result: "success", version, agentState }` or `{ result: "version-mismatch", version, agentState }`
- `message`
- `{ sid, message, localId? }`
- Creates a new session message (encrypted payload) and emits `new-message` update to other connections.
- `session-alive`
- `{ sid, time, thinking? }`
- Emits `ephemeral` activity to user-scoped connections.
- `session-end`
- `{ sid, time }`
- Marks session inactive and emits `ephemeral` activity.
- `usage-report`
- `{ key, sessionId?, tokens, cost }`
- Stores usage report and optionally emits `ephemeral` usage for the session.
- `machine-alive`
- `{ machineId, time }`
- Emits `ephemeral` machine-activity.
- `machine-update-metadata`
- `{ machineId, metadata, expectedVersion }`
- Response: `{ result: "success", version, metadata }` or `{ result: "version-mismatch", version, metadata }`
- `machine-update-state`
- `{ machineId, daemonState, expectedVersion }`
- Response: `{ result: "success", version, daemonState }` or `{ result: "version-mismatch", version, daemonState }`
- `artifact-read`
- `{ artifactId }`
- Response: `{ result: "success", artifact }` or `{ result: "error", message }`
- `artifact-create`
- `{ id, header, body, dataEncryptionKey }`
- Response: `{ result: "success", artifact }` or `{ result: "error", message }`
- `artifact-update`
- `{ artifactId, header?, body? }` where `header` and `body` include `data` + `expectedVersion`
- Response: `{ result: "success", header?, body? }` or `{ result: "version-mismatch", header?, body? }`
- `artifact-delete`
- `{ artifactId }`
- Response: `{ result: "success" }` or `{ result: "error", message }`
- `access-key-get`
- `{ sessionId, machineId }`
- Response: `{ ok: true, accessKey? }` or `{ ok: false, error }`
- `rpc-register`
- `{ method }` -> server emits `rpc-registered`
- `rpc-unregister`
- `{ method }` -> server emits `rpc-unregistered`
- `rpc-call`
- `{ method, params }` -> callback `{ ok, result? | error? }`
- Server forwards to the registered socket via `rpc-request` (ack-based).
## HTTP endpoints by area
See `api.md` for the full HTTP endpoint catalog and auth flows.
## Sequencing and concurrency
- `UpdatePayload.seq` is the per-user update sequence (monotonic) used for sync ordering.
- Sessions, machines, and artifacts have their own `seq` fields used by clients for ordering.
- Versioned fields (metadata, agentState, daemonState, artifact header/body, access keys, KV) use optimistic concurrency with `expectedVersion` and return a version-mismatch response containing the current version/data.
## Implementation references
- API routes: `packages/happy-server/sources/app/api/routes`
- Socket handlers: `packages/happy-server/sources/app/api/socket`
- Event routing: `packages/happy-server/sources/app/events/eventRouter.ts`