Protocol reference

Agent socket

Every message on the agent's WebSocket, both ways.

Messages marked required are the ones a controller must handle for a node to run games. The rest are optional.

How to accept the socket, answer hello and which messages to send when is in the protocol overview.

Closing the socket

Every close that the controller or the agent makes on purpose, with a code from CLOSE_CODES. Any other code, or none, is a broken connection, and the agent redials.

CodeReasonByWhenThen
1000SIGTERM, SIGINT or SIGQUITagentThe agent is stopping, after its shutdown.The controller closes its side with the same code and reason.
1008hello_refusedcontrollerThe controller will not accept the node's hello, because the hello fails its schema or the controller refuses the node. A hello_refused frame just before the close says which.Logs the frame's reason and detail, and stops dialling until it is restarted.
1000noneagentThe controller sent hello_refused and has not closed within 5 seconds.Stays stopped until it is restarted.
1000noneagentA set_config moved the node to another controller address, once its reply has gone.Dials the new address immediately.
1008hello_timeoutcontrollerNo valid hello within 10 seconds of the upgrade.Redials with its backoff.
1000welcome_timeoutagentNo welcome answered its hello within 15 seconds.Redials with its backoff.
1008duplicate_hellocontrollerA second hello arrived on the same socket.Redials with its backoff.
1008node_connectedcontrollerThe node is already connected on another socket.Redials with its backoff, and connects once the other socket closes or times out.
1008rate_limitedcontrollerThe node went over a budget.Redials with its backoff. The controller answers 429 for 60 seconds.
1008agent_deletedcontrollerThe node was deleted in the controller.Stops dialling until it is restarted.
1008resynccontrollerThe controller lost track of the node's state.Redials and sends a fresh hello.
1009nonecontrollerA message over 960 KiB.Redials with its backoff.
1009noneagentThe controller sent a message over 2 MiB.Redials with its backoff.

Agent → controller

What the agent sends: its hello, one reply to each request, and reports of the node as it changes. Until a welcome answers the hello, only replies follow it, and a shutdown if the agent stops.

backup

optionalagent → controller

The round_backup of a match that sends no statistics, which a match with statistics carries in its event_batch instead. Keep the newest of each match to restore it on a new server.

The message wraps the complete event, unchanged, so code that reads a round_backup from the event stream reads backup the same way. The wrapper adds only the server that sent it.

Like phase_change, it is sent once, so a backup is lost if the plugin or the agent is not connected when the round starts. The next round's backup replaces it.

  • type"backup"
  • allocation_idAllocationId

    The server whose match the backup is of.

    AllocationId

    The id the controller picks for each server it launches. It identifies that server and its events from then on.

    The id is a lowercase UUID, new for every launch. UUIDv7 is recommended, because its time-ordered values keep a database index on it compact as events arrive.

    string

  • backupround_backup

    The round_backup event, as the event stream would carry it.

Example
{
  "type": "backup",
  "allocation_id": "5b1e2c7a-8d4f-4c3b-9a6e-2f1d0c9b8a7e",
  "backup": {
    "type": "round_backup",
    "timestamp": 1700001214000,
    "tick": 180000,
    "round": 14,
    "team1_side": "t",
    "team1_score": 7,
    "team2_score": 6,
    "rules": {
      "max_rounds": 24,
      "overtime": true,
      "overtime_max_rounds": 6,
      "friendly_fire": false
    },
    "file": "\"SaveFile\"\n{\n\t\"round\"\t\t\"13\"\n\t…\n}\n",
    "roster": {
      "team1": {
        "name": "Blue",
        "players": [
          "76561198000000001",
          "76561198000000002"
        ]
      },
      "team2": {
        "name": "Orange",
        "players": [
          "76561198000000011",
          "76561198000000012"
        ]
      },
      "spectators": []
    }
  }
}

cs2_update_progress

optionalagent → controller

Progress of a CS2 install or update while it runs. cs2.update in node_status says how it ended.

  • type"cs2_update_progress"
  • update_idUpdateId

    The update it reports on, as cs2.update.id. A frame can arrive before the node_status that names the update.

    UpdateId

    Chosen by the agent for each CS2 install or update it runs or finds running. A lowercase UUID.

    string

  • percentnumber 0–100 | null

    Percent done, as steamcmd reports it. Null when steamcmd reports none.

  • messagestring ≤ 512 chars

    What the install is doing, up to 512 characters.

Examples
{
  "type": "cs2_update_progress",
  "update_id": "7c2e9a41-3b5d-4f68-9e0a-1d4c8b7f2a63",
  "percent": 42.5,
  "message": "downloading"
}
{
  "type": "cs2_update_progress",
  "update_id": "7c2e9a41-3b5d-4f68-9e0a-1d4c8b7f2a63",
  "percent": 100,
  "message": "install/update complete"
}

demo

optionalagent → controller

One demo as it is now, sent when it appears or changes: its file, its upload starting or stopping, or its upload_error. It replaces the demo with the same slot_id and file_name, or adds it. hello lists the newest 50. A demo that appears after the hello is added, however many the node keeps.

  • slot_idSlotId

    The slot whose directory holds it.

    SlotId

    A slot's id, which is also part of the names of its containers.

    string

    Must match ^[A-Za-z0-9_-]{1,32}$

  • file_nameDemoFileName

    The file's name.

    DemoFileName

    A demo's file name, as it sits in its launch's directory, demos/<allocation_id>/ in its slot.

    string

    Must match ^[A-Za-z0-9._-]{1,156}\.dem$

  • size_bytesinteger ≥ 0

    Bytes in the file.

  • modified_atinteger

    Unix milliseconds when the file was last written.

  • recordingboolean

    Whether its server is still recording it. While it is, the demo grows with each report and is neither uploaded nor deleted.

  • uploadingboolean

    Whether it is uploading now. demo_progress reports how far, and a node uploads one demo at a time.

  • upload_errorstring 1–512 chars | null

    Why its upload failed or why it waits, up to 512 characters: a failure being retried, a refusal as invalid, or every demo held while the target refuses the node or has failed for a day. Null once an upload works again or the target changes.

  • type"demo"
Examples
{
  "type": "demo",
  "slot_id": "s1",
  "file_name": "20261008-201500_0199a0b4-5c00-7a6e-8d4f-2f1d0c9b8a7e_de_mirage.dem",
  "size_bytes": 52428800,
  "modified_at": 1759954500000,
  "recording": true,
  "uploading": false,
  "upload_error": null
}
{
  "type": "demo",
  "slot_id": "s1",
  "file_name": "20260928-201500_0199a0b4-5c00-7000-8000-00000000abc1_de_mirage.dem",
  "size_bytes": 181403648,
  "modified_at": 1759090500000,
  "recording": false,
  "uploading": true,
  "upload_error": null
}
{
  "type": "demo",
  "slot_id": "s2",
  "file_name": "20260928-183000_0199a0b4-5c00-7000-8000-00000000abc0_de_nuke.dem",
  "size_bytes": 164626432,
  "modified_at": 1759084200000,
  "recording": false,
  "uploading": false,
  "upload_error": "HTTP 503; retrying in 30 s"
}

demo_progress

optionalagent → controller

How far the uploading demo is, at most once a second while it uploads.

  • type"demo_progress"
  • slot_idSlotId

    The slot whose directory holds it.

    SlotId

    A slot's id, which is also part of the names of its containers.

    string

    Must match ^[A-Za-z0-9_-]{1,32}$

  • file_nameDemoFileName

    The file's name.

    DemoFileName

    A demo's file name, as it sits in its launch's directory, demos/<allocation_id>/ in its slot.

    string

    Must match ^[A-Za-z0-9._-]{1,156}\.dem$

  • sent_bytesinteger ≥ 0

    Bytes sent so far.

  • bytes_per_secondinteger ≥ 0

    Bytes a second since the previous demo_progress of this upload, or since it began.

Example
{
  "type": "demo_progress",
  "slot_id": "s1",
  "file_name": "20260928-201500_0199a0b4-5c00-7000-8000-00000000abc1_de_mirage.dem",
  "sent_bytes": 45350912,
  "bytes_per_second": 12500000
}

demo_removed

optionalagent → controller

A demo that the controller knows of is no longer on the node.

  • type"demo_removed"
  • slot_idSlotId

    The slot whose directory held it.

    SlotId

    A slot's id, which is also part of the names of its containers.

    string

    Must match ^[A-Za-z0-9_-]{1,32}$

  • file_nameDemoFileName

    The file's name.

    DemoFileName

    A demo's file name, as it sits in its launch's directory, demos/<allocation_id>/ in its slot.

    string

    Must match ^[A-Za-z0-9._-]{1,156}\.dem$

  • reasonDemoRemovedReason

    Why it is no longer on the node.

    DemoRemovedReason

    Why a demo is no longer on the node.

    string enum

    • "uploaded"

      the target took it and the agent removed it.

    • "deleted"

      delete_demo or anything else removed it.

Example
{
  "type": "demo_removed",
  "slot_id": "s1",
  "file_name": "20260928-201500_0199a0b4-5c00-7000-8000-00000000abc1_de_mirage.dem",
  "reason": "uploaded"
}

event_backlog

optionalagent → controller

The node's EventBacklog now, replacing the backlog of the last hello or event_backlog. Sent when it changes: after each failed upload, as its counts grow, and once, with nothing stuck, when uploads work again. Rejected when it breaks a rule of EventBacklog.

  • type"event_backlog"
  • serversStuckServer[], ≤ 100 items

    The servers with the oldest stuck events, oldest first, up to 100.

    StuckServer

    A server whose events wait on the node.

    • allocation_idAllocationId

      The server whose plugin sent them.

      AllocationId

      The id the controller picks for each server it launches. It identifies that server and its events from then on.

      The id is a lowercase UUID, new for every launch. UUIDv7 is recommended, because its time-ordered values keep a database index on it compact as events arrive.

      string

    • pendinginteger ≥ 1

      Events of the server still queued on the node.

  • server_countinteger ≥ 0

    Servers with stuck events on the node, those past servers too.

  • event_countinteger ≥ 0

    Events that every stuck server holds together.

  • reasonstring 1–512 chars | null

    Why the last upload failed, up to 512 characters. Null when nothing is stuck, and only then.

Example
{
  "type": "event_backlog",
  "servers": [
    {
      "allocation_id": "5b1e2c7a-8d4f-4c3b-9a6e-2f1d0c9b8a7e",
      "pending": 212
    }
  ],
  "server_count": 1,
  "event_count": 212,
  "reason": "HTTP 503"
}

hello

requiredagent → controller

The agent's first message on every connection, and sent only then. An applied set_config answers with the settings instead.

Answer an accepted hello with welcome. Until then the agent sends only replies, or a shutdown when it stops. Runtime changes use status messages. Reconnecting never changes the node's configuration.

