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.ts — heartbeatSchema

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.ts — ackSchema

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)
codeenumNoOne of: unsupported_type (the receiver does not know the frame’s type: version skew), invalid_frame (the receiver knows the type but dropped the frame as invalid). Absent means unsupported_type
receivedTypestringNoThe 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 as version skew. The connection stays open — a single unknown frame from an ahead-running peer must not tear down the whole link. The receiver tells version skew from malformation with a recognized-type set derived from the schema discriminators. Two classes are 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).

Behavior on an invalid frame of a known type: a frame whose type is recognized but still fails validation is malformed, not skewed. The Platform drops it and logs Invalid orchestrator frame dropped with the message type and the failing field paths, never the field values. When the frame carries a messageId, the Platform answers with a nack whose code is invalid_frame and whose reason names the failing fields. The connection stays open until it sends five invalid frames within 60 seconds; the Platform then closes it with code 4003. A frame that is not JSON, or has no type field, still closes the connection at once. The orchestrator drops an invalid Platform frame with a warning and keeps its uplink open.

Authoritative source: packages/engine/src/protocol/messages/common.ts — nackSchema, 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.ts — errorSchema

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 4; the minimum accepted is also 4, so a 3 — every build from 0.15.x or earlier — is refused)
capabilitiesOrchCapabilitiesYesOrchestrator capabilities. An agent authenticating to its orchestrator sends no capabilities field

OrchCapabilities fields:

FieldTypeRequiredDescription
orchRole"coordinator" | "worker"YesOrchestrator’s role in the cluster (coordinator manages DB/vault, worker is stateless)
dashboardWritesRecord<DashboardWriteOperation, "permissive" | "encrypted" | "disabled">NoSparse per-operation dashboard-write policy map. Each present key sets one DashboardWriteOperation’s posture; a missing key takes that operation’s default posture (encrypted for secrets.set, permissive for every other operation). The wire still accepts a legacy permissive for secrets.set, which resolves to encrypted. encrypted is valid only for the two plaintext operations, secrets.set and variables.set. A value outside the three postures, or encrypted on any other operation, fails to parse. Sent on auth so the upstream cache populates immediately; rebroadcast via orch.capabilities.update on change
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.ts — authRequestSchema

Sent by the server after successful authentication.

FieldTypeRequiredDescription
type"auth.success"YesMessage discriminator
connectionIdstringYesUnique connection ID assigned by server
orgPublicAliasstringYesPublic 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
orgIdstringYesCanonical 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.
githubWebhookUrlstring | nullYesThe org-scoped GitHub App webhook URL of the hosted Platform (<base>/webhook/<orgId>/github). The orchestrator’s manifest flow bakes it into a new GitHub App in platform and hybrid mode. null when the Platform has no public webhook base.
dashboardUrlstringNoThe hosted Platform’s public dashboard base (for example https://app.kici.dev). The orchestrator builds the details_url of its GitHub check runs from it, unless KICI_DASHBOARD_URL overrides it. Absent when the Platform has no public dashboard base.

The Platform sends every required field above to an orchestrator, and dashboardUrl when it has a public dashboard base. The auth.success an orchestrator sends to an agent carries only type and connectionId.

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

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. The one exception is a rejected protocol version: the agent channel still sends auth.failure but closes with 4005 (WS_CLOSE_PROTOCOL_ERROR), so a version mismatch is distinguishable from a bad token. 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.ts — authFailureSchema