Skip to content

Protocol overview

The following diagram shows the typical happy-path message flow across both WebSocket layers, from connection setup through webhook relay to job completion.

These messages are shared across both WebSocket layers.

Authoritative source: packages/engine/src/protocol/messages/common.ts

Sent periodically to keep the WebSocket connection alive. Both orchestrators (to Platform) and agents (to orchestrators) send heartbeats on a 30-second interval.

FieldTypeRequiredDescription
type"heartbeat"YesMessage discriminator
timestampnumberYesUnix timestamp (milliseconds)

Authoritative source: packages/engine/src/protocol/messages/common.tsheartbeatSchema

Positive acknowledgment of a received message. Used for generic message-level acknowledgments.

FieldTypeRequiredDescription
type"ack"YesMessage discriminator
messageIdstringYesID of the message being acknowledged

Authoritative source: packages/engine/src/protocol/messages/common.tsackSchema

Negative acknowledgment — the message was received but could not be processed. Also the version-skew diagnosability signal: when a peer receives a message type it does not understand (the other side is running a newer build), it replies with a NACK naming the unsupported type instead of silently dropping the frame. Without this, an unknown type surfaces only as a downstream proxy timeout; the NACK turns it into an explicit, logged, upgrade-actionable error. nack is a member of both the Platform-to-orchestrator and orchestrator-to-Platform unions — either side can NACK the other.

FieldTypeRequiredDescription
type"nack"YesMessage discriminator
messageIdstringNoID of the offending frame, echoed when it carried one (correlation)
receivedTypestringNoThe unsupported/unrecognized message type that triggered the NACK
reasonstringYesHuman-readable rejection reason, including an upgrade hint

Behavior on an unknown frame: the receiver first runs its normal recognition schemas; only a frame that fails them all AND carries a type this build does not recognize is NACKed. The connection stays open — a single unknown frame from an ahead-running peer must not tear down the whole link. The receiver distinguishes version skew from malformation using a recognized-type set derived from the schema discriminators: a frame whose type IS recognized but still failed validation (e.g. an oversized field past its length bound) is malformed, not skewed, so it gets no NACK — it is handled like any other invalid frame (the Platform closes the connection; the orchestrator drops the frame with a warning rather than tearing down its own uplink). Two classes are deliberately exempt and stay drop-and-warn: a nack itself (loop guard — a NACK is never NACKed) and pure streaming frames (log.chunk / orch-log.chunk, which cannot be correlated). A genuinely malformed frame with no type field is not NACKed either; it falls through to the connection-level close.

Authoritative source: packages/engine/src/protocol/messages/common.tsnackSchema, buildUnsupportedMessageNack

Protocol-level error notification. Sent when something goes wrong at the connection level.

FieldTypeRequiredDescription
type"error"YesMessage discriminator
codestringYesError code identifier
messagestringYesHuman-readable error message

Authoritative source: packages/engine/src/protocol/messages/common.tserrorSchema

Used during WebSocket connection establishment. The connecting party sends auth.request and the server responds with either auth.success or auth.failure.

Authoritative source: packages/engine/src/protocol/messages/auth.ts

Sent by the connecting party (orchestrator to Platform, or agent to orchestrator) to authenticate.

FieldTypeRequiredDescription
type"auth.request"YesMessage discriminator
tokenstringYesAPI key or authentication token
protocolVersionnumber (int > 0)YesProtocol version (currently 1)
capabilitiesOrchCapabilitiesNoOrchestrator capabilities (optional for backward compat with pre-capability orchestrators)

OrchCapabilities fields:

FieldTypeRequiredDescription
orchRole"coordinator" | "worker"NoOrchestrator’s role in the cluster (coordinator manages DB/vault, worker is stateless)
dashboardWritesRecord<string, boolean | "permissive" | "encrypted" | "disabled">NoSparse per-operation dashboard-write policy map. Each present key sets one DashboardWriteOperation’s posture; missing keys default to permissive. The boolean form is the equivalent shorthand (false = disabled). Sent on auth so the upstream cache populates immediately; rebroadcast via orch.capabilities.update on change
supportedDashboardRequestsstring[]NoEvery dashboard.* request type this orchestrator build understands, so the upstream can detect a version mismatch explicitly. Absent means “unknown”, never “supports nothing”
dashboardEncryptionKeyOKP/X25519 JWKNoThe orchestrator’s active dashboard-encryption public key (use: "enc"), used by the browser to seal a secret / variable value under the encrypted posture. Absent when the orchestrator has no key provisioned
dashboardVerifiedIssuerstring | nullNoOrigin the browser should fetch that encryption key from directly, bypassing the control plane. Null / absent means the control-plane proxy is used instead

The schema uses .passthrough() so newer orchestrators can send additional flags without breaking older upstream versions. Unknown flags are preserved.

Authoritative source: packages/engine/src/protocol/messages/auth.tsauthRequestSchema

Sent by the server after successful authentication.

FieldTypeRequiredDescription
type"auth.success"YesMessage discriminator
connectionIdstringYesUnique connection ID assigned by server
orgPublicAliasstringNoPublic alias (oal_<12-char>) of the authenticated orchestrator’s owning org. Used by the orchestrator’s check-run emitter to build a details_url that points at the dashboard’s resolver route, so the canonical org_<12-char> id never appears in public surfaces
orgIdstringNoCanonical org id (org_<...>) of the authenticated orchestrator’s owning org. The orchestrator auto-provisions a remote-run anchor (remote:<orgId>) from it so a relayed kici run remote resolves the real tenant. When absent, that provisioning is skipped
provenanceIssuerstring | nullNoProvenance trust root (the OIDC issuer) the server mints build-provenance tokens under. The orchestrator derives the JWKS URI (<issuer>/.well-known/jwks.json) from it to verify provenance bundles at ingest. null or absent means provenance is not configured, and each attestation’s verdict is recorded as unverifiable rather than verified

Every field after connectionId is optional so an orchestrator can connect to a server that does not supply it — each one degrades to the behavior described in its own row rather than failing the handshake.

Authoritative source: packages/engine/src/protocol/messages/auth.tsauthSuccessSchema

Sent by the server when authentication fails. The WebSocket connection is closed immediately after this message — with code 4010 (WS_CLOSE_AGENT_AUTH_FAILED) on the agent channel and 4001 (WS_CLOSE_UNAUTHORIZED) on the dashboard channel. See the close-code table for the full inventory.

FieldTypeRequiredDescription
type"auth.failure"YesMessage discriminator
reasonstringYesHuman-readable failure reason

Authoritative source: packages/engine/src/protocol/messages/auth.tsauthFailureSchema