Rejected unless slot_status names each slot of config.slots once and names no other slot.

  • type"hello"
  • agentAgentInfo

    The agent sending this.

    AgentInfo

    The running agent.

    • versionstring 1–64 chars

      The agent package version.

    • imageImageReference

      The image this agent's container was created from.

      ImageReference

      A Docker image reference, as cs2_image and agent_image in agent.yml hold it.

      string

      Must match ^[A-Za-z0-9][A-Za-z0-9._\-/:@]{0,254}$

    • image_idImageId

      The id Docker holds for that image. Two builds of one tag differ here.

      ImageId

      The id Docker gives an image: sha256: and 64 hex digits.

      string

      Must match ^sha256:[0-9a-f]{64}$

  • hostHostInfo

    The host the agent runs on.

    HostInfo

    The machine the agent runs on.

    • cpu_countinteger ≥ 1

      Logical CPUs the agent can use.

    • os_namestring 1–256 chars | null

      The host's os-release PRETTY_NAME, up to 256 characters. Null when unreadable.

    • kernelstring 1–256 chars

      The kernel release, which the agent's container shares with the host.

  • public_ipIpv4 | null

    The node's public IPv4 address as the agent looked it up at start. Null when the lookup failed, and players then reach the node only at config.pinned_ip. Players connect to config.pinned_ip whenever it is set. The agent looks again only when it restarts.

    Ipv4

    A dotted IPv4 address. Rejected unless it is four numbers from 0 to 255 joined by dots, each without leading zeros.

    string

  • countryCountryCode | null

    Where the node's address is, as a geolocation lookup reports it when the agent starts. Null when that lookup failed. The agent tries again only when it restarts.

    CountryCode

    An ISO 3166-1 alpha-2 country code: DK, FR, or GB for the United Kingdom.

    string

    Must match ^[A-Z]{2}$

  • configNodeSettings

    The configuration the agent runs with.

    NodeSettings

    The part of agent.yml that the controller may edit, as hello reports it applied.

    • slotsSlotLayout

      The slots servers run in.

      SlotLayout

      Up to 64 slots. Rejected when two slots share an id, or when two ports are equal. Every slot's port and tv_port count, so a slot's two ports differ too. It is also rejected when a port is 4500, where the agent listens for plugins.

      ReportedSlot

      A slot as the agent reports it, with the last ten characters of its token in place of the token.

      • idSlotId

        The slot's id, 1 to 32 letters, digits, _ and -. It is unique on its node.

        SlotId

        A slot's id, which is also part of the names of its containers.

        string

        Must match ^[A-Za-z0-9_-]{1,32}$

      • portPort

        The game port, UDP. Every port and tv_port on a node is different, free on the host, and not 4500, the port the agent listens on for plugins. Host networking explains why.

        Port

        A TCP or UDP port.

        integer 1–65535

      • tv_portPort

        The GOTV port, UDP, under the same rules as port.

        Port

        A TCP or UDP port.

        integer 1–65535

      • gslt_hintGsltHint

        The last ten characters of the slot's token.

        GsltHint

        The last ten characters of a token, which the agent reports instead of the token. They are enough to tell apart every token on one Steam account.

        string

        Must match ^[A-Za-z0-9]{10}$

    • pinned_ipIpv4 | null

      The address players connect to. Null for the node's public_ip. Servers already running keep the address their launch announced.

      Ipv4

      A dotted IPv4 address. Rejected unless it is four numbers from 0 to 255 joined by dots, each without leading zeros.

      string

    • controllerControllerAddress

      Where the agent dials the controller. It redials when any part of it changes.

      ControllerAddress

      Where a node dials the controller: a host with an optional port, whether it encrypts, and the paths it reaches there.

      • hostControllerHost

        The controller's host with an optional port.

        ControllerHost

        A host with an optional port and no scheme or path, such as cs.example.com, 192.168.0.10:3000 or [fd00::1].

        Rejected when it contains whitespace, a control character, /, \, ? or #. It is also rejected unless http:// followed by the value parses as a URL that has a host, an optional port and nothing else, such as a user name.

        string

      • tlsboolean

        Whether the agent dials over TLS.

      • socket_pathControllerPath

        The path of the agent socket. It is /ws/agent unless agent.yml sets controller.socket_path.

        ControllerPath

        A path on the controller's host. It starts with /, followed by URL path characters and percent escapes, without a query or fragment.

        string ≤ 255 chars

        Must match ^\/(?:[A-Za-z0-9\-._~!$&'()*+,;=:@/]|%[0-9A-Fa-f]{2})*$

      • events_pathControllerPath

        The path event batches are posted to. It is /api/node/events unless agent.yml sets controller.events_path.

        ControllerPath

        A path on the controller's host. It starts with /, followed by URL path characters and percent escapes, without a query or fragment.

        string ≤ 255 chars

        Must match ^\/(?:[A-Za-z0-9\-._~!$&'()*+,;=:@/]|%[0-9A-Fa-f]{2})*$

    • auto_updateboolean

      Whether the node keeps itself up to date: CS2 on cs2.latest_build_id, and its agent and cs2 images on the newest release of their major. When false, the node stays on cs2.build_id until an update_cs2, and accepts launches while it is behind cs2.latest_build_id, so players on a newer client cannot join its servers. Its images then change only with update_agent and update_cs2_image.

    • demo_uploadReportedDemoUpload | null

      Where the agent uploads demos. Null for no uploads, which keeps demos on the node. The header's value is reported as its SHA-256.

      ReportedDemoUpload

      A demo upload as the node reports it, with the header's value replaced by its hash, because the value is a credential that the controller set.

      • urlDemoUploadUrl

        The upload target.

        DemoUploadUrl

        The URL finished demos are posted to. A target that needs credentials takes them in the configured header, such as Authorization: Basic …, which the node reports only as a hash.

        Rejected when the URL carries a user name or a password.

        string ≤ 255 chars, uri

      • headerobject | null

        The header sent with each upload. Null for none.

        • namestring

          The header's name, up to 64 letters, digits and -. Rejected when it is, in any case, one of host, content-type, content-length, transfer-encoding, connection, keep-alive, upgrade, expect, te, trailer, ankor-protocol, ankor-file-name, ankor-match-id and ankor-allocation-id, which the agent sets itself or which change how the request is framed.

          string

          Must match ^[A-Za-z0-9-]{1,64}$

        • value_sha256string

          The lowercase hex SHA-256 of the value's UTF-8 bytes, to compare with the value the controller set.

          string

          Must match ^[0-9a-f]{64}$

      • limit_mbitDemoUploadLimit | null

        Megabits a second an upload may send while a game server runs on the node. Null for no limit.

        DemoUploadLimit

        Whole megabits a second, from 1 to 10 Gbit/s.

        integer 1–10000

    • memory_mbMemoryLimit | null

      MiB of memory each game container may use. Null for no cap. A change applies to servers launched afterwards, and CS2 installs and updates are exempt.

      Rejected when it is not a whole number from 1024 to 65536.

      MemoryLimit

      MiB of memory each game container may use. CS2 installs and updates are exempt.

      Rejected when it is not a whole number from 1024 to 65536.

      integer 1024–65536

  • slot_statusSlotStatus[], ≤ 64 items

    One per configured slot.

    SlotStatus

    One slot as the agent sees it.

    Rejected when an idle slot names an allocation_id, a match_id or a server_steam_id, or when a slot in any other state has no allocation_id. It is also rejected when a slot that is not idle is reserved.

    • slot_idSlotId

      The slot this describes.

      SlotId

      A slot's id, which is also part of the names of its containers.

      string

      Must match ^[A-Za-z0-9_-]{1,32}$

    • stateSlotState

      What the slot is doing.

      SlotState

      What a slot is doing.

      string enum

      • "idle"

        the slot has no server.

      • "starting"

        the slot holds a server that its plugin has not yet reported ready for players with a server_status with ready. A server that a restarted agent took over starts here too.

      • "running"

        the plugin has reported the server ready.

      • "stopping"

        the server is being stopped.

    • allocation_idAllocationId | null

      The server on the slot. Null when the slot is idle.

      AllocationId

      The id the controller picks for each server it launches. It identifies that server and its events from then on.

      The id is a lowercase UUID, new for every launch. UUIDv7 is recommended, because its time-ordered values keep a database index on it compact as events arrive.

      string

    • match_idMatchId | null

      The server's match id. Null for a server without a match or a match launched without one.

      MatchId

      The id of one map's match, which the controller chooses. A series of maps is a grouping that the controller keeps. The id is a lowercase UUID, because demo file names carry it and case matters there.

      string

    • plugin_onlineboolean

      Whether the server's plugin is connected to the agent. get_server_status, console_command, restore_round and start_match reach it only while it is.

    • gslt_stateGsltState

      Whether Steam accepts the slot's token. start_server never picks a slot whose token is invalid.

      GsltState

      Whether Steam accepts a slot's token.

      string enum

      • "ok"

        a logon with the token succeeded, either the agent's own or that of a server that turned ready.

      • "invalid"

        Steam refused the token.

      • "unchecked"

        the agent has seen neither since the token was set.

    • reservedboolean

      Whether the idle slot is kept from launches, while the controller replaces its token with reserve_slot or while the agent logs its token on to Steam.

    • server_steam_idSteamId64 | null

      The server_steam_id that the server's plugin reported. The agent sends a slot_status as soon as it learns the ID. Null when the slot is idle or until the server is ready.

      SteamId64

      A SteamID64 as 17 decimal digits.

      string

      Must match ^\d{17}$

  • cs2Cs2State

    The game install and image.

    Cs2State

    The node's game install and image.

    • build_idBuildId | null

      The installed game build. Null before the first install.

      BuildId

      A Steam build id, in decimal.

      string

      Must match ^\d{1,20}$

    • latest_build_idBuildId | null

      The newest public CS2 build the agent read from Steam. Null until its first successful read. The agent updates an installed node to it on its own once every slot is idle, unless the node's auto_update is false.

      BuildId

      A Steam build id, in decimal.

      string

      Must match ^\d{1,20}$

    • imageImageReference

      The cs2 image set in agent.yml, which future launches use.

      ImageReference

      A Docker image reference, as cs2_image and agent_image in agent.yml hold it.

      string

      Must match ^[A-Za-z0-9][A-Za-z0-9._\-/:@]{0,254}$

    • image_idImageId | null

      The local id that image resolves to on the node. Null until the image is there.

      ImageId

      The id Docker gives an image: sha256: and 64 hex digits.

      string

      Must match ^sha256:[0-9a-f]{64}$

    • updateCs2Update | null

      The install or update that runs now, or else the last one to end since the agent started. Null before any. An agent that starts while an update that it started earlier still runs reports that update.

      Cs2Update

      A CS2 install or update on the node: one running, or the last one to end since the agent started.

      • idUpdateId

        Identifies the update and its output, the cs2_update LogSource with this update_id.

        UpdateId

        Chosen by the agent for each CS2 install or update it runs or finds running. A lowercase UUID.

        string

      • modeCs2UpdateMode

        How the update runs.

        Cs2UpdateMode

        How thoroughly a CS2 install or update runs.

        string enum

        • "update"

          downloads what changed.

        • "verify"

          also re-hashes every file.

        • "reinstall"

          wipes the install volume first and downloads all of it.

      • automaticboolean

        Whether the agent started the update on its own, to reach the newest public build. When false, an update_cs2 started it.

      • outcomeCs2UpdateOutcome | null

        How the update ended. Null while it runs.

        Cs2UpdateOutcome

        How a CS2 install or update ended.

        An update container that an earlier agent process started reports progress again until it exits. It ends completed or failed by its exit code, or unobserved.

        string enum

        • "completed"

          the game files are installed.

        • "pull_failed"

          the cs2 image could not be pulled.

        • "overlay_failed"

          its addon overlay could not be extracted.

        • "failed"

          any other failure, such as the update container exiting with an error.

        • "unobserved"

          an update container that an earlier agent process started exited, and the agent did not see the exit.

      • detailstring 1–512 chars | null

        Why the update failed, up to 512 characters. Null unless it failed.

    • image_updateCs2ImageUpdate | null

      The switch of image that runs now, or else the last one to end since the agent started. Null before any.

      Cs2ImageUpdate

      A switch of the cs2 image: the one running now, or the last one to end since the agent started.

      • imageImageReference

        The image the switch is to.

        ImageReference

        A Docker image reference, as cs2_image and agent_image in agent.yml hold it.

        string

        Must match ^[A-Za-z0-9][A-Za-z0-9._\-/:@]{0,254}$

      • stepImageUpdateStep | null

        Where the switch is while it runs. Null once it ended.

        ImageUpdateStep

        Where an update_agent or update_cs2_image is.

        string enum

        • "pulling"

          the image is being pulled.

        • "switching"

          the image is on the node and is put into use. A switch of the agent replaces its container, so the node goes offline and comes back. A switch of the cs2 image extracts its plugin files for future launches.

      • outcomeCs2ImageUpdateOutcome | null

        How the switch ended. Null while it runs.

        Cs2ImageUpdateOutcome

        How an update_cs2_image ended.

        string enum

        • "completed"

          future launches use the image.

        • "pull_failed"

          the image could not be pulled.

        • "overlay_failed"

          its addon overlay could not be extracted.

        • "failed"

          anything else, such as the agent not saving the image setting.

      • detailstring 1–512 chars | null

        Why the switch failed, up to 512 characters. Null unless it failed.

  • unavailableNodeUnavailable | null

    Why the node cannot run servers. Null when it can. Changes arrive as node_status.

    NodeUnavailable

    Why the node cannot run servers now, with maintenance first while it lasts. Meanwhile start_server is refused with maintenance_running for maintenance and node_unavailable otherwise. Everything else works.

    Rejected when operation is null while code is maintenance, or set while code is another value.

    • codeNodeUnavailableCode

      NodeUnavailableCode

      Why a node cannot run servers, one of the values of NODE_UNAVAILABLE.

      string enum

      • "maintenance"

        The MaintenanceOperation in operation runs. It clears when the operation ends.

      • "event_storage"

        The agent cannot use the local store that keeps gameplay events until the controller takes them. detail says why. - If a call to the store waits past its limit, as on a disk that hangs, the code clears once the store answers. Deleting the store or restarting the agent loses what waits to be written. - If the agent cannot open the store, such as one that a newer agent wrote, a corrupt file or one it cannot write, the code lasts until a restart. Update the agent, or delete the store on the host and restart the agent. Deleting it loses the events and server exits it keeps.

      • "disk_space"

        The disk under the agent's data directory has less than 1024 MiB free, too little for a match's events and demo. The agent asks the disk every 30 seconds and before each launch, so the code clears within 30 seconds of space being freed, and a launch that arrives sooner is taken if the disk has room by then.

    • operationMaintenanceOperation | null

      The maintenance operation that runs when code is maintenance. Null for any other code.

      MaintenanceOperation

      The maintenance operation that runs on the node.

      string enum

      • "cs2_update"

        a CS2 install or update, also one an earlier agent started.

      • "agent_update"

        an update_agent, until its replacement has proved it stays up.

      • "cs2_image_update"

        an update_cs2_image.

      • "image_cleanup"

        the agent's removal of replaced images when it starts.

    • detailstring 1–512 chars

      The agent's message, up to 512 characters.

  • demosDemoFile[], ≤ 50 items

    The demos kept on the node, up to 50. demo reports each change afterwards.

    DemoFile

    A demo kept on the node.

    • slot_idSlotId

      The slot whose directory holds it.

      SlotId

      A slot's id, which is also part of the names of its containers.

      string

      Must match ^[A-Za-z0-9_-]{1,32}$

    • file_nameDemoFileName

      The file's name.

      DemoFileName

      A demo's file name, as it sits in its launch's directory, demos/<allocation_id>/ in its slot.

      string

      Must match ^[A-Za-z0-9._-]{1,156}\.dem$

    • size_bytesinteger ≥ 0

      Bytes in the file.

    • modified_atinteger

      Unix milliseconds when the file was last written.

    • recordingboolean

      Whether its server is still recording it. While it is, the demo grows with each report and is neither uploaded nor deleted.

    • uploadingboolean

      Whether it is uploading now. demo_progress reports how far, and a node uploads one demo at a time.

    • upload_errorstring 1–512 chars | null

      Why its upload failed or why it waits, up to 512 characters: a failure being retried, a refusal as invalid, or every demo held while the target refuses the node or has failed for a day. Null once an upload works again or the target changes.

  • server_exitsServerExit[], ≤ 64 items

    Servers that ended while no connection was welcomed, oldest first, each with its reason. Their slots already show idle here, so a server_exited after the welcome would come too late. It carries the oldest 64 at most, and the rest follow as server_exited after the welcome.

    ServerExit

    A server whose container is gone, and why.

    • slot_idSlotId

      The slot it ran in.

      SlotId

      A slot's id, which is also part of the names of its containers.

      string

      Must match ^[A-Za-z0-9_-]{1,32}$

    • allocation_idAllocationId

      The server that ended.

      AllocationId

      The id the controller picks for each server it launches. It identifies that server and its events from then on.

      The id is a lowercase UUID, new for every launch. UUIDv7 is recommended, because its time-ordered values keep a database index on it compact as events arrive.

      string

    • reasonServerExitReason

      Why it ended.

      ServerExitReason

      Why a server ended: one of the AgentStopReason values when the agent stopped it on its own, or one of these.

      string enum

      • "idle_empty"

        the server stayed empty for its launch's idle_shutdown_seconds.

      • "config_rejected"

        the plugin refused its match config, as config_error says.

      • "start_timeout"

        the server was still starting 60 seconds after its launch, or after a restarted agent took it over.

      • "quit_timeout"

        the server was still running 10 seconds after its plugin sent quitting.

      • "slot_removed"

        a restarted agent found the server running on a slot that agent.yml no longer has.

      • "requested"

        a stop_server from the controller stopped it.

      • "clean_exit"

        it ended on its own with exit code 0, as after its match or a console quit.

      • "crashed"

        it ended on its own with another exit code.

      • "launch_failed"

        its container never started.

      • "lost"

        it ended and the agent did not see how, such as a container removed by hand.

    • exit_codeinteger | null

      The container's exit code. Null when the agent did not see one.

    • detailstring 1–512 chars | null

      What the reason does not say, up to 512 characters: the plugin's config_error for config_rejected, and Docker's message for launch_failed. Null for every other reason.

  • event_backlogEventBacklog

    The node's event backlog when the hello was built. event_backlog replaces it.

    EventBacklog

    The events that wait on the node because uploading them fails, as the agent read them. While uploads work, nothing is stuck, so servers is empty, the counts are zero and reason is null.

    Rejected unless servers holds as many entries as server_count, or 100 when server_count is larger. It is also rejected when event_count is below the sum of pending over servers. A reason is rejected when server_count is zero, and a null reason when it is not.

    • serversStuckServer[], ≤ 100 items

      The servers with the oldest stuck events, oldest first, up to 100.

      StuckServer

      A server whose events wait on the node.

      • allocation_idAllocationId

        The server whose plugin sent them.

        AllocationId

        The id the controller picks for each server it launches. It identifies that server and its events from then on.

        The id is a lowercase UUID, new for every launch. UUIDv7 is recommended, because its time-ordered values keep a database index on it compact as events arrive.

        string

      • pendinginteger ≥ 1

        Events of the server still queued on the node.

    • server_countinteger ≥ 0

      Servers with stuck events on the node, those past servers too.

    • event_countinteger ≥ 0

      Events that every stuck server holds together.

    • reasonstring 1–512 chars | null

      Why the last upload failed, up to 512 characters. Null when nothing is stuck, and only then.

  • agent_updateAgentUpdate | null

    The swap in progress, or else the one this agent saw end, or else the last that failed. A failure is kept, across agent restarts, until a swap holds or node-setup.sh starts the agent. Null when there is none of these. A swap that held shows in agent.image.

    AgentUpdate

    An agent update: the one in progress, one this agent saw end, or the last one to fail.

    • imageImageReference

      The agent image the update was to.

      ImageReference

      A Docker image reference, as cs2_image and agent_image in agent.yml hold it.

      string

      Must match ^[A-Za-z0-9][A-Za-z0-9._\-/:@]{0,254}$

    • stepImageUpdateStep | null

      Where the update is while it runs, with the replacement still on trial in switching. Null once it ended.

      ImageUpdateStep

      Where an update_agent or update_cs2_image is.

      string enum

      • "pulling"

        the image is being pulled.

      • "switching"

        the image is on the node and is put into use. A switch of the agent replaces its container, so the node goes offline and comes back. A switch of the cs2 image extracts its plugin files for future launches.

    • outcomeAgentUpdateOutcome | null

      How the update ended. Null while it runs.

      AgentUpdateOutcome

      How an update_agent ended.

      string enum

      • "completed"

        the replacement agent stayed up and the old container is removed.

      • "unchanged"

        the node already ran that image, so nothing was swapped.

      • "pull_failed"

        the image could not be pulled, so the node never tried it.

      • "failed"

        the swap did not hold and the old agent was put back, or its helper did not start.

    • detailstring 1–512 chars | null

      Why the update failed, up to 512 characters. Null unless it failed.

Example
{
  "type": "hello",
  "agent": {
    "version": "0.1.0",
    "image": "ghcr.io/ankorcs/ankor-agent:1.2.0",
    "image_id": "sha256:2c26b46b68ffc68ff99b453c1d30413413422d706483bfa0f98a5e886266e7ae"
  },
  "host": {
    "cpu_count": 8,
    "os_name": "Debian GNU/Linux 13 (trixie)",
    "kernel": "6.12.43+deb13-amd64"
  },
  "public_ip": "203.0.113.7",
  "country": "FR",
  "config": {
    "slots": [
      {
        "id": "s1",
        "port": 27015,
        "tv_port": 27020,
        "gslt_hint": "7E8F9A0B1C"
      }
    ],
    "pinned_ip": null,
    "controller": {
      "host": "cs.example.com",
      "tls": true,
      "socket_path": "/ws/agent",
      "events_path": "/api/node/events"
    },
    "auto_update": true,
    "memory_mb": null,
    "demo_upload": null
  },
  "slot_status": [
    {
      "slot_id": "s1",
      "state": "idle",
      "allocation_id": null,
      "match_id": null,
      "plugin_online": false,
      "gslt_state": "ok",
      "reserved": false,
      "server_steam_id": null
    }
  ],
  "cs2": {
    "build_id": "20114612",
    "latest_build_id": "20114612",
    "image": "ghcr.io/ankorcs/ankor-cs2:1.2.0",
    "image_id": "sha256:9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08",
    "update": null,
    "image_update": null
  },
  "unavailable": null,
  "demos": [
    {
      "slot_id": "s2",
      "file_name": "20260928-183000_0199a0b4-5c00-7000-8000-00000000abc0_de_nuke.dem",
      "size_bytes": 164626432,
      "modified_at": 1759084200000,
      "recording": false,
      "uploading": false,
      "upload_error": null
    }
  ],
  "server_exits": [
    {
      "slot_id": "s1",
      "allocation_id": "9c04e1d2-3a7b-4f6e-8d21-5e0b9c7a4f13",
      "reason": "clean_exit",
      "exit_code": 0,
      "detail": null
    }
  ],
  "event_backlog": {
    "servers": [
      {
        "allocation_id": "5b1e2c7a-8d4f-4c3b-9a6e-2f1d0c9b8a7e",
        "pending": 212
      }
    ],
    "server_count": 1,
    "event_count": 212,
    "reason": "HTTP 503"
  },
  "agent_update": {
    "image": "ghcr.io/ankorcs/ankor-agent:1.4.0",
    "step": null,
    "outcome": "completed",
    "detail": null
  }
}

log_batch

optionalagent → controller

One source's lines, sent only while a watch_logs holds the lease of that source. A replay of the source starts with reset and may span several frames. Between replays, new lines follow in batches. A replay larger than the remaining log budget of the agent leaves out the oldest lines.

  • type"log_batch"
  • sourceLogSource

    The log these lines belong to.

    LogSource

    One log of a node: the agent's log, one CS2 install or update's output, or one server's output.

    One of 3, told apart by kind:

    • kind"agent"
  • replay_idReplayId | null

    The watch_logs replay these frames answer, for that watcher alone. Null for new lines and for a replay sent to every watcher.

    ReplayId

    Identifies one watcher's replay, so the frames answering it reach only that watcher.

    string 1–64 chars

  • resetboolean

    Whether a replay starts, so the controller drops every line it holds of this source first.

  • linesLogLine[]

    The lines, oldest first.

    LogLine

    One log line.

    • atinteger ≥ 0

      Unix milliseconds when the agent logged or read the line.

    • levelAgentLogLevel | null

      How serious the agent rated the line. Null for server and CS2 update output.

      AgentLogLevel

      How serious an agent log line is.

      "info" | "warn" | "error"

    • linestring ≤ 2000 chars

      The line without its newline. The agent's lines carry the prefix of the component that logged them. A line keeps its ANSI colour escapes: SGR, ESC[ with digits and ;, then m. The agent removes every other escape, every control character but tab, and the Unicode characters that reorder text or break a line. The length limit may cut the last escape short.

Examples
{
  "type": "log_batch",
  "source": {
    "kind": "server",
    "allocation_id": "5b1e2c7a-8d4f-4c3b-9a6e-2f1d0c9b8a7e"
  },
  "replay_id": null,
  "reset": true,
  "lines": [
    {
      "at": 1700000014000,
      "level": null,
      "line": "Host activate: Loading (de_mirage)"
    }
  ]
}
{
  "type": "log_batch",
  "source": {
    "kind": "agent"
  },
  "replay_id": "0d6f1c52-3a7e-4b9d-8c21-5e4f7a9b2c13",
  "reset": false,
  "lines": [
    {
      "at": 1700000013000,
      "level": "warn",
      "line": "[agent:ws] reconnecting in 4000ms"
    }
  ]
}

log_index

optionalagent → controller

Every source the agent keeps, whole every time: its log, each CS2 install or update running or among the last 1 ended, and each server with output, running or among the last 10 ended.

Sent while any log lease runs, with each lease that starts and each replay asked for. Also sent when an update starts or ends, a server's first line arrives, a server ends, or a source's output is dropped, and every 30 seconds while lines arrive. Counts and last_line_at are as of that frame. A source missing from it is no longer kept.

  • type"log_index"
  • sourcesLogSourceSummary[], ≤ 77 items

    The agent's log first, then CS2 updates in the order they started, then servers in the order their output began.

    LogSourceSummary

    One source the agent keeps.

    • sourceLogSource

      Which log.

      LogSource

      One log of a node: the agent's log, one CS2 install or update's output, or one server's output.

      One of 3, told apart by kind:

      • kind"agent"
    • line_countinteger ≥ 0

      Lines kept of it.

    • last_line_atinteger ≥ 0 | null

      Unix milliseconds of the newest line kept of it. Null for a source with no lines kept.

    • endedboolean

      Whether the source has ended, as a server whose slot went idle or a CS2 install or update that ended. Always false for the agent's log.

Example
{
  "type": "log_index",
  "sources": [
    {
      "source": {
        "kind": "agent"
      },
      "line_count": 312,
      "last_line_at": 1700000014000,
      "ended": false
    },
    {
      "source": {
        "kind": "cs2_update",
        "update_id": "7c2e9a41-3b5d-4f68-9e0a-1d4c8b7f2a63"
      },
      "line_count": 0,
      "last_line_at": null,
      "ended": false
    },
    {
      "source": {
        "kind": "server",
        "allocation_id": "5b1e2c7a-8d4f-4c3b-9a6e-2f1d0c9b8a7e"
      },
      "line_count": 1840,
      "last_line_at": 1700000013000,
      "ended": false
    },
    {
      "source": {
        "kind": "server",
        "allocation_id": "9c04e1d2-3a7b-4f5e-8d6c-1b2a3c4d5e6f"
      },
      "line_count": 2000,
      "last_line_at": 1699999400000,
      "ended": true
    }
  ]
}

node_status

requiredagent → controller

Replaces cs2, unavailable and agent_update of the last hello.

Sent when they change:

  • When a maintenance operation starts or ends, so also when a CS2 install or update finishes, whoever started it, or update_cs2_image has switched the image.
  • When agent_update or cs2.image_update moves from pulling to switching.
  • When the newest public build changes.
  • When an agent swap that is still proving its replacement ends.

Each frame is the node's state when it was read, not a step in a history, so a change that is over before its report reads the state may go unreported.

  • type"node_status"
  • cs2Cs2State

    The game install and image.

    Cs2State

    The node's game install and image.

    • build_idBuildId | null

      The installed game build. Null before the first install.

      BuildId

      A Steam build id, in decimal.

      string

      Must match ^\d{1,20}$

    • latest_build_idBuildId | null

      The newest public CS2 build the agent read from Steam. Null until its first successful read. The agent updates an installed node to it on its own once every slot is idle, unless the node's auto_update is false.

      BuildId

      A Steam build id, in decimal.

      string

      Must match ^\d{1,20}$

    • imageImageReference

      The cs2 image set in agent.yml, which future launches use.

      ImageReference

      A Docker image reference, as cs2_image and agent_image in agent.yml hold it.

      string

      Must match ^[A-Za-z0-9][A-Za-z0-9._\-/:@]{0,254}$

    • image_idImageId | null

      The local id that image resolves to on the node. Null until the image is there.

      ImageId

      The id Docker gives an image: sha256: and 64 hex digits.

      string

      Must match ^sha256:[0-9a-f]{64}$

    • updateCs2Update | null

      The install or update that runs now, or else the last one to end since the agent started. Null before any. An agent that starts while an update that it started earlier still runs reports that update.

      Cs2Update

      A CS2 install or update on the node: one running, or the last one to end since the agent started.

      • idUpdateId

        Identifies the update and its output, the cs2_update LogSource with this update_id.

        UpdateId

        Chosen by the agent for each CS2 install or update it runs or finds running. A lowercase UUID.

        string

      • modeCs2UpdateMode

        How the update runs.

        Cs2UpdateMode

        How thoroughly a CS2 install or update runs.

        string enum

        • "update"

          downloads what changed.

        • "verify"

          also re-hashes every file.

        • "reinstall"

          wipes the install volume first and downloads all of it.

      • automaticboolean

        Whether the agent started the update on its own, to reach the newest public build. When false, an update_cs2 started it.

      • outcomeCs2UpdateOutcome | null

        How the update ended. Null while it runs.

        Cs2UpdateOutcome

        How a CS2 install or update ended.

        An update container that an earlier agent process started reports progress again until it exits. It ends completed or failed by its exit code, or unobserved.

        string enum

        • "completed"

          the game files are installed.

        • "pull_failed"

          the cs2 image could not be pulled.

        • "overlay_failed"

          its addon overlay could not be extracted.

        • "failed"

          any other failure, such as the update container exiting with an error.

        • "unobserved"

          an update container that an earlier agent process started exited, and the agent did not see the exit.

      • detailstring 1–512 chars | null

        Why the update failed, up to 512 characters. Null unless it failed.

    • image_updateCs2ImageUpdate | null

      The switch of image that runs now, or else the last one to end since the agent started. Null before any.

      Cs2ImageUpdate

      A switch of the cs2 image: the one running now, or the last one to end since the agent started.

      • imageImageReference

        The image the switch is to.

        ImageReference

        A Docker image reference, as cs2_image and agent_image in agent.yml hold it.

        string

        Must match ^[A-Za-z0-9][A-Za-z0-9._\-/:@]{0,254}$

      • stepImageUpdateStep | null

        Where the switch is while it runs. Null once it ended.

        ImageUpdateStep

        Where an update_agent or update_cs2_image is.

        string enum

        • "pulling"

          the image is being pulled.

        • "switching"

          the image is on the node and is put into use. A switch of the agent replaces its container, so the node goes offline and comes back. A switch of the cs2 image extracts its plugin files for future launches.

      • outcomeCs2ImageUpdateOutcome | null

        How the switch ended. Null while it runs.

        Cs2ImageUpdateOutcome

        How an update_cs2_image ended.

        string enum

        • "completed"

          future launches use the image.

        • "pull_failed"

          the image could not be pulled.

        • "overlay_failed"

          its addon overlay could not be extracted.

        • "failed"

          anything else, such as the agent not saving the image setting.

      • detailstring 1–512 chars | null

        Why the switch failed, up to 512 characters. Null unless it failed.

  • unavailableNodeUnavailable | null

    Why the node cannot run servers. Null when it can.

    NodeUnavailable

    Why the node cannot run servers now, with maintenance first while it lasts. Meanwhile start_server is refused with maintenance_running for maintenance and node_unavailable otherwise. Everything else works.

    Rejected when operation is null while code is maintenance, or set while code is another value.

    • codeNodeUnavailableCode

      NodeUnavailableCode

      Why a node cannot run servers, one of the values of NODE_UNAVAILABLE.

      string enum

      • "maintenance"

        The MaintenanceOperation in operation runs. It clears when the operation ends.

      • "event_storage"

        The agent cannot use the local store that keeps gameplay events until the controller takes them. detail says why. - If a call to the store waits past its limit, as on a disk that hangs, the code clears once the store answers. Deleting the store or restarting the agent loses what waits to be written. - If the agent cannot open the store, such as one that a newer agent wrote, a corrupt file or one it cannot write, the code lasts until a restart. Update the agent, or delete the store on the host and restart the agent. Deleting it loses the events and server exits it keeps.

      • "disk_space"

        The disk under the agent's data directory has less than 1024 MiB free, too little for a match's events and demo. The agent asks the disk every 30 seconds and before each launch, so the code clears within 30 seconds of space being freed, and a launch that arrives sooner is taken if the disk has room by then.

    • operationMaintenanceOperation | null

      The maintenance operation that runs when code is maintenance. Null for any other code.

      MaintenanceOperation

      The maintenance operation that runs on the node.

      string enum

      • "cs2_update"

        a CS2 install or update, also one an earlier agent started.

      • "agent_update"

        an update_agent, until its replacement has proved it stays up.

      • "cs2_image_update"

        an update_cs2_image.

      • "image_cleanup"

        the agent's removal of replaced images when it starts.

    • detailstring 1–512 chars

      The agent's message, up to 512 characters.

  • agent_updateAgentUpdate | null

    The last agent swap, as hello has it.

    AgentUpdate

    An agent update: the one in progress, one this agent saw end, or the last one to fail.

    • imageImageReference

      The agent image the update was to.

      ImageReference

      A Docker image reference, as cs2_image and agent_image in agent.yml hold it.

      string

      Must match ^[A-Za-z0-9][A-Za-z0-9._\-/:@]{0,254}$

    • stepImageUpdateStep | null

      Where the update is while it runs, with the replacement still on trial in switching. Null once it ended.

      ImageUpdateStep

      Where an update_agent or update_cs2_image is.

      string enum

      • "pulling"

        the image is being pulled.

      • "switching"

        the image is on the node and is put into use. A switch of the agent replaces its container, so the node goes offline and comes back. A switch of the cs2 image extracts its plugin files for future launches.

    • outcomeAgentUpdateOutcome | null

      How the update ended. Null while it runs.

      AgentUpdateOutcome

      How an update_agent ended.

      string enum

      • "completed"

        the replacement agent stayed up and the old container is removed.

      • "unchanged"

        the node already ran that image, so nothing was swapped.

      • "pull_failed"

        the image could not be pulled, so the node never tried it.

      • "failed"

        the swap did not hold and the old agent was put back, or its helper did not start.

    • detailstring 1–512 chars | null

      Why the update failed, up to 512 characters. Null unless it failed.

Examples
{
  "type": "node_status",
  "cs2": {
    "build_id": "20114612",
    "latest_build_id": "20114670",
    "image": "ghcr.io/ankorcs/ankor-cs2:1.2.0",
    "image_id": "sha256:9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08",
    "update": null,
    "image_update": {
      "image": "ghcr.io/ankorcs/ankor-cs2:1.2.0",
      "step": null,
      "outcome": "completed",
      "detail": null
    }
  },
  "unavailable": null,
  "agent_update": null
}
{
  "type": "node_status",
  "cs2": {
    "build_id": "20114612",
    "latest_build_id": "20114670",
    "image": "ghcr.io/ankorcs/ankor-cs2:1.2.0",
    "image_id": "sha256:9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08",
    "update": {
      "id": "7c2e9a41-3b5d-4f68-9e0a-1d4c8b7f2a63",
      "mode": "update",
      "automatic": true,
      "outcome": null,
      "detail": null
    },
    "image_update": null
  },
  "unavailable": {
    "code": "maintenance",
    "operation": "cs2_update",
    "detail": "CS2 update"
  },
  "agent_update": null
}
{
  "type": "node_status",
  "cs2": {
    "build_id": "20114670",
    "latest_build_id": "20114670",
    "image": "ghcr.io/ankorcs/ankor-cs2:1.2.0",
    "image_id": "sha256:9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08",
    "update": {
      "id": "7c2e9a41-3b5d-4f68-9e0a-1d4c8b7f2a63",
      "mode": "update",
      "automatic": true,
      "outcome": "completed",
      "detail": null
    },
    "image_update": null
  },
  "unavailable": {
    "code": "event_storage",
    "operation": null,
    "detail": "event spool schema mismatch: /srv/ankor/events/streams.sqlite is at version 3, newer than this agent's 2. Update the agent, or delete /srv/ankor/events on the host and restart it to start a new spool, which deletes every event not yet delivered to the controller"
  },
  "agent_update": null
}
{
  "type": "node_status",
  "cs2": {
    "build_id": "20114612",
    "latest_build_id": "20114670",
    "image": "ghcr.io/ankorcs/ankor-cs2:1.2.0",
    "image_id": "sha256:9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08",
    "update": null,
    "image_update": {
      "image": "ghcr.io/ankorcs/ankor-cs2:1.3.0",
      "step": "pulling",
      "outcome": null,
      "detail": null
    }
  },
  "unavailable": {
    "code": "maintenance",
    "operation": "cs2_image_update",
    "detail": "CS2 image update"
  },
  "agent_update": null
}
{
  "type": "node_status",
  "cs2": {
    "build_id": "20114612",
    "latest_build_id": "20114670",
    "image": "ghcr.io/ankorcs/ankor-cs2:1.2.0",
    "image_id": "sha256:9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08",
    "update": null,
    "image_update": null
  },
  "unavailable": {
    "code": "maintenance",
    "operation": "agent_update",
    "detail": "agent update"
  },
  "agent_update": {
    "image": "ghcr.io/ankorcs/ankor-agent:1.5.0",
    "step": "pulling",
    "outcome": null,
    "detail": null
  }
}

phase_change

optionalagent → controller

A match moved to another phase, as its plugin reports it, whether or not the match sends statistics. An event_batch carries the events behind it, such as going_live and map_end.

The agent sends it once. If the plugin is not connected to the agent, or the controller has not welcomed the agent's hello, it is lost, and get_server_status reads the phase again.

  • type"phase_change"
  • allocation_idAllocationId

    The server whose match changed phase.

    AllocationId

    The id the controller picks for each server it launches. It identifies that server and its events from then on.

    The id is a lowercase UUID, new for every launch. UUIDv7 is recommended, because its time-ordered values keep a database index on it compact as events arrive.

    string

  • phaseMatchPhase

    The phase the match is in now.

    MatchPhase

    Where a match is.

    string enum

    • "loading"

      its map is loading.

    • "warmup"

      warmup.

    • "pending_restore"

      a launch with a restore went here from warmup while the engine loads the backup, and goes to live once the restored round starts. A load that fails leaves the match here, where the plugin reports nothing, until a load is tried again or the server is stopped.

    • "knife"

      the knife round.

    • "side_choice"

      the side choice.

    • "live"

      live play.

    • "post_live"

      after the last round.

Example
{
  "type": "phase_change",
  "allocation_id": "5b1e2c7a-8d4f-4c3b-9a6e-2f1d0c9b8a7e",
  "phase": "live"
}

reply

requiredagent → controller

The answer to a request, in any order. A success carries the request's data, and a failure its reason.

One of 2:

AgentReplySuccess

A request that succeeded. request names the request, so data can be read without the request that reply_to answers. The settings in a set_config reply are the node's, whichever edit asked.

One of 12, told apart by request:

  • type"reply"
  • reply_toRequestId

    The id of the request this answers.

    RequestId

    Chosen by the controller. The reply repeats it as reply_to. While a request is running, the agent ignores another with the same id on that connection, because a reply could not tell the two apart.

    string 1–64 chars

  • request"start_server"
  • oktrue

    The request succeeded.

  • dataStartServerReply

    StartServerReply

    The server that started, and the slot the agent picked for it.

    • allocation_idAllocationId

      The allocation the request launched.

      AllocationId

      The id the controller picks for each server it launches. It identifies that server and its events from then on.

      The id is a lowercase UUID, new for every launch. UUIDv7 is recommended, because its time-ordered values keep a database index on it compact as events arrive.

      string

    • slot_idSlotId

      The slot the server runs in.

      SlotId

      A slot's id, which is also part of the names of its containers.

      string

      Must match ^[A-Za-z0-9_-]{1,32}$

Examples
{
  "type": "reply",
  "reply_to": "r2",
  "request": "stop_server",
  "ok": true,
  "data": {
    "allocation_id": "5b1e2c7a-8d4f-4c3b-9a6e-2f1d0c9b8a7e"
  }
}
{
  "type": "reply",
  "reply_to": "r9",
  "request": "stop_server",
  "ok": false,
  "error": {
    "code": "unknown_allocation",
    "detail": "unknown_allocation"
  }
}

resource_batch

optionalagent → controller

The agent's resource ticks, oldest first. A watch_resources lease that starts receives every tick the agent holds, then each new tick while it runs. The agent sends nothing while nobody watches, and nothing is stored.

  • type"resource_batch"
  • ticksResourceTick[], 1–100 items

    The ticks, oldest first: every tick the agent holds, up to the last 100, or the ticks since the previous frame.

    ResourceTick

    One sample of the node's disk, memory and CPU, taken every 3 seconds.

    • atinteger ≥ 0

      Unix milliseconds when the tick was taken.

    • diskResourceUsage | null

      The filesystem holding the node's data directory, as df counts it. Null when it could not be read.

      ResourceUsage

      A capacity and how much of it is in use, in bytes.

      • capinteger ≥ 0

        Bytes of capacity.

      • usedinteger ≥ 0

        Bytes in use.

    • memoryResourceUsage

      The memory the node may use, and how much it uses. Cache the kernel can free counts as free.

      ResourceUsage

      A capacity and how much of it is in use, in bytes.

      • capinteger ≥ 0

        Bytes of capacity.

      • usedinteger ≥ 0

        Bytes in use.

    • cpuobject | null

      How busy the node's CPUs are, in percent of all the CPUs it may use. Null in the first tick.

      • maxinteger 0–100

        Percent busy at the busiest moment of the tick.

      • averageinteger 0–100

        Percent busy on average over the tick.

  • resetboolean

    Whether ticks is every tick the agent holds and replaces the ticks the controller holds. It is true when a watch_resources starts the lease or asks for a replay, and on the first tick after each accepted hello while the lease runs. When false, ticks adds to them.

Example
{
  "type": "resource_batch",
  "ticks": [
    {
      "at": 1700000014000,
      "disk": {
        "cap": 107374182400,
        "used": 64424509440
      },
      "memory": {
        "cap": 17179869184,
        "used": 6442450944
      },
      "cpu": {
        "max": 71,
        "average": 38
      }
    }
  ],
  "reset": true
}

score

optionalagent → controller

A match's score changed, as its plugin reads it off the scoreboard, whether or not the match sends statistics: after a round, when going live resets it, or after a restore. An event_batch carries the rounds behind it as round_end and match_restored.

Like phase_change, it is sent once, and get_server_status reads the score again.

  • type"score"
  • allocation_idAllocationId

    The server whose match scored.

    AllocationId

    The id the controller picks for each server it launches. It identifies that server and its events from then on.

    The id is a lowercase UUID, new for every launch. UUIDv7 is recommended, because its time-ordered values keep a database index on it compact as events arrive.

    string

  • team1_scoreinteger ≥ 0

    Team 1's score.

  • team2_scoreinteger ≥ 0

    Team 2's score.

Example
{
  "type": "score",
  "allocation_id": "5b1e2c7a-8d4f-4c3b-9a6e-2f1d0c9b8a7e",
  "team1_score": 7,
  "team2_score": 5
}

server_exited

optionalagent → controller

A server has ended and its container is gone. Sent ahead of the idle slot_status of its slot, also for a launch that failed before or while its container started.

A server that ended while no connection was welcomed goes in server_exits of the next hello instead. If it ended after that hello went out, it is sent after the welcome, ahead of the slot statuses.

The agent keeps a server_exited until the socket takes its write, across its restarts, for up to 7 days, so one may also come again after a restart. One the socket took is not sent again, even if the connection drops before the controller reads it. The agent does not keep them while it cannot open that store, which it reports as unavailable with event_storage. While the store does not answer, the agent keeps one only once the store answers again. A server that ends while the agent is not running is missing from its next hello.

  • slot_idSlotId

    The slot it ran in.

    SlotId

    A slot's id, which is also part of the names of its containers.

    string

    Must match ^[A-Za-z0-9_-]{1,32}$

  • allocation_idAllocationId

    The server that ended.

    AllocationId

    The id the controller picks for each server it launches. It identifies that server and its events from then on.

    The id is a lowercase UUID, new for every launch. UUIDv7 is recommended, because its time-ordered values keep a database index on it compact as events arrive.

    string

  • reasonServerExitReason

    Why it ended.

    ServerExitReason

    Why a server ended: one of the AgentStopReason values when the agent stopped it on its own, or one of these.

    string enum

    • "idle_empty"

      the server stayed empty for its launch's idle_shutdown_seconds.

    • "config_rejected"

      the plugin refused its match config, as config_error says.

    • "start_timeout"

      the server was still starting 60 seconds after its launch, or after a restarted agent took it over.

    • "quit_timeout"

      the server was still running 10 seconds after its plugin sent quitting.

    • "slot_removed"

      a restarted agent found the server running on a slot that agent.yml no longer has.

    • "requested"

      a stop_server from the controller stopped it.

    • "clean_exit"

      it ended on its own with exit code 0, as after its match or a console quit.

    • "crashed"

      it ended on its own with another exit code.

    • "launch_failed"

      its container never started.

    • "lost"

      it ended and the agent did not see how, such as a container removed by hand.

  • exit_codeinteger | null

    The container's exit code. Null when the agent did not see one.

  • detailstring 1–512 chars | null

    What the reason does not say, up to 512 characters: the plugin's config_error for config_rejected, and Docker's message for launch_failed. Null for every other reason.

  • type"server_exited"
Examples
{
  "type": "server_exited",
  "slot_id": "s2",
  "allocation_id": "9c04e1d2-3a7b-4f6e-8d21-5e0b9c7a4f13",
  "reason": "clean_exit",
  "exit_code": 0,
  "detail": null
}
{
  "type": "server_exited",
  "slot_id": "s1",
  "allocation_id": "5b1e2c7a-8d4f-4c3b-9a6e-2f1d0c9b8a7e",
  "reason": "crashed",
  "exit_code": 139,
  "detail": null
}
{
  "type": "server_exited",
  "slot_id": "s2",
  "allocation_id": "5b1e2c7a-8d4f-4c3b-9a6e-2f1d0c9b8a7e",
  "reason": "config_rejected",
  "exit_code": 0,
  "detail": "roster.team1.players: 6 players, more than max_players allows"
}

shutdown

optionalagent → controller

The agent is stopping cleanly and starts no new work. It answers a request that arrives after this frame with shutting_down at once. Replies to the requests it is running may still follow for up to 5 seconds, and then it closes with code 1000.

Send no new requests after it. A request without a reply when the socket closes may or may not have run; the node's reports after it reconnects say which. Its servers keep running, and the agent takes them over when it returns. It is sent whether or not the controller has welcomed the hello.

  • type"shutdown"
  • signal"SIGINT" | "SIGTERM" | "SIGQUIT"

    The signal that stopped the agent.

Example
{
  "type": "shutdown",
  "signal": "SIGTERM"
}

slot_status

requiredagent → controller

A slot's status changed since the last hello or slot_status for it. Rejected when it breaks a rule of SlotStatus.

  • type"slot_status"
  • slot_idSlotId

    The slot this describes.

    SlotId

    A slot's id, which is also part of the names of its containers.

    string

    Must match ^[A-Za-z0-9_-]{1,32}$

  • stateSlotState

    What the slot is doing.

    SlotState

    What a slot is doing.

    string enum

    • "idle"

      the slot has no server.

    • "starting"

      the slot holds a server that its plugin has not yet reported ready for players with a server_status with ready. A server that a restarted agent took over starts here too.

    • "running"

      the plugin has reported the server ready.

    • "stopping"

      the server is being stopped.

  • allocation_idAllocationId | null

    The server on the slot. Null when the slot is idle.

    AllocationId

    The id the controller picks for each server it launches. It identifies that server and its events from then on.

    The id is a lowercase UUID, new for every launch. UUIDv7 is recommended, because its time-ordered values keep a database index on it compact as events arrive.

    string

  • match_idMatchId | null

    The server's match id. Null for a server without a match or a match launched without one.

    MatchId

    The id of one map's match, which the controller chooses. A series of maps is a grouping that the controller keeps. The id is a lowercase UUID, because demo file names carry it and case matters there.

    string

  • plugin_onlineboolean

    Whether the server's plugin is connected to the agent. get_server_status, console_command, restore_round and start_match reach it only while it is.

  • gslt_stateGsltState

    Whether Steam accepts the slot's token. start_server never picks a slot whose token is invalid.

    GsltState

    Whether Steam accepts a slot's token.

    string enum

    • "ok"

      a logon with the token succeeded, either the agent's own or that of a server that turned ready.

    • "invalid"

      Steam refused the token.

    • "unchecked"

      the agent has seen neither since the token was set.

  • reservedboolean

    Whether the idle slot is kept from launches, while the controller replaces its token with reserve_slot or while the agent logs its token on to Steam.

  • server_steam_idSteamId64 | null

    The server_steam_id that the server's plugin reported. The agent sends a slot_status as soon as it learns the ID. Null when the slot is idle or until the server is ready.

    SteamId64

    A SteamID64 as 17 decimal digits.

    string

    Must match ^\d{17}$

Examples
{
  "type": "slot_status",
  "slot_id": "s1",
  "state": "running",
  "allocation_id": "5b1e2c7a-8d4f-4c3b-9a6e-2f1d0c9b8a7e",
  "match_id": "0199a0b4-5c00-7000-8000-00000000abc1",
  "plugin_online": true,
  "gslt_state": "ok",
  "reserved": false,
  "server_steam_id": "85568392936840254"
}
{
  "type": "slot_status",
  "slot_id": "s2",
  "state": "idle",
  "allocation_id": null,
  "match_id": null,
  "plugin_online": false,
  "gslt_state": "ok",
  "reserved": false,
  "server_steam_id": null
}

Controller → agent

What the controller sends:

  • The welcome that accepts a hello, or the hello_refused that refuses it.
  • Requests, which carry an id and receive one reply each.
  • The watches that start or renew a stream of logs or resource samples.
  • check_cs2_build, which receives no answer.

check_cs2_build

optionalcontroller → agent

Read the newest public CS2 build from Steam now instead of at the next scheduled read, which comes every 30 minutes.

It receives no reply. The agent handles at most one per 60 seconds and drops any sooner one. One sent during a scheduled read shares it. Neither busy slots nor maintenance stop it. A changed build arrives as node_status, and an installed node updates to it once every slot is idle, as it always does.

  • type"check_cs2_build"
Example
{
  "type": "check_cs2_build"
}

console_command

optionalcontroller → agent

Run a console command on a server through its plugin.

The reply comes once the plugin has handed the command to the engine, which reports nothing back, so the reply cannot say that it worked. Refused with:

  • unknown_allocation when the node does not run the server.
  • plugin_offline while the slot's plugin_online is false.
  • plugin_status_timeout when the plugin does not answer.
  • command_blocked when the server runs a match and the line names a command of mp_restartgame, mp_warmup_start, mp_warmup_end, mp_warmup_pausetimer, mp_warmuptime, mp_swapteams, mp_switchteams, mp_scrambleteams, mp_backup_restore_load_file, mp_backup_round_file, mp_backup_round_auto, restart, changelevel, map, host_workshop_map, ds_workshop_changelevel, tv_record, tv_stoprecord and alias. The plugin then runs none of the line.

The output of the command joins the server's log, which log_batch streams while a watch_logs lease covers it and replays when one starts.

  • type"console_command"
  • idRequestId

    Chosen by the controller and unique among its open requests. The reply repeats it as reply_to. The agent ignores a request whose id is still running on the connection.

    RequestId

    Chosen by the controller. The reply repeats it as reply_to. While a request is running, the agent ignores another with the same id on that connection, because a reply could not tell the two apart.

    string 1–64 chars

  • allocation_idAllocationId

    The server to run it on.

    AllocationId

    The id the controller picks for each server it launches. It identifies that server and its events from then on.

    The id is a lowercase UUID, new for every launch. UUIDv7 is recommended, because its time-ordered values keep a database index on it compact as events arrive.

    string

  • commandstring ≤ 500 chars

    The console command line, up to 500 characters, handed to the engine as it is, so a newline or ; runs more than one command. It must not be blank, and tabs and newlines are its only control characters.

Example
{
  "type": "console_command",
  "id": "r7",
  "allocation_id": "5b1e2c7a-8d4f-4c3b-9a6e-2f1d0c9b8a7e",
  "command": "mp_restartgame 1"
}

Reply

One of 2, told apart by ok:

  • type"reply"
  • reply_toRequestId

    The id of the request this answers.

    RequestId

    Chosen by the controller. The reply repeats it as reply_to. While a request is running, the agent ignores another with the same id on that connection, because a reply could not tell the two apart.

    string 1–64 chars

  • request"console_command"
  • oktrue

    The request succeeded.

  • dataConsoleCommandReply

    The server the command was handed to.

    ConsoleCommandReply

    The server the command was handed to.

    • allocation_idAllocationId

      The allocation the command went to.

      AllocationId

      The id the controller picks for each server it launches. It identifies that server and its events from then on.

      The id is a lowercase UUID, new for every launch. UUIDv7 is recommended, because its time-ordered values keep a database index on it compact as events arrive.

      string

Reply examples
{
  "type": "reply",
  "reply_to": "r7",
  "request": "console_command",
  "ok": true,
  "data": {
    "allocation_id": "5b1e2c7a-8d4f-4c3b-9a6e-2f1d0c9b8a7e"
  }
}
{
  "type": "reply",
  "reply_to": "r7",
  "request": "console_command",
  "ok": false,
  "error": {
    "code": "plugin_offline",
    "detail": "plugin_offline"
  }
}

delete_demo

optionalcontroller → agent

Delete one demo by slot and file name.

The reply comes once the file is removed. Refused with:

  • unknown_slot when the node has no directory for the slot.
  • unknown_demo when the slot has no demo of that name.
  • demo_recording while its server records the demo.
  • demo_uploading while the demo uploads.
  • invalid_request when the name is outside the slot's demos/ directory.
  • failed when the removal fails.
  • type"delete_demo"
  • idRequestId

    Chosen by the controller and unique among its open requests. The reply repeats it as reply_to. The agent ignores a request whose id is still running on the connection.

    RequestId

    Chosen by the controller. The reply repeats it as reply_to. While a request is running, the agent ignores another with the same id on that connection, because a reply could not tell the two apart.

    string 1–64 chars

  • slot_idSlotId

    The slot whose directory holds it.

    SlotId

    A slot's id, which is also part of the names of its containers.

    string

    Must match ^[A-Za-z0-9_-]{1,32}$

  • file_nameDemoFileName

    The file to delete.

    DemoFileName

    A demo's file name, as it sits in its launch's directory, demos/<allocation_id>/ in its slot.

    string

    Must match ^[A-Za-z0-9._-]{1,156}\.dem$

Example
{
  "type": "delete_demo",
  "id": "r8",
  "slot_id": "s1",
  "file_name": "20260928-201500_0199a0b4-5c00-7000-8000-00000000abc1_de_mirage.dem"
}

Reply

One of 2, told apart by ok:

  • type"reply"
  • reply_toRequestId

    The id of the request this answers.

    RequestId

    Chosen by the controller. The reply repeats it as reply_to. While a request is running, the agent ignores another with the same id on that connection, because a reply could not tell the two apart.

    string 1–64 chars

  • request"delete_demo"
  • oktrue

    The request succeeded.

  • dataDeleteDemoReply

    The demo that was removed.

    DeleteDemoReply

    The demo that was removed.

    • slot_idSlotId

      The slot whose directory held it.

      SlotId

      A slot's id, which is also part of the names of its containers.

      string

      Must match ^[A-Za-z0-9_-]{1,32}$

    • file_nameDemoFileName

      The file removed.

      DemoFileName

      A demo's file name, as it sits in its launch's directory, demos/<allocation_id>/ in its slot.

      string

      Must match ^[A-Za-z0-9._-]{1,156}\.dem$

Reply examples
{
  "type": "reply",
  "reply_to": "r8",
  "request": "delete_demo",
  "ok": true,
  "data": {
    "slot_id": "s1",
    "file_name": "20260928-201500_0199a0b4-5c00-7000-8000-00000000abc1_de_mirage.dem"
  }
}
{
  "type": "reply",
  "reply_to": "r8",
  "request": "delete_demo",
  "ok": false,
  "error": {
    "code": "demo_uploading",
    "detail": "20261008-201500_0199a0b4-5c00-7a6e-8d4f-2f1d0c9b8a7e_de_mirage.dem is uploading"
  }
}

get_server_status

optionalcontroller → agent

Read a server's state as its plugin reports it now.

The reply comes within 10 seconds. Refused with:

  • unknown_allocation when the node does not run the server.
  • plugin_offline while the slot's plugin_online is false or when the plugin leaves before answering.
  • plugin_status_timeout after 10 seconds.
  • type"get_server_status"
  • idRequestId

    Chosen by the controller and unique among its open requests. The reply repeats it as reply_to. The agent ignores a request whose id is still running on the connection.

    RequestId

    Chosen by the controller. The reply repeats it as reply_to. While a request is running, the agent ignores another with the same id on that connection, because a reply could not tell the two apart.

    string 1–64 chars

  • allocation_idAllocationId

    The server to ask.

    AllocationId

    The id the controller picks for each server it launches. It identifies that server and its events from then on.

    The id is a lowercase UUID, new for every launch. UUIDv7 is recommended, because its time-ordered values keep a database index on it compact as events arrive.

    string

Example
{
  "type": "get_server_status",
  "id": "r5",
  "allocation_id": "5b1e2c7a-8d4f-4c3b-9a6e-2f1d0c9b8a7e"
}

Reply

One of 2, told apart by ok:

  • type"reply"
  • reply_toRequestId

    The id of the request this answers.

    RequestId

    Chosen by the controller. The reply repeats it as reply_to. While a request is running, the agent ignores another with the same id on that connection, because a reply could not tell the two apart.

    string 1–64 chars

  • request"get_server_status"
  • oktrue

    The request succeeded.

  • dataGetServerStatusReply

    The server asked about, and its state as its plugin reports it now.

    GetServerStatusReply

    The server asked about, and its state as its plugin reports it now.

    • map_namestring ≤ 64 chars

      The map the server is on, up to 64 characters.

    • matchobject | null

      The loaded match and its team state. Null on a server without a match or after a refused config.

      • match_idMatchId | null

        The match_id the server was launched with. Null for a match without one. The launch's config is the controller's to keep, so it is not echoed.

        MatchId

        The id of one map's match, which the controller chooses. A series of maps is a grouping that the controller keeps. The id is a lowercase UUID, because demo file names carry it and case matters there.

        string

      • phaseMatchPhase

        Where the match is.

        MatchPhase

        Where a match is.

        string enum

        • "loading"

          its map is loading.

        • "warmup"

          warmup.

        • "pending_restore"

          a launch with a restore went here from warmup while the engine loads the backup, and goes to live once the restored round starts. A load that fails leaves the match here, where the plugin reports nothing, until a load is tried again or the server is stopped.

        • "knife"

          the knife round.

        • "side_choice"

          the side choice.

        • "live"

          live play.

        • "post_live"

          after the last round.

      • team1_sidePlayingSide

        Team 1's current side.

        PlayingSide

        A playing side, for a field that never holds spec.

        string enum

        • "t"

          terrorists.

        • "ct"

          counter-terrorists.

      • team2_sidePlayingSide

        Team 2's current side, always the other side, sent so a reader shows each team without deriving it.

        PlayingSide

        A playing side, for a field that never holds spec.

        string enum

        • "t"

          terrorists.

        • "ct"

          counter-terrorists.

      • team1_scoreinteger ≥ 0

        Team 1's score.

      • team2_scoreinteger ≥ 0

        Team 2's score.

    • playersobject[], ≤ 64 items

      Everyone connected, up to 64.

      • steam_idSteamId64

        The player's SteamID64.

        SteamId64

        A SteamID64 as 17 decimal digits.

        string

        Must match ^\d{17}$

      • namePlayerName

        The player's name.

        PlayerName

        A player's name, cut to up to 64 characters, without control characters. Rejected when it holds a lone UTF-16 surrogate.

        string ≤ 64 chars

        Must match ^[^\p{Cc}]*$

      • sideSide

        The player's side.

        Side

        The side a player is on.

        string enum

        • "t"

          terrorists, a playing side.

        • "ct"

          counter-terrorists, a playing side.

        • "spec"

          spectators and players on no team.

    • config_errorstring ≤ 512 chars | null

      Why the plugin could not read or use the match config it was launched with, up to 512 characters. Null when the server has no match config or the plugin accepted it.

    • readyboolean

      Whether players can join. When true, the server is logged on to Steam and has loaded the map that its launch asked for, the workshop map when it has one. It stays true once set.

      CS2 does not fall back to an anonymous logon when Steam refuses its token, so true also indicates that Steam accepted the GSLT and that its last use can be updated.

    • server_steam_idSteamId64 | null

      The SteamID64 that Steam gave the server when it logged on with its GSLT. Each token keeps one SteamID across logons, so a controller can look the server up in Steam's server list by it. The plugin also adds the allocation_id without its dashes to sv_tags, which that list shows. Null until ready.

      SteamId64

      A SteamID64 as 17 decimal digits.

      string

      Must match ^\d{17}$

    • allocation_idAllocationId

      The allocation the request asked about.

      AllocationId

      The id the controller picks for each server it launches. It identifies that server and its events from then on.

      The id is a lowercase UUID, new for every launch. UUIDv7 is recommended, because its time-ordered values keep a database index on it compact as events arrive.

      string

Reply examples
{
  "type": "reply",
  "reply_to": "r5",
  "request": "get_server_status",
  "ok": true,
  "data": {
    "allocation_id": "5b1e2c7a-8d4f-4c3b-9a6e-2f1d0c9b8a7e",
    "map_name": "de_mirage",
    "match": {
      "match_id": "0199a0b4-5c00-7000-8000-00000000abc1",
      "phase": "live",
      "team1_side": "t",
      "team2_side": "ct",
      "team1_score": 7,
      "team2_score": 6
    },
    "players": [
      {
        "steam_id": "76561198000000001",
        "name": "alpha",
        "side": "t"
      }
    ],
    "config_error": null,
    "ready": true,
    "server_steam_id": "85568392936840254"
  }
}
{
  "type": "reply",
  "reply_to": "r5",
  "request": "get_server_status",
  "ok": false,
  "error": {
    "code": "plugin_offline",
    "detail": "plugin_offline"
  }
}

hello_refused

optionalcontroller → agent

The controller's answer to a hello it will not accept, followed by a close with code 1008 and reason hello_refused. The agent stops dialling until it is restarted, because the same hello would be refused again.

  • type"hello_refused"
  • reasonHelloRefusedReason

    HelloRefusedReason

    Why the controller refused a hello: invalid_hello, the hello fails its schema. A controller refuses nothing else at hello: an agent on a version it does not support connects, so it can still be updated, and receives no launches.

    "invalid_hello"

  • detailstring ≤ 512 chars

    A reason for the operator reading the agent's log, such as the field that failed.

Example
{
  "type": "hello_refused",
  "reason": "invalid_hello",
  "detail": "missing host.cpu_count"
}

reserve_slot

optionalcontroller → agent

Keep an idle slot from launches while the controller replaces its token, so that no server starts on a token that the controller is about to revoke at Steam.

The agent marks the slot reserved before it replies, the same way a launch marks its slot starting, so no start_server can take the slot once the reply is sent. The reply comes immediately. Refused with slot_held for a slot holding a server, and with unknown_slot for a slot the node does not have.

The reservation ends with the set_config that gives the slot a new token, with the slot's removal, after 60 seconds, or with the agent's restart. Asking again restarts the wait.

  • type"reserve_slot"
  • idRequestId

    Chosen by the controller and unique among its open requests. The reply repeats it as reply_to. The agent ignores a request whose id is still running on the connection.

    RequestId

    Chosen by the controller. The reply repeats it as reply_to. While a request is running, the agent ignores another with the same id on that connection, because a reply could not tell the two apart.

    string 1–64 chars

  • slot_idSlotId

    The slot to reserve.

    SlotId

    A slot's id, which is also part of the names of its containers.

    string

    Must match ^[A-Za-z0-9_-]{1,32}$

Example
{
  "type": "reserve_slot",
  "id": "r9",
  "slot_id": "s1"
}

Reply

One of 2, told apart by ok:

  • type"reply"
  • reply_toRequestId

    The id of the request this answers.

    RequestId

    Chosen by the controller. The reply repeats it as reply_to. While a request is running, the agent ignores another with the same id on that connection, because a reply could not tell the two apart.

    string 1–64 chars

  • request"reserve_slot"
  • oktrue

    The request succeeded.

  • dataReserveSlotReply

    The slot that is reserved.

    ReserveSlotReply

    The slot that is reserved.

    • slot_idSlotId

      The slot reserved.

      SlotId

      A slot's id, which is also part of the names of its containers.

      string

      Must match ^[A-Za-z0-9_-]{1,32}$

Reply examples
{
  "type": "reply",
  "reply_to": "r9",
  "request": "reserve_slot",
  "ok": true,
  "data": {
    "slot_id": "s1"
  }
}
{
  "type": "reply",
  "reply_to": "r9",
  "request": "reserve_slot",
  "ok": false,
  "error": {
    "code": "slot_held",
    "detail": "slot s1 holds a server"
  }
}

restore_round

optionalcontroller → agent

Play a running match on from one of its earlier rounds, on the same server, without a new launch. The engine writes a backup as each live round starts, and the node keeps the backups for the server's life. This request loads the one for round.

The reply comes once the plugin has asked the engine to load the backup. The match_restored event, which the plugin sends ahead of the restored round's round_start, confirms that the engine did. Refused with:

  • unknown_allocation when the node does not run the server.
  • no_backup when the node has no backup of that round, as in a game mode without backups (GameMode).
  • plugin_offline while the slot's plugin_online is false.
  • plugin_status_timeout when the plugin does not answer.
  • no_match, not_live or at_halftime when the plugin refuses the restore.

The node keeps the backup of every round it has played, including rounds after one it was restored to. If round is past the current round, the server loads play that the match has since left. Whether to allow that is the controller's rule.

  • type"restore_round"
  • idRequestId

    Chosen by the controller and unique among its open requests. The reply repeats it as reply_to. The agent ignores a request whose id is still running on the connection.

    RequestId

    Chosen by the controller. The reply repeats it as reply_to. While a request is running, the agent ignores another with the same id on that connection, because a reply could not tell the two apart.

    string 1–64 chars

  • allocation_idAllocationId

    The server whose match to restore.

    AllocationId

    The id the controller picks for each server it launches. It identifies that server and its events from then on.

    The id is a lowercase UUID, new for every launch. UUIDv7 is recommended, because its time-ordered values keep a database index on it compact as events arrive.

    string

  • roundRoundNumber

    The one-based round to play on from, whose backup the engine wrote as the round started.

    RoundNumber

    A one-based round number. A map's history holds far fewer than the 1000 it allows.

    integer 1–1000

Example
{
  "type": "restore_round",
  "id": "r10",
  "allocation_id": "5b1e2c7a-8d4f-4c3b-9a6e-2f1d0c9b8a7e",
  "round": 14
}

Reply

One of 2, told apart by ok:

  • type"reply"
  • reply_toRequestId

    The id of the request this answers.

    RequestId

    Chosen by the controller. The reply repeats it as reply_to. While a request is running, the agent ignores another with the same id on that connection, because a reply could not tell the two apart.

    string 1–64 chars

  • request"restore_round"
  • oktrue

    The request succeeded.

  • dataRestoreRoundReply

    The server and round the restore was handed to.

    RestoreRoundReply

    The server and round the restore was handed to.

    • allocation_idAllocationId

      The allocation the restore went to.

      AllocationId

      The id the controller picks for each server it launches. It identifies that server and its events from then on.

      The id is a lowercase UUID, new for every launch. UUIDv7 is recommended, because its time-ordered values keep a database index on it compact as events arrive.

      string

    • roundRoundNumber

      The round it loads.

      RoundNumber

      A one-based round number. A map's history holds far fewer than the 1000 it allows.

      integer 1–1000

Reply examples
{
  "type": "reply",
  "reply_to": "r10",
  "request": "restore_round",
  "ok": true,
  "data": {
    "allocation_id": "5b1e2c7a-8d4f-4c3b-9a6e-2f1d0c9b8a7e",
    "round": 14
  }
}
{
  "type": "reply",
  "reply_to": "r10",
  "request": "restore_round",
  "ok": false,
  "error": {
    "code": "no_backup",
    "detail": "no backup of round 14: ankor_round13.txt is not on the node"
  }
}

set_config

optionalcontroller → agent

Apply an edit to agent.yml, or refuse it without saving any part. Nothing is queued.

The reply comes once the edit is saved and applied, and carries the node's current settings, the NodeSettings that hello.config reports. An edit that changes nothing is not saved again. A slot missing from the reply's slots has been removed. Refused with:

  • slot_held when the edit would remove or re-port a held slot, or give its server's token to another slot.
  • slots_held when the edit changes any key under controller (its address, TLS or paths) while any slot is held.
  • slot_token_required or demo_header_value_required when the edit keeps a value that the node does not have.
  • invalid_request when two slots have one token or anything else fails validation.

After the reply, the agent sends a slot_status for each slot that the edit added or gave a new token. The slot reads unchecked until the agent has logged the new token on to Steam, which the agent starts then unless a server on the node is using the token. A change to controller closes the socket with 1000 once the reply has gone, and the agent sends its hello to the new address.

Required of a controller that hands nodes an agent.yml without slots, because it is how they receive any.

  • type"set_config"
  • idRequestId

    Chosen by the controller and unique among its open requests. The reply repeats it as reply_to. The agent ignores a request whose id is still running on the connection.

    RequestId

    Chosen by the controller. The reply repeats it as reply_to. While a request is running, the agent ignores another with the same id on that connection, because a reply could not tell the two apart.

    string 1–64 chars

  • configNodeSettingsEdit

    The settings to change.

    NodeSettingsEdit

    NodeSettings as set_config asks for them. A missing field keeps the node's value. Null is a value, as NodeSettings describes. slots, when present, is the whole layout.

    • slotsSlotEdit[], ≤ 64 itemsoptional

      The slots servers run in. SlotEdit describes their tokens.

      Rejected when two slots share an id or a gslt, or when two ports are equal. Every slot's port and tv_port count, so a slot's two ports differ too. It is also rejected when a port is 4500, where the agent listens for plugins. The agent fills in the kept tokens and refuses a layout in which two slots then share one.

      SlotEdit

      A slot as set_config asks for it.

      gslt is a new token, which may replace the token of a slot that runs a server. That server keeps the token it started with, and no other slot may use that token until the server ends (slot_held). Without gslt the slot keeps its current token, so a slot the node does not have yet needs one (slot_token_required), and a slot from a hello can be sent back unchanged.

      • idSlotId

        The slot's id, 1 to 32 letters, digits, _ and -. It is unique on its node.

        SlotId

        A slot's id, which is also part of the names of its containers.

        string

        Must match ^[A-Za-z0-9_-]{1,32}$

      • portPort

        The game port, UDP. Every port and tv_port on a node is different, free on the host, and not 4500, the port the agent listens on for plugins. Host networking explains why.

        Port

        A TCP or UDP port.

        integer 1–65535

      • tv_portPort

        The GOTV port, UDP, under the same rules as port.

        Port

        A TCP or UDP port.

        integer 1–65535

      • gsltanyoptional

        The slot's new token. Without one, the slot keeps the token it has.

    • pinned_ipIpv4 | nulloptional

      The address players connect to. Null for the node's public_ip. Servers already running keep the address their launch announced.

      Ipv4

      A dotted IPv4 address. Rejected unless it is four numbers from 0 to 255 joined by dots, each without leading zeros.

      string

    • controlleranyoptional

      Where the agent dials the controller. It redials when any part of it changes.

    • auto_updatebooleanoptional

      Whether the node keeps itself up to date: CS2 on cs2.latest_build_id, and its agent and cs2 images on the newest release of their major. When false, the node stays on cs2.build_id until an update_cs2, and accepts launches while it is behind cs2.latest_build_id, so players on a newer client cannot join its servers. Its images then change only with update_agent and update_cs2_image.

    • demo_uploadDemoUploadEdit | nulloptional

      Where the agent uploads demos. Null for no uploads, which keeps demos on the node. DemoUploadEdit describes its header.

      DemoUploadEdit

      A demo upload as set_config sets it. A header without its value keeps the value the node has.

      • urlDemoUploadUrl

        The upload target.

        DemoUploadUrl

        The URL finished demos are posted to. A target that needs credentials takes them in the configured header, such as Authorization: Basic …, which the node reports only as a hash.

        Rejected when the URL carries a user name or a password.

        string ≤ 255 chars, uri

      • headerobject | null

        A header to send with each upload. Null for none. Without value, the header keeps the value of the node's current header of that name, so the demo_upload of a hello can be sent back unchanged.

        • namestring

          The header's name, up to 64 letters, digits and -. Rejected when it is, in any case, one of host, content-type, content-length, transfer-encoding, connection, keep-alive, upgrade, expect, te, trailer, ankor-protocol, ankor-file-name, ankor-match-id and ankor-allocation-id, which the agent sets itself or which change how the request is framed.

          string

          Must match ^[A-Za-z0-9-]{1,64}$

        • valuestring ≤ 1024 charsoptional

          The header's value, sent verbatim, without line breaks.

          string ≤ 1024 chars

          Must match ^[^\r\n\u0000]*$

      • limit_mbitDemoUploadLimit | null

        Megabits a second an upload may send while a game server runs on the node. Null for no limit.

        DemoUploadLimit

        Whole megabits a second, from 1 to 10 Gbit/s.

        integer 1–10000

    • memory_mbMemoryLimit | nulloptional

      MiB of memory each game container may use. Null for no cap. A change applies to servers launched afterwards, and CS2 installs and updates are exempt.

      Rejected when it is not a whole number from 1024 to 65536.

      MemoryLimit

      MiB of memory each game container may use. CS2 installs and updates are exempt.

      Rejected when it is not a whole number from 1024 to 65536.

      integer 1024–65536

Example
{
  "type": "set_config",
  "id": "r4",
  "config": {
    "slots": [
      {
        "id": "s1",
        "port": 27015,
        "tv_port": 27020
      },
      {
        "id": "s2",
        "port": 27016,
        "tv_port": 27021,
        "gslt": "Q4T8N2WX7C1RJ5KM9BH3ZD6LP0YF4SG2"
      }
    ],
    "pinned_ip": null,
    "controller": {
      "host": "cs.example.com",
      "tls": true,
      "socket_path": "/ws/agent",
      "events_path": "/api/node/events"
    },
    "auto_update": true,
    "memory_mb": 6144,
    "demo_upload": {
      "url": "https://demos.example.com/upload",
      "header": {
        "name": "Authorization",
        "value": "Bearer 9b2e"
      },
      "limit_mbit": 40
    }
  }
}

Reply

One of 2, told apart by ok:

  • type"reply"
  • reply_toRequestId

    The id of the request this answers.

    RequestId

    Chosen by the controller. The reply repeats it as reply_to. While a request is running, the agent ignores another with the same id on that connection, because a reply could not tell the two apart.

    string 1–64 chars

  • request"set_config"
  • oktrue

    The request succeeded.

  • dataNodeSettings

    The part of agent.yml that the controller may edit, as hello reports it applied.

    NodeSettings

    The part of agent.yml that the controller may edit, as hello reports it applied.

    • slotsSlotLayout

      The slots servers run in.

      SlotLayout

      Up to 64 slots. Rejected when two slots share an id, or when two ports are equal. Every slot's port and tv_port count, so a slot's two ports differ too. It is also rejected when a port is 4500, where the agent listens for plugins.

      ReportedSlot

      A slot as the agent reports it, with the last ten characters of its token in place of the token.

      • idSlotId

        The slot's id, 1 to 32 letters, digits, _ and -. It is unique on its node.

        SlotId

        A slot's id, which is also part of the names of its containers.

        string

        Must match ^[A-Za-z0-9_-]{1,32}$

      • portPort

        The game port, UDP. Every port and tv_port on a node is different, free on the host, and not 4500, the port the agent listens on for plugins. Host networking explains why.

        Port

        A TCP or UDP port.

        integer 1–65535

      • tv_portPort

        The GOTV port, UDP, under the same rules as port.

        Port

        A TCP or UDP port.

        integer 1–65535

      • gslt_hintGsltHint

        The last ten characters of the slot's token.

        GsltHint

        The last ten characters of a token, which the agent reports instead of the token. They are enough to tell apart every token on one Steam account.

        string

        Must match ^[A-Za-z0-9]{10}$

    • pinned_ipIpv4 | null

      The address players connect to. Null for the node's public_ip. Servers already running keep the address their launch announced.

      Ipv4

      A dotted IPv4 address. Rejected unless it is four numbers from 0 to 255 joined by dots, each without leading zeros.

      string

    • controllerControllerAddress

      Where the agent dials the controller. It redials when any part of it changes.

      ControllerAddress

      Where a node dials the controller: a host with an optional port, whether it encrypts, and the paths it reaches there.

      • hostControllerHost

        The controller's host with an optional port.

        ControllerHost

        A host with an optional port and no scheme or path, such as cs.example.com, 192.168.0.10:3000 or [fd00::1].

        Rejected when it contains whitespace, a control character, /, \, ? or #. It is also rejected unless http:// followed by the value parses as a URL that has a host, an optional port and nothing else, such as a user name.

        string

      • tlsboolean

        Whether the agent dials over TLS.

      • socket_pathControllerPath

        The path of the agent socket. It is /ws/agent unless agent.yml sets controller.socket_path.

        ControllerPath

        A path on the controller's host. It starts with /, followed by URL path characters and percent escapes, without a query or fragment.

        string ≤ 255 chars

        Must match ^\/(?:[A-Za-z0-9\-._~!$&'()*+,;=:@/]|%[0-9A-Fa-f]{2})*$

      • events_pathControllerPath

        The path event batches are posted to. It is /api/node/events unless agent.yml sets controller.events_path.

        ControllerPath

        A path on the controller's host. It starts with /, followed by URL path characters and percent escapes, without a query or fragment.

        string ≤ 255 chars

        Must match ^\/(?:[A-Za-z0-9\-._~!$&'()*+,;=:@/]|%[0-9A-Fa-f]{2})*$

    • auto_updateboolean

      Whether the node keeps itself up to date: CS2 on cs2.latest_build_id, and its agent and cs2 images on the newest release of their major. When false, the node stays on cs2.build_id until an update_cs2, and accepts launches while it is behind cs2.latest_build_id, so players on a newer client cannot join its servers. Its images then change only with update_agent and update_cs2_image.

    • demo_uploadReportedDemoUpload | null

      Where the agent uploads demos. Null for no uploads, which keeps demos on the node. The header's value is reported as its SHA-256.

      ReportedDemoUpload

      A demo upload as the node reports it, with the header's value replaced by its hash, because the value is a credential that the controller set.

      • urlDemoUploadUrl

        The upload target.

        DemoUploadUrl

        The URL finished demos are posted to. A target that needs credentials takes them in the configured header, such as Authorization: Basic …, which the node reports only as a hash.

        Rejected when the URL carries a user name or a password.

        string ≤ 255 chars, uri

      • headerobject | null

        The header sent with each upload. Null for none.

        • namestring

          The header's name, up to 64 letters, digits and -. Rejected when it is, in any case, one of host, content-type, content-length, transfer-encoding, connection, keep-alive, upgrade, expect, te, trailer, ankor-protocol, ankor-file-name, ankor-match-id and ankor-allocation-id, which the agent sets itself or which change how the request is framed.

          string

          Must match ^[A-Za-z0-9-]{1,64}$

        • value_sha256string

          The lowercase hex SHA-256 of the value's UTF-8 bytes, to compare with the value the controller set.

          string

          Must match ^[0-9a-f]{64}$

      • limit_mbitDemoUploadLimit | null

        Megabits a second an upload may send while a game server runs on the node. Null for no limit.

        DemoUploadLimit

        Whole megabits a second, from 1 to 10 Gbit/s.

        integer 1–10000

    • memory_mbMemoryLimit | null

      MiB of memory each game container may use. Null for no cap. A change applies to servers launched afterwards, and CS2 installs and updates are exempt.

      Rejected when it is not a whole number from 1024 to 65536.

      MemoryLimit

      MiB of memory each game container may use. CS2 installs and updates are exempt.

      Rejected when it is not a whole number from 1024 to 65536.

      integer 1024–65536

Reply examples
{
  "type": "reply",
  "reply_to": "r4",
  "request": "set_config",
  "ok": true,
  "data": {
    "slots": [
      {
        "id": "s1",
        "port": 27015,
        "tv_port": 27020,
        "gslt_hint": "7E8F9A0B1C"
      },
      {
        "id": "s2",
        "port": 27016,
        "tv_port": 27021,
        "gslt_hint": "6LP0YF4SG2"
      }
    ],
    "pinned_ip": null,
    "controller": {
      "host": "cs.example.com",
      "tls": true,
      "socket_path": "/ws/agent",
      "events_path": "/api/node/events"
    },
    "auto_update": true,
    "memory_mb": 6144,
    "demo_upload": {
      "url": "https://demos.example.com/upload",
      "header": {
        "name": "Authorization",
        "value_sha256": "e39ca46d71d4b93e08230ee35a7a64f6e188958014de50dfd8be910631edb679"
      },
      "limit_mbit": 40
    }
  }
}
{
  "type": "reply",
  "reply_to": "r4",
  "request": "set_config",
  "ok": false,
  "error": {
    "code": "slot_held",
    "detail": "held slots would change: s1"
  }
}

start_match

optionalcontroller → agent

Start a match in warmup whatever its ready_mode, without waiting for anyone to be ready. An admin can always start the match this way, and under external it is the only way to.

The reply comes once the plugin has started the countdown to the knife round or live play. Refused with:

  • unknown_allocation when the node does not run the server.
  • plugin_offline while the slot's plugin_online is false.
  • plugin_status_timeout when the plugin does not answer.
  • no_match, not_in_warmup or team_empty when the plugin refuses the start.

A repeat while the countdown runs is accepted and changes nothing.

  • type"start_match"
  • idRequestId

    Chosen by the controller and unique among its open requests. The reply repeats it as reply_to. The agent ignores a request whose id is still running on the connection.

    RequestId

    Chosen by the controller. The reply repeats it as reply_to. While a request is running, the agent ignores another with the same id on that connection, because a reply could not tell the two apart.

    string 1–64 chars

  • allocation_idAllocationId

    The server whose match to start.

    AllocationId

    The id the controller picks for each server it launches. It identifies that server and its events from then on.

    The id is a lowercase UUID, new for every launch. UUIDv7 is recommended, because its time-ordered values keep a database index on it compact as events arrive.

    string

Example
{
  "type": "start_match",
  "id": "r11",
  "allocation_id": "5b1e2c7a-8d4f-4c3b-9a6e-2f1d0c9b8a7e"
}

Reply

One of 2, told apart by ok:

  • type"reply"
  • reply_toRequestId

    The id of the request this answers.

    RequestId

    Chosen by the controller. The reply repeats it as reply_to. While a request is running, the agent ignores another with the same id on that connection, because a reply could not tell the two apart.

    string 1–64 chars

  • request"start_match"
  • oktrue

    The request succeeded.

  • dataStartMatchReply

    The server the start was handed to.

    StartMatchReply

    The server the start was handed to.

    • allocation_idAllocationId

      The allocation the start went to.

      AllocationId

      The id the controller picks for each server it launches. It identifies that server and its events from then on.

      The id is a lowercase UUID, new for every launch. UUIDv7 is recommended, because its time-ordered values keep a database index on it compact as events arrive.

      string

Reply examples
{
  "type": "reply",
  "reply_to": "r11",
  "request": "start_match",
  "ok": true,
  "data": {
    "allocation_id": "5b1e2c7a-8d4f-4c3b-9a6e-2f1d0c9b8a7e"
  }
}
{
  "type": "reply",
  "reply_to": "r11",
  "request": "start_match",
  "ok": false,
  "error": {
    "code": "plugin_offline",
    "detail": "plugin_offline"
  }
}

start_server

requiredcontroller → agent

Launch a server on an idle slot the agent picks. The server logs in with the slot's token. The agent reports the slot starting before any container work, so two launches never share one.

The reply comes once the container has started, before the server is up, and slot_status reports it running later. The agent pulls no image here. A launch on a cs2_image whose plugin files the agent has not extracted yet waits while it does. Refused with:

  • A StartRefusal when the node cannot take the launch.
  • allocation_in_use when the node is already launching the allocation or its server holds a slot.
  • allocation_reused when the node launched the allocation before.
  • failed when Docker fails the launch, once its container is gone, after a server_exited with launch_failed.
  • type"start_server"
  • idRequestId

    Chosen by the controller and unique among its open requests. The reply repeats it as reply_to. The agent ignores a request whose id is still running on the connection.

    RequestId

    Chosen by the controller. The reply repeats it as reply_to. While a request is running, the agent ignores another with the same id on that connection, because a reply could not tell the two apart.

    string 1–64 chars

  • allocation_idAllocationId

    Identifies the server from now on. Every launch needs a new one, a relaunch too, because the node files the server's events and demos under it. A launch that a StartRefusal turned away used nothing, so the same id may be sent again. Any other failure uses it up.

    AllocationId

    The id the controller picks for each server it launches. It identifies that server and its events from then on.

    The id is a lowercase UUID, new for every launch. UUIDv7 is recommended, because its time-ordered values keep a database index on it compact as events arrive.

    string

  • launchServerLaunchConfig

    What to launch.

    ServerLaunchConfig

    What a game server is launched with: any CS2 server, and the match Ankor's plugin plays on it, if any.

    Rejected when both workshop_id and map_name are null. With a match, it is also rejected:

    • when workshop_id is set, because a match plays a stock map.
    • when the roster's players and spectators together outnumber max_players.
    • when game_mode is not casual, competitive or wingman.
    • when ready_mode is external and ready_timeout_seconds is set.

    • hostnamestring | nulloptional

      The server's name, which players see in game, such as on the scoreboard. It is up to 128 characters. Null or empty leaves the game's own name, and null is the value when absent. Rejected when it holds an ASCII control character, such as a line break.

    • steam_web_api_keySteamWebApiKey | "" | nulloptional

      The Steam Web API key that workshop downloads need. Null or empty skips them, and null is the value when absent. Whoever runs the node can read the key.

      One of 2:

      SteamWebApiKey

      A Steam Web API key, which the server needs to download workshop maps.

      string

      Must match ^[A-Za-z0-9]{1,64}$

    • game_passwordGamePassword | nulloptional

      The password players type to join. Null or empty for none, and null when absent.

      GamePassword

      The password players type. It is printable ASCII without spaces, " or ;, because the server would read those as separators.

      string ≤ 128 chars

      Must match ^[!#-:<-~]*$

    • chat_taganyoptional

      The tag in front of the plugin's chat lines. When absent, each of its fields takes its default.

    • game_modeanyoptional

      The game mode, which sets the game's rules. When absent, competitive.

    • max_playersinteger 1–64

      The player capacity of the server, as -maxplayers. A match's roster, players and spectators together, must fit.

    • workshop_idWorkshopId | nulloptional

      A workshop map to play. Null for a stock map, and when absent. A match plays a stock map.

      WorkshopId

      A Steam Workshop map's id.

      string

      Must match ^\d{1,20}$

    • map_nameStockMapName | null

      The stock map to play, required without workshop_id. With a workshop_id, the server boots this map before it switches to the workshop map. Null for de_dust2 in that case.

      StockMapName

      The name of a stock map, such as de_mirage. A name of all digits would be a workshop id.

      string

      Must match ^(?!\d+$)[A-Za-z0-9_-]{1,64}$

    • gotvobject | nulloptional

      GOTV that spectators can watch. Null for none, and null when absent. A match that records a demo still runs GOTV then, because demos are recorded through it. That GOTV runs with tv_delay 0 and a random tv_password that the agent sends nowhere, so no spectator can join.

      Whether GOTV runs is set on the command line, because GOTV starts with the map. The other settings apply on every map load and when a match goes live, never while a demo records.

      • passwordGamePassword | nulloptional

        The GOTV password, as tv_password, apart from game_password. Null or empty for none, and null when absent.

        GamePassword

        The password players type. It is printable ASCII without spaces, " or ;, because the server would read those as separators.

        string ≤ 128 chars

        Must match ^[!#-:<-~]*$

      • delay_secondsinteger ≥ 0

        Seconds GOTV runs behind the game.

    • idle_shutdown_secondsinteger ≥ 1 | null

      Seconds the server may stay empty once its slot is running before the agent stops it, reporting idle_empty as its ServerExitReason. Null for a server that runs until stopped. Hibernation must stay off. See When the cfgs run.

    • execCfgName[], ≤ 16 items

      Cfgs the server runs in order, after the game mode's cfg and the match's rules. A server without a match runs them 1 second after every map load, and a match server runs them at warmup and when the match goes live. Warmup and the knife round run Ankor's cfgs over them.

      CfgName

      A cfg the server runs, relative to game/csgo/cfg/ and without .cfg, such as node/<name> from the node's cfgs.

      string ≤ 128 chars

      Must match ^[A-Za-z0-9_-]+(\/[A-Za-z0-9_-]+)*$

    • commandsstring[], ≤ 128 items

      Console lines that the server runs after exec, in order and at the same moments, so they override exec and rules. A match server leaves out a line that names a command of mp_restartgame, mp_warmup_start, mp_warmup_end, mp_warmup_pausetimer, mp_warmuptime, mp_swapteams, mp_switchteams, mp_scrambleteams, mp_backup_restore_load_file, mp_backup_round_file, mp_backup_round_auto, restart, changelevel, map, host_workshop_map, ds_workshop_changelevel, tv_record, tv_stoprecord and alias. Rejected when a line holds an ASCII control character, such as a line break, which would end the console line.

    • matchMatchConfig | nulloptional

      The match Ankor's plugin plays. Null runs a plain CS2 server, and is the value when absent. The plugin then runs no match, and the map, exec, commands and the node's plugins set the game.

      MatchConfig

      The match a server is launched with, which covers one map. Each server plays one match. A restore launches the same match_id again under a new allocation_id, from a round_backup that the first server sent.

      • match_idMatchId | nulloptional

        The match's id, which names its demos and which slot_status and get_server_status report. Null for none, and when absent.

        MatchId

        The id of one map's match, which the controller chooses. A series of maps is a grouping that the controller keeps. The id is a lowercase UUID, because demo file names carry it and case matters there.

        string

      • send_statisticsbooleanoptional

        Whether the plugin sends the match's gameplay events, its round_backup among them, which the agent uploads in an event_batch. When absent, false.

        Every match reports its phase_change and score. A match without statistics sends each round_backup in backup instead, and a match with statistics only in its events.

      • rosterRoster

        Who may join, and on which team.

        Roster

        The teams and spectators of a match. Each player holds one place on the server, whichever team or list the player is in.

        Rejected when a SteamID appears twice across both teams and spectators, or when they hold more than 64 together.

        • team1RosterTeam

          Team 1, which events number 1.

          RosterTeam

          One team of a Roster.

          • namestring ≤ 30 chars

            The team name that players see, up to 30 characters. Rejected when it is blank, or when it holds a NUL or a lone UTF-16 surrogate.

            string ≤ 30 chars

            Must match \S

          • playersSteamId64[], ≥ 1 items

            The players of the team, captain first. The captain may use !forceready and picks the side after the knife round. A team has at least one player, so it always has a captain.

            SteamId64

            A SteamID64 as 17 decimal digits.

            string

            Must match ^\d{17}$

        • team2RosterTeam

          Team 2, which events number 2.

          RosterTeam

          One team of a Roster.

          • namestring ≤ 30 chars

            The team name that players see, up to 30 characters. Rejected when it is blank, or when it holds a NUL or a lone UTF-16 surrogate.

            string ≤ 30 chars

            Must match \S

          • playersSteamId64[], ≥ 1 items

            The players of the team, captain first. The captain may use !forceready and picks the side after the knife round. A team has at least one player, so it always has a captain.

            SteamId64

            A SteamID64 as 17 decimal digits.

            string

            Must match ^\d{17}$

        • spectatorsSteamId64[], ≤ 2 items

          Players allowed in as spectators, who watch with the game's free camera, up to 2. When ready_mode is manual, they ready up too.

          SteamId64

          A SteamID64 as 17 decimal digits.

          string

          Must match ^\d{17}$

      • sidesSidesMode

        How starting sides are decided.

        SidesMode

        How starting sides are decided.

        string enum

        • "knife"

          a knife round decides them.

        • "team1_ct"

          team 1 starts as CT.

        • "team1_t"

          team 1 starts as T.

      • knife_decision_secondsinteger 5–300optional

        Seconds the knife winners' captain has to type !stay, !swap, !ct or !t, from 5 to 300. The server stays in warmup meanwhile, with its clock counting these seconds down. An answer starts the match immediately. Without one, the team stays on its side when the clock runs out. When absent, 60.

      • ready_modeanyoptional

        How warmup ends. When absent, auto.

      • spectators_ready_upbooleanoptional

        Whether, under manual, the match also waits for every rostered spectator to connect and type !ready, and any of them can force-ready the spectators. When absent, false, so the match waits for the teams alone.

      • allow_force_readybooleanoptional

        Whether, under manual, captains and rostered spectators may type !forceready. When absent, false, so the match waits for every rostered player's !ready.

      • ready_timeout_secondsinteger 1–1800 | nulloptional

        Seconds from warmup until both teams must be ready, up to 1800. Null for no time limit, and when absent.

        When the time passes, the server kicks every player and the map ends with map_end, which says who was connected and ready. The controller decides what that means for the match. Must be null with external, where only the controller starts the match.

      • record_demoboolean

        Whether GOTV records the live match as a demo.

      • rulesanyoptional

        The rules the match is played under, applied over those of the game mode. When absent, every rule takes its default.

      • restoreRestoreFile | nulloptional

        A live round to play on from, taken from the round_backup that reported it. Null for a match that plays the map from its start, and when absent.

        With a restore, the server skips the knife round and the side choice whatever sides says, and after warmup it loads the backup instead of going live. Team 1 takes the restore's team1_side, the round and the scores come from the file, and the restore's rules replace the match's rules. The agent writes file to the slot for mp_backup_restore_load_file.

        RestoreFile

        What a server needs to play on from the backup of a live round. That is the engine's backup file, which holds the round, the scores and the money and equipment of every player, and the two things that the file does not hold.

        A launch carries one as match.restore.

        • team1_sidePlayingSide

          Team 1's side in that round. On a launch, the plugin seats players by it through warmup, before the load.

          PlayingSide

          A playing side, for a field that never holds spec.

          string enum

          • "t"

            terrorists.

          • "ct"

            counter-terrorists.

        • rulesMatchRules

          The rules that the map was played under. A restore plays on under these rules instead of the rules of its launch.

          MatchRules

          The rules of a match, as a launch asks for them and as going_live and round_backup report the server playing them.

          The plugin reads the round settings off the server's cvars. friendly_fire comes from the launch, and Ankor applies it through the damage reductions instead of mp_friendlyfire.

          • max_roundsinteger 1–240

            Rounds in regulation, as mp_maxrounds, up to 240. MR12 is 24 and MR15 is 30.

          • overtimeboolean

            Whether a tie after regulation goes to overtime, as mp_overtime_enable.

          • overtime_max_roundsinteger 1–60

            Rounds in one overtime, both halves together, as mp_overtime_maxrounds. The bound is 60, ten times the stock 6.

          • friendly_fireboolean

            Whether bullets and melee hurt teammates. Grenades always do.

        • filestring 1–524288 chars

          The backup file as the engine wrote it, up to 524288 characters. The file holds the money and equipment of every player, keyed by account, and only mp_backup_restore_load_file reads it.

          The file is text with tabs and line breaks, so a file at the length cap stays within the frame limits once JSON escapes it. Rejected when it holds a lone UTF-16 surrogate.

          string 1–524288 chars

          Must match ^[\t\n\r\P{Cc}]*$

Examples
{
  "type": "start_server",
  "id": "r1",
  "allocation_id": "5b1e2c7a-8d4f-4c3b-9a6e-2f1d0c9b8a7e",
  "launch": {
    "map_name": "de_mirage",
    "workshop_id": null,
    "hostname": "Ankor | cs.example.com",
    "steam_web_api_key": null,
    "game_password": "hunter2",
    "chat_tag": {
      "text": "Ankor",
      "color": "yellow"
    },
    "game_mode": "competitive",
    "max_players": 2,
    "gotv": {
      "password": "hunter2",
      "delay_seconds": 90
    },
    "idle_shutdown_seconds": null,
    "exec": [],
    "commands": [
      "mp_halftime_duration 15",
      "mp_team_timeout_max 2",
      "mp_overtime_startmoney 12500"
    ],
    "match": {
      "match_id": "0199a0b4-5c00-7000-8000-00000000abc1",
      "send_statistics": true,
      "roster": {
        "team1": {
          "name": "team_alpha",
          "players": [
            "76561198000000001"
          ]
        },
        "team2": {
          "name": "team_bravo",
          "players": [
            "76561198000000002"
          ]
        },
        "spectators": []
      },
      "sides": "knife",
      "knife_decision_seconds": 60,
      "ready_mode": "manual",
      "spectators_ready_up": false,
      "allow_force_ready": false,
      "ready_timeout_seconds": null,
      "record_demo": true,
      "rules": {
        "max_rounds": 24,
        "overtime": true,
        "overtime_max_rounds": 6,
        "friendly_fire": true
      },
      "restore": null
    }
  }
}
{
  "type": "start_server",
  "id": "r11",
  "allocation_id": "3f8a2c61-9d4e-4b7a-a1c5-7e2d0b9f6a48",
  "launch": {
    "map_name": "de_inferno",
    "workshop_id": null,
    "hostname": "Ankor | cs.example.com",
    "steam_web_api_key": null,
    "game_password": "hunter2",
    "chat_tag": {
      "text": "Ankor",
      "color": "yellow"
    },
    "game_mode": "wingman",
    "max_players": 4,
    "gotv": {
      "password": "hunter2",
      "delay_seconds": 90
    },
    "idle_shutdown_seconds": null,
    "exec": [],
    "commands": [],
    "match": {
      "match_id": "0199a0b4-5c00-7000-8000-00000000abc2",
      "send_statistics": false,
      "roster": {
        "team1": {
          "name": "team_alpha",
          "players": [
            "76561198000000001",
            "76561198000000003"
          ]
        },
        "team2": {
          "name": "team_bravo",
          "players": [
            "76561198000000002",
            "76561198000000004"
          ]
        },
        "spectators": []
      },
      "sides": "team1_ct",
      "knife_decision_seconds": 60,
      "ready_mode": "auto",
      "spectators_ready_up": false,
      "allow_force_ready": false,
      "ready_timeout_seconds": null,
      "record_demo": true,
      "rules": {
        "max_rounds": 16,
        "overtime": false,
        "overtime_max_rounds": 6,
        "friendly_fire": true
      },
      "restore": null
    }
  }
}
{
  "type": "start_server",
  "id": "r8",
  "allocation_id": "c1d2e3f4-5a6b-4c7d-8e9f-0a1b2c3d4e5f",
  "launch": {
    "map_name": "de_mirage",
    "workshop_id": null,
    "hostname": "Ankor | cs.example.com",
    "steam_web_api_key": null,
    "game_password": "hunter2",
    "chat_tag": {
      "text": "Ankor",
      "color": "yellow"
    },
    "game_mode": "competitive",
    "max_players": 2,
    "gotv": {
      "password": "hunter2",
      "delay_seconds": 90
    },
    "idle_shutdown_seconds": null,
    "exec": [],
    "commands": [],
    "match": {
      "match_id": "0199a0b4-5c00-7000-8000-00000000abc1",
      "send_statistics": true,
      "roster": {
        "team1": {
          "name": "team_alpha",
          "players": [
            "76561198000000001"
          ]
        },
        "team2": {
          "name": "team_bravo",
          "players": [
            "76561198000000002"
          ]
        },
        "spectators": []
      },
      "sides": "knife",
      "knife_decision_seconds": 60,
      "ready_mode": "manual",
      "spectators_ready_up": false,
      "allow_force_ready": false,
      "ready_timeout_seconds": null,
      "record_demo": true,
      "rules": {
        "max_rounds": 24,
        "overtime": true,
        "overtime_max_rounds": 6,
        "friendly_fire": true
      },
      "restore": {
        "team1_side": "t",
        "rules": {
          "max_rounds": 24,
          "overtime": true,
          "overtime_max_rounds": 6,
          "friendly_fire": true
        },
        "file": "\"SaveFile\"\n{\n\t\"timestamp\"\t\t\"Tue Oct 20 20:14:00 2026\"\n\t\"round\"\t\t\"13\"\n}\n"
      }
    }
  }
}
{
  "type": "start_server",
  "id": "r9",
  "allocation_id": "9c04e1d2-3a7b-4f6e-8d21-5e0b9c7a4f13",
  "launch": {
    "workshop_id": "3070987642",
    "map_name": null,
    "hostname": "Retakes | cs.example.com",
    "steam_web_api_key": "0123456789ABCDEF0123456789ABCDEF",
    "game_password": "hunter2",
    "chat_tag": {
      "text": "Ankor",
      "color": "yellow"
    },
    "game_mode": "competitive",
    "max_players": 10,
    "gotv": {
      "password": null,
      "delay_seconds": 0
    },
    "idle_shutdown_seconds": 180,
    "exec": [
      "node/retakes"
    ],
    "commands": [
      "mp_roundtime 2"
    ],
    "match": null
  }
}
{
  "type": "start_server",
  "id": "r3",
  "allocation_id": "e7a3d9c1-6b2f-4e8a-9c5d-1f0b3a7e2d64",
  "launch": {
    "hostname": "Ankor",
    "steam_web_api_key": null,
    "game_password": null,
    "chat_tag": {
      "text": "Ankor",
      "color": "yellow"
    },
    "game_mode": "competitive",
    "max_players": 10,
    "workshop_id": null,
    "map_name": "de_dust2",
    "gotv": {
      "password": null,
      "delay_seconds": 0
    },
    "idle_shutdown_seconds": null,
    "exec": [],
    "commands": [],
    "match": null
  }
}

Reply

One of 2, told apart by ok:

  • type"reply"
  • reply_toRequestId

    The id of the request this answers.

    RequestId

    Chosen by the controller. The reply repeats it as reply_to. While a request is running, the agent ignores another with the same id on that connection, because a reply could not tell the two apart.

    string 1–64 chars

  • request"start_server"
  • oktrue

    The request succeeded.

  • dataStartServerReply

    The server that started, and the slot the agent picked for it.

    StartServerReply

    The server that started, and the slot the agent picked for it.

    • allocation_idAllocationId

      The allocation the request launched.

      AllocationId

      The id the controller picks for each server it launches. It identifies that server and its events from then on.

      The id is a lowercase UUID, new for every launch. UUIDv7 is recommended, because its time-ordered values keep a database index on it compact as events arrive.

      string

    • slot_idSlotId

      The slot the server runs in.

      SlotId

      A slot's id, which is also part of the names of its containers.

      string

      Must match ^[A-Za-z0-9_-]{1,32}$

Reply examples
{
  "type": "reply",
  "reply_to": "r1",
  "request": "start_server",
  "ok": true,
  "data": {
    "allocation_id": "5b1e2c7a-8d4f-4c3b-9a6e-2f1d0c9b8a7e",
    "slot_id": "s1"
  }
}
{
  "type": "reply",
  "reply_to": "r1",
  "request": "start_server",
  "ok": false,
  "error": {
    "code": "no_free_slot",
    "detail": "no_free_slot"
  }
}
{
  "type": "reply",
  "reply_to": "r1",
  "request": "start_server",
  "ok": false,
  "error": {
    "code": "allocation_in_use",
    "detail": "allocation 5b1e2c7a-8d4f-4c3b-9a6e-2f1d0c9b8a7e already holds a slot"
  }
}

stop_server

optionalcontroller → agent

Stop an allocation's server. Once asked, the agent stops it: a stop that Docker fails is answered with failed, and the agent tries it again until the server is gone. The slot stays held until then.

The reply comes once its container has stopped: up to 10 seconds before the kill, then up to 35 seconds seconds for the kill and the reply. A stop during a launch of the allocation first waits for the launch's current step to end. That step can take minutes on the first launch after an image switch, which extracts the overlay, so the reply can come after the session's wait. If the launch is stopped between its steps, it starts no server and its start_server fails. If it is stopped while Docker starts its container, its start_server succeeds and the server is then stopped. Refused with unknown_allocation for an allocation the node does not run.

  • type"stop_server"
  • allocation_idAllocationId

    The server to stop.

    AllocationId

    The id the controller picks for each server it launches. It identifies that server and its events from then on.

    The id is a lowercase UUID, new for every launch. UUIDv7 is recommended, because its time-ordered values keep a database index on it compact as events arrive.

    string

  • idRequestId

    Chosen by the controller and unique among its open requests. The reply repeats it as reply_to. The agent ignores a request whose id is still running on the connection.

    RequestId

    Chosen by the controller. The reply repeats it as reply_to. While a request is running, the agent ignores another with the same id on that connection, because a reply could not tell the two apart.

    string 1–64 chars

Example
{
  "type": "stop_server",
  "id": "r2",
  "allocation_id": "5b1e2c7a-8d4f-4c3b-9a6e-2f1d0c9b8a7e"
}

Reply

One of 2, told apart by ok:

  • type"reply"
  • reply_toRequestId

    The id of the request this answers.

    RequestId

    Chosen by the controller. The reply repeats it as reply_to. While a request is running, the agent ignores another with the same id on that connection, because a reply could not tell the two apart.

    string 1–64 chars

  • request"stop_server"
  • oktrue

    The request succeeded.

  • dataStopServerReply

    The server that stopped.

    StopServerReply

    The server that stopped.

    • allocation_idAllocationId

      The allocation the request stopped.

      AllocationId

      The id the controller picks for each server it launches. It identifies that server and its events from then on.

      The id is a lowercase UUID, new for every launch. UUIDv7 is recommended, because its time-ordered values keep a database index on it compact as events arrive.

      string

Reply examples
{
  "type": "reply",
  "reply_to": "r2",
  "request": "stop_server",
  "ok": true,
  "data": {
    "allocation_id": "5b1e2c7a-8d4f-4c3b-9a6e-2f1d0c9b8a7e"
  }
}
{
  "type": "reply",
  "reply_to": "r2",
  "request": "stop_server",
  "ok": false,
  "error": {
    "code": "unknown_allocation",
    "detail": "unknown_allocation"
  }
}

update_agent

optionalcontroller → agent

Replace the agent container with another image. Running servers keep running.

The reply comes immediately with what happens next, as UpdateAgentReply, and agent_update reports the rest: its step while it runs, then how it ended. Refused with maintenance_running while a maintenance operation runs, with image_missing for an image the node lacks without pull, and with image_not_allowed for an image outside the official agent repository while the node's official_images_only is on.

Switching stops this agent, and the agent that comes up, the new one or the old one put back, reports how it went. The update counts as a maintenance operation until it ends, and a switch until its replacement has proved it stays up.

  • type"update_agent"
  • idRequestId

    Chosen by the controller and unique among its open requests. The reply repeats it as reply_to. The agent ignores a request whose id is still running on the connection.

    RequestId

    Chosen by the controller. The reply repeats it as reply_to. While a request is running, the agent ignores another with the same id on that connection, because a reply could not tell the two apart.

    string 1–64 chars

  • imageImageReference

    The agent image to run.

    ImageReference

    A Docker image reference, as cs2_image and agent_image in agent.yml hold it.

    string

    Must match ^[A-Za-z0-9][A-Za-z0-9._\-/:@]{0,254}$

  • pullboolean

    Whether to pull the image. When false, the agent uses an image already on the node.

Example
{
  "type": "update_agent",
  "id": "r5",
  "image": "ghcr.io/ankorcs/ankor-agent:1.2.0",
  "pull": true
}

Reply

One of 2, told apart by ok:

  • type"reply"
  • reply_toRequestId

    The id of the request this answers.

    RequestId

    Chosen by the controller. The reply repeats it as reply_to. While a request is running, the agent ignores another with the same id on that connection, because a reply could not tell the two apart.

    string 1–64 chars

  • request"update_agent"
  • oktrue

    The request succeeded.

  • dataUpdateAgentReply

    What follows an accepted update_agent: the update's first ImageUpdateStep, or unchanged when the node runs that image already, as the registry or the local image says, and nothing more happens.

    UpdateAgentReply

    What follows an accepted update_agent: the update's first ImageUpdateStep, or unchanged when the node runs that image already, as the registry or the local image says, and nothing more happens.

    • imageImageReference

      The agent image the update is to.

      ImageReference

      A Docker image reference, as cs2_image and agent_image in agent.yml hold it.

      string

      Must match ^[A-Za-z0-9][A-Za-z0-9._\-/:@]{0,254}$

    • stepstring enum

      The next step.

      string enum

      • "pulling"

        the image is being pulled.

      • "switching"

        the image is on the node and is put into use. A switch of the agent replaces its container, so the node goes offline and comes back. A switch of the cs2 image extracts its plugin files for future launches.

      • "unchanged"
Reply examples
{
  "type": "reply",
  "reply_to": "r5",
  "request": "update_agent",
  "ok": true,
  "data": {
    "image": "ghcr.io/ankorcs/ankor-agent:1.2.0",
    "step": "pulling"
  }
}
{
  "type": "reply",
  "reply_to": "r5",
  "request": "update_agent",
  "ok": true,
  "data": {
    "image": "ghcr.io/ankorcs/ankor-agent:1.2.0",
    "step": "unchanged"
  }
}
{
  "type": "reply",
  "reply_to": "r5",
  "request": "update_agent",
  "ok": false,
  "error": {
    "code": "maintenance_running",
    "detail": "maintenance_running"
  }
}

update_cs2

requiredcontroller → agent

Install or update CS2 on the node's shared volume. The first install happens only through this request.

The reply comes immediately with the update's id, as UpdateCs2Reply, and the update runs after it, which can take tens of minutes. Refused with slots_held while any slot is held and maintenance_running while a maintenance operation runs.

cs2.update in node_status reports the update from outcome: null to how it ended, a failed pull of the cs2 image included. Meanwhile cs2_update_progress and log_batch report on it, each naming it by that id.

An installed node also updates itself to cs2.latest_build_id once every slot is idle, unless its auto_update is false.

  • type"update_cs2"
  • idRequestId

    Chosen by the controller and unique among its open requests. The reply repeats it as reply_to. The agent ignores a request whose id is still running on the connection.

    RequestId

    Chosen by the controller. The reply repeats it as reply_to. While a request is running, the agent ignores another with the same id on that connection, because a reply could not tell the two apart.

    string 1–64 chars

  • modeCs2UpdateMode

    How thoroughly to update.

    Cs2UpdateMode

    How thoroughly a CS2 install or update runs.

    string enum

    • "update"

      downloads what changed.

    • "verify"

      also re-hashes every file.

    • "reinstall"

      wipes the install volume first and downloads all of it.

Example
{
  "type": "update_cs2",
  "id": "r3",
  "mode": "update"
}

Reply

One of 2, told apart by ok:

  • type"reply"
  • reply_toRequestId

    The id of the request this answers.

    RequestId

    Chosen by the controller. The reply repeats it as reply_to. While a request is running, the agent ignores another with the same id on that connection, because a reply could not tell the two apart.

    string 1–64 chars

  • request"update_cs2"
  • oktrue

    The request succeeded.

  • dataUpdateCs2Reply

    The update an accepted update_cs2 started.

    UpdateCs2Reply

    The update an accepted update_cs2 started.

    • update_idUpdateId

      The update's id, as cs2.update.id, cs2_update_progress and its log source carry it.

      UpdateId

      Chosen by the agent for each CS2 install or update it runs or finds running. A lowercase UUID.

      string

Reply examples
{
  "type": "reply",
  "reply_to": "r3",
  "request": "update_cs2",
  "ok": true,
  "data": {
    "update_id": "7c2e9a41-3b5d-4f68-9e0a-1d4c8b7f2a63"
  }
}
{
  "type": "reply",
  "reply_to": "r3",
  "request": "update_cs2",
  "ok": false,
  "error": {
    "code": "slots_held",
    "detail": "slots_held"
  }
}

update_cs2_image

optionalcontroller → agent

Switch future launches to another cs2 image. Running servers keep theirs, and so does a launch that took its slot before the switch.

The reply comes immediately with what happens next, as UpdateCs2ImageReply. Refused with maintenance_running while a maintenance operation runs, with image_missing for an image the node lacks without pull, and with image_not_allowed for an image outside the official cs2 repository while the node's official_images_only is on.

The agent then pulls the image when asked to, extracts its plugin files and removes the images it supersedes. cs2.image_update in node_status reports its step while it runs, then how it ended, in the same node_status as the new cs2.image.

  • type"update_cs2_image"
  • idRequestId

    Chosen by the controller and unique among its open requests. The reply repeats it as reply_to. The agent ignores a request whose id is still running on the connection.

    RequestId

    Chosen by the controller. The reply repeats it as reply_to. While a request is running, the agent ignores another with the same id on that connection, because a reply could not tell the two apart.

    string 1–64 chars

  • imageImageReference

    The cs2 image future launches use.

    ImageReference

    A Docker image reference, as cs2_image and agent_image in agent.yml hold it.

    string

    Must match ^[A-Za-z0-9][A-Za-z0-9._\-/:@]{0,254}$

  • pullboolean

    Whether to pull the image. When false, the agent uses an image already on the node.

Example
{
  "type": "update_cs2_image",
  "id": "r6",
  "image": "ghcr.io/ankorcs/ankor-cs2:1.2.0",
  "pull": true
}

Reply

One of 2, told apart by ok:

  • type"reply"
  • reply_toRequestId

    The id of the request this answers.

    RequestId

    Chosen by the controller. The reply repeats it as reply_to. While a request is running, the agent ignores another with the same id on that connection, because a reply could not tell the two apart.

    string 1–64 chars

  • request"update_cs2_image"
  • oktrue

    The request succeeded.

  • dataUpdateCs2ImageReply

    What follows an accepted update_cs2_image: its first ImageUpdateStep.

    UpdateCs2ImageReply

    What follows an accepted update_cs2_image: its first ImageUpdateStep.

    • imageImageReference

      The cs2 image the switch is to.

      ImageReference

      A Docker image reference, as cs2_image and agent_image in agent.yml hold it.

      string

      Must match ^[A-Za-z0-9][A-Za-z0-9._\-/:@]{0,254}$

    • stepImageUpdateStep

      The next step.

      ImageUpdateStep

      Where an update_agent or update_cs2_image is.

      string enum

      • "pulling"

        the image is being pulled.

      • "switching"

        the image is on the node and is put into use. A switch of the agent replaces its container, so the node goes offline and comes back. A switch of the cs2 image extracts its plugin files for future launches.

Reply examples
{
  "type": "reply",
  "reply_to": "r6",
  "request": "update_cs2_image",
  "ok": true,
  "data": {
    "image": "ghcr.io/ankorcs/ankor-cs2:1.2.0",
    "step": "pulling"
  }
}
{
  "type": "reply",
  "reply_to": "r6",
  "request": "update_cs2_image",
  "ok": false,
  "error": {
    "code": "maintenance_running",
    "detail": "maintenance_running"
  }
}

watch_logs

optionalcontroller → agent

Start or renew the lease of one log source, which lasts 2 minutes. It receives no reply.

While the lease runs, the agent streams the source's log_batch frames. While any source's lease runs, the agent also sends log_index. A lease that starts replays the source, to every watcher unless a replay_id names the one that asked. Any number of sources can be watched together, each on a lease of its own.

  • type"watch_logs"
  • sourceLogSource

    The log to stream. A source the agent does not keep streams nothing until it prints.

    LogSource

    One log of a node: the agent's log, one CS2 install or update's output, or one server's output.

    One of 3, told apart by kind:

    • kind"agent"
  • replay_idReplayId | null

    The id that the replay frames carry, for the watcher that asked for the replay. Null for no replay, as in a renewal. A lease that starts still replays the source, to every watcher.

    ReplayId

    Identifies one watcher's replay, so the frames answering it reach only that watcher.

    string 1–64 chars

Examples
{
  "type": "watch_logs",
  "source": {
    "kind": "server",
    "allocation_id": "5b1e2c7a-8d4f-4c3b-9a6e-2f1d0c9b8a7e"
  },
  "replay_id": "0d6f1c52-3a7e-4b9d-8c21-5e4f7a9b2c13"
}
{
  "type": "watch_logs",
  "source": {
    "kind": "agent"
  },
  "replay_id": null
}

watch_resources

optionalcontroller → agent

Start or renew the resource lease, which lasts 2 minutes. It receives no reply.

While the lease runs, the agent streams resource_batch. A lease that starts first sends every tick the agent holds.

  • type"watch_resources"
  • replayboolean

    Whether to send every tick the agent holds now, also while the lease runs.

Example
{
  "type": "watch_resources",
  "replay": false
}

welcome

requiredcontroller → agent

The controller's answer to a hello it accepted, before anything else on the socket. Without one the agent redials after 15 seconds.

After each welcome the agent sends what changed since the hello, its pending server_exited first and then the changed statuses, demos, backlog and watched streams. It sends each state once, so a slot that went from starting to running arrives as running.

  • type"welcome"
Example
{
  "type": "welcome"
}