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.
| Code | Reason | By | When | Then |
|---|---|---|---|---|
1000 | SIGTERM, SIGINT or SIGQUIT | agent | The agent is stopping, after its shutdown. | The controller closes its side with the same code and reason. |
1008 | hello_refused | controller | The 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. |
1000 | none | agent | The controller sent hello_refused and has not closed within 5 seconds. | Stays stopped until it is restarted. |
1000 | none | agent | A set_config moved the node to another controller address, once its reply has gone. | Dials the new address immediately. |
1008 | hello_timeout | controller | No valid hello within 10 seconds of the upgrade. | Redials with its backoff. |
1000 | welcome_timeout | agent | No welcome answered its hello within 15 seconds. | Redials with its backoff. |
1008 | duplicate_hello | controller | A second hello arrived on the same socket. | Redials with its backoff. |
1008 | node_connected | controller | The node is already connected on another socket. | Redials with its backoff, and connects once the other socket closes or times out. |
1008 | rate_limited | controller | The node went over a budget. | Redials with its backoff. The controller answers 429 for 60 seconds. |
1008 | agent_deleted | controller | The node was deleted in the controller. | Stops dialling until it is restarted. |
1008 | resync | controller | The controller lost track of the node's state. | Redials and sends a fresh hello. |
1009 | none | controller | A message over 960 KiB. | Redials with its backoff. |
1009 | none | agent | The 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_idAllocationIdThe server whose match the backup is of.
AllocationIdThe 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_backupThe
round_backupevent, 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_idUpdateIdThe update it reports on, as
cs2.update.id. A frame can arrive before thenode_statusthat names the update.UpdateIdChosen by the agent for each CS2 install or update it runs or finds running. A lowercase UUID.
string
percentnumber 0–100 | nullPercent done, as
steamcmdreports it. Null whensteamcmdreports none.messagestring ≤ 512 charsWhat 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_idSlotIdThe slot whose directory holds it.
SlotIdA slot's id, which is also part of the names of its containers.
string
Must match
^[A-Za-z0-9_-]{1,32}$file_nameDemoFileNameThe file's name.
DemoFileNameA 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 ≥ 0Bytes in the file.
modified_atintegerUnix milliseconds when the file was last written.
recordingbooleanWhether its server is still recording it. While it is, the demo grows with each report and is neither uploaded nor deleted.
uploadingbooleanWhether it is uploading now.
demo_progressreports how far, and a node uploads one demo at a time.upload_errorstring 1–512 chars | nullWhy 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_idSlotIdThe slot whose directory holds it.
SlotIdA slot's id, which is also part of the names of its containers.
string
Must match
^[A-Za-z0-9_-]{1,32}$file_nameDemoFileNameThe file's name.
DemoFileNameA 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 ≥ 0Bytes sent so far.
bytes_per_secondinteger ≥ 0Bytes a second since the previous
demo_progressof 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_idSlotIdThe slot whose directory held it.
SlotIdA slot's id, which is also part of the names of its containers.
string
Must match
^[A-Za-z0-9_-]{1,32}$file_nameDemoFileNameThe file's name.
DemoFileNameA 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$reasonDemoRemovedReasonWhy it is no longer on the node.
DemoRemovedReasonWhy a demo is no longer on the node.
string enum
"uploaded"the target took it and the agent removed it.
"deleted"delete_demoor 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 itemsThe servers with the oldest stuck events, oldest first, up to 100.
StuckServerA server whose events wait on the node.
allocation_idAllocationIdThe server whose plugin sent them.
AllocationIdThe 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 ≥ 1Events of the server still queued on the node.
server_countinteger ≥ 0Servers with stuck events on the node, those past
serverstoo.event_countinteger ≥ 0Events that every stuck server holds together.
reasonstring 1–512 chars | nullWhy 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"agentAgentInfoThe agent sending this.
AgentInfoThe running agent.
versionstring 1–64 charsThe agent package version.
imageImageReferenceThe image this agent's container was created from.
ImageReferenceA Docker image reference, as
cs2_imageandagent_imageinagent.ymlhold it.string
Must match
^[A-Za-z0-9][A-Za-z0-9._\-/:@]{0,254}$image_idImageIdThe id Docker holds for that image. Two builds of one tag differ here.
ImageIdThe id Docker gives an image:
sha256:and 64 hex digits.string
Must match
^sha256:[0-9a-f]{64}$
hostHostInfoThe host the agent runs on.
HostInfoThe machine the agent runs on.
cpu_countinteger ≥ 1Logical CPUs the agent can use.
os_namestring 1–256 chars | nullThe host's os-release
PRETTY_NAME, up to 256 characters. Null when unreadable.kernelstring 1–256 charsThe kernel release, which the agent's container shares with the host.
public_ipIpv4| nullThe 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 toconfig.pinned_ipwhenever it is set. The agent looks again only when it restarts.Ipv4A dotted IPv4 address. Rejected unless it is four numbers from 0 to 255 joined by dots, each without leading zeros.
string
countryCountryCode| nullWhere 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.
CountryCodeAn ISO 3166-1 alpha-2 country code:
DK,FR, orGBfor the United Kingdom.string
Must match
^[A-Z]{2}$configNodeSettingsThe configuration the agent runs with.
NodeSettingsThe part of
agent.ymlthat the controller may edit, ashelloreports it applied.slotsSlotLayoutThe slots servers run in.
SlotLayoutUp to 64 slots. Rejected when two slots share an
id, or when two ports are equal. Every slot'sportandtv_portcount, so a slot's two ports differ too. It is also rejected when a port is 4500, where the agent listens for plugins.ReportedSlotA slot as the agent reports it, with the last ten characters of its token in place of the token.
idSlotIdThe slot's id, 1 to 32 letters, digits,
_and-. It is unique on its node.SlotIdA slot's id, which is also part of the names of its containers.
string
Must match
^[A-Za-z0-9_-]{1,32}$portPortThe game port, UDP. Every
portandtv_porton a node is different, free on the host, and not 4500, the port the agent listens on for plugins. Host networking explains why.PortA TCP or UDP port.
integer 1–65535
tv_portPortThe GOTV port, UDP, under the same rules as
port.PortA TCP or UDP port.
integer 1–65535
gslt_hintGsltHintThe last ten characters of the slot's token.
GsltHintThe 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| nullThe address players connect to. Null for the node's
public_ip. Servers already running keep the address their launch announced.Ipv4A dotted IPv4 address. Rejected unless it is four numbers from 0 to 255 joined by dots, each without leading zeros.
string
controllerControllerAddressWhere the agent dials the controller. It redials when any part of it changes.
ControllerAddressWhere a node dials the controller: a host with an optional port, whether it encrypts, and the paths it reaches there.
hostControllerHostThe controller's host with an optional port.
ControllerHostA host with an optional port and no scheme or path, such as
cs.example.com,192.168.0.10:3000or[fd00::1].Rejected when it contains whitespace, a control character,
/,\,?or#. It is also rejected unlesshttp://followed by the value parses as a URL that has a host, an optional port and nothing else, such as a user name.string
tlsbooleanWhether the agent dials over TLS.
socket_pathControllerPathThe path of the agent socket. It is
/ws/agentunlessagent.ymlsetscontroller.socket_path.ControllerPathA 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_pathControllerPathThe path event batches are posted to. It is
/api/node/eventsunlessagent.ymlsetscontroller.events_path.ControllerPathA 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_updatebooleanWhether 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 oncs2.build_iduntil anupdate_cs2, and accepts launches while it is behindcs2.latest_build_id, so players on a newer client cannot join its servers. Its images then change only withupdate_agentandupdate_cs2_image.demo_uploadReportedDemoUpload| nullWhere the agent uploads demos. Null for no uploads, which keeps demos on the node. The header's value is reported as its SHA-256.
ReportedDemoUploadA 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.
urlDemoUploadUrlThe upload target.
DemoUploadUrlThe URL finished demos are posted to. A target that needs credentials takes them in the configured
header, such asAuthorization: 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 | nullThe header sent with each upload. Null for none.
namestringThe header's name, up to 64 letters, digits and
-. Rejected when it is, in any case, one ofhost,content-type,content-length,transfer-encoding,connection,keep-alive,upgrade,expect,te,trailer,ankor-protocol,ankor-file-name,ankor-match-idandankor-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_sha256stringThe 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| nullMegabits a second an upload may send while a game server runs on the node. Null for no limit.
DemoUploadLimitWhole megabits a second, from 1 to 10 Gbit/s.
integer 1–10000
memory_mbMemoryLimit| nullMiB 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.
MemoryLimitMiB 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 itemsOne per configured slot.
SlotStatusOne slot as the agent sees it.
Rejected when an
idleslot names anallocation_id, amatch_idor aserver_steam_id, or when a slot in any other state has noallocation_id. It is also rejected when a slot that is notidleisreserved.slot_idSlotIdThe slot this describes.
SlotIdA slot's id, which is also part of the names of its containers.
string
Must match
^[A-Za-z0-9_-]{1,32}$stateSlotStateWhat the slot is doing.
SlotStateWhat 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_statuswithready. 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| nullThe server on the slot. Null when the slot is
idle.AllocationIdThe 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| nullThe server's match id. Null for a server without a match or a match launched without one.
MatchIdThe 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_onlinebooleanWhether the server's plugin is connected to the agent.
get_server_status,console_command,restore_roundandstart_matchreach it only while it is.gslt_stateGsltStateWhether Steam accepts the slot's token.
start_servernever picks a slot whose token isinvalid.GsltStateWhether 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.
reservedbooleanWhether the idle slot is kept from launches, while the controller replaces its token with
reserve_slotor while the agent logs its token on to Steam.server_steam_idSteamId64| nullThe
server_steam_idthat the server's plugin reported. The agent sends aslot_statusas soon as it learns the ID. Null when the slot isidleor until the server isready.
cs2Cs2StateThe game install and image.
Cs2StateThe node's game install and image.
build_idBuildId| nullThe installed game build. Null before the first install.
latest_build_idBuildId| nullThe 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_updateisfalse.imageImageReferenceThe cs2 image set in
agent.yml, which future launches use.ImageReferenceA Docker image reference, as
cs2_imageandagent_imageinagent.ymlhold it.string
Must match
^[A-Za-z0-9][A-Za-z0-9._\-/:@]{0,254}$image_idImageId| nullThe local id that
imageresolves to on the node. Null until the image is there.ImageIdThe id Docker gives an image:
sha256:and 64 hex digits.string
Must match
^sha256:[0-9a-f]{64}$updateCs2Update| nullThe 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.
Cs2UpdateA CS2 install or update on the node: one running, or the last one to end since the agent started.
modeCs2UpdateModeHow the update runs.
Cs2UpdateModeHow 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.
automaticbooleanWhether the agent started the update on its own, to reach the newest public build. When false, an
update_cs2started it.outcomeCs2UpdateOutcome| nullHow the update ended. Null while it runs.
Cs2UpdateOutcomeHow a CS2 install or update ended.
An update container that an earlier agent process started reports progress again until it exits. It ends
completedorfailedby its exit code, orunobserved.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 | nullWhy the update failed, up to 512 characters. Null unless it failed.
image_updateCs2ImageUpdate| nullThe switch of
imagethat runs now, or else the last one to end since the agent started. Null before any.Cs2ImageUpdateA switch of the cs2 image: the one running now, or the last one to end since the agent started.
imageImageReferenceThe image the switch is to.
ImageReferenceA Docker image reference, as
cs2_imageandagent_imageinagent.ymlhold it.string
Must match
^[A-Za-z0-9][A-Za-z0-9._\-/:@]{0,254}$stepImageUpdateStep| nullWhere the switch is while it runs. Null once it ended.
ImageUpdateStepWhere an
update_agentorupdate_cs2_imageis.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| nullHow the switch ended. Null while it runs.
Cs2ImageUpdateOutcomeHow an
update_cs2_imageended.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 | nullWhy the switch failed, up to 512 characters. Null unless it failed.
unavailableNodeUnavailable| nullWhy the node cannot run servers. Null when it can. Changes arrive as
node_status.NodeUnavailableWhy the node cannot run servers now, with
maintenancefirst while it lasts. Meanwhilestart_serveris refused withmaintenance_runningformaintenanceandnode_unavailableotherwise. Everything else works.Rejected when
operationis null whilecodeismaintenance, or set whilecodeis another value.codeNodeUnavailableCodeNodeUnavailableCodeWhy a node cannot run servers, one of the values of
NODE_UNAVAILABLE.string enum
"maintenance"The
MaintenanceOperationinoperationruns. It clears when the operation ends."event_storage"The agent cannot use the local store that keeps gameplay events until the controller takes them.
detailsays 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| nullThe maintenance operation that runs when
codeismaintenance. Null for any othercode.MaintenanceOperationThe 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 charsThe agent's message, up to 512 characters.
demosDemoFile[], ≤ 50 itemsThe demos kept on the node, up to 50.
demoreports each change afterwards.DemoFileA demo kept on the node.
slot_idSlotIdThe slot whose directory holds it.
SlotIdA slot's id, which is also part of the names of its containers.
string
Must match
^[A-Za-z0-9_-]{1,32}$file_nameDemoFileNameThe file's name.
DemoFileNameA 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 ≥ 0Bytes in the file.
modified_atintegerUnix milliseconds when the file was last written.
recordingbooleanWhether its server is still recording it. While it is, the demo grows with each report and is neither uploaded nor deleted.
uploadingbooleanWhether it is uploading now.
demo_progressreports how far, and a node uploads one demo at a time.upload_errorstring 1–512 chars | nullWhy 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 itemsServers that ended while no connection was welcomed, oldest first, each with its reason. Their slots already show idle here, so a
server_exitedafter thewelcomewould come too late. It carries the oldest 64 at most, and the rest follow asserver_exitedafter thewelcome.ServerExitA server whose container is gone, and why.
slot_idSlotIdThe slot it ran in.
SlotIdA slot's id, which is also part of the names of its containers.
string
Must match
^[A-Za-z0-9_-]{1,32}$allocation_idAllocationIdThe server that ended.
AllocationIdThe 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
reasonServerExitReasonWhy it ended.
ServerExitReasonWhy a server ended: one of the
AgentStopReasonvalues 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_errorsays."start_timeout"the server was still
starting60 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.ymlno longer has."requested"a
stop_serverfrom the controller stopped it."clean_exit"it ended on its own with exit code
0, as after its match or a consolequit."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 | nullThe container's exit code. Null when the agent did not see one.
detailstring 1–512 chars | nullWhat the reason does not say, up to 512 characters: the plugin's
config_errorforconfig_rejected, and Docker's message forlaunch_failed. Null for every other reason.
event_backlogEventBacklogThe node's event backlog when the
hellowas built.event_backlogreplaces it.EventBacklogThe events that wait on the node because uploading them fails, as the agent read them. While uploads work, nothing is stuck, so
serversis empty, the counts are zero andreasonisnull.Rejected unless
serversholds as many entries asserver_count, or 100 whenserver_countis larger. It is also rejected whenevent_countis below the sum ofpendingoverservers. Areasonis rejected whenserver_countis zero, and a nullreasonwhen it is not.serversStuckServer[], ≤ 100 itemsThe servers with the oldest stuck events, oldest first, up to 100.
StuckServerA server whose events wait on the node.
allocation_idAllocationIdThe server whose plugin sent them.
AllocationIdThe 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 ≥ 1Events of the server still queued on the node.
server_countinteger ≥ 0Servers with stuck events on the node, those past
serverstoo.event_countinteger ≥ 0Events that every stuck server holds together.
reasonstring 1–512 chars | nullWhy the last upload failed, up to 512 characters. Null when nothing is stuck, and only then.
agent_updateAgentUpdate| nullThe 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.shstarts the agent. Null when there is none of these. A swap that held shows inagent.image.AgentUpdateAn agent update: the one in progress, one this agent saw end, or the last one to fail.
imageImageReferenceThe agent image the update was to.
ImageReferenceA Docker image reference, as
cs2_imageandagent_imageinagent.ymlhold it.string
Must match
^[A-Za-z0-9][A-Za-z0-9._\-/:@]{0,254}$stepImageUpdateStep| nullWhere the update is while it runs, with the replacement still on trial in
switching. Null once it ended.ImageUpdateStepWhere an
update_agentorupdate_cs2_imageis.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| nullHow the update ended. Null while it runs.
AgentUpdateOutcomeHow an
update_agentended.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 | nullWhy 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"sourceLogSourceThe log these lines belong to.
LogSourceOne 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"
kind"cs2_update"update_idUpdateIdThe install or update that printed it, the
idof itscs2.update.UpdateIdChosen by the agent for each CS2 install or update it runs or finds running. A lowercase UUID.
string
kind"server"allocation_idAllocationIdThe server that printed it.
AllocationIdThe 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
replay_idReplayId| nullThe
watch_logsreplay these frames answer, for that watcher alone. Null for new lines and for a replay sent to every watcher.ReplayIdIdentifies one watcher's replay, so the frames answering it reach only that watcher.
string 1–64 chars
resetbooleanWhether a replay starts, so the controller drops every line it holds of this source first.
linesLogLine[]The lines, oldest first.
LogLineOne log line.
atinteger ≥ 0Unix milliseconds when the agent logged or read the line.
levelAgentLogLevel| nullHow serious the agent rated the line. Null for server and CS2 update output.
AgentLogLevelHow serious an agent log line is.
"info"|"warn"|"error"linestring ≤ 2000 charsThe 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;, thenm. 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 itemsThe agent's log first, then CS2 updates in the order they started, then servers in the order their output began.
LogSourceSummaryOne source the agent keeps.
sourceLogSourceWhich log.
LogSourceOne 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"
kind"cs2_update"update_idUpdateIdThe install or update that printed it, the
idof itscs2.update.UpdateIdChosen by the agent for each CS2 install or update it runs or finds running. A lowercase UUID.
string
kind"server"allocation_idAllocationIdThe server that printed it.
AllocationIdThe 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
line_countinteger ≥ 0Lines kept of it.
last_line_atinteger ≥ 0 | nullUnix milliseconds of the newest line kept of it. Null for a source with no lines kept.
endedbooleanWhether the source has ended, as a server whose slot went idle or a CS2 install or update that ended. Always
falsefor 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_imagehas switched the image. - When
agent_updateorcs2.image_updatemoves frompullingtoswitching. - 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"cs2Cs2StateThe game install and image.
Cs2StateThe node's game install and image.
build_idBuildId| nullThe installed game build. Null before the first install.
latest_build_idBuildId| nullThe 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_updateisfalse.imageImageReferenceThe cs2 image set in
agent.yml, which future launches use.ImageReferenceA Docker image reference, as
cs2_imageandagent_imageinagent.ymlhold it.string
Must match
^[A-Za-z0-9][A-Za-z0-9._\-/:@]{0,254}$image_idImageId| nullThe local id that
imageresolves to on the node. Null until the image is there.ImageIdThe id Docker gives an image:
sha256:and 64 hex digits.string
Must match
^sha256:[0-9a-f]{64}$updateCs2Update| nullThe 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.
Cs2UpdateA CS2 install or update on the node: one running, or the last one to end since the agent started.
modeCs2UpdateModeHow the update runs.
Cs2UpdateModeHow 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.
automaticbooleanWhether the agent started the update on its own, to reach the newest public build. When false, an
update_cs2started it.outcomeCs2UpdateOutcome| nullHow the update ended. Null while it runs.
Cs2UpdateOutcomeHow a CS2 install or update ended.
An update container that an earlier agent process started reports progress again until it exits. It ends
completedorfailedby its exit code, orunobserved.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 | nullWhy the update failed, up to 512 characters. Null unless it failed.
image_updateCs2ImageUpdate| nullThe switch of
imagethat runs now, or else the last one to end since the agent started. Null before any.Cs2ImageUpdateA switch of the cs2 image: the one running now, or the last one to end since the agent started.
imageImageReferenceThe image the switch is to.
ImageReferenceA Docker image reference, as
cs2_imageandagent_imageinagent.ymlhold it.string
Must match
^[A-Za-z0-9][A-Za-z0-9._\-/:@]{0,254}$stepImageUpdateStep| nullWhere the switch is while it runs. Null once it ended.
ImageUpdateStepWhere an
update_agentorupdate_cs2_imageis.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| nullHow the switch ended. Null while it runs.
Cs2ImageUpdateOutcomeHow an
update_cs2_imageended.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 | nullWhy the switch failed, up to 512 characters. Null unless it failed.
unavailableNodeUnavailable| nullWhy the node cannot run servers. Null when it can.
NodeUnavailableWhy the node cannot run servers now, with
maintenancefirst while it lasts. Meanwhilestart_serveris refused withmaintenance_runningformaintenanceandnode_unavailableotherwise. Everything else works.Rejected when
operationis null whilecodeismaintenance, or set whilecodeis another value.codeNodeUnavailableCodeNodeUnavailableCodeWhy a node cannot run servers, one of the values of
NODE_UNAVAILABLE.string enum
"maintenance"The
MaintenanceOperationinoperationruns. It clears when the operation ends."event_storage"The agent cannot use the local store that keeps gameplay events until the controller takes them.
detailsays 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| nullThe maintenance operation that runs when
codeismaintenance. Null for any othercode.MaintenanceOperationThe 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 charsThe agent's message, up to 512 characters.
agent_updateAgentUpdate| nullThe last agent swap, as
hellohas it.AgentUpdateAn agent update: the one in progress, one this agent saw end, or the last one to fail.
imageImageReferenceThe agent image the update was to.
ImageReferenceA Docker image reference, as
cs2_imageandagent_imageinagent.ymlhold it.string
Must match
^[A-Za-z0-9][A-Za-z0-9._\-/:@]{0,254}$stepImageUpdateStep| nullWhere the update is while it runs, with the replacement still on trial in
switching. Null once it ended.ImageUpdateStepWhere an
update_agentorupdate_cs2_imageis.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| nullHow the update ended. Null while it runs.
AgentUpdateOutcomeHow an
update_agentended.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 | nullWhy 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_idAllocationIdThe server whose match changed phase.
AllocationIdThe 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
phaseMatchPhaseThe phase the match is in now.
MatchPhaseWhere 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
liveonce 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:
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_toRequestIdThe
idof the request this answers.RequestIdChosen 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"oktrueThe request succeeded.
dataStartServerReplyStartServerReplyThe server that started, and the slot the agent picked for it.
allocation_idAllocationIdThe allocation the request launched.
AllocationIdThe 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_idSlotIdThe slot the server runs in.
SlotIdA slot's id, which is also part of the names of its containers.
string
Must match
^[A-Za-z0-9_-]{1,32}$
type"reply"reply_toRequestIdThe
idof the request this answers.RequestIdChosen 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"oktrueThe request succeeded.
dataStopServerReplyStopServerReplyThe server that stopped.
allocation_idAllocationIdThe allocation the request stopped.
AllocationIdThe 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
type"reply"reply_toRequestIdThe
idof the request this answers.RequestIdChosen 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"oktrueThe request succeeded.
dataGetServerStatusReplyGetServerStatusReplyThe server asked about, and its state as its plugin reports it now.
map_namestring ≤ 64 charsThe map the server is on, up to 64 characters.
matchobject | nullThe loaded match and its team state. Null on a server without a match or after a refused config.
match_idMatchId| nullThe
match_idthe 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.MatchIdThe 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
phaseMatchPhaseWhere the match is.
MatchPhaseWhere 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
liveonce 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_sidePlayingSideTeam 1's current side.
PlayingSideA playing side, for a field that never holds
spec.string enum
"t"terrorists.
"ct"counter-terrorists.
team2_sidePlayingSideTeam 2's current side, always the other side, sent so a reader shows each team without deriving it.
PlayingSideA playing side, for a field that never holds
spec.string enum
"t"terrorists.
"ct"counter-terrorists.
team1_scoreinteger ≥ 0Team 1's score.
team2_scoreinteger ≥ 0Team 2's score.
playersobject[], ≤ 64 itemsEveryone connected, up to 64.
steam_idSteamId64The player's SteamID64.
namePlayerNameThe player's name.
PlayerNameA 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}]*$sideSideThe player's side.
SideThe 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 | nullWhy 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.
readybooleanWhether 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 staystrueonce set.CS2 does not fall back to an anonymous logon when Steam refuses its token, so
truealso indicates that Steam accepted the GSLT and that its last use can be updated.server_steam_idSteamId64| nullThe 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_idwithout its dashes tosv_tags, which that list shows. Null untilready.allocation_idAllocationIdThe allocation the request asked about.
AllocationIdThe 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
type"reply"reply_toRequestIdThe
idof the request this answers.RequestIdChosen 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"oktrueThe request succeeded.
dataUpdateCs2ReplyUpdateCs2ReplyThe update an accepted
update_cs2started.update_idUpdateIdThe update's id, as
cs2.update.id,cs2_update_progressand its log source carry it.UpdateIdChosen by the agent for each CS2 install or update it runs or finds running. A lowercase UUID.
string
type"reply"reply_toRequestIdThe
idof the request this answers.RequestIdChosen 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"oktrueThe request succeeded.
dataDeleteDemoReplyDeleteDemoReplyThe demo that was removed.
slot_idSlotIdThe slot whose directory held it.
SlotIdA slot's id, which is also part of the names of its containers.
string
Must match
^[A-Za-z0-9_-]{1,32}$file_nameDemoFileNameThe file removed.
DemoFileNameA 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$
type"reply"reply_toRequestIdThe
idof the request this answers.RequestIdChosen 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"oktrueThe request succeeded.
dataReserveSlotReplyReserveSlotReplyThe slot that is reserved.
slot_idSlotIdThe slot reserved.
SlotIdA slot's id, which is also part of the names of its containers.
string
Must match
^[A-Za-z0-9_-]{1,32}$
type"reply"reply_toRequestIdThe
idof the request this answers.RequestIdChosen 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"oktrueThe request succeeded.
dataNodeSettingsNodeSettingsThe part of
agent.ymlthat the controller may edit, ashelloreports it applied.slotsSlotLayoutThe slots servers run in.
SlotLayoutUp to 64 slots. Rejected when two slots share an
id, or when two ports are equal. Every slot'sportandtv_portcount, so a slot's two ports differ too. It is also rejected when a port is 4500, where the agent listens for plugins.ReportedSlotA slot as the agent reports it, with the last ten characters of its token in place of the token.
idSlotIdThe slot's id, 1 to 32 letters, digits,
_and-. It is unique on its node.SlotIdA slot's id, which is also part of the names of its containers.
string
Must match
^[A-Za-z0-9_-]{1,32}$portPortThe game port, UDP. Every
portandtv_porton a node is different, free on the host, and not 4500, the port the agent listens on for plugins. Host networking explains why.PortA TCP or UDP port.
integer 1–65535
tv_portPortThe GOTV port, UDP, under the same rules as
port.PortA TCP or UDP port.
integer 1–65535
gslt_hintGsltHintThe last ten characters of the slot's token.
GsltHintThe 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| nullThe address players connect to. Null for the node's
public_ip. Servers already running keep the address their launch announced.Ipv4A dotted IPv4 address. Rejected unless it is four numbers from 0 to 255 joined by dots, each without leading zeros.
string
controllerControllerAddressWhere the agent dials the controller. It redials when any part of it changes.
ControllerAddressWhere a node dials the controller: a host with an optional port, whether it encrypts, and the paths it reaches there.
hostControllerHostThe controller's host with an optional port.
ControllerHostA host with an optional port and no scheme or path, such as
cs.example.com,192.168.0.10:3000or[fd00::1].Rejected when it contains whitespace, a control character,
/,\,?or#. It is also rejected unlesshttp://followed by the value parses as a URL that has a host, an optional port and nothing else, such as a user name.string
tlsbooleanWhether the agent dials over TLS.
socket_pathControllerPathThe path of the agent socket. It is
/ws/agentunlessagent.ymlsetscontroller.socket_path.ControllerPathA 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_pathControllerPathThe path event batches are posted to. It is
/api/node/eventsunlessagent.ymlsetscontroller.events_path.ControllerPathA 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_updatebooleanWhether 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 oncs2.build_iduntil anupdate_cs2, and accepts launches while it is behindcs2.latest_build_id, so players on a newer client cannot join its servers. Its images then change only withupdate_agentandupdate_cs2_image.demo_uploadReportedDemoUpload| nullWhere the agent uploads demos. Null for no uploads, which keeps demos on the node. The header's value is reported as its SHA-256.
ReportedDemoUploadA 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.
urlDemoUploadUrlThe upload target.
DemoUploadUrlThe URL finished demos are posted to. A target that needs credentials takes them in the configured
header, such asAuthorization: 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 | nullThe header sent with each upload. Null for none.
namestringThe header's name, up to 64 letters, digits and
-. Rejected when it is, in any case, one ofhost,content-type,content-length,transfer-encoding,connection,keep-alive,upgrade,expect,te,trailer,ankor-protocol,ankor-file-name,ankor-match-idandankor-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_sha256stringThe 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| nullMegabits a second an upload may send while a game server runs on the node. Null for no limit.
DemoUploadLimitWhole megabits a second, from 1 to 10 Gbit/s.
integer 1–10000
memory_mbMemoryLimit| nullMiB 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.
MemoryLimitMiB 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
type"reply"reply_toRequestIdThe
idof the request this answers.RequestIdChosen 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"oktrueThe request succeeded.
dataUpdateAgentReplyUpdateAgentReplyWhat follows an accepted
update_agent: the update's firstImageUpdateStep, orunchangedwhen the node runs that image already, as the registry or the local image says, and nothing more happens.imageImageReferenceThe agent image the update is to.
ImageReferenceA Docker image reference, as
cs2_imageandagent_imageinagent.ymlhold it.string
Must match
^[A-Za-z0-9][A-Za-z0-9._\-/:@]{0,254}$stepstring enumThe 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"
type"reply"reply_toRequestIdThe
idof the request this answers.RequestIdChosen 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"oktrueThe request succeeded.
dataUpdateCs2ImageReplyUpdateCs2ImageReplyWhat follows an accepted
update_cs2_image: its firstImageUpdateStep.imageImageReferenceThe cs2 image the switch is to.
ImageReferenceA Docker image reference, as
cs2_imageandagent_imageinagent.ymlhold it.string
Must match
^[A-Za-z0-9][A-Za-z0-9._\-/:@]{0,254}$stepImageUpdateStepThe next step.
ImageUpdateStepWhere an
update_agentorupdate_cs2_imageis.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.
type"reply"reply_toRequestIdThe
idof the request this answers.RequestIdChosen 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"oktrueThe request succeeded.
dataConsoleCommandReplyConsoleCommandReplyThe server the command was handed to.
allocation_idAllocationIdThe allocation the command went to.
AllocationIdThe 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
type"reply"reply_toRequestIdThe
idof the request this answers.RequestIdChosen 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"oktrueThe request succeeded.
dataRestoreRoundReplyRestoreRoundReplyThe server and round the restore was handed to.
allocation_idAllocationIdThe allocation the restore went to.
AllocationIdThe 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
roundRoundNumberThe round it loads.
RoundNumberA one-based round number. A map's history holds far fewer than the 1000 it allows.
integer 1–1000
type"reply"reply_toRequestIdThe
idof the request this answers.RequestIdChosen 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"oktrueThe request succeeded.
dataStartMatchReplyStartMatchReplyThe server the start was handed to.
allocation_idAllocationIdThe allocation the start went to.
AllocationIdThe 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
A request that failed.
type"reply"reply_toRequestIdThe
idof the request this answers.RequestIdChosen 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
requestRequestType| nullThe
typeof the request this answers. Null when the agent did not know it.RequestTypeThe
typeof each request the controller sends, which itsreplyrepeats asrequest.string enum
"start_server""stop_server""get_server_status""update_cs2""delete_demo""reserve_slot""set_config""update_agent""update_cs2_image""console_command""restore_round""start_match"
okfalseThe request failed.
errorAgentErrorWhy it failed.
AgentErrorWhy a request failed. Branch on
code.detailis for people.codeAgentErrorCodeWhat went wrong. Branch on it.
AgentErrorCodeThe
codeof a failed reply, one of the values ofAGENT_ERRORS.string enum
"no_free_slot"No slot is idle, unreserved and on a token Steam has not refused.
"cs2_outdated"A newer public CS2 build is out and the node has not updated to it yet, so its
cs2.build_idis behindcs2.latest_build_id. The node takes launches again once it has updated. A node whoseauto_updateisfalsenever refuses with this."cs2_not_installed"CS2 is not installed on the node, so its
cs2.build_idisnull. The node takes launches once anupdate_cs2has installed it."node_unavailable"The node reports
unavailablefor a reason other thanmaintenance."slots_held"An operation that needs every slot idle found one held:
update_cs2, or aset_configthat changes any key undercontroller."slot_held"A slot holds a server that is
starting,runningorstopping, andset_configwould remove or re-port the slot or give its server's token to another slot, orreserve_slotasked for it.detailnames the slots."unknown_slot"The node has no slot with that id.
"slot_token_required"set_configkept the token of a slot the node does not have yet. A new slot needs a new token."demo_header_value_required"set_configkept the value of a demo upload header the node does not send. A new header needs its value."unknown_allocation"No server with that allocation id.
"maintenance_running"A maintenance operation runs on the node. That operation is unaffected, and
unavailableshows it asmaintenance."image_missing"The image is not on the node and the request asked for no pull. Asking again with
pullfetches it."image_not_allowed"The image is not an official one, and the node's
official_images_onlyis on. The node's operator decides which images it runs."demo_uploading"The demo is uploading. It can be deleted once the upload ends.
"unknown_demo"The slot has no demo of that name, such as one an upload or a delete has already removed.
"demo_recording"The demo is still being recorded. It can be deleted once its server ends the recording.
"plugin_offline"The server's plugin is not connected to the agent, or left before it answered.
"plugin_status_timeout"The plugin did not answer
get_server_status,console_command,start_matchorrestore_roundwithin 10 seconds."no_match"The server runs without a match.
"not_in_warmup"The match is past its warmup.
"team_empty"A team has no player connected.
"not_live"The match is not live, such as in warmup, during the knife round or after it ended.
"at_halftime"The match is in its halftime break, while the sides are about to swap. Ask again once the next round has started.
"no_backup"The node has no backup of the round that
restore_roundnamed for that server."command_blocked"console_commandnamed a command ofmp_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_stoprecordandaliaswhile the server runs a match. A new launch restarts a match, andrestore_roundrestores one."invalid_request"The request did not match its schema.
"allocation_in_use"start_servernamed an allocation this node is already launching or running. Its server exists or is on its way, as after astart_serversent again when its reply was slow."allocation_reused"start_servernamed an allocation that this node launched before, among the last 100. The node files a server's events and demos under its allocation, so every launch needs a new one."unsupported_request"The agent does not know the request's
type, or the plugin the command's."shutting_down"The agent is stopping and started no work for the request, as after its
shutdown. Send the request again once the node reconnects."failed"Any failure that no other code names, such as a Docker failure.
detailstring ≤ 512 charsThe rest of the reason, such as Docker's message, up to 512 characters.
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 itemsThe ticks, oldest first: every tick the agent holds, up to the last 100, or the ticks since the previous frame.
ResourceTickOne sample of the node's disk, memory and CPU, taken every 3 seconds.
atinteger ≥ 0Unix milliseconds when the tick was taken.
diskResourceUsage| nullThe filesystem holding the node's data directory, as
dfcounts it. Null when it could not be read.ResourceUsageA capacity and how much of it is in use, in bytes.
capinteger ≥ 0Bytes of capacity.
usedinteger ≥ 0Bytes in use.
memoryResourceUsageThe memory the node may use, and how much it uses. Cache the kernel can free counts as free.
ResourceUsageA capacity and how much of it is in use, in bytes.
capinteger ≥ 0Bytes of capacity.
usedinteger ≥ 0Bytes in use.
cpuobject | nullHow busy the node's CPUs are, in percent of all the CPUs it may use. Null in the first tick.
maxinteger 0–100Percent busy at the busiest moment of the tick.
averageinteger 0–100Percent busy on average over the tick.
resetbooleanWhether
ticksis every tick the agent holds and replaces the ticks the controller holds. It istruewhen awatch_resourcesstarts the lease or asks for a replay, and on the first tick after each acceptedhellowhile the lease runs. When false,ticksadds 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_idAllocationIdThe server whose match scored.
AllocationIdThe 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 ≥ 0Team 1's score.
team2_scoreinteger ≥ 0Team 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_idSlotIdThe slot it ran in.
SlotIdA slot's id, which is also part of the names of its containers.
string
Must match
^[A-Za-z0-9_-]{1,32}$allocation_idAllocationIdThe server that ended.
AllocationIdThe 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
reasonServerExitReasonWhy it ended.
ServerExitReasonWhy a server ended: one of the
AgentStopReasonvalues 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_errorsays."start_timeout"the server was still
starting60 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.ymlno longer has."requested"a
stop_serverfrom the controller stopped it."clean_exit"it ended on its own with exit code
0, as after its match or a consolequit."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 | nullThe container's exit code. Null when the agent did not see one.
detailstring 1–512 chars | nullWhat the reason does not say, up to 512 characters: the plugin's
config_errorforconfig_rejected, and Docker's message forlaunch_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_idSlotIdThe slot this describes.
SlotIdA slot's id, which is also part of the names of its containers.
string
Must match
^[A-Za-z0-9_-]{1,32}$stateSlotStateWhat the slot is doing.
SlotStateWhat 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_statuswithready. 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| nullThe server on the slot. Null when the slot is
idle.AllocationIdThe 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| nullThe server's match id. Null for a server without a match or a match launched without one.
MatchIdThe 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_onlinebooleanWhether the server's plugin is connected to the agent.
get_server_status,console_command,restore_roundandstart_matchreach it only while it is.gslt_stateGsltStateWhether Steam accepts the slot's token.
start_servernever picks a slot whose token isinvalid.GsltStateWhether 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.
reservedbooleanWhether the idle slot is kept from launches, while the controller replaces its token with
reserve_slotor while the agent logs its token on to Steam.server_steam_idSteamId64| nullThe
server_steam_idthat the server's plugin reported. The agent sends aslot_statusas soon as it learns the ID. Null when the slot isidleor until the server isready.
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
welcomethat accepts ahello, or thehello_refusedthat refuses it. - Requests, which carry an
idand receive onereplyeach. - 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_allocationwhen the node does not run the server.plugin_offlinewhile the slot'splugin_onlineisfalse.plugin_status_timeoutwhen the plugin does not answer.command_blockedwhen the server runs a match and the line names a command ofmp_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_stoprecordandalias. 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"idRequestIdChosen 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.RequestIdChosen 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_idAllocationIdThe server to run it on.
AllocationIdThe 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 charsThe 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_toRequestIdThe
idof the request this answers.RequestIdChosen 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"oktrueThe request succeeded.
dataConsoleCommandReplyThe server the command was handed to.
ConsoleCommandReplyThe server the command was handed to.
allocation_idAllocationIdThe allocation the command went to.
AllocationIdThe 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
A request that failed.
type"reply"reply_toRequestIdThe
idof the request this answers.RequestIdChosen 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
requestRequestType| nullThe
typeof the request this answers. Null when the agent did not know it.RequestTypeThe
typeof each request the controller sends, which itsreplyrepeats asrequest.string enum
"start_server""stop_server""get_server_status""update_cs2""delete_demo""reserve_slot""set_config""update_agent""update_cs2_image""console_command""restore_round""start_match"
okfalseThe request failed.
errorAgentErrorWhy it failed.
AgentErrorWhy a request failed. Branch on
code.detailis for people.codeAgentErrorCodeWhat went wrong. Branch on it.
AgentErrorCodeThe
codeof a failed reply, one of the values ofAGENT_ERRORS.string enum
"no_free_slot"No slot is idle, unreserved and on a token Steam has not refused.
"cs2_outdated"A newer public CS2 build is out and the node has not updated to it yet, so its
cs2.build_idis behindcs2.latest_build_id. The node takes launches again once it has updated. A node whoseauto_updateisfalsenever refuses with this."cs2_not_installed"CS2 is not installed on the node, so its
cs2.build_idisnull. The node takes launches once anupdate_cs2has installed it."node_unavailable"The node reports
unavailablefor a reason other thanmaintenance."slots_held"An operation that needs every slot idle found one held:
update_cs2, or aset_configthat changes any key undercontroller."slot_held"A slot holds a server that is
starting,runningorstopping, andset_configwould remove or re-port the slot or give its server's token to another slot, orreserve_slotasked for it.detailnames the slots."unknown_slot"The node has no slot with that id.
"slot_token_required"set_configkept the token of a slot the node does not have yet. A new slot needs a new token."demo_header_value_required"set_configkept the value of a demo upload header the node does not send. A new header needs its value."unknown_allocation"No server with that allocation id.
"maintenance_running"A maintenance operation runs on the node. That operation is unaffected, and
unavailableshows it asmaintenance."image_missing"The image is not on the node and the request asked for no pull. Asking again with
pullfetches it."image_not_allowed"The image is not an official one, and the node's
official_images_onlyis on. The node's operator decides which images it runs."demo_uploading"The demo is uploading. It can be deleted once the upload ends.
"unknown_demo"The slot has no demo of that name, such as one an upload or a delete has already removed.
"demo_recording"The demo is still being recorded. It can be deleted once its server ends the recording.
"plugin_offline"The server's plugin is not connected to the agent, or left before it answered.
"plugin_status_timeout"The plugin did not answer
get_server_status,console_command,start_matchorrestore_roundwithin 10 seconds."no_match"The server runs without a match.
"not_in_warmup"The match is past its warmup.
"team_empty"A team has no player connected.
"not_live"The match is not live, such as in warmup, during the knife round or after it ended.
"at_halftime"The match is in its halftime break, while the sides are about to swap. Ask again once the next round has started.
"no_backup"The node has no backup of the round that
restore_roundnamed for that server."command_blocked"console_commandnamed a command ofmp_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_stoprecordandaliaswhile the server runs a match. A new launch restarts a match, andrestore_roundrestores one."invalid_request"The request did not match its schema.
"allocation_in_use"start_servernamed an allocation this node is already launching or running. Its server exists or is on its way, as after astart_serversent again when its reply was slow."allocation_reused"start_servernamed an allocation that this node launched before, among the last 100. The node files a server's events and demos under its allocation, so every launch needs a new one."unsupported_request"The agent does not know the request's
type, or the plugin the command's."shutting_down"The agent is stopping and started no work for the request, as after its
shutdown. Send the request again once the node reconnects."failed"Any failure that no other code names, such as a Docker failure.
detailstring ≤ 512 charsThe rest of the reason, such as Docker's message, up to 512 characters.
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_slotwhen the node has no directory for the slot.unknown_demowhen the slot has no demo of that name.demo_recordingwhile its server records the demo.demo_uploadingwhile the demo uploads.invalid_requestwhen the name is outside the slot'sdemos/directory.failedwhen the removal fails.
type"delete_demo"idRequestIdChosen 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.RequestIdChosen 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_idSlotIdThe slot whose directory holds it.
SlotIdA slot's id, which is also part of the names of its containers.
string
Must match
^[A-Za-z0-9_-]{1,32}$file_nameDemoFileNameThe file to delete.
DemoFileNameA 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_toRequestIdThe
idof the request this answers.RequestIdChosen 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"oktrueThe request succeeded.
dataDeleteDemoReplyThe demo that was removed.
DeleteDemoReplyThe demo that was removed.
slot_idSlotIdThe slot whose directory held it.
SlotIdA slot's id, which is also part of the names of its containers.
string
Must match
^[A-Za-z0-9_-]{1,32}$file_nameDemoFileNameThe file removed.
DemoFileNameA 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$
A request that failed.
type"reply"reply_toRequestIdThe
idof the request this answers.RequestIdChosen 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
requestRequestType| nullThe
typeof the request this answers. Null when the agent did not know it.RequestTypeThe
typeof each request the controller sends, which itsreplyrepeats asrequest.string enum
"start_server""stop_server""get_server_status""update_cs2""delete_demo""reserve_slot""set_config""update_agent""update_cs2_image""console_command""restore_round""start_match"
okfalseThe request failed.
errorAgentErrorWhy it failed.
AgentErrorWhy a request failed. Branch on
code.detailis for people.codeAgentErrorCodeWhat went wrong. Branch on it.
AgentErrorCodeThe
codeof a failed reply, one of the values ofAGENT_ERRORS.string enum
"no_free_slot"No slot is idle, unreserved and on a token Steam has not refused.
"cs2_outdated"A newer public CS2 build is out and the node has not updated to it yet, so its
cs2.build_idis behindcs2.latest_build_id. The node takes launches again once it has updated. A node whoseauto_updateisfalsenever refuses with this."cs2_not_installed"CS2 is not installed on the node, so its
cs2.build_idisnull. The node takes launches once anupdate_cs2has installed it."node_unavailable"The node reports
unavailablefor a reason other thanmaintenance."slots_held"An operation that needs every slot idle found one held:
update_cs2, or aset_configthat changes any key undercontroller."slot_held"A slot holds a server that is
starting,runningorstopping, andset_configwould remove or re-port the slot or give its server's token to another slot, orreserve_slotasked for it.detailnames the slots."unknown_slot"The node has no slot with that id.
"slot_token_required"set_configkept the token of a slot the node does not have yet. A new slot needs a new token."demo_header_value_required"set_configkept the value of a demo upload header the node does not send. A new header needs its value."unknown_allocation"No server with that allocation id.
"maintenance_running"A maintenance operation runs on the node. That operation is unaffected, and
unavailableshows it asmaintenance."image_missing"The image is not on the node and the request asked for no pull. Asking again with
pullfetches it."image_not_allowed"The image is not an official one, and the node's
official_images_onlyis on. The node's operator decides which images it runs."demo_uploading"The demo is uploading. It can be deleted once the upload ends.
"unknown_demo"The slot has no demo of that name, such as one an upload or a delete has already removed.
"demo_recording"The demo is still being recorded. It can be deleted once its server ends the recording.
"plugin_offline"The server's plugin is not connected to the agent, or left before it answered.
"plugin_status_timeout"The plugin did not answer
get_server_status,console_command,start_matchorrestore_roundwithin 10 seconds."no_match"The server runs without a match.
"not_in_warmup"The match is past its warmup.
"team_empty"A team has no player connected.
"not_live"The match is not live, such as in warmup, during the knife round or after it ended.
"at_halftime"The match is in its halftime break, while the sides are about to swap. Ask again once the next round has started.
"no_backup"The node has no backup of the round that
restore_roundnamed for that server."command_blocked"console_commandnamed a command ofmp_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_stoprecordandaliaswhile the server runs a match. A new launch restarts a match, andrestore_roundrestores one."invalid_request"The request did not match its schema.
"allocation_in_use"start_servernamed an allocation this node is already launching or running. Its server exists or is on its way, as after astart_serversent again when its reply was slow."allocation_reused"start_servernamed an allocation that this node launched before, among the last 100. The node files a server's events and demos under its allocation, so every launch needs a new one."unsupported_request"The agent does not know the request's
type, or the plugin the command's."shutting_down"The agent is stopping and started no work for the request, as after its
shutdown. Send the request again once the node reconnects."failed"Any failure that no other code names, such as a Docker failure.
detailstring ≤ 512 charsThe rest of the reason, such as Docker's message, up to 512 characters.
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_allocationwhen the node does not run the server.plugin_offlinewhile the slot'splugin_onlineisfalseor when the plugin leaves before answering.plugin_status_timeoutafter 10 seconds.
type"get_server_status"idRequestIdChosen 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.RequestIdChosen 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_idAllocationIdThe server to ask.
AllocationIdThe 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_toRequestIdThe
idof the request this answers.RequestIdChosen 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"oktrueThe request succeeded.
dataGetServerStatusReplyThe server asked about, and its state as its plugin reports it now.
GetServerStatusReplyThe server asked about, and its state as its plugin reports it now.
map_namestring ≤ 64 charsThe map the server is on, up to 64 characters.
matchobject | nullThe loaded match and its team state. Null on a server without a match or after a refused config.
match_idMatchId| nullThe
match_idthe 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.MatchIdThe 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
phaseMatchPhaseWhere the match is.
MatchPhaseWhere 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
liveonce 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_sidePlayingSideTeam 1's current side.
PlayingSideA playing side, for a field that never holds
spec.string enum
"t"terrorists.
"ct"counter-terrorists.
team2_sidePlayingSideTeam 2's current side, always the other side, sent so a reader shows each team without deriving it.
PlayingSideA playing side, for a field that never holds
spec.string enum
"t"terrorists.
"ct"counter-terrorists.
team1_scoreinteger ≥ 0Team 1's score.
team2_scoreinteger ≥ 0Team 2's score.
playersobject[], ≤ 64 itemsEveryone connected, up to 64.
steam_idSteamId64The player's SteamID64.
namePlayerNameThe player's name.
PlayerNameA 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}]*$sideSideThe player's side.
SideThe 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 | nullWhy 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.
readybooleanWhether 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 staystrueonce set.CS2 does not fall back to an anonymous logon when Steam refuses its token, so
truealso indicates that Steam accepted the GSLT and that its last use can be updated.server_steam_idSteamId64| nullThe 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_idwithout its dashes tosv_tags, which that list shows. Null untilready.allocation_idAllocationIdThe allocation the request asked about.
AllocationIdThe 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
A request that failed.
type"reply"reply_toRequestIdThe
idof the request this answers.RequestIdChosen 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
requestRequestType| nullThe
typeof the request this answers. Null when the agent did not know it.RequestTypeThe
typeof each request the controller sends, which itsreplyrepeats asrequest.string enum
"start_server""stop_server""get_server_status""update_cs2""delete_demo""reserve_slot""set_config""update_agent""update_cs2_image""console_command""restore_round""start_match"
okfalseThe request failed.
errorAgentErrorWhy it failed.
AgentErrorWhy a request failed. Branch on
code.detailis for people.codeAgentErrorCodeWhat went wrong. Branch on it.
AgentErrorCodeThe
codeof a failed reply, one of the values ofAGENT_ERRORS.string enum
"no_free_slot"No slot is idle, unreserved and on a token Steam has not refused.
"cs2_outdated"A newer public CS2 build is out and the node has not updated to it yet, so its
cs2.build_idis behindcs2.latest_build_id. The node takes launches again once it has updated. A node whoseauto_updateisfalsenever refuses with this."cs2_not_installed"CS2 is not installed on the node, so its
cs2.build_idisnull. The node takes launches once anupdate_cs2has installed it."node_unavailable"The node reports
unavailablefor a reason other thanmaintenance."slots_held"An operation that needs every slot idle found one held:
update_cs2, or aset_configthat changes any key undercontroller."slot_held"A slot holds a server that is
starting,runningorstopping, andset_configwould remove or re-port the slot or give its server's token to another slot, orreserve_slotasked for it.detailnames the slots."unknown_slot"The node has no slot with that id.
"slot_token_required"set_configkept the token of a slot the node does not have yet. A new slot needs a new token."demo_header_value_required"set_configkept the value of a demo upload header the node does not send. A new header needs its value."unknown_allocation"No server with that allocation id.
"maintenance_running"A maintenance operation runs on the node. That operation is unaffected, and
unavailableshows it asmaintenance."image_missing"The image is not on the node and the request asked for no pull. Asking again with
pullfetches it."image_not_allowed"The image is not an official one, and the node's
official_images_onlyis on. The node's operator decides which images it runs."demo_uploading"The demo is uploading. It can be deleted once the upload ends.
"unknown_demo"The slot has no demo of that name, such as one an upload or a delete has already removed.
"demo_recording"The demo is still being recorded. It can be deleted once its server ends the recording.
"plugin_offline"The server's plugin is not connected to the agent, or left before it answered.
"plugin_status_timeout"The plugin did not answer
get_server_status,console_command,start_matchorrestore_roundwithin 10 seconds."no_match"The server runs without a match.
"not_in_warmup"The match is past its warmup.
"team_empty"A team has no player connected.
"not_live"The match is not live, such as in warmup, during the knife round or after it ended.
"at_halftime"The match is in its halftime break, while the sides are about to swap. Ask again once the next round has started.
"no_backup"The node has no backup of the round that
restore_roundnamed for that server."command_blocked"console_commandnamed a command ofmp_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_stoprecordandaliaswhile the server runs a match. A new launch restarts a match, andrestore_roundrestores one."invalid_request"The request did not match its schema.
"allocation_in_use"start_servernamed an allocation this node is already launching or running. Its server exists or is on its way, as after astart_serversent again when its reply was slow."allocation_reused"start_servernamed an allocation that this node launched before, among the last 100. The node files a server's events and demos under its allocation, so every launch needs a new one."unsupported_request"The agent does not know the request's
type, or the plugin the command's."shutting_down"The agent is stopping and started no work for the request, as after its
shutdown. Send the request again once the node reconnects."failed"Any failure that no other code names, such as a Docker failure.
detailstring ≤ 512 charsThe rest of the reason, such as Docker's message, up to 512 characters.
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"reasonHelloRefusedReasonHelloRefusedReasonWhy the controller refused a
hello:invalid_hello, thehellofails 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 charsA 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"idRequestIdChosen 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.RequestIdChosen 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_idSlotIdThe slot to reserve.
SlotIdA 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_toRequestIdThe
idof the request this answers.RequestIdChosen 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"oktrueThe request succeeded.
dataReserveSlotReplyThe slot that is reserved.
ReserveSlotReplyThe slot that is reserved.
slot_idSlotIdThe slot reserved.
SlotIdA slot's id, which is also part of the names of its containers.
string
Must match
^[A-Za-z0-9_-]{1,32}$
A request that failed.
type"reply"reply_toRequestIdThe
idof the request this answers.RequestIdChosen 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
requestRequestType| nullThe
typeof the request this answers. Null when the agent did not know it.RequestTypeThe
typeof each request the controller sends, which itsreplyrepeats asrequest.string enum
"start_server""stop_server""get_server_status""update_cs2""delete_demo""reserve_slot""set_config""update_agent""update_cs2_image""console_command""restore_round""start_match"
okfalseThe request failed.
errorAgentErrorWhy it failed.
AgentErrorWhy a request failed. Branch on
code.detailis for people.codeAgentErrorCodeWhat went wrong. Branch on it.
AgentErrorCodeThe
codeof a failed reply, one of the values ofAGENT_ERRORS.string enum
"no_free_slot"No slot is idle, unreserved and on a token Steam has not refused.
"cs2_outdated"A newer public CS2 build is out and the node has not updated to it yet, so its
cs2.build_idis behindcs2.latest_build_id. The node takes launches again once it has updated. A node whoseauto_updateisfalsenever refuses with this."cs2_not_installed"CS2 is not installed on the node, so its
cs2.build_idisnull. The node takes launches once anupdate_cs2has installed it."node_unavailable"The node reports
unavailablefor a reason other thanmaintenance."slots_held"An operation that needs every slot idle found one held:
update_cs2, or aset_configthat changes any key undercontroller."slot_held"A slot holds a server that is
starting,runningorstopping, andset_configwould remove or re-port the slot or give its server's token to another slot, orreserve_slotasked for it.detailnames the slots."unknown_slot"The node has no slot with that id.
"slot_token_required"set_configkept the token of a slot the node does not have yet. A new slot needs a new token."demo_header_value_required"set_configkept the value of a demo upload header the node does not send. A new header needs its value."unknown_allocation"No server with that allocation id.
"maintenance_running"A maintenance operation runs on the node. That operation is unaffected, and
unavailableshows it asmaintenance."image_missing"The image is not on the node and the request asked for no pull. Asking again with
pullfetches it."image_not_allowed"The image is not an official one, and the node's
official_images_onlyis on. The node's operator decides which images it runs."demo_uploading"The demo is uploading. It can be deleted once the upload ends.
"unknown_demo"The slot has no demo of that name, such as one an upload or a delete has already removed.
"demo_recording"The demo is still being recorded. It can be deleted once its server ends the recording.
"plugin_offline"The server's plugin is not connected to the agent, or left before it answered.
"plugin_status_timeout"The plugin did not answer
get_server_status,console_command,start_matchorrestore_roundwithin 10 seconds."no_match"The server runs without a match.
"not_in_warmup"The match is past its warmup.
"team_empty"A team has no player connected.
"not_live"The match is not live, such as in warmup, during the knife round or after it ended.
"at_halftime"The match is in its halftime break, while the sides are about to swap. Ask again once the next round has started.
"no_backup"The node has no backup of the round that
restore_roundnamed for that server."command_blocked"console_commandnamed a command ofmp_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_stoprecordandaliaswhile the server runs a match. A new launch restarts a match, andrestore_roundrestores one."invalid_request"The request did not match its schema.
"allocation_in_use"start_servernamed an allocation this node is already launching or running. Its server exists or is on its way, as after astart_serversent again when its reply was slow."allocation_reused"start_servernamed an allocation that this node launched before, among the last 100. The node files a server's events and demos under its allocation, so every launch needs a new one."unsupported_request"The agent does not know the request's
type, or the plugin the command's."shutting_down"The agent is stopping and started no work for the request, as after its
shutdown. Send the request again once the node reconnects."failed"Any failure that no other code names, such as a Docker failure.
detailstring ≤ 512 charsThe rest of the reason, such as Docker's message, up to 512 characters.
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_allocationwhen the node does not run the server.no_backupwhen the node has no backup of that round, as in a game mode without backups (GameMode).plugin_offlinewhile the slot'splugin_onlineisfalse.plugin_status_timeoutwhen the plugin does not answer.no_match,not_liveorat_halftimewhen 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"idRequestIdChosen 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.RequestIdChosen 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_idAllocationIdThe server whose match to restore.
AllocationIdThe 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
roundRoundNumberThe one-based round to play on from, whose backup the engine wrote as the round started.
RoundNumberA 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_toRequestIdThe
idof the request this answers.RequestIdChosen 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"oktrueThe request succeeded.
dataRestoreRoundReplyThe server and round the restore was handed to.
RestoreRoundReplyThe server and round the restore was handed to.
allocation_idAllocationIdThe allocation the restore went to.
AllocationIdThe 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
roundRoundNumberThe round it loads.
RoundNumberA one-based round number. A map's history holds far fewer than the 1000 it allows.
integer 1–1000
A request that failed.
type"reply"reply_toRequestIdThe
idof the request this answers.RequestIdChosen 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
requestRequestType| nullThe
typeof the request this answers. Null when the agent did not know it.RequestTypeThe
typeof each request the controller sends, which itsreplyrepeats asrequest.string enum
"start_server""stop_server""get_server_status""update_cs2""delete_demo""reserve_slot""set_config""update_agent""update_cs2_image""console_command""restore_round""start_match"
okfalseThe request failed.
errorAgentErrorWhy it failed.
AgentErrorWhy a request failed. Branch on
code.detailis for people.codeAgentErrorCodeWhat went wrong. Branch on it.
AgentErrorCodeThe
codeof a failed reply, one of the values ofAGENT_ERRORS.string enum
"no_free_slot"No slot is idle, unreserved and on a token Steam has not refused.
"cs2_outdated"A newer public CS2 build is out and the node has not updated to it yet, so its
cs2.build_idis behindcs2.latest_build_id. The node takes launches again once it has updated. A node whoseauto_updateisfalsenever refuses with this."cs2_not_installed"CS2 is not installed on the node, so its
cs2.build_idisnull. The node takes launches once anupdate_cs2has installed it."node_unavailable"The node reports
unavailablefor a reason other thanmaintenance."slots_held"An operation that needs every slot idle found one held:
update_cs2, or aset_configthat changes any key undercontroller."slot_held"A slot holds a server that is
starting,runningorstopping, andset_configwould remove or re-port the slot or give its server's token to another slot, orreserve_slotasked for it.detailnames the slots."unknown_slot"The node has no slot with that id.
"slot_token_required"set_configkept the token of a slot the node does not have yet. A new slot needs a new token."demo_header_value_required"set_configkept the value of a demo upload header the node does not send. A new header needs its value."unknown_allocation"No server with that allocation id.
"maintenance_running"A maintenance operation runs on the node. That operation is unaffected, and
unavailableshows it asmaintenance."image_missing"The image is not on the node and the request asked for no pull. Asking again with
pullfetches it."image_not_allowed"The image is not an official one, and the node's
official_images_onlyis on. The node's operator decides which images it runs."demo_uploading"The demo is uploading. It can be deleted once the upload ends.
"unknown_demo"The slot has no demo of that name, such as one an upload or a delete has already removed.
"demo_recording"The demo is still being recorded. It can be deleted once its server ends the recording.
"plugin_offline"The server's plugin is not connected to the agent, or left before it answered.
"plugin_status_timeout"The plugin did not answer
get_server_status,console_command,start_matchorrestore_roundwithin 10 seconds."no_match"The server runs without a match.
"not_in_warmup"The match is past its warmup.
"team_empty"A team has no player connected.
"not_live"The match is not live, such as in warmup, during the knife round or after it ended.
"at_halftime"The match is in its halftime break, while the sides are about to swap. Ask again once the next round has started.
"no_backup"The node has no backup of the round that
restore_roundnamed for that server."command_blocked"console_commandnamed a command ofmp_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_stoprecordandaliaswhile the server runs a match. A new launch restarts a match, andrestore_roundrestores one."invalid_request"The request did not match its schema.
"allocation_in_use"start_servernamed an allocation this node is already launching or running. Its server exists or is on its way, as after astart_serversent again when its reply was slow."allocation_reused"start_servernamed an allocation that this node launched before, among the last 100. The node files a server's events and demos under its allocation, so every launch needs a new one."unsupported_request"The agent does not know the request's
type, or the plugin the command's."shutting_down"The agent is stopping and started no work for the request, as after its
shutdown. Send the request again once the node reconnects."failed"Any failure that no other code names, such as a Docker failure.
detailstring ≤ 512 charsThe rest of the reason, such as Docker's message, up to 512 characters.
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_heldwhen the edit would remove or re-port a held slot, or give its server's token to another slot.slots_heldwhen the edit changes any key undercontroller(its address, TLS or paths) while any slot is held.slot_token_requiredordemo_header_value_requiredwhen the edit keeps a value that the node does not have.invalid_requestwhen 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"idRequestIdChosen 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.RequestIdChosen 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
configNodeSettingsEditThe settings to change.
NodeSettingsEditNodeSettingsasset_configasks for them. A missing field keeps the node's value. Null is a value, asNodeSettingsdescribes.slots, when present, is the whole layout.slotsSlotEdit[], ≤ 64 itemsoptionalThe slots servers run in.
SlotEditdescribes their tokens.Rejected when two slots share an
idor agslt, or when two ports are equal. Every slot'sportandtv_portcount, 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.SlotEditA slot as
set_configasks for it.gsltis 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). Withoutgsltthe slot keeps its current token, so a slot the node does not have yet needs one (slot_token_required), and a slot from ahellocan be sent back unchanged.idSlotIdThe slot's id, 1 to 32 letters, digits,
_and-. It is unique on its node.SlotIdA slot's id, which is also part of the names of its containers.
string
Must match
^[A-Za-z0-9_-]{1,32}$portPortThe game port, UDP. Every
portandtv_porton a node is different, free on the host, and not 4500, the port the agent listens on for plugins. Host networking explains why.PortA TCP or UDP port.
integer 1–65535
tv_portPortThe GOTV port, UDP, under the same rules as
port.PortA TCP or UDP port.
integer 1–65535
gsltanyoptionalThe slot's new token. Without one, the slot keeps the token it has.
pinned_ipIpv4| nulloptionalThe address players connect to. Null for the node's
public_ip. Servers already running keep the address their launch announced.Ipv4A dotted IPv4 address. Rejected unless it is four numbers from 0 to 255 joined by dots, each without leading zeros.
string
controlleranyoptionalWhere the agent dials the controller. It redials when any part of it changes.
auto_updatebooleanoptionalWhether 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 oncs2.build_iduntil anupdate_cs2, and accepts launches while it is behindcs2.latest_build_id, so players on a newer client cannot join its servers. Its images then change only withupdate_agentandupdate_cs2_image.demo_uploadDemoUploadEdit| nulloptionalWhere the agent uploads demos. Null for no uploads, which keeps demos on the node.
DemoUploadEditdescribes its header.DemoUploadEditA demo upload as
set_configsets it. A header without its value keeps the value the node has.urlDemoUploadUrlThe upload target.
DemoUploadUrlThe URL finished demos are posted to. A target that needs credentials takes them in the configured
header, such asAuthorization: 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 | nullA 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 thedemo_uploadof ahellocan be sent back unchanged.namestringThe header's name, up to 64 letters, digits and
-. Rejected when it is, in any case, one ofhost,content-type,content-length,transfer-encoding,connection,keep-alive,upgrade,expect,te,trailer,ankor-protocol,ankor-file-name,ankor-match-idandankor-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 charsoptionalThe header's value, sent verbatim, without line breaks.
string ≤ 1024 chars
Must match
^[^\r\n\u0000]*$
limit_mbitDemoUploadLimit| nullMegabits a second an upload may send while a game server runs on the node. Null for no limit.
DemoUploadLimitWhole megabits a second, from 1 to 10 Gbit/s.
integer 1–10000
memory_mbMemoryLimit| nulloptionalMiB 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.
MemoryLimitMiB 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_toRequestIdThe
idof the request this answers.RequestIdChosen 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"oktrueThe request succeeded.
dataNodeSettingsThe part of
agent.ymlthat the controller may edit, ashelloreports it applied.NodeSettingsThe part of
agent.ymlthat the controller may edit, ashelloreports it applied.slotsSlotLayoutThe slots servers run in.
SlotLayoutUp to 64 slots. Rejected when two slots share an
id, or when two ports are equal. Every slot'sportandtv_portcount, so a slot's two ports differ too. It is also rejected when a port is 4500, where the agent listens for plugins.ReportedSlotA slot as the agent reports it, with the last ten characters of its token in place of the token.
idSlotIdThe slot's id, 1 to 32 letters, digits,
_and-. It is unique on its node.SlotIdA slot's id, which is also part of the names of its containers.
string
Must match
^[A-Za-z0-9_-]{1,32}$portPortThe game port, UDP. Every
portandtv_porton a node is different, free on the host, and not 4500, the port the agent listens on for plugins. Host networking explains why.PortA TCP or UDP port.
integer 1–65535
tv_portPortThe GOTV port, UDP, under the same rules as
port.PortA TCP or UDP port.
integer 1–65535
gslt_hintGsltHintThe last ten characters of the slot's token.
GsltHintThe 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| nullThe address players connect to. Null for the node's
public_ip. Servers already running keep the address their launch announced.Ipv4A dotted IPv4 address. Rejected unless it is four numbers from 0 to 255 joined by dots, each without leading zeros.
string
controllerControllerAddressWhere the agent dials the controller. It redials when any part of it changes.
ControllerAddressWhere a node dials the controller: a host with an optional port, whether it encrypts, and the paths it reaches there.
hostControllerHostThe controller's host with an optional port.
ControllerHostA host with an optional port and no scheme or path, such as
cs.example.com,192.168.0.10:3000or[fd00::1].Rejected when it contains whitespace, a control character,
/,\,?or#. It is also rejected unlesshttp://followed by the value parses as a URL that has a host, an optional port and nothing else, such as a user name.string
tlsbooleanWhether the agent dials over TLS.
socket_pathControllerPathThe path of the agent socket. It is
/ws/agentunlessagent.ymlsetscontroller.socket_path.ControllerPathA 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_pathControllerPathThe path event batches are posted to. It is
/api/node/eventsunlessagent.ymlsetscontroller.events_path.ControllerPathA 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_updatebooleanWhether 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 oncs2.build_iduntil anupdate_cs2, and accepts launches while it is behindcs2.latest_build_id, so players on a newer client cannot join its servers. Its images then change only withupdate_agentandupdate_cs2_image.demo_uploadReportedDemoUpload| nullWhere the agent uploads demos. Null for no uploads, which keeps demos on the node. The header's value is reported as its SHA-256.
ReportedDemoUploadA 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.
urlDemoUploadUrlThe upload target.
DemoUploadUrlThe URL finished demos are posted to. A target that needs credentials takes them in the configured
header, such asAuthorization: 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 | nullThe header sent with each upload. Null for none.
namestringThe header's name, up to 64 letters, digits and
-. Rejected when it is, in any case, one ofhost,content-type,content-length,transfer-encoding,connection,keep-alive,upgrade,expect,te,trailer,ankor-protocol,ankor-file-name,ankor-match-idandankor-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_sha256stringThe 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| nullMegabits a second an upload may send while a game server runs on the node. Null for no limit.
DemoUploadLimitWhole megabits a second, from 1 to 10 Gbit/s.
integer 1–10000
memory_mbMemoryLimit| nullMiB 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.
MemoryLimitMiB 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
A request that failed.
type"reply"reply_toRequestIdThe
idof the request this answers.RequestIdChosen 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
requestRequestType| nullThe
typeof the request this answers. Null when the agent did not know it.RequestTypeThe
typeof each request the controller sends, which itsreplyrepeats asrequest.string enum
"start_server""stop_server""get_server_status""update_cs2""delete_demo""reserve_slot""set_config""update_agent""update_cs2_image""console_command""restore_round""start_match"
okfalseThe request failed.
errorAgentErrorWhy it failed.
AgentErrorWhy a request failed. Branch on
code.detailis for people.codeAgentErrorCodeWhat went wrong. Branch on it.
AgentErrorCodeThe
codeof a failed reply, one of the values ofAGENT_ERRORS.string enum
"no_free_slot"No slot is idle, unreserved and on a token Steam has not refused.
"cs2_outdated"A newer public CS2 build is out and the node has not updated to it yet, so its
cs2.build_idis behindcs2.latest_build_id. The node takes launches again once it has updated. A node whoseauto_updateisfalsenever refuses with this."cs2_not_installed"CS2 is not installed on the node, so its
cs2.build_idisnull. The node takes launches once anupdate_cs2has installed it."node_unavailable"The node reports
unavailablefor a reason other thanmaintenance."slots_held"An operation that needs every slot idle found one held:
update_cs2, or aset_configthat changes any key undercontroller."slot_held"A slot holds a server that is
starting,runningorstopping, andset_configwould remove or re-port the slot or give its server's token to another slot, orreserve_slotasked for it.detailnames the slots."unknown_slot"The node has no slot with that id.
"slot_token_required"set_configkept the token of a slot the node does not have yet. A new slot needs a new token."demo_header_value_required"set_configkept the value of a demo upload header the node does not send. A new header needs its value."unknown_allocation"No server with that allocation id.
"maintenance_running"A maintenance operation runs on the node. That operation is unaffected, and
unavailableshows it asmaintenance."image_missing"The image is not on the node and the request asked for no pull. Asking again with
pullfetches it."image_not_allowed"The image is not an official one, and the node's
official_images_onlyis on. The node's operator decides which images it runs."demo_uploading"The demo is uploading. It can be deleted once the upload ends.
"unknown_demo"The slot has no demo of that name, such as one an upload or a delete has already removed.
"demo_recording"The demo is still being recorded. It can be deleted once its server ends the recording.
"plugin_offline"The server's plugin is not connected to the agent, or left before it answered.
"plugin_status_timeout"The plugin did not answer
get_server_status,console_command,start_matchorrestore_roundwithin 10 seconds."no_match"The server runs without a match.
"not_in_warmup"The match is past its warmup.
"team_empty"A team has no player connected.
"not_live"The match is not live, such as in warmup, during the knife round or after it ended.
"at_halftime"The match is in its halftime break, while the sides are about to swap. Ask again once the next round has started.
"no_backup"The node has no backup of the round that
restore_roundnamed for that server."command_blocked"console_commandnamed a command ofmp_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_stoprecordandaliaswhile the server runs a match. A new launch restarts a match, andrestore_roundrestores one."invalid_request"The request did not match its schema.
"allocation_in_use"start_servernamed an allocation this node is already launching or running. Its server exists or is on its way, as after astart_serversent again when its reply was slow."allocation_reused"start_servernamed an allocation that this node launched before, among the last 100. The node files a server's events and demos under its allocation, so every launch needs a new one."unsupported_request"The agent does not know the request's
type, or the plugin the command's."shutting_down"The agent is stopping and started no work for the request, as after its
shutdown. Send the request again once the node reconnects."failed"Any failure that no other code names, such as a Docker failure.
detailstring ≤ 512 charsThe rest of the reason, such as Docker's message, up to 512 characters.
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_allocationwhen the node does not run the server.plugin_offlinewhile the slot'splugin_onlineisfalse.plugin_status_timeoutwhen the plugin does not answer.no_match,not_in_warmuporteam_emptywhen the plugin refuses the start.
A repeat while the countdown runs is accepted and changes nothing.
type"start_match"idRequestIdChosen 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.RequestIdChosen 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_idAllocationIdThe server whose match to start.
AllocationIdThe 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_toRequestIdThe
idof the request this answers.RequestIdChosen 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"oktrueThe request succeeded.
dataStartMatchReplyThe server the start was handed to.
StartMatchReplyThe server the start was handed to.
allocation_idAllocationIdThe allocation the start went to.
AllocationIdThe 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
A request that failed.
type"reply"reply_toRequestIdThe
idof the request this answers.RequestIdChosen 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
requestRequestType| nullThe
typeof the request this answers. Null when the agent did not know it.RequestTypeThe
typeof each request the controller sends, which itsreplyrepeats asrequest.string enum
"start_server""stop_server""get_server_status""update_cs2""delete_demo""reserve_slot""set_config""update_agent""update_cs2_image""console_command""restore_round""start_match"
okfalseThe request failed.
errorAgentErrorWhy it failed.
AgentErrorWhy a request failed. Branch on
code.detailis for people.codeAgentErrorCodeWhat went wrong. Branch on it.
AgentErrorCodeThe
codeof a failed reply, one of the values ofAGENT_ERRORS.string enum
"no_free_slot"No slot is idle, unreserved and on a token Steam has not refused.
"cs2_outdated"A newer public CS2 build is out and the node has not updated to it yet, so its
cs2.build_idis behindcs2.latest_build_id. The node takes launches again once it has updated. A node whoseauto_updateisfalsenever refuses with this."cs2_not_installed"CS2 is not installed on the node, so its
cs2.build_idisnull. The node takes launches once anupdate_cs2has installed it."node_unavailable"The node reports
unavailablefor a reason other thanmaintenance."slots_held"An operation that needs every slot idle found one held:
update_cs2, or aset_configthat changes any key undercontroller."slot_held"A slot holds a server that is
starting,runningorstopping, andset_configwould remove or re-port the slot or give its server's token to another slot, orreserve_slotasked for it.detailnames the slots."unknown_slot"The node has no slot with that id.
"slot_token_required"set_configkept the token of a slot the node does not have yet. A new slot needs a new token."demo_header_value_required"set_configkept the value of a demo upload header the node does not send. A new header needs its value."unknown_allocation"No server with that allocation id.
"maintenance_running"A maintenance operation runs on the node. That operation is unaffected, and
unavailableshows it asmaintenance."image_missing"The image is not on the node and the request asked for no pull. Asking again with
pullfetches it."image_not_allowed"The image is not an official one, and the node's
official_images_onlyis on. The node's operator decides which images it runs."demo_uploading"The demo is uploading. It can be deleted once the upload ends.
"unknown_demo"The slot has no demo of that name, such as one an upload or a delete has already removed.
"demo_recording"The demo is still being recorded. It can be deleted once its server ends the recording.
"plugin_offline"The server's plugin is not connected to the agent, or left before it answered.
"plugin_status_timeout"The plugin did not answer
get_server_status,console_command,start_matchorrestore_roundwithin 10 seconds."no_match"The server runs without a match.
"not_in_warmup"The match is past its warmup.
"team_empty"A team has no player connected.
"not_live"The match is not live, such as in warmup, during the knife round or after it ended.
"at_halftime"The match is in its halftime break, while the sides are about to swap. Ask again once the next round has started.
"no_backup"The node has no backup of the round that
restore_roundnamed for that server."command_blocked"console_commandnamed a command ofmp_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_stoprecordandaliaswhile the server runs a match. A new launch restarts a match, andrestore_roundrestores one."invalid_request"The request did not match its schema.
"allocation_in_use"start_servernamed an allocation this node is already launching or running. Its server exists or is on its way, as after astart_serversent again when its reply was slow."allocation_reused"start_servernamed an allocation that this node launched before, among the last 100. The node files a server's events and demos under its allocation, so every launch needs a new one."unsupported_request"The agent does not know the request's
type, or the plugin the command's."shutting_down"The agent is stopping and started no work for the request, as after its
shutdown. Send the request again once the node reconnects."failed"Any failure that no other code names, such as a Docker failure.
detailstring ≤ 512 charsThe rest of the reason, such as Docker's message, up to 512 characters.
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
StartRefusalwhen the node cannot take the launch. allocation_in_usewhen the node is already launching the allocation or its server holds a slot.allocation_reusedwhen the node launched the allocation before.failedwhen Docker fails the launch, once its container is gone, after aserver_exitedwithlaunch_failed.
type"start_server"idRequestIdChosen 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.RequestIdChosen 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_idAllocationIdIdentifies 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
StartRefusalturned away used nothing, so the same id may be sent again. Any other failure uses it up.AllocationIdThe 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
launchServerLaunchConfigWhat to launch.
ServerLaunchConfigWhat a game server is launched with: any CS2 server, and the match Ankor's plugin plays on it, if any.
Rejected when both
workshop_idandmap_nameare null. With amatch, it is also rejected:- when
workshop_idis set, because a match plays a stock map. - when the roster's players and spectators together outnumber
max_players. - when
game_modeis notcasual,competitiveorwingman. - when
ready_modeisexternalandready_timeout_secondsis set.
hostnamestring | nulloptionalThe 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|""| nulloptionalThe 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:
A Steam Web API key, which the server needs to download workshop maps.
string
Must match
^[A-Za-z0-9]{1,64}$""game_passwordGamePassword| nulloptionalThe password players type to join. Null or empty for none, and null when absent.
GamePasswordThe password players type. It is printable ASCII without spaces,
"or;, because the server would read those as separators.string ≤ 128 chars
Must match
^[!#-:<-~]*$chat_taganyoptionalThe tag in front of the plugin's chat lines. When absent, each of its fields takes its default.
game_modeanyoptionalThe game mode, which sets the game's rules. When absent,
competitive.max_playersinteger 1–64The player capacity of the server, as
-maxplayers. A match's roster, players and spectators together, must fit.workshop_idWorkshopId| nulloptionalA workshop map to play. Null for a stock map, and when absent. A match plays a stock map.
map_nameStockMapName| nullThe stock map to play, required without
workshop_id. With aworkshop_id, the server boots this map before it switches to the workshop map. Null forde_dust2in that case.StockMapNameThe 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 | nulloptionalGOTV 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 0and a randomtv_passwordthat 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| nulloptionalThe GOTV password, as
tv_password, apart fromgame_password. Null or empty for none, and null when absent.GamePasswordThe 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 ≥ 0Seconds GOTV runs behind the game.
idle_shutdown_secondsinteger ≥ 1 | nullSeconds the server may stay empty once its slot is
runningbefore the agent stops it, reportingidle_emptyas itsServerExitReason. Null for a server that runs until stopped. Hibernation must stay off. See When the cfgs run.execCfgName[], ≤ 16 itemsCfgs 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.CfgNameA cfg the server runs, relative to
game/csgo/cfg/and without.cfg, such asnode/<name>from the node's cfgs.string ≤ 128 chars
Must match
^[A-Za-z0-9_-]+(\/[A-Za-z0-9_-]+)*$commandsstring[], ≤ 128 itemsConsole lines that the server runs after
exec, in order and at the same moments, so they overrideexecandrules. A match server leaves out a line that names a command ofmp_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_stoprecordandalias. Rejected when a line holds an ASCII control character, such as a line break, which would end the console line.matchMatchConfig| nulloptionalThe 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,commandsand the node's plugins set the game.MatchConfigThe match a server is launched with, which covers one map. Each server plays one match. A restore launches the same
match_idagain under a newallocation_id, from around_backupthat the first server sent.match_idMatchId| nulloptionalThe match's id, which names its demos and which
slot_statusandget_server_statusreport. Null for none, and when absent.MatchIdThe 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_statisticsbooleanoptionalWhether the plugin sends the match's gameplay events, its
round_backupamong them, which the agent uploads in anevent_batch. When absent,false.Every match reports its
phase_changeandscore. A match without statistics sends eachround_backupinbackupinstead, and a match with statistics only in its events.rosterRosterWho may join, and on which team.
RosterThe 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.team1RosterTeamTeam 1, which events number
1.RosterTeamOne team of a
Roster.namestring ≤ 30 charsThe 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
\SplayersSteamId64[], ≥ 1 itemsThe players of the team, captain first. The captain may use
!forcereadyand picks the side after the knife round. A team has at least one player, so it always has a captain.
team2RosterTeamTeam 2, which events number
2.RosterTeamOne team of a
Roster.namestring ≤ 30 charsThe 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
\SplayersSteamId64[], ≥ 1 itemsThe players of the team, captain first. The captain may use
!forcereadyand picks the side after the knife round. A team has at least one player, so it always has a captain.
spectatorsSteamId64[], ≤ 2 itemsPlayers allowed in as spectators, who watch with the game's free camera, up to 2. When
ready_modeismanual, they ready up too.
sidesSidesModeHow starting sides are decided.
SidesModeHow 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–300optionalSeconds the knife winners' captain has to type
!stay,!swap,!ctor!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_modeanyoptionalHow warmup ends. When absent,
auto.spectators_ready_upbooleanoptionalWhether, 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_readybooleanoptionalWhether, 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 | nulloptionalSeconds 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 withexternal, where only the controller starts the match.record_demobooleanWhether GOTV records the live match as a demo.
rulesanyoptionalThe rules the match is played under, applied over those of the game mode. When absent, every rule takes its default.
restoreRestoreFile| nulloptionalA live round to play on from, taken from the
round_backupthat 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
sidessays, and after warmup it loads the backup instead of going live. Team 1 takes the restore'steam1_side, the round and the scores come from the file, and the restore'srulesreplace the match'srules. The agent writesfileto the slot formp_backup_restore_load_file.RestoreFileWhat 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_sidePlayingSideTeam 1's side in that round. On a launch, the plugin seats players by it through warmup, before the load.
PlayingSideA playing side, for a field that never holds
spec.string enum
"t"terrorists.
"ct"counter-terrorists.
rulesMatchRulesThe rules that the map was played under. A restore plays on under these rules instead of the rules of its launch.
MatchRulesThe rules of a match, as a launch asks for them and as
going_liveandround_backupreport the server playing them.The plugin reads the round settings off the server's cvars.
friendly_firecomes from the launch, and Ankor applies it through the damage reductions instead ofmp_friendlyfire.max_roundsinteger 1–240Rounds in regulation, as
mp_maxrounds, up to 240. MR12 is 24 and MR15 is 30.overtimebooleanWhether a tie after regulation goes to overtime, as
mp_overtime_enable.overtime_max_roundsinteger 1–60Rounds in one overtime, both halves together, as
mp_overtime_maxrounds. The bound is 60, ten times the stock 6.friendly_firebooleanWhether bullets and melee hurt teammates. Grenades always do.
filestring 1–524288 charsThe 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_filereads 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}]*$
- when
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_toRequestIdThe
idof the request this answers.RequestIdChosen 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"oktrueThe request succeeded.
dataStartServerReplyThe server that started, and the slot the agent picked for it.
StartServerReplyThe server that started, and the slot the agent picked for it.
allocation_idAllocationIdThe allocation the request launched.
AllocationIdThe 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_idSlotIdThe slot the server runs in.
SlotIdA slot's id, which is also part of the names of its containers.
string
Must match
^[A-Za-z0-9_-]{1,32}$
A request that failed.
type"reply"reply_toRequestIdThe
idof the request this answers.RequestIdChosen 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
requestRequestType| nullThe
typeof the request this answers. Null when the agent did not know it.RequestTypeThe
typeof each request the controller sends, which itsreplyrepeats asrequest.string enum
"start_server""stop_server""get_server_status""update_cs2""delete_demo""reserve_slot""set_config""update_agent""update_cs2_image""console_command""restore_round""start_match"
okfalseThe request failed.
errorAgentErrorWhy it failed.
AgentErrorWhy a request failed. Branch on
code.detailis for people.codeAgentErrorCodeWhat went wrong. Branch on it.
AgentErrorCodeThe
codeof a failed reply, one of the values ofAGENT_ERRORS.string enum
"no_free_slot"No slot is idle, unreserved and on a token Steam has not refused.
"cs2_outdated"A newer public CS2 build is out and the node has not updated to it yet, so its
cs2.build_idis behindcs2.latest_build_id. The node takes launches again once it has updated. A node whoseauto_updateisfalsenever refuses with this."cs2_not_installed"CS2 is not installed on the node, so its
cs2.build_idisnull. The node takes launches once anupdate_cs2has installed it."node_unavailable"The node reports
unavailablefor a reason other thanmaintenance."slots_held"An operation that needs every slot idle found one held:
update_cs2, or aset_configthat changes any key undercontroller."slot_held"A slot holds a server that is
starting,runningorstopping, andset_configwould remove or re-port the slot or give its server's token to another slot, orreserve_slotasked for it.detailnames the slots."unknown_slot"The node has no slot with that id.
"slot_token_required"set_configkept the token of a slot the node does not have yet. A new slot needs a new token."demo_header_value_required"set_configkept the value of a demo upload header the node does not send. A new header needs its value."unknown_allocation"No server with that allocation id.
"maintenance_running"A maintenance operation runs on the node. That operation is unaffected, and
unavailableshows it asmaintenance."image_missing"The image is not on the node and the request asked for no pull. Asking again with
pullfetches it."image_not_allowed"The image is not an official one, and the node's
official_images_onlyis on. The node's operator decides which images it runs."demo_uploading"The demo is uploading. It can be deleted once the upload ends.
"unknown_demo"The slot has no demo of that name, such as one an upload or a delete has already removed.
"demo_recording"The demo is still being recorded. It can be deleted once its server ends the recording.
"plugin_offline"The server's plugin is not connected to the agent, or left before it answered.
"plugin_status_timeout"The plugin did not answer
get_server_status,console_command,start_matchorrestore_roundwithin 10 seconds."no_match"The server runs without a match.
"not_in_warmup"The match is past its warmup.
"team_empty"A team has no player connected.
"not_live"The match is not live, such as in warmup, during the knife round or after it ended.
"at_halftime"The match is in its halftime break, while the sides are about to swap. Ask again once the next round has started.
"no_backup"The node has no backup of the round that
restore_roundnamed for that server."command_blocked"console_commandnamed a command ofmp_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_stoprecordandaliaswhile the server runs a match. A new launch restarts a match, andrestore_roundrestores one."invalid_request"The request did not match its schema.
"allocation_in_use"start_servernamed an allocation this node is already launching or running. Its server exists or is on its way, as after astart_serversent again when its reply was slow."allocation_reused"start_servernamed an allocation that this node launched before, among the last 100. The node files a server's events and demos under its allocation, so every launch needs a new one."unsupported_request"The agent does not know the request's
type, or the plugin the command's."shutting_down"The agent is stopping and started no work for the request, as after its
shutdown. Send the request again once the node reconnects."failed"Any failure that no other code names, such as a Docker failure.
detailstring ≤ 512 charsThe rest of the reason, such as Docker's message, up to 512 characters.
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_idAllocationIdThe server to stop.
AllocationIdThe 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
idRequestIdChosen 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.RequestIdChosen 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_toRequestIdThe
idof the request this answers.RequestIdChosen 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"oktrueThe request succeeded.
dataStopServerReplyThe server that stopped.
StopServerReplyThe server that stopped.
allocation_idAllocationIdThe allocation the request stopped.
AllocationIdThe 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
A request that failed.
type"reply"reply_toRequestIdThe
idof the request this answers.RequestIdChosen 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
requestRequestType| nullThe
typeof the request this answers. Null when the agent did not know it.RequestTypeThe
typeof each request the controller sends, which itsreplyrepeats asrequest.string enum
"start_server""stop_server""get_server_status""update_cs2""delete_demo""reserve_slot""set_config""update_agent""update_cs2_image""console_command""restore_round""start_match"
okfalseThe request failed.
errorAgentErrorWhy it failed.
AgentErrorWhy a request failed. Branch on
code.detailis for people.codeAgentErrorCodeWhat went wrong. Branch on it.
AgentErrorCodeThe
codeof a failed reply, one of the values ofAGENT_ERRORS.string enum
"no_free_slot"No slot is idle, unreserved and on a token Steam has not refused.
"cs2_outdated"A newer public CS2 build is out and the node has not updated to it yet, so its
cs2.build_idis behindcs2.latest_build_id. The node takes launches again once it has updated. A node whoseauto_updateisfalsenever refuses with this."cs2_not_installed"CS2 is not installed on the node, so its
cs2.build_idisnull. The node takes launches once anupdate_cs2has installed it."node_unavailable"The node reports
unavailablefor a reason other thanmaintenance."slots_held"An operation that needs every slot idle found one held:
update_cs2, or aset_configthat changes any key undercontroller."slot_held"A slot holds a server that is
starting,runningorstopping, andset_configwould remove or re-port the slot or give its server's token to another slot, orreserve_slotasked for it.detailnames the slots."unknown_slot"The node has no slot with that id.
"slot_token_required"set_configkept the token of a slot the node does not have yet. A new slot needs a new token."demo_header_value_required"set_configkept the value of a demo upload header the node does not send. A new header needs its value."unknown_allocation"No server with that allocation id.
"maintenance_running"A maintenance operation runs on the node. That operation is unaffected, and
unavailableshows it asmaintenance."image_missing"The image is not on the node and the request asked for no pull. Asking again with
pullfetches it."image_not_allowed"The image is not an official one, and the node's
official_images_onlyis on. The node's operator decides which images it runs."demo_uploading"The demo is uploading. It can be deleted once the upload ends.
"unknown_demo"The slot has no demo of that name, such as one an upload or a delete has already removed.
"demo_recording"The demo is still being recorded. It can be deleted once its server ends the recording.
"plugin_offline"The server's plugin is not connected to the agent, or left before it answered.
"plugin_status_timeout"The plugin did not answer
get_server_status,console_command,start_matchorrestore_roundwithin 10 seconds."no_match"The server runs without a match.
"not_in_warmup"The match is past its warmup.
"team_empty"A team has no player connected.
"not_live"The match is not live, such as in warmup, during the knife round or after it ended.
"at_halftime"The match is in its halftime break, while the sides are about to swap. Ask again once the next round has started.
"no_backup"The node has no backup of the round that
restore_roundnamed for that server."command_blocked"console_commandnamed a command ofmp_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_stoprecordandaliaswhile the server runs a match. A new launch restarts a match, andrestore_roundrestores one."invalid_request"The request did not match its schema.
"allocation_in_use"start_servernamed an allocation this node is already launching or running. Its server exists or is on its way, as after astart_serversent again when its reply was slow."allocation_reused"start_servernamed an allocation that this node launched before, among the last 100. The node files a server's events and demos under its allocation, so every launch needs a new one."unsupported_request"The agent does not know the request's
type, or the plugin the command's."shutting_down"The agent is stopping and started no work for the request, as after its
shutdown. Send the request again once the node reconnects."failed"Any failure that no other code names, such as a Docker failure.
detailstring ≤ 512 charsThe rest of the reason, such as Docker's message, up to 512 characters.
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"idRequestIdChosen 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.RequestIdChosen 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
imageImageReferenceThe agent image to run.
ImageReferenceA Docker image reference, as
cs2_imageandagent_imageinagent.ymlhold it.string
Must match
^[A-Za-z0-9][A-Za-z0-9._\-/:@]{0,254}$pullbooleanWhether 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_toRequestIdThe
idof the request this answers.RequestIdChosen 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"oktrueThe request succeeded.
dataUpdateAgentReplyWhat follows an accepted
update_agent: the update's firstImageUpdateStep, orunchangedwhen the node runs that image already, as the registry or the local image says, and nothing more happens.UpdateAgentReplyWhat follows an accepted
update_agent: the update's firstImageUpdateStep, orunchangedwhen the node runs that image already, as the registry or the local image says, and nothing more happens.imageImageReferenceThe agent image the update is to.
ImageReferenceA Docker image reference, as
cs2_imageandagent_imageinagent.ymlhold it.string
Must match
^[A-Za-z0-9][A-Za-z0-9._\-/:@]{0,254}$stepstring enumThe 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"
A request that failed.
type"reply"reply_toRequestIdThe
idof the request this answers.RequestIdChosen 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
requestRequestType| nullThe
typeof the request this answers. Null when the agent did not know it.RequestTypeThe
typeof each request the controller sends, which itsreplyrepeats asrequest.string enum
"start_server""stop_server""get_server_status""update_cs2""delete_demo""reserve_slot""set_config""update_agent""update_cs2_image""console_command""restore_round""start_match"
okfalseThe request failed.
errorAgentErrorWhy it failed.
AgentErrorWhy a request failed. Branch on
code.detailis for people.codeAgentErrorCodeWhat went wrong. Branch on it.
AgentErrorCodeThe
codeof a failed reply, one of the values ofAGENT_ERRORS.string enum
"no_free_slot"No slot is idle, unreserved and on a token Steam has not refused.
"cs2_outdated"A newer public CS2 build is out and the node has not updated to it yet, so its
cs2.build_idis behindcs2.latest_build_id. The node takes launches again once it has updated. A node whoseauto_updateisfalsenever refuses with this."cs2_not_installed"CS2 is not installed on the node, so its
cs2.build_idisnull. The node takes launches once anupdate_cs2has installed it."node_unavailable"The node reports
unavailablefor a reason other thanmaintenance."slots_held"An operation that needs every slot idle found one held:
update_cs2, or aset_configthat changes any key undercontroller."slot_held"A slot holds a server that is
starting,runningorstopping, andset_configwould remove or re-port the slot or give its server's token to another slot, orreserve_slotasked for it.detailnames the slots."unknown_slot"The node has no slot with that id.
"slot_token_required"set_configkept the token of a slot the node does not have yet. A new slot needs a new token."demo_header_value_required"set_configkept the value of a demo upload header the node does not send. A new header needs its value."unknown_allocation"No server with that allocation id.
"maintenance_running"A maintenance operation runs on the node. That operation is unaffected, and
unavailableshows it asmaintenance."image_missing"The image is not on the node and the request asked for no pull. Asking again with
pullfetches it."image_not_allowed"The image is not an official one, and the node's
official_images_onlyis on. The node's operator decides which images it runs."demo_uploading"The demo is uploading. It can be deleted once the upload ends.
"unknown_demo"The slot has no demo of that name, such as one an upload or a delete has already removed.
"demo_recording"The demo is still being recorded. It can be deleted once its server ends the recording.
"plugin_offline"The server's plugin is not connected to the agent, or left before it answered.
"plugin_status_timeout"The plugin did not answer
get_server_status,console_command,start_matchorrestore_roundwithin 10 seconds."no_match"The server runs without a match.
"not_in_warmup"The match is past its warmup.
"team_empty"A team has no player connected.
"not_live"The match is not live, such as in warmup, during the knife round or after it ended.
"at_halftime"The match is in its halftime break, while the sides are about to swap. Ask again once the next round has started.
"no_backup"The node has no backup of the round that
restore_roundnamed for that server."command_blocked"console_commandnamed a command ofmp_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_stoprecordandaliaswhile the server runs a match. A new launch restarts a match, andrestore_roundrestores one."invalid_request"The request did not match its schema.
"allocation_in_use"start_servernamed an allocation this node is already launching or running. Its server exists or is on its way, as after astart_serversent again when its reply was slow."allocation_reused"start_servernamed an allocation that this node launched before, among the last 100. The node files a server's events and demos under its allocation, so every launch needs a new one."unsupported_request"The agent does not know the request's
type, or the plugin the command's."shutting_down"The agent is stopping and started no work for the request, as after its
shutdown. Send the request again once the node reconnects."failed"Any failure that no other code names, such as a Docker failure.
detailstring ≤ 512 charsThe rest of the reason, such as Docker's message, up to 512 characters.
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"idRequestIdChosen 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.RequestIdChosen 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
modeCs2UpdateModeHow thoroughly to update.
Cs2UpdateModeHow 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_toRequestIdThe
idof the request this answers.RequestIdChosen 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"oktrueThe request succeeded.
dataUpdateCs2ReplyThe update an accepted
update_cs2started.UpdateCs2ReplyThe update an accepted
update_cs2started.update_idUpdateIdThe update's id, as
cs2.update.id,cs2_update_progressand its log source carry it.UpdateIdChosen by the agent for each CS2 install or update it runs or finds running. A lowercase UUID.
string
A request that failed.
type"reply"reply_toRequestIdThe
idof the request this answers.RequestIdChosen 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
requestRequestType| nullThe
typeof the request this answers. Null when the agent did not know it.RequestTypeThe
typeof each request the controller sends, which itsreplyrepeats asrequest.string enum
"start_server""stop_server""get_server_status""update_cs2""delete_demo""reserve_slot""set_config""update_agent""update_cs2_image""console_command""restore_round""start_match"
okfalseThe request failed.
errorAgentErrorWhy it failed.
AgentErrorWhy a request failed. Branch on
code.detailis for people.codeAgentErrorCodeWhat went wrong. Branch on it.
AgentErrorCodeThe
codeof a failed reply, one of the values ofAGENT_ERRORS.string enum
"no_free_slot"No slot is idle, unreserved and on a token Steam has not refused.
"cs2_outdated"A newer public CS2 build is out and the node has not updated to it yet, so its
cs2.build_idis behindcs2.latest_build_id. The node takes launches again once it has updated. A node whoseauto_updateisfalsenever refuses with this."cs2_not_installed"CS2 is not installed on the node, so its
cs2.build_idisnull. The node takes launches once anupdate_cs2has installed it."node_unavailable"The node reports
unavailablefor a reason other thanmaintenance."slots_held"An operation that needs every slot idle found one held:
update_cs2, or aset_configthat changes any key undercontroller."slot_held"A slot holds a server that is
starting,runningorstopping, andset_configwould remove or re-port the slot or give its server's token to another slot, orreserve_slotasked for it.detailnames the slots."unknown_slot"The node has no slot with that id.
"slot_token_required"set_configkept the token of a slot the node does not have yet. A new slot needs a new token."demo_header_value_required"set_configkept the value of a demo upload header the node does not send. A new header needs its value."unknown_allocation"No server with that allocation id.
"maintenance_running"A maintenance operation runs on the node. That operation is unaffected, and
unavailableshows it asmaintenance."image_missing"The image is not on the node and the request asked for no pull. Asking again with
pullfetches it."image_not_allowed"The image is not an official one, and the node's
official_images_onlyis on. The node's operator decides which images it runs."demo_uploading"The demo is uploading. It can be deleted once the upload ends.
"unknown_demo"The slot has no demo of that name, such as one an upload or a delete has already removed.
"demo_recording"The demo is still being recorded. It can be deleted once its server ends the recording.
"plugin_offline"The server's plugin is not connected to the agent, or left before it answered.
"plugin_status_timeout"The plugin did not answer
get_server_status,console_command,start_matchorrestore_roundwithin 10 seconds."no_match"The server runs without a match.
"not_in_warmup"The match is past its warmup.
"team_empty"A team has no player connected.
"not_live"The match is not live, such as in warmup, during the knife round or after it ended.
"at_halftime"The match is in its halftime break, while the sides are about to swap. Ask again once the next round has started.
"no_backup"The node has no backup of the round that
restore_roundnamed for that server."command_blocked"console_commandnamed a command ofmp_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_stoprecordandaliaswhile the server runs a match. A new launch restarts a match, andrestore_roundrestores one."invalid_request"The request did not match its schema.
"allocation_in_use"start_servernamed an allocation this node is already launching or running. Its server exists or is on its way, as after astart_serversent again when its reply was slow."allocation_reused"start_servernamed an allocation that this node launched before, among the last 100. The node files a server's events and demos under its allocation, so every launch needs a new one."unsupported_request"The agent does not know the request's
type, or the plugin the command's."shutting_down"The agent is stopping and started no work for the request, as after its
shutdown. Send the request again once the node reconnects."failed"Any failure that no other code names, such as a Docker failure.
detailstring ≤ 512 charsThe rest of the reason, such as Docker's message, up to 512 characters.
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"idRequestIdChosen 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.RequestIdChosen 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
imageImageReferenceThe cs2 image future launches use.
ImageReferenceA Docker image reference, as
cs2_imageandagent_imageinagent.ymlhold it.string
Must match
^[A-Za-z0-9][A-Za-z0-9._\-/:@]{0,254}$pullbooleanWhether 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_toRequestIdThe
idof the request this answers.RequestIdChosen 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"oktrueThe request succeeded.
dataUpdateCs2ImageReplyWhat follows an accepted
update_cs2_image: its firstImageUpdateStep.UpdateCs2ImageReplyWhat follows an accepted
update_cs2_image: its firstImageUpdateStep.imageImageReferenceThe cs2 image the switch is to.
ImageReferenceA Docker image reference, as
cs2_imageandagent_imageinagent.ymlhold it.string
Must match
^[A-Za-z0-9][A-Za-z0-9._\-/:@]{0,254}$stepImageUpdateStepThe next step.
ImageUpdateStepWhere an
update_agentorupdate_cs2_imageis.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.
A request that failed.
type"reply"reply_toRequestIdThe
idof the request this answers.RequestIdChosen 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
requestRequestType| nullThe
typeof the request this answers. Null when the agent did not know it.RequestTypeThe
typeof each request the controller sends, which itsreplyrepeats asrequest.string enum
"start_server""stop_server""get_server_status""update_cs2""delete_demo""reserve_slot""set_config""update_agent""update_cs2_image""console_command""restore_round""start_match"
okfalseThe request failed.
errorAgentErrorWhy it failed.
AgentErrorWhy a request failed. Branch on
code.detailis for people.codeAgentErrorCodeWhat went wrong. Branch on it.
AgentErrorCodeThe
codeof a failed reply, one of the values ofAGENT_ERRORS.string enum
"no_free_slot"No slot is idle, unreserved and on a token Steam has not refused.
"cs2_outdated"A newer public CS2 build is out and the node has not updated to it yet, so its
cs2.build_idis behindcs2.latest_build_id. The node takes launches again once it has updated. A node whoseauto_updateisfalsenever refuses with this."cs2_not_installed"CS2 is not installed on the node, so its
cs2.build_idisnull. The node takes launches once anupdate_cs2has installed it."node_unavailable"The node reports
unavailablefor a reason other thanmaintenance."slots_held"An operation that needs every slot idle found one held:
update_cs2, or aset_configthat changes any key undercontroller."slot_held"A slot holds a server that is
starting,runningorstopping, andset_configwould remove or re-port the slot or give its server's token to another slot, orreserve_slotasked for it.detailnames the slots."unknown_slot"The node has no slot with that id.
"slot_token_required"set_configkept the token of a slot the node does not have yet. A new slot needs a new token."demo_header_value_required"set_configkept the value of a demo upload header the node does not send. A new header needs its value."unknown_allocation"No server with that allocation id.
"maintenance_running"A maintenance operation runs on the node. That operation is unaffected, and
unavailableshows it asmaintenance."image_missing"The image is not on the node and the request asked for no pull. Asking again with
pullfetches it."image_not_allowed"The image is not an official one, and the node's
official_images_onlyis on. The node's operator decides which images it runs."demo_uploading"The demo is uploading. It can be deleted once the upload ends.
"unknown_demo"The slot has no demo of that name, such as one an upload or a delete has already removed.
"demo_recording"The demo is still being recorded. It can be deleted once its server ends the recording.
"plugin_offline"The server's plugin is not connected to the agent, or left before it answered.
"plugin_status_timeout"The plugin did not answer
get_server_status,console_command,start_matchorrestore_roundwithin 10 seconds."no_match"The server runs without a match.
"not_in_warmup"The match is past its warmup.
"team_empty"A team has no player connected.
"not_live"The match is not live, such as in warmup, during the knife round or after it ended.
"at_halftime"The match is in its halftime break, while the sides are about to swap. Ask again once the next round has started.
"no_backup"The node has no backup of the round that
restore_roundnamed for that server."command_blocked"console_commandnamed a command ofmp_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_stoprecordandaliaswhile the server runs a match. A new launch restarts a match, andrestore_roundrestores one."invalid_request"The request did not match its schema.
"allocation_in_use"start_servernamed an allocation this node is already launching or running. Its server exists or is on its way, as after astart_serversent again when its reply was slow."allocation_reused"start_servernamed an allocation that this node launched before, among the last 100. The node files a server's events and demos under its allocation, so every launch needs a new one."unsupported_request"The agent does not know the request's
type, or the plugin the command's."shutting_down"The agent is stopping and started no work for the request, as after its
shutdown. Send the request again once the node reconnects."failed"Any failure that no other code names, such as a Docker failure.
detailstring ≤ 512 charsThe rest of the reason, such as Docker's message, up to 512 characters.
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"sourceLogSourceThe log to stream. A source the agent does not keep streams nothing until it prints.
LogSourceOne 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"
kind"cs2_update"update_idUpdateIdThe install or update that printed it, the
idof itscs2.update.UpdateIdChosen by the agent for each CS2 install or update it runs or finds running. A lowercase UUID.
string
kind"server"allocation_idAllocationIdThe server that printed it.
AllocationIdThe 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
replay_idReplayId| nullThe 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.
ReplayIdIdentifies 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"replaybooleanWhether 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"
}