wire protocol is generated now instead of being hard coded with versioning for commands.

This commit is contained in:
KingKDot
2026-07-30 12:38:28 -05:00
parent 3f49eb07f7
commit 477b9a5353
27 changed files with 2628 additions and 204 deletions
+8
View File
@@ -28,9 +28,14 @@ jobs:
filters: |
server:
- 'Overlord-Server/**'
- 'Overlord-Client/cmd/agent/wire/wire_contract_generated.go'
- 'protocol/**'
- 'scripts/generate-wire-protocol.ts'
- '.github/workflows/tests.yml'
client:
- 'Overlord-Client/**'
- 'protocol/**'
- 'scripts/generate-wire-protocol.ts'
- 'go.work'
- 'go.work.sum'
@@ -60,6 +65,9 @@ jobs:
- name: Typecheck server
run: bun run typecheck
- name: Verify generated wire protocol
run: bun run protocol:check
- name: Run server tests
run: bun run test
+1 -1
View File
@@ -17,7 +17,7 @@ import (
"time"
)
var AgentVersion = "2.6.1"
var AgentVersion = "3.0.0"
var DefaultPersistence = "false"
var DefaultServerURL = "wss://127.0.0.1:5173"
@@ -587,7 +587,8 @@ func extractCaptureDLLBytes(payload map[string]interface{}) []byte {
func HandleCommand(ctx context.Context, env *runtime.Env, envelope map[string]interface{}) error {
//garble:controlflow block_splits=10 junk_jumps=10 flatten_passes=2
cmdID, _ := envelope["id"].(string)
action, _ := envelope["commandType"].(string)
actionValue, _ := envelope["commandType"].(string)
action := wire.CommandType(actionValue)
switch action {
case "screenshot":
@@ -8,6 +8,7 @@ import (
"overlord-client/cmd/agent/capture"
"overlord-client/cmd/agent/runtime"
"overlord-client/cmd/agent/wire"
)
type Dispatcher struct {
@@ -31,6 +32,10 @@ func (d *Dispatcher) Dispatch(ctx context.Context, envelope map[string]interface
log.Printf("dispatcher: missing type (keys=%v)", maps.Keys(envelope))
return nil
}
if !wire.IsServerToAgentMessageType(msgType) {
log.Printf("dispatcher: unknown message type=%v", msgType)
return nil
}
switch msgType {
case "hello_ack":
return HandleHelloAck(ctx, d.Env, envelope)
@@ -43,6 +48,40 @@ func (d *Dispatcher) Dispatch(ctx context.Context, envelope map[string]interface
return nil
case "command":
cmdType, _ := envelope["commandType"].(string)
if !wire.IsCommandType(cmdType) {
log.Printf("dispatcher: unknown command type=%s", cmdType)
return nil
}
commandVersion := toInt(envelope["commandVersion"])
if commandVersion <= 0 {
commandVersion = 1
}
envelope["commandVersion"] = commandVersion
if !wire.IsSupportedCommandVersion(cmdType, commandVersion) {
cmdID, _ := envelope["id"].(string)
command := wire.CommandType(cmdType)
supported := wire.CommandVersionSupport[command]
log.Printf(
"dispatcher: unsupported command version type=%s version=%d supported=%d-%d",
cmdType,
commandVersion,
supported.Min,
supported.Max,
)
if d.Env == nil || d.Env.Conn == nil {
return nil
}
return wire.WriteMsg(ctx, d.Env.Conn, wire.CommandResult{
Type: "command_result",
CommandID: cmdID,
CommandType: command,
CommandVersion: commandVersion,
OK: false,
Message: fmt.Sprintf("unsupported %s command version %d", cmdType, commandVersion),
ErrorCode: "unsupported_command_version",
SupportedVersions: &supported,
})
}
if !isInputCommand(cmdType) {
log.Printf("dispatcher: handling command type=%s", cmdType)
}
@@ -62,7 +101,7 @@ func (d *Dispatcher) Dispatch(ctx context.Context, envelope map[string]interface
}
return nil
default:
log.Printf("dispatcher: unknown message type=%v", msgType)
log.Printf("dispatcher: unhandled contract message type=%v", msgType)
return nil
}
}
@@ -6,6 +6,7 @@ import (
"strings"
"overlord-client/cmd/agent/runtime"
"overlord-client/cmd/agent/wire"
)
func HandleHelloAck(_ context.Context, env *runtime.Env, envelope map[string]interface{}) error {
@@ -13,6 +14,9 @@ func HandleHelloAck(_ context.Context, env *runtime.Env, envelope map[string]int
if env == nil || envelope == nil {
return nil
}
if version := toInt(envelope["protocolVersion"]); version > 0 {
log.Printf("hello ack: negotiated wire protocol v%d (agent v%d)", version, wire.WireProtocolVersion)
}
var keywords []string
minInterval := 0
+2
View File
@@ -649,6 +649,8 @@ func runSession(ctx context.Context, cancel context.CancelFunc, conn *websocket.
PublicKey: publicKeyB64,
Signature: signatureB64,
InMemory: isRunningInMemory(),
ProtocolVersion: wire.WireProtocolVersion,
CommandVersions: wire.SupportedCommandVersionRanges(),
}
hw := sysinfo.Collect()
+8 -1
View File
@@ -28,6 +28,8 @@ type Hello struct {
PublicIP string `msgpack:"publicIP,omitempty"`
LastCrashReason string `msgpack:"lastCrashReason,omitempty"`
LastCrashDetail string `msgpack:"lastCrashDetail,omitempty"`
ProtocolVersion int `msgpack:"protocolVersion,omitempty"`
CommandVersions map[string]CommandVersionRange `msgpack:"commandVersions,omitempty"`
}
type EnrollmentChallenge struct {
@@ -57,7 +59,8 @@ type Pong struct {
type Command struct {
Type string `msgpack:"type"`
CommandType string `msgpack:"commandType"`
CommandType CommandType `msgpack:"commandType"`
CommandVersion int `msgpack:"commandVersion,omitempty"`
Payload interface{} `msgpack:"payload,omitempty"`
ID string `msgpack:"id,omitempty"`
}
@@ -65,8 +68,12 @@ type Command struct {
type CommandResult struct {
Type string `msgpack:"type"`
CommandID string `msgpack:"commandId,omitempty"`
CommandType CommandType `msgpack:"commandType,omitempty"`
CommandVersion int `msgpack:"commandVersion,omitempty"`
OK bool `msgpack:"ok"`
Message string `msgpack:"message,omitempty"`
ErrorCode string `msgpack:"errorCode,omitempty"`
SupportedVersions *CommandVersionRange `msgpack:"supportedVersions,omitempty"`
}
type DesktopEncoderProfile struct {
@@ -207,7 +207,8 @@ func TestPingPongMarshaling(t *testing.T) {
func TestCommandMarshaling(t *testing.T) {
cmd := Command{
Type: "command",
CommandType: "test_command",
CommandType: "process_list",
CommandVersion: 1,
ID: "cmd-123",
Payload: map[string]interface{}{"key": "value"},
}
@@ -228,6 +229,9 @@ func TestCommandMarshaling(t *testing.T) {
if decoded.ID != cmd.ID {
t.Errorf("ID mismatch: got %s, want %s", decoded.ID, cmd.ID)
}
if decoded.CommandVersion != cmd.CommandVersion {
t.Errorf("CommandVersion mismatch: got %d, want %d", decoded.CommandVersion, cmd.CommandVersion)
}
}
func TestCommandResultMarshaling(t *testing.T) {
@@ -0,0 +1,612 @@
// Code generated by scripts/generate-wire-protocol.ts; DO NOT EDIT.
package wire
const (
WireSchemaVersion = 1
WireProtocolVersion = 1
)
type MessageType string
const (
MessageBackstageBrowserCheckResult MessageType = "backstage_browser_check_result"
MessageBackstageBrowserLaunchStatus MessageType = "backstage_browser_launch_status"
MessageBackstageCloneProgress MessageType = "backstage_clone_progress"
MessageBackstageDxgiStatus MessageType = "backstage_dxgi_status"
MessageBackstageInstalledAppsResult MessageType = "backstage_installed_apps_result"
MessageBackstageLookupResult MessageType = "backstage_lookup_result"
MessageBackstageWindowListResult MessageType = "backstage_window_list_result"
MessageClientLogsResult MessageType = "client_logs_result"
MessageClipboardContent MessageType = "clipboard_content"
MessageCommand MessageType = "command"
MessageCommandAbort MessageType = "command_abort"
MessageCommandProgress MessageType = "command_progress"
MessageCommandResult MessageType = "command_result"
MessageConsoleOutput MessageType = "console_output"
MessageDesktopAudioUplink MessageType = "desktop_audio_uplink"
MessageDesktopCursor MessageType = "desktop_cursor"
MessageDesktopEncoderCapabilities MessageType = "desktop_encoder_capabilities"
MessageDesktopStreamStats MessageType = "desktop_stream_stats"
MessageDisconnectInfo MessageType = "disconnect_info"
MessageEnrollmentChallenge MessageType = "enrollment_challenge"
MessageEnrollmentStatus MessageType = "enrollment_status"
MessageFileDirsizeResult MessageType = "file_dirsize_result"
MessageFileDownload MessageType = "file_download"
MessageFileHashResult MessageType = "file_hash_result"
MessageFileIconResult MessageType = "file_icon_result"
MessageFileListResult MessageType = "file_list_result"
MessageFilePeekResult MessageType = "file_peek_result"
MessageFileReadResult MessageType = "file_read_result"
MessageFileSearchResult MessageType = "file_search_result"
MessageFileThumbResult MessageType = "file_thumb_result"
MessageFileUploadResult MessageType = "file_upload_result"
MessageFrame MessageType = "frame"
MessageFrameAck MessageType = "frame_ack"
MessageHello MessageType = "hello"
MessageHelloAck MessageType = "hello_ack"
MessageKeylogClearResult MessageType = "keylog_clear_result"
MessageKeylogDeleteResult MessageType = "keylog_delete_result"
MessageKeylogFileContent MessageType = "keylog_file_content"
MessageKeylogFileList MessageType = "keylog_file_list"
MessageKeylogPermissionResult MessageType = "keylog_permission_result"
MessageNotification MessageType = "notification"
MessageNotificationConfig MessageType = "notification_config"
MessagePing MessageType = "ping"
MessagePluginEvent MessageType = "plugin_event"
MessagePong MessageType = "pong"
MessageProcessIconResult MessageType = "process_icon_result"
MessageProcessListResult MessageType = "process_list_result"
MessageProxyClose MessageType = "proxy_close"
MessageProxyData MessageType = "proxy_data"
MessageScreenshotResult MessageType = "screenshot_result"
MessageScriptResult MessageType = "script_result"
MessageStatus MessageType = "status"
MessageVoiceUplink MessageType = "voice_uplink"
MessageWebcamDevices MessageType = "webcam_devices"
MessageWebrtcP2pAnswer MessageType = "webrtc_p2p_answer"
MessageWebrtcP2pIce MessageType = "webrtc_p2p_ice"
)
type CommandType string
const (
CommandAgentUpdate CommandType = "agent_update"
CommandBackstageBrowserCheck CommandType = "backstage_browser_check"
CommandBackstageEnableCursor CommandType = "backstage_enable_cursor"
CommandBackstageEnableDxgi CommandType = "backstage_enable_dxgi"
CommandBackstageEnableKeyboard CommandType = "backstage_enable_keyboard"
CommandBackstageEnableMouse CommandType = "backstage_enable_mouse"
CommandBackstageEnableUia CommandType = "backstage_enable_uia"
CommandBackstageInstalledApps CommandType = "backstage_installed_apps"
CommandBackstageKeyDown CommandType = "backstage_key_down"
CommandBackstageKeyUp CommandType = "backstage_key_up"
CommandBackstageKillAll CommandType = "backstage_kill_all"
CommandBackstageLookup CommandType = "backstage_lookup"
CommandBackstageMouseDown CommandType = "backstage_mouse_down"
CommandBackstageMouseMove CommandType = "backstage_mouse_move"
CommandBackstageMouseUp CommandType = "backstage_mouse_up"
CommandBackstageMouseWheel CommandType = "backstage_mouse_wheel"
CommandBackstageRequestKeyframe CommandType = "backstage_request_keyframe"
CommandBackstageSelectDisplay CommandType = "backstage_select_display"
CommandBackstageSetFps CommandType = "backstage_set_fps"
CommandBackstageSetQuality CommandType = "backstage_set_quality"
CommandBackstageSetResolution CommandType = "backstage_set_resolution"
CommandBackstageStart CommandType = "backstage_start"
CommandBackstageStartBrowserInjected CommandType = "backstage_start_browser_injected"
CommandBackstageStartChromeInjected CommandType = "backstage_start_chrome_injected"
CommandBackstageStartProcess CommandType = "backstage_start_process"
CommandBackstageStartProcessInjected CommandType = "backstage_start_process_injected"
CommandBackstageStop CommandType = "backstage_stop"
CommandBackstageWindowList CommandType = "backstage_window_list"
CommandClientLogsRequest CommandType = "client_logs_request"
CommandClipboardSet CommandType = "clipboard_set"
CommandClipboardSyncStart CommandType = "clipboard_sync_start"
CommandClipboardSyncStop CommandType = "clipboard_sync_stop"
CommandConsoleInput CommandType = "console_input"
CommandConsoleResize CommandType = "console_resize"
CommandConsoleStart CommandType = "console_start"
CommandConsoleStop CommandType = "console_stop"
CommandDarwinRequestPermissions CommandType = "darwin_request_permissions"
CommandDesktopAudioStart CommandType = "desktop_audio_start"
CommandDesktopAudioStop CommandType = "desktop_audio_stop"
CommandDesktopEnableCursor CommandType = "desktop_enable_cursor"
CommandDesktopEnableKeyboard CommandType = "desktop_enable_keyboard"
CommandDesktopEnableMouse CommandType = "desktop_enable_mouse"
CommandDesktopEncoderCapabilities CommandType = "desktop_encoder_capabilities"
CommandDesktopKeyDown CommandType = "desktop_key_down"
CommandDesktopKeyUp CommandType = "desktop_key_up"
CommandDesktopMouseDown CommandType = "desktop_mouse_down"
CommandDesktopMouseMove CommandType = "desktop_mouse_move"
CommandDesktopMouseUp CommandType = "desktop_mouse_up"
CommandDesktopMouseWheel CommandType = "desktop_mouse_wheel"
CommandDesktopRequestKeyframe CommandType = "desktop_request_keyframe"
CommandDesktopSelectDisplay CommandType = "desktop_select_display"
CommandDesktopSetBitrate CommandType = "desktop_set_bitrate"
CommandDesktopSetDuplication CommandType = "desktop_set_duplication"
CommandDesktopSetFps CommandType = "desktop_set_fps"
CommandDesktopSetProfile CommandType = "desktop_set_profile"
CommandDesktopSetQuality CommandType = "desktop_set_quality"
CommandDesktopSetResolution CommandType = "desktop_set_resolution"
CommandDesktopStart CommandType = "desktop_start"
CommandDesktopStop CommandType = "desktop_stop"
CommandDesktopText CommandType = "desktop_text"
CommandDisconnect CommandType = "disconnect"
CommandElevate CommandType = "elevate"
CommandFileChmod CommandType = "file_chmod"
CommandFileCopy CommandType = "file_copy"
CommandFileDelete CommandType = "file_delete"
CommandFileDirsize CommandType = "file_dirsize"
CommandFileDownload CommandType = "file_download"
CommandFileExecute CommandType = "file_execute"
CommandFileHash CommandType = "file_hash"
CommandFileIcon CommandType = "file_icon"
CommandFileList CommandType = "file_list"
CommandFileMkdir CommandType = "file_mkdir"
CommandFileMove CommandType = "file_move"
CommandFilePeek CommandType = "file_peek"
CommandFileRead CommandType = "file_read"
CommandFileRequestAccess CommandType = "file_request_access"
CommandFileSearch CommandType = "file_search"
CommandFileThumb CommandType = "file_thumb"
CommandFileUpload CommandType = "file_upload"
CommandFileUploadHttp CommandType = "file_upload_http"
CommandFileWrite CommandType = "file_write"
CommandFileZip CommandType = "file_zip"
CommandKeylogClearAll CommandType = "keylog_clear_all"
CommandKeylogDelete CommandType = "keylog_delete"
CommandKeylogList CommandType = "keylog_list"
CommandKeylogRequestPermission CommandType = "keylog_request_permission"
CommandKeylogRetrieve CommandType = "keylog_retrieve"
CommandPluginLoad CommandType = "plugin_load"
CommandPluginLoadHttp CommandType = "plugin_load_http"
CommandPluginUnload CommandType = "plugin_unload"
CommandPrivacyStart CommandType = "privacy_start"
CommandPrivacyStatus CommandType = "privacy_status"
CommandPrivacyStop CommandType = "privacy_stop"
CommandProcessIcon CommandType = "process_icon"
CommandProcessKill CommandType = "process_kill"
CommandProcessList CommandType = "process_list"
CommandProcessResume CommandType = "process_resume"
CommandProcessSuspend CommandType = "process_suspend"
CommandProxyClose CommandType = "proxy_close"
CommandProxyConnect CommandType = "proxy_connect"
CommandProxyData CommandType = "proxy_data"
CommandReconnect CommandType = "reconnect"
CommandScreenshot CommandType = "screenshot"
CommandScriptExec CommandType = "script_exec"
CommandSilentExec CommandType = "silent_exec"
CommandUninstall CommandType = "uninstall"
CommandVirtualEnableCursor CommandType = "virtual_enable_cursor"
CommandVirtualEnableKeyboard CommandType = "virtual_enable_keyboard"
CommandVirtualEnableMouse CommandType = "virtual_enable_mouse"
CommandVirtualKeyDown CommandType = "virtual_key_down"
CommandVirtualKeyUp CommandType = "virtual_key_up"
CommandVirtualKillAll CommandType = "virtual_kill_all"
CommandVirtualMouseDown CommandType = "virtual_mouse_down"
CommandVirtualMouseMove CommandType = "virtual_mouse_move"
CommandVirtualMouseUp CommandType = "virtual_mouse_up"
CommandVirtualMouseWheel CommandType = "virtual_mouse_wheel"
CommandVirtualRequestKeyframe CommandType = "virtual_request_keyframe"
CommandVirtualSetFps CommandType = "virtual_set_fps"
CommandVirtualSetQuality CommandType = "virtual_set_quality"
CommandVirtualStart CommandType = "virtual_start"
CommandVirtualStartProcess CommandType = "virtual_start_process"
CommandVirtualStop CommandType = "virtual_stop"
CommandVirtualWindowList CommandType = "virtual_window_list"
CommandVoiceCapabilities CommandType = "voice_capabilities"
CommandVoiceDownlink CommandType = "voice_downlink"
CommandVoiceSessionStart CommandType = "voice_session_start"
CommandVoiceSessionStop CommandType = "voice_session_stop"
CommandWebcamList CommandType = "webcam_list"
CommandWebcamSelect CommandType = "webcam_select"
CommandWebcamSetFps CommandType = "webcam_set_fps"
CommandWebcamSetQuality CommandType = "webcam_set_quality"
CommandWebcamStart CommandType = "webcam_start"
CommandWebcamStop CommandType = "webcam_stop"
CommandWebrtcP2pIce CommandType = "webrtc_p2p_ice"
CommandWebrtcP2pOffer CommandType = "webrtc_p2p_offer"
CommandWebrtcP2pStop CommandType = "webrtc_p2p_stop"
CommandWebrtcPublish CommandType = "webrtc_publish"
CommandWebrtcStop CommandType = "webrtc_stop"
CommandWinreInstall CommandType = "winre_install"
CommandWinreProbe CommandType = "winre_probe"
CommandWinreUninstall CommandType = "winre_uninstall"
)
var AgentToServerMessageTypes = [...]MessageType{
MessageBackstageBrowserCheckResult,
MessageBackstageBrowserLaunchStatus,
MessageBackstageCloneProgress,
MessageBackstageDxgiStatus,
MessageBackstageInstalledAppsResult,
MessageBackstageLookupResult,
MessageBackstageWindowListResult,
MessageClientLogsResult,
MessageClipboardContent,
MessageCommandProgress,
MessageCommandResult,
MessageConsoleOutput,
MessageDesktopAudioUplink,
MessageDesktopCursor,
MessageDesktopEncoderCapabilities,
MessageDesktopStreamStats,
MessageDisconnectInfo,
MessageFileDirsizeResult,
MessageFileDownload,
MessageFileHashResult,
MessageFileIconResult,
MessageFileListResult,
MessageFilePeekResult,
MessageFileReadResult,
MessageFileSearchResult,
MessageFileThumbResult,
MessageFileUploadResult,
MessageFrame,
MessageHello,
MessageKeylogClearResult,
MessageKeylogDeleteResult,
MessageKeylogFileContent,
MessageKeylogFileList,
MessageKeylogPermissionResult,
MessageNotification,
MessagePing,
MessagePluginEvent,
MessagePong,
MessageProcessIconResult,
MessageProcessListResult,
MessageProxyClose,
MessageProxyData,
MessageScreenshotResult,
MessageScriptResult,
MessageStatus,
MessageVoiceUplink,
MessageWebcamDevices,
MessageWebrtcP2pAnswer,
MessageWebrtcP2pIce,
}
var ServerToAgentMessageTypes = [...]MessageType{
MessageCommand,
MessageCommandAbort,
MessageEnrollmentChallenge,
MessageEnrollmentStatus,
MessageFrameAck,
MessageHelloAck,
MessageNotificationConfig,
MessagePing,
MessagePluginEvent,
MessagePong,
}
var CommandTypes = [...]CommandType{
CommandAgentUpdate,
CommandBackstageBrowserCheck,
CommandBackstageEnableCursor,
CommandBackstageEnableDxgi,
CommandBackstageEnableKeyboard,
CommandBackstageEnableMouse,
CommandBackstageEnableUia,
CommandBackstageInstalledApps,
CommandBackstageKeyDown,
CommandBackstageKeyUp,
CommandBackstageKillAll,
CommandBackstageLookup,
CommandBackstageMouseDown,
CommandBackstageMouseMove,
CommandBackstageMouseUp,
CommandBackstageMouseWheel,
CommandBackstageRequestKeyframe,
CommandBackstageSelectDisplay,
CommandBackstageSetFps,
CommandBackstageSetQuality,
CommandBackstageSetResolution,
CommandBackstageStart,
CommandBackstageStartBrowserInjected,
CommandBackstageStartChromeInjected,
CommandBackstageStartProcess,
CommandBackstageStartProcessInjected,
CommandBackstageStop,
CommandBackstageWindowList,
CommandClientLogsRequest,
CommandClipboardSet,
CommandClipboardSyncStart,
CommandClipboardSyncStop,
CommandConsoleInput,
CommandConsoleResize,
CommandConsoleStart,
CommandConsoleStop,
CommandDarwinRequestPermissions,
CommandDesktopAudioStart,
CommandDesktopAudioStop,
CommandDesktopEnableCursor,
CommandDesktopEnableKeyboard,
CommandDesktopEnableMouse,
CommandDesktopEncoderCapabilities,
CommandDesktopKeyDown,
CommandDesktopKeyUp,
CommandDesktopMouseDown,
CommandDesktopMouseMove,
CommandDesktopMouseUp,
CommandDesktopMouseWheel,
CommandDesktopRequestKeyframe,
CommandDesktopSelectDisplay,
CommandDesktopSetBitrate,
CommandDesktopSetDuplication,
CommandDesktopSetFps,
CommandDesktopSetProfile,
CommandDesktopSetQuality,
CommandDesktopSetResolution,
CommandDesktopStart,
CommandDesktopStop,
CommandDesktopText,
CommandDisconnect,
CommandElevate,
CommandFileChmod,
CommandFileCopy,
CommandFileDelete,
CommandFileDirsize,
CommandFileDownload,
CommandFileExecute,
CommandFileHash,
CommandFileIcon,
CommandFileList,
CommandFileMkdir,
CommandFileMove,
CommandFilePeek,
CommandFileRead,
CommandFileRequestAccess,
CommandFileSearch,
CommandFileThumb,
CommandFileUpload,
CommandFileUploadHttp,
CommandFileWrite,
CommandFileZip,
CommandKeylogClearAll,
CommandKeylogDelete,
CommandKeylogList,
CommandKeylogRequestPermission,
CommandKeylogRetrieve,
CommandPluginLoad,
CommandPluginLoadHttp,
CommandPluginUnload,
CommandPrivacyStart,
CommandPrivacyStatus,
CommandPrivacyStop,
CommandProcessIcon,
CommandProcessKill,
CommandProcessList,
CommandProcessResume,
CommandProcessSuspend,
CommandProxyClose,
CommandProxyConnect,
CommandProxyData,
CommandReconnect,
CommandScreenshot,
CommandScriptExec,
CommandSilentExec,
CommandUninstall,
CommandVirtualEnableCursor,
CommandVirtualEnableKeyboard,
CommandVirtualEnableMouse,
CommandVirtualKeyDown,
CommandVirtualKeyUp,
CommandVirtualKillAll,
CommandVirtualMouseDown,
CommandVirtualMouseMove,
CommandVirtualMouseUp,
CommandVirtualMouseWheel,
CommandVirtualRequestKeyframe,
CommandVirtualSetFps,
CommandVirtualSetQuality,
CommandVirtualStart,
CommandVirtualStartProcess,
CommandVirtualStop,
CommandVirtualWindowList,
CommandVoiceCapabilities,
CommandVoiceDownlink,
CommandVoiceSessionStart,
CommandVoiceSessionStop,
CommandWebcamList,
CommandWebcamSelect,
CommandWebcamSetFps,
CommandWebcamSetQuality,
CommandWebcamStart,
CommandWebcamStop,
CommandWebrtcP2pIce,
CommandWebrtcP2pOffer,
CommandWebrtcP2pStop,
CommandWebrtcPublish,
CommandWebrtcStop,
CommandWinreInstall,
CommandWinreProbe,
CommandWinreUninstall,
}
type CommandVersionRange struct {
Min int `msgpack:"min" json:"min"`
Max int `msgpack:"max" json:"max"`
}
var CommandVersionSupport = map[CommandType]CommandVersionRange{
CommandAgentUpdate: {Min: 1, Max: 1},
CommandBackstageBrowserCheck: {Min: 1, Max: 1},
CommandBackstageEnableCursor: {Min: 1, Max: 1},
CommandBackstageEnableDxgi: {Min: 1, Max: 1},
CommandBackstageEnableKeyboard: {Min: 1, Max: 1},
CommandBackstageEnableMouse: {Min: 1, Max: 1},
CommandBackstageEnableUia: {Min: 1, Max: 1},
CommandBackstageInstalledApps: {Min: 1, Max: 1},
CommandBackstageKeyDown: {Min: 1, Max: 1},
CommandBackstageKeyUp: {Min: 1, Max: 1},
CommandBackstageKillAll: {Min: 1, Max: 1},
CommandBackstageLookup: {Min: 1, Max: 1},
CommandBackstageMouseDown: {Min: 1, Max: 1},
CommandBackstageMouseMove: {Min: 1, Max: 1},
CommandBackstageMouseUp: {Min: 1, Max: 1},
CommandBackstageMouseWheel: {Min: 1, Max: 1},
CommandBackstageRequestKeyframe: {Min: 1, Max: 1},
CommandBackstageSelectDisplay: {Min: 1, Max: 1},
CommandBackstageSetFps: {Min: 1, Max: 1},
CommandBackstageSetQuality: {Min: 1, Max: 1},
CommandBackstageSetResolution: {Min: 1, Max: 1},
CommandBackstageStart: {Min: 1, Max: 1},
CommandBackstageStartBrowserInjected: {Min: 1, Max: 1},
CommandBackstageStartChromeInjected: {Min: 1, Max: 1},
CommandBackstageStartProcess: {Min: 1, Max: 1},
CommandBackstageStartProcessInjected: {Min: 1, Max: 1},
CommandBackstageStop: {Min: 1, Max: 1},
CommandBackstageWindowList: {Min: 1, Max: 1},
CommandClientLogsRequest: {Min: 1, Max: 1},
CommandClipboardSet: {Min: 1, Max: 1},
CommandClipboardSyncStart: {Min: 1, Max: 1},
CommandClipboardSyncStop: {Min: 1, Max: 1},
CommandConsoleInput: {Min: 1, Max: 1},
CommandConsoleResize: {Min: 1, Max: 1},
CommandConsoleStart: {Min: 1, Max: 1},
CommandConsoleStop: {Min: 1, Max: 1},
CommandDarwinRequestPermissions: {Min: 1, Max: 1},
CommandDesktopAudioStart: {Min: 1, Max: 1},
CommandDesktopAudioStop: {Min: 1, Max: 1},
CommandDesktopEnableCursor: {Min: 1, Max: 1},
CommandDesktopEnableKeyboard: {Min: 1, Max: 1},
CommandDesktopEnableMouse: {Min: 1, Max: 1},
CommandDesktopEncoderCapabilities: {Min: 1, Max: 1},
CommandDesktopKeyDown: {Min: 1, Max: 1},
CommandDesktopKeyUp: {Min: 1, Max: 1},
CommandDesktopMouseDown: {Min: 1, Max: 1},
CommandDesktopMouseMove: {Min: 1, Max: 1},
CommandDesktopMouseUp: {Min: 1, Max: 1},
CommandDesktopMouseWheel: {Min: 1, Max: 1},
CommandDesktopRequestKeyframe: {Min: 1, Max: 1},
CommandDesktopSelectDisplay: {Min: 1, Max: 1},
CommandDesktopSetBitrate: {Min: 1, Max: 1},
CommandDesktopSetDuplication: {Min: 1, Max: 1},
CommandDesktopSetFps: {Min: 1, Max: 1},
CommandDesktopSetProfile: {Min: 1, Max: 1},
CommandDesktopSetQuality: {Min: 1, Max: 1},
CommandDesktopSetResolution: {Min: 1, Max: 1},
CommandDesktopStart: {Min: 1, Max: 1},
CommandDesktopStop: {Min: 1, Max: 1},
CommandDesktopText: {Min: 1, Max: 1},
CommandDisconnect: {Min: 1, Max: 1},
CommandElevate: {Min: 1, Max: 1},
CommandFileChmod: {Min: 1, Max: 1},
CommandFileCopy: {Min: 1, Max: 1},
CommandFileDelete: {Min: 1, Max: 1},
CommandFileDirsize: {Min: 1, Max: 1},
CommandFileDownload: {Min: 1, Max: 1},
CommandFileExecute: {Min: 1, Max: 1},
CommandFileHash: {Min: 1, Max: 1},
CommandFileIcon: {Min: 1, Max: 1},
CommandFileList: {Min: 1, Max: 1},
CommandFileMkdir: {Min: 1, Max: 1},
CommandFileMove: {Min: 1, Max: 1},
CommandFilePeek: {Min: 1, Max: 1},
CommandFileRead: {Min: 1, Max: 1},
CommandFileRequestAccess: {Min: 1, Max: 1},
CommandFileSearch: {Min: 1, Max: 1},
CommandFileThumb: {Min: 1, Max: 1},
CommandFileUpload: {Min: 1, Max: 1},
CommandFileUploadHttp: {Min: 1, Max: 1},
CommandFileWrite: {Min: 1, Max: 1},
CommandFileZip: {Min: 1, Max: 1},
CommandKeylogClearAll: {Min: 1, Max: 1},
CommandKeylogDelete: {Min: 1, Max: 1},
CommandKeylogList: {Min: 1, Max: 1},
CommandKeylogRequestPermission: {Min: 1, Max: 1},
CommandKeylogRetrieve: {Min: 1, Max: 1},
CommandPluginLoad: {Min: 1, Max: 1},
CommandPluginLoadHttp: {Min: 1, Max: 1},
CommandPluginUnload: {Min: 1, Max: 1},
CommandPrivacyStart: {Min: 1, Max: 1},
CommandPrivacyStatus: {Min: 1, Max: 1},
CommandPrivacyStop: {Min: 1, Max: 1},
CommandProcessIcon: {Min: 1, Max: 1},
CommandProcessKill: {Min: 1, Max: 1},
CommandProcessList: {Min: 1, Max: 1},
CommandProcessResume: {Min: 1, Max: 1},
CommandProcessSuspend: {Min: 1, Max: 1},
CommandProxyClose: {Min: 1, Max: 1},
CommandProxyConnect: {Min: 1, Max: 1},
CommandProxyData: {Min: 1, Max: 1},
CommandReconnect: {Min: 1, Max: 1},
CommandScreenshot: {Min: 1, Max: 1},
CommandScriptExec: {Min: 1, Max: 1},
CommandSilentExec: {Min: 1, Max: 1},
CommandUninstall: {Min: 1, Max: 1},
CommandVirtualEnableCursor: {Min: 1, Max: 1},
CommandVirtualEnableKeyboard: {Min: 1, Max: 1},
CommandVirtualEnableMouse: {Min: 1, Max: 1},
CommandVirtualKeyDown: {Min: 1, Max: 1},
CommandVirtualKeyUp: {Min: 1, Max: 1},
CommandVirtualKillAll: {Min: 1, Max: 1},
CommandVirtualMouseDown: {Min: 1, Max: 1},
CommandVirtualMouseMove: {Min: 1, Max: 1},
CommandVirtualMouseUp: {Min: 1, Max: 1},
CommandVirtualMouseWheel: {Min: 1, Max: 1},
CommandVirtualRequestKeyframe: {Min: 1, Max: 1},
CommandVirtualSetFps: {Min: 1, Max: 1},
CommandVirtualSetQuality: {Min: 1, Max: 1},
CommandVirtualStart: {Min: 1, Max: 1},
CommandVirtualStartProcess: {Min: 1, Max: 1},
CommandVirtualStop: {Min: 1, Max: 1},
CommandVirtualWindowList: {Min: 1, Max: 1},
CommandVoiceCapabilities: {Min: 1, Max: 1},
CommandVoiceDownlink: {Min: 1, Max: 1},
CommandVoiceSessionStart: {Min: 1, Max: 1},
CommandVoiceSessionStop: {Min: 1, Max: 1},
CommandWebcamList: {Min: 1, Max: 1},
CommandWebcamSelect: {Min: 1, Max: 1},
CommandWebcamSetFps: {Min: 1, Max: 1},
CommandWebcamSetQuality: {Min: 1, Max: 1},
CommandWebcamStart: {Min: 1, Max: 1},
CommandWebcamStop: {Min: 1, Max: 1},
CommandWebrtcP2pIce: {Min: 1, Max: 1},
CommandWebrtcP2pOffer: {Min: 1, Max: 1},
CommandWebrtcP2pStop: {Min: 1, Max: 1},
CommandWebrtcPublish: {Min: 1, Max: 1},
CommandWebrtcStop: {Min: 1, Max: 1},
CommandWinreInstall: {Min: 1, Max: 1},
CommandWinreProbe: {Min: 1, Max: 1},
CommandWinreUninstall: {Min: 1, Max: 1},
}
var commandTypeSet = func() map[CommandType]struct{} {
values := make(map[CommandType]struct{}, len(CommandTypes))
for _, value := range CommandTypes {
values[value] = struct{}{}
}
return values
}()
var serverToAgentMessageTypeSet = func() map[MessageType]struct{} {
values := make(map[MessageType]struct{}, len(ServerToAgentMessageTypes))
for _, value := range ServerToAgentMessageTypes {
values[value] = struct{}{}
}
return values
}()
func IsCommandType(value string) bool {
_, ok := commandTypeSet[CommandType(value)]
return ok
}
func IsSupportedCommandVersion(command string, version int) bool {
rangeValue, ok := CommandVersionSupport[CommandType(command)]
return ok && version >= rangeValue.Min && version <= rangeValue.Max
}
func SupportedCommandVersionRanges() map[string]CommandVersionRange {
values := make(map[string]CommandVersionRange, len(CommandVersionSupport))
for command, rangeValue := range CommandVersionSupport {
values[string(command)] = rangeValue
}
return values
}
func IsServerToAgentMessageType(value string) bool {
_, ok := serverToAgentMessageTypeSet[MessageType(value)]
return ok
}
@@ -0,0 +1,45 @@
package wire
import "testing"
func TestGeneratedWireContract(t *testing.T) {
if WireProtocolVersion != 1 {
t.Fatalf("unexpected wire protocol version: %d", WireProtocolVersion)
}
if len(CommandTypes) != 141 {
t.Fatalf("unexpected command count: %d", len(CommandTypes))
}
if len(CommandVersionSupport) != len(CommandTypes) {
t.Fatalf("command version catalog has %d entries, want %d", len(CommandVersionSupport), len(CommandTypes))
}
for _, command := range CommandTypes {
versionRange, ok := CommandVersionSupport[command]
if !ok {
t.Fatalf("missing version range for %s", command)
}
if versionRange.Min != 1 || versionRange.Max != 1 {
t.Fatalf("unexpected version range for %s: %+v", command, versionRange)
}
if !IsSupportedCommandVersion(string(command), 1) {
t.Fatalf("%s must support v1", command)
}
if IsSupportedCommandVersion(string(command), 2) {
t.Fatalf("%s must not support undeclared v2", command)
}
}
if !IsCommandType("desktop_start") {
t.Fatal("desktop_start must be a known command")
}
if !IsCommandType("virtual_window_list") {
t.Fatal("virtual_window_list must be a known command")
}
if IsCommandType("not_a_command") {
t.Fatal("unknown command was accepted")
}
if !IsServerToAgentMessageType("hello_ack") {
t.Fatal("hello_ack must be a known server message")
}
if IsServerToAgentMessageType("console_output") {
t.Fatal("agent-only message was accepted as a server message")
}
}
+3 -1
View File
@@ -1,6 +1,6 @@
{
"name": "overlord-server",
"version": "2.6.1",
"version": "3.0.0",
"private": true,
"type": "module",
"scripts": {
@@ -20,6 +20,8 @@
"watch:css": "tailwindcss -i ./src/styles.css -o ./public/assets/tailwind.css --watch=always",
"minify": "bun run scripts/minify-public.ts",
"bench:client-db": "bun run scripts/client-db-stress.ts",
"protocol:generate": "bun ../scripts/generate-wire-protocol.ts",
"protocol:check": "bun ../scripts/generate-wire-protocol.ts --check",
"typecheck": "tsc --noEmit"
},
"dependencies": {
@@ -0,0 +1,71 @@
import { describe, expect, test } from "bun:test";
import {
getCommandCompatibility,
getNegotiatedCommandVersions,
requireCommandVersion,
sanitizeCommandVersionRanges,
versionCommandForClient,
} from "./command-compatibility";
describe("command version negotiation", () => {
test("sanitizes advertised ranges and ignores unknown commands", () => {
expect(sanitizeCommandVersionRanges({
process_list: { min: 1, max: 3 },
desktop_start: { min: 3, max: 2 },
not_a_command: { min: 1, max: 1 },
})).toEqual({
process_list: { min: 1, max: 3 },
});
});
test("treats pre-3.0 agents as v1 for backward compatibility", () => {
expect(getCommandCompatibility({}, "process_list")).toEqual({
supported: true,
version: 1,
server: { min: 1, max: 1 },
agent: { min: 1, max: 1 },
legacyAssumption: true,
});
});
test("selects the highest shared version", () => {
const client = {
commandVersions: {
process_list: { min: 1, max: 3 },
},
};
expect(requireCommandVersion(client, "process_list")).toBe(1);
expect(getNegotiatedCommandVersions(client)).toEqual({ process_list: 1 });
expect(versionCommandForClient(client, {
type: "command",
commandType: "process_list",
id: "command-123",
})).toMatchObject({
commandType: "process_list",
commandVersion: 1,
});
});
test("strictly reports missing commands and non-overlapping versions", () => {
expect(getCommandCompatibility(
{ commandVersions: {} },
"process_list",
)).toMatchObject({
supported: false,
reason: "agent_missing_command",
});
const incompatible = {
commandVersions: {
process_list: { min: 2, max: 3 },
},
};
expect(getCommandCompatibility(incompatible, "process_list")).toMatchObject({
supported: false,
reason: "no_common_version",
});
expect(() => requireCommandVersion(incompatible, "process_list")).toThrow(
"no shared version",
);
});
});
@@ -0,0 +1,120 @@
import {
COMMAND_TYPES,
COMMAND_VERSION_SUPPORT,
isCommandType,
type CommandType,
type CommandVersionRange,
} from "./generated/wire-contract";
import type { ClientInfo } from "./types";
import type { Command } from "./protocol";
export type CommandCompatibility = {
supported: boolean;
version?: number;
server: CommandVersionRange;
agent?: CommandVersionRange;
reason?: "agent_missing_command" | "no_common_version";
legacyAssumption?: boolean;
};
function cleanPositiveInt(value: unknown): number | undefined {
if (typeof value !== "number" || !Number.isFinite(value)) return undefined;
const result = Math.floor(value);
return result > 0 && result <= 65_535 ? result : undefined;
}
export function sanitizeCommandVersionRanges(
value: unknown,
): Partial<Record<CommandType, CommandVersionRange>> | undefined {
if (!value || typeof value !== "object" || Array.isArray(value)) return undefined;
const result: Partial<Record<CommandType, CommandVersionRange>> = {};
let count = 0;
for (const [name, rawRange] of Object.entries(value as Record<string, unknown>)) {
if (count >= COMMAND_TYPES.length || !isCommandType(name)) continue;
if (!rawRange || typeof rawRange !== "object" || Array.isArray(rawRange)) continue;
const min = cleanPositiveInt((rawRange as any).min);
const max = cleanPositiveInt((rawRange as any).max);
if (min === undefined || max === undefined || min > max) continue;
result[name] = { min, max };
count += 1;
}
return result;
}
export function getCommandCompatibility(
client: Pick<ClientInfo, "protocolVersion" | "commandVersions">,
command: CommandType,
): CommandCompatibility {
const server = COMMAND_VERSION_SUPPORT[command];
const advertised = client.commandVersions;
// Pre-3.0 agents did not advertise a catalog. They remain v1 best-effort.
if (!advertised) {
const supported = server.min <= 1 && server.max >= 1;
return supported
? { supported: true, version: 1, server, agent: { min: 1, max: 1 }, legacyAssumption: true }
: { supported: false, server, agent: { min: 1, max: 1 }, reason: "no_common_version", legacyAssumption: true };
}
const agent = advertised[command];
if (!agent) {
return { supported: false, server, reason: "agent_missing_command" };
}
const min = Math.max(server.min, agent.min);
const max = Math.min(server.max, agent.max);
if (min > max) {
return { supported: false, server, agent, reason: "no_common_version" };
}
return { supported: true, version: max, server, agent };
}
export function getCommandCompatibilityCatalog(
client: Pick<ClientInfo, "protocolVersion" | "commandVersions">,
): Record<CommandType, CommandCompatibility> {
return Object.fromEntries(
COMMAND_TYPES.map((command) => [command, getCommandCompatibility(client, command)]),
) as Record<CommandType, CommandCompatibility>;
}
export function getNegotiatedCommandVersions(
client: Pick<ClientInfo, "protocolVersion" | "commandVersions">,
): Partial<Record<CommandType, number>> {
const result: Partial<Record<CommandType, number>> = {};
for (const command of COMMAND_TYPES) {
const compatibility = getCommandCompatibility(client, command);
if (compatibility.supported && compatibility.version !== undefined) {
result[command] = compatibility.version;
}
}
return result;
}
export function requireCommandVersion(
client: Pick<ClientInfo, "protocolVersion" | "commandVersions">,
command: CommandType,
): number {
const compatibility = getCommandCompatibility(client, command);
if (!compatibility.supported || compatibility.version === undefined) {
const reason = compatibility.reason === "agent_missing_command"
? "agent does not advertise this command"
: `no shared version (server ${compatibility.server.min}-${compatibility.server.max}, agent ${compatibility.agent?.min ?? "none"}-${compatibility.agent?.max ?? "none"})`;
throw new Error(`Command ${command} is unsupported: ${reason}`);
}
return compatibility.version;
}
/**
* Use this at target-aware send boundaries. It fails before transmission when
* the connected agent does not support the command and pins the negotiated
* version into the envelope.
*/
export function versionCommandForClient(
client: Pick<ClientInfo, "protocolVersion" | "commandVersions">,
command: Command,
): Command {
return {
...command,
commandVersion: requireCommandVersion(client, command.commandType),
};
}
@@ -0,0 +1,879 @@
// Code generated by scripts/generate-wire-protocol.ts; DO NOT EDIT.
export const WIRE_SCHEMA_VERSION = 1 as const;
export const WIRE_PROTOCOL_VERSION = 1 as const;
export const WIRE_MESSAGE_TYPES = [
"backstage_browser_check_result",
"backstage_browser_launch_status",
"backstage_clone_progress",
"backstage_dxgi_status",
"backstage_installed_apps_result",
"backstage_lookup_result",
"backstage_window_list_result",
"client_logs_result",
"clipboard_content",
"command",
"command_abort",
"command_progress",
"command_result",
"console_output",
"desktop_audio_uplink",
"desktop_cursor",
"desktop_encoder_capabilities",
"desktop_stream_stats",
"disconnect_info",
"enrollment_challenge",
"enrollment_status",
"file_dirsize_result",
"file_download",
"file_hash_result",
"file_icon_result",
"file_list_result",
"file_peek_result",
"file_read_result",
"file_search_result",
"file_thumb_result",
"file_upload_result",
"frame",
"frame_ack",
"hello",
"hello_ack",
"keylog_clear_result",
"keylog_delete_result",
"keylog_file_content",
"keylog_file_list",
"keylog_permission_result",
"notification",
"notification_config",
"ping",
"plugin_event",
"pong",
"process_icon_result",
"process_list_result",
"proxy_close",
"proxy_data",
"screenshot_result",
"script_result",
"status",
"voice_uplink",
"webcam_devices",
"webrtc_p2p_answer",
"webrtc_p2p_ice"
] as const;
export type MessageKind = (typeof WIRE_MESSAGE_TYPES)[number];
export const SERVER_TO_AGENT_MESSAGE_TYPES = [
"command",
"command_abort",
"enrollment_challenge",
"enrollment_status",
"frame_ack",
"hello_ack",
"notification_config",
"ping",
"plugin_event",
"pong"
] as const;
export type ServerToAgentMessageType = (typeof SERVER_TO_AGENT_MESSAGE_TYPES)[number];
export const AGENT_TO_SERVER_MESSAGE_TYPES = [
"backstage_browser_check_result",
"backstage_browser_launch_status",
"backstage_clone_progress",
"backstage_dxgi_status",
"backstage_installed_apps_result",
"backstage_lookup_result",
"backstage_window_list_result",
"client_logs_result",
"clipboard_content",
"command_progress",
"command_result",
"console_output",
"desktop_audio_uplink",
"desktop_cursor",
"desktop_encoder_capabilities",
"desktop_stream_stats",
"disconnect_info",
"file_dirsize_result",
"file_download",
"file_hash_result",
"file_icon_result",
"file_list_result",
"file_peek_result",
"file_read_result",
"file_search_result",
"file_thumb_result",
"file_upload_result",
"frame",
"hello",
"keylog_clear_result",
"keylog_delete_result",
"keylog_file_content",
"keylog_file_list",
"keylog_permission_result",
"notification",
"ping",
"plugin_event",
"pong",
"process_icon_result",
"process_list_result",
"proxy_close",
"proxy_data",
"screenshot_result",
"script_result",
"status",
"voice_uplink",
"webcam_devices",
"webrtc_p2p_answer",
"webrtc_p2p_ice"
] as const;
export type AgentToServerMessageType = (typeof AGENT_TO_SERVER_MESSAGE_TYPES)[number];
export const COMMAND_TYPES = [
"agent_update",
"backstage_browser_check",
"backstage_enable_cursor",
"backstage_enable_dxgi",
"backstage_enable_keyboard",
"backstage_enable_mouse",
"backstage_enable_uia",
"backstage_installed_apps",
"backstage_key_down",
"backstage_key_up",
"backstage_kill_all",
"backstage_lookup",
"backstage_mouse_down",
"backstage_mouse_move",
"backstage_mouse_up",
"backstage_mouse_wheel",
"backstage_request_keyframe",
"backstage_select_display",
"backstage_set_fps",
"backstage_set_quality",
"backstage_set_resolution",
"backstage_start",
"backstage_start_browser_injected",
"backstage_start_chrome_injected",
"backstage_start_process",
"backstage_start_process_injected",
"backstage_stop",
"backstage_window_list",
"client_logs_request",
"clipboard_set",
"clipboard_sync_start",
"clipboard_sync_stop",
"console_input",
"console_resize",
"console_start",
"console_stop",
"darwin_request_permissions",
"desktop_audio_start",
"desktop_audio_stop",
"desktop_enable_cursor",
"desktop_enable_keyboard",
"desktop_enable_mouse",
"desktop_encoder_capabilities",
"desktop_key_down",
"desktop_key_up",
"desktop_mouse_down",
"desktop_mouse_move",
"desktop_mouse_up",
"desktop_mouse_wheel",
"desktop_request_keyframe",
"desktop_select_display",
"desktop_set_bitrate",
"desktop_set_duplication",
"desktop_set_fps",
"desktop_set_profile",
"desktop_set_quality",
"desktop_set_resolution",
"desktop_start",
"desktop_stop",
"desktop_text",
"disconnect",
"elevate",
"file_chmod",
"file_copy",
"file_delete",
"file_dirsize",
"file_download",
"file_execute",
"file_hash",
"file_icon",
"file_list",
"file_mkdir",
"file_move",
"file_peek",
"file_read",
"file_request_access",
"file_search",
"file_thumb",
"file_upload",
"file_upload_http",
"file_write",
"file_zip",
"keylog_clear_all",
"keylog_delete",
"keylog_list",
"keylog_request_permission",
"keylog_retrieve",
"plugin_load",
"plugin_load_http",
"plugin_unload",
"privacy_start",
"privacy_status",
"privacy_stop",
"process_icon",
"process_kill",
"process_list",
"process_resume",
"process_suspend",
"proxy_close",
"proxy_connect",
"proxy_data",
"reconnect",
"screenshot",
"script_exec",
"silent_exec",
"uninstall",
"virtual_enable_cursor",
"virtual_enable_keyboard",
"virtual_enable_mouse",
"virtual_key_down",
"virtual_key_up",
"virtual_kill_all",
"virtual_mouse_down",
"virtual_mouse_move",
"virtual_mouse_up",
"virtual_mouse_wheel",
"virtual_request_keyframe",
"virtual_set_fps",
"virtual_set_quality",
"virtual_start",
"virtual_start_process",
"virtual_stop",
"virtual_window_list",
"voice_capabilities",
"voice_downlink",
"voice_session_start",
"voice_session_stop",
"webcam_list",
"webcam_select",
"webcam_set_fps",
"webcam_set_quality",
"webcam_start",
"webcam_stop",
"webrtc_p2p_ice",
"webrtc_p2p_offer",
"webrtc_p2p_stop",
"webrtc_publish",
"webrtc_stop",
"winre_install",
"winre_probe",
"winre_uninstall"
] as const;
export type CommandType = (typeof COMMAND_TYPES)[number];
export type CommandVersionRange = Readonly<{ min: number; max: number }>;
export const COMMAND_VERSION_SUPPORT = {
"agent_update": {
"min": 1,
"max": 1
},
"backstage_browser_check": {
"min": 1,
"max": 1
},
"backstage_enable_cursor": {
"min": 1,
"max": 1
},
"backstage_enable_dxgi": {
"min": 1,
"max": 1
},
"backstage_enable_keyboard": {
"min": 1,
"max": 1
},
"backstage_enable_mouse": {
"min": 1,
"max": 1
},
"backstage_enable_uia": {
"min": 1,
"max": 1
},
"backstage_installed_apps": {
"min": 1,
"max": 1
},
"backstage_key_down": {
"min": 1,
"max": 1
},
"backstage_key_up": {
"min": 1,
"max": 1
},
"backstage_kill_all": {
"min": 1,
"max": 1
},
"backstage_lookup": {
"min": 1,
"max": 1
},
"backstage_mouse_down": {
"min": 1,
"max": 1
},
"backstage_mouse_move": {
"min": 1,
"max": 1
},
"backstage_mouse_up": {
"min": 1,
"max": 1
},
"backstage_mouse_wheel": {
"min": 1,
"max": 1
},
"backstage_request_keyframe": {
"min": 1,
"max": 1
},
"backstage_select_display": {
"min": 1,
"max": 1
},
"backstage_set_fps": {
"min": 1,
"max": 1
},
"backstage_set_quality": {
"min": 1,
"max": 1
},
"backstage_set_resolution": {
"min": 1,
"max": 1
},
"backstage_start": {
"min": 1,
"max": 1
},
"backstage_start_browser_injected": {
"min": 1,
"max": 1
},
"backstage_start_chrome_injected": {
"min": 1,
"max": 1
},
"backstage_start_process": {
"min": 1,
"max": 1
},
"backstage_start_process_injected": {
"min": 1,
"max": 1
},
"backstage_stop": {
"min": 1,
"max": 1
},
"backstage_window_list": {
"min": 1,
"max": 1
},
"client_logs_request": {
"min": 1,
"max": 1
},
"clipboard_set": {
"min": 1,
"max": 1
},
"clipboard_sync_start": {
"min": 1,
"max": 1
},
"clipboard_sync_stop": {
"min": 1,
"max": 1
},
"console_input": {
"min": 1,
"max": 1
},
"console_resize": {
"min": 1,
"max": 1
},
"console_start": {
"min": 1,
"max": 1
},
"console_stop": {
"min": 1,
"max": 1
},
"darwin_request_permissions": {
"min": 1,
"max": 1
},
"desktop_audio_start": {
"min": 1,
"max": 1
},
"desktop_audio_stop": {
"min": 1,
"max": 1
},
"desktop_enable_cursor": {
"min": 1,
"max": 1
},
"desktop_enable_keyboard": {
"min": 1,
"max": 1
},
"desktop_enable_mouse": {
"min": 1,
"max": 1
},
"desktop_encoder_capabilities": {
"min": 1,
"max": 1
},
"desktop_key_down": {
"min": 1,
"max": 1
},
"desktop_key_up": {
"min": 1,
"max": 1
},
"desktop_mouse_down": {
"min": 1,
"max": 1
},
"desktop_mouse_move": {
"min": 1,
"max": 1
},
"desktop_mouse_up": {
"min": 1,
"max": 1
},
"desktop_mouse_wheel": {
"min": 1,
"max": 1
},
"desktop_request_keyframe": {
"min": 1,
"max": 1
},
"desktop_select_display": {
"min": 1,
"max": 1
},
"desktop_set_bitrate": {
"min": 1,
"max": 1
},
"desktop_set_duplication": {
"min": 1,
"max": 1
},
"desktop_set_fps": {
"min": 1,
"max": 1
},
"desktop_set_profile": {
"min": 1,
"max": 1
},
"desktop_set_quality": {
"min": 1,
"max": 1
},
"desktop_set_resolution": {
"min": 1,
"max": 1
},
"desktop_start": {
"min": 1,
"max": 1
},
"desktop_stop": {
"min": 1,
"max": 1
},
"desktop_text": {
"min": 1,
"max": 1
},
"disconnect": {
"min": 1,
"max": 1
},
"elevate": {
"min": 1,
"max": 1
},
"file_chmod": {
"min": 1,
"max": 1
},
"file_copy": {
"min": 1,
"max": 1
},
"file_delete": {
"min": 1,
"max": 1
},
"file_dirsize": {
"min": 1,
"max": 1
},
"file_download": {
"min": 1,
"max": 1
},
"file_execute": {
"min": 1,
"max": 1
},
"file_hash": {
"min": 1,
"max": 1
},
"file_icon": {
"min": 1,
"max": 1
},
"file_list": {
"min": 1,
"max": 1
},
"file_mkdir": {
"min": 1,
"max": 1
},
"file_move": {
"min": 1,
"max": 1
},
"file_peek": {
"min": 1,
"max": 1
},
"file_read": {
"min": 1,
"max": 1
},
"file_request_access": {
"min": 1,
"max": 1
},
"file_search": {
"min": 1,
"max": 1
},
"file_thumb": {
"min": 1,
"max": 1
},
"file_upload": {
"min": 1,
"max": 1
},
"file_upload_http": {
"min": 1,
"max": 1
},
"file_write": {
"min": 1,
"max": 1
},
"file_zip": {
"min": 1,
"max": 1
},
"keylog_clear_all": {
"min": 1,
"max": 1
},
"keylog_delete": {
"min": 1,
"max": 1
},
"keylog_list": {
"min": 1,
"max": 1
},
"keylog_request_permission": {
"min": 1,
"max": 1
},
"keylog_retrieve": {
"min": 1,
"max": 1
},
"plugin_load": {
"min": 1,
"max": 1
},
"plugin_load_http": {
"min": 1,
"max": 1
},
"plugin_unload": {
"min": 1,
"max": 1
},
"privacy_start": {
"min": 1,
"max": 1
},
"privacy_status": {
"min": 1,
"max": 1
},
"privacy_stop": {
"min": 1,
"max": 1
},
"process_icon": {
"min": 1,
"max": 1
},
"process_kill": {
"min": 1,
"max": 1
},
"process_list": {
"min": 1,
"max": 1
},
"process_resume": {
"min": 1,
"max": 1
},
"process_suspend": {
"min": 1,
"max": 1
},
"proxy_close": {
"min": 1,
"max": 1
},
"proxy_connect": {
"min": 1,
"max": 1
},
"proxy_data": {
"min": 1,
"max": 1
},
"reconnect": {
"min": 1,
"max": 1
},
"screenshot": {
"min": 1,
"max": 1
},
"script_exec": {
"min": 1,
"max": 1
},
"silent_exec": {
"min": 1,
"max": 1
},
"uninstall": {
"min": 1,
"max": 1
},
"virtual_enable_cursor": {
"min": 1,
"max": 1
},
"virtual_enable_keyboard": {
"min": 1,
"max": 1
},
"virtual_enable_mouse": {
"min": 1,
"max": 1
},
"virtual_key_down": {
"min": 1,
"max": 1
},
"virtual_key_up": {
"min": 1,
"max": 1
},
"virtual_kill_all": {
"min": 1,
"max": 1
},
"virtual_mouse_down": {
"min": 1,
"max": 1
},
"virtual_mouse_move": {
"min": 1,
"max": 1
},
"virtual_mouse_up": {
"min": 1,
"max": 1
},
"virtual_mouse_wheel": {
"min": 1,
"max": 1
},
"virtual_request_keyframe": {
"min": 1,
"max": 1
},
"virtual_set_fps": {
"min": 1,
"max": 1
},
"virtual_set_quality": {
"min": 1,
"max": 1
},
"virtual_start": {
"min": 1,
"max": 1
},
"virtual_start_process": {
"min": 1,
"max": 1
},
"virtual_stop": {
"min": 1,
"max": 1
},
"virtual_window_list": {
"min": 1,
"max": 1
},
"voice_capabilities": {
"min": 1,
"max": 1
},
"voice_downlink": {
"min": 1,
"max": 1
},
"voice_session_start": {
"min": 1,
"max": 1
},
"voice_session_stop": {
"min": 1,
"max": 1
},
"webcam_list": {
"min": 1,
"max": 1
},
"webcam_select": {
"min": 1,
"max": 1
},
"webcam_set_fps": {
"min": 1,
"max": 1
},
"webcam_set_quality": {
"min": 1,
"max": 1
},
"webcam_start": {
"min": 1,
"max": 1
},
"webcam_stop": {
"min": 1,
"max": 1
},
"webrtc_p2p_ice": {
"min": 1,
"max": 1
},
"webrtc_p2p_offer": {
"min": 1,
"max": 1
},
"webrtc_p2p_stop": {
"min": 1,
"max": 1
},
"webrtc_publish": {
"min": 1,
"max": 1
},
"webrtc_stop": {
"min": 1,
"max": 1
},
"winre_install": {
"min": 1,
"max": 1
},
"winre_probe": {
"min": 1,
"max": 1
},
"winre_uninstall": {
"min": 1,
"max": 1
}
} as const satisfies
Record<CommandType, CommandVersionRange>;
const commandTypeSet: ReadonlySet<string> = new Set(COMMAND_TYPES);
const agentMessageTypeSet: ReadonlySet<string> = new Set(AGENT_TO_SERVER_MESSAGE_TYPES);
export function isCommandType(value: string): value is CommandType {
return commandTypeSet.has(value);
}
export function getCommandVersionRange(command: CommandType): CommandVersionRange {
return COMMAND_VERSION_SUPPORT[command];
}
export function isSupportedCommandVersion(command: CommandType, version: number): boolean {
const range = getCommandVersionRange(command);
return Number.isInteger(version) && version >= range.min && version <= range.max;
}
export function getImplicitCommandVersion(command: CommandType): number {
const range = getCommandVersionRange(command);
if (range.min !== range.max) {
throw new Error(
`Command ${command} supports versions ${range.min}-${range.max}; negotiate an explicit commandVersion`,
);
}
return range.min;
}
export function isAgentToServerMessageType(value: string): value is AgentToServerMessageType {
return agentMessageTypeSet.has(value);
}
@@ -0,0 +1,45 @@
import { describe, expect, test } from "bun:test";
import {
AGENT_TO_SERVER_MESSAGE_TYPES,
COMMAND_TYPES,
COMMAND_VERSION_SUPPORT,
WIRE_PROTOCOL_VERSION,
getImplicitCommandVersion,
isAgentToServerMessageType,
isCommandType,
isSupportedCommandVersion,
} from "./generated/wire-contract";
import {
ALLOWED_CLIENT_MESSAGE_TYPES,
isAllowedClientMessageType,
} from "./wsValidation";
describe("generated wire protocol contract", () => {
test("publishes the complete command catalog", () => {
expect(WIRE_PROTOCOL_VERSION).toBe(1);
expect(COMMAND_TYPES.length).toBe(141);
expect(new Set(COMMAND_TYPES).size).toBe(COMMAND_TYPES.length);
expect(isCommandType("desktop_start")).toBe(true);
expect(isCommandType("virtual_window_list")).toBe(true);
expect(isCommandType("not_a_command")).toBe(false);
});
test("publishes a version range for every command", () => {
expect(Object.keys(COMMAND_VERSION_SUPPORT).length).toBe(COMMAND_TYPES.length);
for (const command of COMMAND_TYPES) {
expect(COMMAND_VERSION_SUPPORT[command]).toEqual({ min: 1, max: 1 });
expect(isSupportedCommandVersion(command, 1)).toBe(true);
expect(isSupportedCommandVersion(command, 2)).toBe(false);
expect(getImplicitCommandVersion(command)).toBe(1);
}
});
test("drives the inbound client allowlist", () => {
expect([...ALLOWED_CLIENT_MESSAGE_TYPES].sort()).toEqual(
[...AGENT_TO_SERVER_MESSAGE_TYPES].sort(),
);
expect(isAgentToServerMessageType("hello")).toBe(true);
expect(isAllowedClientMessageType("hello")).toBe(true);
expect(isAllowedClientMessageType("hello_ack")).toBe(false);
});
});
+12
View File
@@ -5,6 +5,7 @@ import {
type Frame,
type Hello,
type Ping,
type Command,
} from "./protocol";
const sampleHello: Hello = {
@@ -39,6 +40,17 @@ describe("protocol encode/decode", () => {
expect(decoded.ts).toBe(123);
});
test("adds the unambiguous v1 command version to legacy call sites", () => {
const command: Command = {
type: "command",
commandType: "process_list",
id: "command-123",
};
const decoded = decodeMessage(encodeMessage(command)) as Command;
expect(decoded.commandVersion).toBe(1);
});
test("round trips h264 frame metadata", () => {
const frame: Frame = {
type: "frame",
+29 -93
View File
@@ -1,25 +1,16 @@
import { encode, decode } from "@msgpack/msgpack";
export type MessageKind =
| "hello"
| "hello_ack"
| "ping"
| "pong"
| "command"
| "command_result"
| "desktop_encoder_capabilities"
| "desktop_stream_stats"
| "desktop_cursor"
| "client_logs_result"
| "screenshot_result"
| "frame"
| "status"
| "plugin_event"
| "notification"
| "webcam_devices"
| "notification_config"
| "enrollment_challenge"
| "enrollment_status";
import {
getImplicitCommandVersion,
isCommandType,
type CommandType,
type CommandVersionRange,
type MessageKind,
} from "./generated/wire-contract";
export type {
CommandType,
CommandVersionRange,
MessageKind,
} from "./generated/wire-contract";
export type Hello = {
type: "hello";
@@ -48,6 +39,8 @@ export type Hello = {
publicIP?: string;
lastCrashReason?: string;
lastCrashDetail?: string;
protocolVersion?: number;
commandVersions?: Partial<Record<CommandType, CommandVersionRange>>;
};
export type EnrollmentChallenge = {
@@ -63,6 +56,8 @@ export type EnrollmentStatusMsg = {
export type HelloAck = {
type: "hello_ack";
id: string;
protocolVersion?: number;
commandVersions?: Partial<Record<CommandType, number>>;
commands?: Command[];
notification?: {
keywords: string[];
@@ -73,81 +68,10 @@ export type HelloAck = {
export type Ping = { type: "ping"; ts?: number };
export type Pong = { type: "pong"; ts?: number };
export type CommandType =
| "input"
| "remote_start"
| "remote_stop"
| "webcam_start"
| "webcam_stop"
| "webcam_list"
| "webcam_select"
| "webcam_set_fps"
| "disconnect"
| "reconnect"
| "screenshot"
| "ping"
| "console_start"
| "console_input"
| "console_stop"
| "console_resize"
| "file_list"
| "file_request_access"
| "file_download"
| "file_upload"
| "file_delete"
| "file_mkdir"
| "file_zip"
| "file_read"
| "file_write"
| "file_search"
| "file_copy"
| "file_move"
| "file_chmod"
| "file_execute"
| "silent_exec"
| "voice_session_start"
| "voice_session_stop"
| "voice_downlink"
| "voice_capabilities"
| "client_logs_request"
| "desktop_audio_start"
| "desktop_audio_stop"
| "process_list"
| "process_kill"
| "plugin_load"
| "plugin_load_http"
| "plugin_unload"
| "agent_update"
| "clipboard_set"
| "clipboard_sync_start"
| "clipboard_sync_stop"
| "desktop_start"
| "desktop_stop"
| "desktop_select_display"
| "desktop_enable_mouse"
| "desktop_enable_keyboard"
| "desktop_set_fps"
| "desktop_set_bitrate"
| "darwin_request_permissions"
| "webrtc_publish"
| "webrtc_stop"
| "webrtc_p2p_offer"
| "webrtc_p2p_ice"
| "webrtc_p2p_stop"
| "script_exec"
| "uninstall"
| "elevate"
| "file_upload_http"
| "winre_probe"
| "winre_install"
| "winre_uninstall"
| "file_icon"
| "file_thumb"
| "file_dirsize";
export type Command = {
type: "command";
commandType: CommandType;
commandVersion?: number;
payload?: unknown;
id?: string;
};
@@ -155,8 +79,12 @@ export type Command = {
export type CommandResult = {
type: "command_result";
commandId?: string;
commandType?: CommandType;
commandVersion?: number;
ok: boolean;
message?: string;
errorCode?: "unsupported_command_version" | string;
supportedVersions?: CommandVersionRange;
};
export type DesktopCodecCapability = {
@@ -579,6 +507,14 @@ export type WireMessage =
| EnrollmentStatusMsg;
export function encodeMessage(msg: WireMessage): Uint8Array {
if (msg.type === "command") {
if (!isCommandType(msg.commandType)) {
throw new Error(`Unknown command type: ${String(msg.commandType)}`);
}
const commandVersion =
msg.commandVersion ?? getImplicitCommandVersion(msg.commandType);
return encode({ ...msg, commandVersion });
}
return encode(msg);
}
@@ -36,6 +36,7 @@ import { clearThumbnail } from "../../thumbnails";
import { handleClientCommandRoute } from "./client-command-routes";
import { handleClientGroupRoutes } from "./client-group-routes";
import { decryptClientLogBlob, extractSecureLogBlobs } from "../client-log-crypto";
import { getCommandCompatibilityCatalog } from "../../command-compatibility";
type RequestIpProvider = {
requestIP: (req: Request) => { address?: string } | null | undefined;
@@ -68,7 +69,15 @@ function mergeLiveClientState(result: any) {
items: (result.items || []).map((item: any) => {
const live = clientManager.getClient(item.id);
return live
? { ...item, online: true, lastSeen: live.lastSeen, pingMs: live.pingMs ?? item.pingMs }
? {
...item,
online: true,
lastSeen: live.lastSeen,
pingMs: live.pingMs ?? item.pingMs,
protocolVersion: live.protocolVersion ?? 1,
commandVersions: live.commandVersions,
commandCompatibility: getCommandCompatibilityCatalog(live),
}
: item;
}),
};
@@ -14,6 +14,8 @@ import { getConfig } from "../../config";
import { logger } from "../../logger";
import { metrics } from "../../metrics";
import { decodeMessage, encodeMessage, type WireMessage, type Hello, type Ping } from "../../protocol";
import { WIRE_PROTOCOL_VERSION } from "../../generated/wire-contract";
import { getNegotiatedCommandVersions } from "../../command-compatibility";
import * as sessionManager from "../../sessions/sessionManager";
import type { SocketData } from "../../sessions/types";
import type { ClientInfo } from "../../types";
@@ -705,6 +707,8 @@ export async function handleWebSocketMessage(
encodeMessage({
type: "hello_ack",
id: resolvedId,
protocolVersion: WIRE_PROTOCOL_VERSION,
commandVersions: getNegotiatedCommandVersions(infoObj),
notification: {
keywords: notificationConfig.keywords || [],
minIntervalMs: notificationConfig.minIntervalMs || 8000,
+5
View File
@@ -55,6 +55,11 @@ export type ClientInfo = {
groupName?: string | null;
groupColor?: string | null;
notificationsMuted?: boolean;
protocolVersion?: number;
commandVersions?: Partial<Record<
import("./generated/wire-contract").CommandType,
import("./generated/wire-contract").CommandVersionRange
>>;
};
export type ListFilters = {
+1 -1
View File
@@ -1 +1 @@
export const SERVER_VERSION = "2.6.1";
export const SERVER_VERSION = "3.0.0";
+6
View File
@@ -1,4 +1,5 @@
import { encodeMessage, type Hello, type Ping, type WireMessage } from "./protocol";
import { sanitizeCommandVersionRanges } from "./command-compatibility";
let _geoip: typeof import("geoip-lite");
async function getGeoip() {
@@ -86,6 +87,11 @@ export async function handleHello(
info.arch = sanitizeInfoString(payload.arch, 32);
info.hostArch = sanitizeInfoString((payload as any).hostArch, 32) || info.arch;
info.version = sanitizeInfoString(payload.version, 64);
const rawProtocolVersion = Number((payload as any).protocolVersion);
info.protocolVersion = Number.isInteger(rawProtocolVersion) && rawProtocolVersion > 0
? Math.min(rawProtocolVersion, 65_535)
: 1;
info.commandVersions = sanitizeCommandVersionRanges((payload as any).commandVersions);
info.user = sanitizeInfoString(payload.user);
info.monitors = sanitizeMonitorCount(payload.monitors) ?? info.monitors;
const cleanMonitorInfo = sanitizeJsonField((payload as any).monitorInfo, {
+4 -51
View File
@@ -1,58 +1,11 @@
import { AGENT_TO_SERVER_MESSAGE_TYPES } from "./generated/wire-contract";
export type SocketRole = "client" | "viewer" | "console_viewer" | "rd_viewer" | "webcam_viewer" | "backstage_viewer" | "file_browser_viewer" | "process_viewer" | "keylogger_viewer" | "voice_viewer" | "desktop_audio_viewer" | "notifications_viewer";
const textEncoder = new TextEncoder();
export const ALLOWED_CLIENT_MESSAGE_TYPES = new Set([
"hello",
"ping",
"pong",
"frame",
"status",
"console_output",
"file_list_result",
"file_download",
"file_upload_result",
"file_read_result",
"file_search_result",
"file_icon_result",
"file_thumb_result",
"file_dirsize_result",
"file_peek_result",
"file_hash_result",
"command_result",
"desktop_encoder_capabilities",
"desktop_stream_stats",
"desktop_cursor",
"client_logs_result",
"screenshot_result",
"command_progress",
"process_list_result",
"process_icon_result",
"script_result",
"plugin_event",
"notification",
"keylog_file_list",
"keylog_file_content",
"keylog_clear_result",
"keylog_delete_result",
"keylog_permission_result",
"voice_uplink",
"desktop_audio_uplink",
"webcam_devices",
"backstage_clone_progress",
"backstage_lookup_result",
"backstage_browser_check_result",
"backstage_installed_apps_result",
"backstage_dxgi_status",
"backstage_browser_launch_status",
"backstage_window_list_result",
"clipboard_content",
"proxy_data",
"proxy_close",
"disconnect_info",
"webrtc_p2p_answer",
"webrtc_p2p_ice",
]);
export const ALLOWED_CLIENT_MESSAGE_TYPES: ReadonlySet<string> =
new Set(AGENT_TO_SERVER_MESSAGE_TYPES);
export function isAllowedClientMessageType(type: string): boolean {
return ALLOWED_CLIENT_MESSAGE_TYPES.has(type);
+71
View File
@@ -0,0 +1,71 @@
# Wire protocol contract
`wire-contract.json` is the canonical catalog for the MessagePack protocol
shared by the server and agent. Generated TypeScript and Go artifacts must not
be edited directly.
Generate artifacts from the repository root:
```text
bun scripts/generate-wire-protocol.ts
```
Verify that checked-in artifacts match the contract:
```text
bun scripts/generate-wire-protocol.ts --check
```
Protocol version 1 follows additive compatibility:
- Existing message and field meanings may not change.
- New optional fields, message types, and commands may be added.
- Fields may not become required for peers that advertise an older or absent
protocol version.
- Removing or redefining a wire value requires a new protocol version.
Agents that predate protocol negotiation omit `protocolVersion`; the server
treats them as protocol version 1.
## Command versions
Every command supports version 1 by default. A breaking change must be added
under `commandVersioning.overrides`; an existing command version must never be
edited to introduce a breaking payload or semantic change.
An override declares the latest contiguous version and documents every
breaking transition:
```json
{
"desktop_start": {
"latestVersion": 2,
"changes": {
"2": {
"breaking": true,
"summary": "Replace numeric quality with a named profile",
"migration": "Map quality 1-40 to low, 41-80 to balanced, and 81-100 to high"
}
}
}
}
```
Rules:
- Adding an optional field with an unchanged meaning does not require a bump.
- Removing or renaming a field requires a new command version.
- Changing a field type, units, default, valid range, or meaning requires a
new command version.
- Changing result semantics requires a new command version.
- Versions are contiguous and old handlers remain available while supported.
- A new version requires migration text and compatibility fixtures.
- A command with more than one version may not be encoded implicitly; the
server must select a mutually supported version for the connected agent.
At target-aware server boundaries, construct envelopes with
`versionCommandForClient`. It selects the highest shared version and throws
before transmission when the agent omitted the command or has no overlapping
range. Generic legacy send sites remain safe while all declared commands have
exactly one version; the encoder refuses implicit encoding as soon as a command
becomes multi-version.
+217
View File
@@ -0,0 +1,217 @@
{
"$schema": "./wire-contract.schema.json",
"schemaVersion": 1,
"protocolVersion": 1,
"encoding": "messagepack",
"compatibility": "additive",
"commandVersioning": {
"defaultVersion": 1,
"overrides": {}
},
"serverToAgentMessageTypes": [
"enrollment_challenge",
"enrollment_status",
"hello_ack",
"ping",
"pong",
"command",
"command_abort",
"frame_ack",
"plugin_event",
"notification_config"
],
"agentToServerMessageTypes": [
"hello",
"ping",
"pong",
"frame",
"status",
"console_output",
"file_list_result",
"file_download",
"file_upload_result",
"file_read_result",
"file_search_result",
"file_icon_result",
"file_thumb_result",
"file_dirsize_result",
"file_peek_result",
"file_hash_result",
"command_result",
"desktop_encoder_capabilities",
"desktop_stream_stats",
"desktop_cursor",
"client_logs_result",
"screenshot_result",
"command_progress",
"process_list_result",
"process_icon_result",
"script_result",
"plugin_event",
"notification",
"keylog_file_list",
"keylog_file_content",
"keylog_clear_result",
"keylog_delete_result",
"keylog_permission_result",
"voice_uplink",
"desktop_audio_uplink",
"webcam_devices",
"backstage_clone_progress",
"backstage_lookup_result",
"backstage_browser_check_result",
"backstage_installed_apps_result",
"backstage_dxgi_status",
"backstage_browser_launch_status",
"backstage_window_list_result",
"clipboard_content",
"proxy_data",
"proxy_close",
"disconnect_info",
"webrtc_p2p_answer",
"webrtc_p2p_ice"
],
"commands": [
"agent_update",
"backstage_browser_check",
"backstage_enable_cursor",
"backstage_enable_dxgi",
"backstage_enable_keyboard",
"backstage_enable_mouse",
"backstage_enable_uia",
"backstage_installed_apps",
"backstage_key_down",
"backstage_key_up",
"backstage_kill_all",
"backstage_lookup",
"backstage_mouse_down",
"backstage_mouse_move",
"backstage_mouse_up",
"backstage_mouse_wheel",
"backstage_request_keyframe",
"backstage_select_display",
"backstage_set_fps",
"backstage_set_quality",
"backstage_set_resolution",
"backstage_start",
"backstage_start_browser_injected",
"backstage_start_chrome_injected",
"backstage_start_process",
"backstage_start_process_injected",
"backstage_stop",
"backstage_window_list",
"client_logs_request",
"clipboard_set",
"clipboard_sync_start",
"clipboard_sync_stop",
"console_input",
"console_resize",
"console_start",
"console_stop",
"darwin_request_permissions",
"desktop_audio_start",
"desktop_audio_stop",
"desktop_enable_cursor",
"desktop_enable_keyboard",
"desktop_enable_mouse",
"desktop_encoder_capabilities",
"desktop_key_down",
"desktop_key_up",
"desktop_mouse_down",
"desktop_mouse_move",
"desktop_mouse_up",
"desktop_mouse_wheel",
"desktop_request_keyframe",
"desktop_select_display",
"desktop_set_bitrate",
"desktop_set_duplication",
"desktop_set_fps",
"desktop_set_profile",
"desktop_set_quality",
"desktop_set_resolution",
"desktop_start",
"desktop_stop",
"desktop_text",
"disconnect",
"elevate",
"file_chmod",
"file_copy",
"file_delete",
"file_dirsize",
"file_download",
"file_execute",
"file_hash",
"file_icon",
"file_list",
"file_mkdir",
"file_move",
"file_peek",
"file_read",
"file_request_access",
"file_search",
"file_thumb",
"file_upload",
"file_upload_http",
"file_write",
"file_zip",
"keylog_clear_all",
"keylog_delete",
"keylog_list",
"keylog_request_permission",
"keylog_retrieve",
"plugin_load",
"plugin_load_http",
"plugin_unload",
"privacy_start",
"privacy_status",
"privacy_stop",
"process_icon",
"process_kill",
"process_list",
"process_resume",
"process_suspend",
"proxy_close",
"proxy_connect",
"proxy_data",
"reconnect",
"screenshot",
"script_exec",
"silent_exec",
"uninstall",
"virtual_enable_cursor",
"virtual_enable_keyboard",
"virtual_enable_mouse",
"virtual_key_down",
"virtual_key_up",
"virtual_kill_all",
"virtual_mouse_down",
"virtual_mouse_move",
"virtual_mouse_up",
"virtual_mouse_wheel",
"virtual_request_keyframe",
"virtual_set_fps",
"virtual_set_quality",
"virtual_start",
"virtual_start_process",
"virtual_stop",
"virtual_window_list",
"voice_capabilities",
"voice_downlink",
"voice_session_start",
"voice_session_stop",
"webcam_list",
"webcam_select",
"webcam_set_fps",
"webcam_set_quality",
"webcam_start",
"webcam_stop",
"webrtc_p2p_ice",
"webrtc_p2p_offer",
"webrtc_p2p_stop",
"webrtc_publish",
"webrtc_stop",
"winre_install",
"winre_probe",
"winre_uninstall"
]
}
+81
View File
@@ -0,0 +1,81 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://overlord.local/schemas/wire-contract.schema.json",
"title": "Overlord wire contract",
"type": "object",
"additionalProperties": false,
"required": [
"schemaVersion",
"protocolVersion",
"encoding",
"compatibility",
"commandVersioning",
"serverToAgentMessageTypes",
"agentToServerMessageTypes",
"commands"
],
"properties": {
"$schema": { "type": "string" },
"schemaVersion": { "type": "integer", "minimum": 1 },
"protocolVersion": { "type": "integer", "minimum": 1 },
"encoding": { "const": "messagepack" },
"compatibility": { "enum": ["additive"] },
"commandVersioning": {
"type": "object",
"additionalProperties": false,
"required": ["defaultVersion", "overrides"],
"properties": {
"defaultVersion": { "const": 1 },
"overrides": {
"type": "object",
"propertyNames": { "$ref": "#/$defs/wireName" },
"additionalProperties": {
"type": "object",
"additionalProperties": false,
"required": ["latestVersion", "changes"],
"properties": {
"latestVersion": { "type": "integer", "minimum": 2 },
"changes": {
"type": "object",
"patternProperties": {
"^[2-9][0-9]*$": {
"type": "object",
"additionalProperties": false,
"required": ["breaking", "summary", "migration"],
"properties": {
"breaking": { "const": true },
"summary": { "type": "string", "minLength": 1 },
"migration": { "type": "string", "minLength": 1 }
}
}
},
"additionalProperties": false
}
}
}
}
}
},
"serverToAgentMessageTypes": {
"type": "array",
"uniqueItems": true,
"items": { "$ref": "#/$defs/wireName" }
},
"agentToServerMessageTypes": {
"type": "array",
"uniqueItems": true,
"items": { "$ref": "#/$defs/wireName" }
},
"commands": {
"type": "array",
"uniqueItems": true,
"items": { "$ref": "#/$defs/wireName" }
}
},
"$defs": {
"wireName": {
"type": "string",
"pattern": "^[a-z][a-z0-9]*(?:_[a-z0-9]+)*$"
}
}
}
+291
View File
@@ -0,0 +1,291 @@
import { dirname, resolve } from "node:path";
import { mkdir } from "node:fs/promises";
type Contract = {
schemaVersion: number;
protocolVersion: number;
encoding: "messagepack";
compatibility: "additive";
commandVersioning: {
defaultVersion: 1;
overrides: Record<string, {
latestVersion: number;
changes: Record<string, {
breaking: true;
summary: string;
migration: string;
}>;
}>;
};
serverToAgentMessageTypes: string[];
agentToServerMessageTypes: string[];
commands: string[];
};
const root = resolve(import.meta.dir, "..");
const contractPath = resolve(root, "protocol", "wire-contract.json");
const tsOutputPath = resolve(root, "Overlord-Server", "src", "generated", "wire-contract.ts");
const goOutputPath = resolve(root, "Overlord-Client", "cmd", "agent", "wire", "wire_contract_generated.go");
const checkOnly = process.argv.includes("--check");
const contract = await Bun.file(contractPath).json() as Contract;
function validateNames(label: string, values: string[]): void {
const pattern = /^[a-z][a-z0-9]*(?:_[a-z0-9]+)*$/;
const seen = new Set<string>();
for (const value of values) {
if (!pattern.test(value)) throw new Error(`${label} contains invalid wire name: ${value}`);
if (seen.has(value)) throw new Error(`${label} contains duplicate wire name: ${value}`);
seen.add(value);
}
}
if (!Number.isInteger(contract.schemaVersion) || contract.schemaVersion < 1) {
throw new Error("schemaVersion must be a positive integer");
}
if (!Number.isInteger(contract.protocolVersion) || contract.protocolVersion < 1) {
throw new Error("protocolVersion must be a positive integer");
}
if (contract.encoding !== "messagepack" || contract.compatibility !== "additive") {
throw new Error("unsupported encoding or compatibility policy");
}
validateNames("serverToAgentMessageTypes", contract.serverToAgentMessageTypes);
validateNames("agentToServerMessageTypes", contract.agentToServerMessageTypes);
validateNames("commands", contract.commands);
if (contract.commandVersioning?.defaultVersion !== 1) {
throw new Error("commandVersioning.defaultVersion must be 1");
}
for (const [command, override] of Object.entries(contract.commandVersioning.overrides)) {
if (!contract.commands.includes(command)) {
throw new Error(`command version override references unknown command: ${command}`);
}
if (!Number.isInteger(override.latestVersion) || override.latestVersion < 2) {
throw new Error(`latestVersion for ${command} must be at least 2`);
}
for (let version = 2; version <= override.latestVersion; version += 1) {
const change = override.changes[String(version)];
if (
!change ||
change.breaking !== true ||
!change.summary?.trim() ||
!change.migration?.trim()
) {
throw new Error(`${command} v${version} requires breaking-change summary and migration text`);
}
}
const unexpected = Object.keys(override.changes)
.filter((value) => !Number.isInteger(Number(value)) || Number(value) < 2 || Number(value) > override.latestVersion);
if (unexpected.length > 0) {
throw new Error(`${command} has invalid change versions: ${unexpected.join(", ")}`);
}
}
const handlerPath = resolve(root, "Overlord-Client", "cmd", "agent", "handlers", "command.go");
const handlerSource = await Bun.file(handlerPath).text();
const handlerCommands = [...handlerSource.matchAll(/^\tcase "([a-z][a-z0-9]*(?:_[a-z0-9]+)*)":/gm)]
.map((match) => match[1])
.filter((value, index, values) => values.indexOf(value) === index)
.sort();
const declaredCommands = [...contract.commands].sort();
const missingFromContract = handlerCommands.filter((value) => !declaredCommands.includes(value));
const missingFromHandler = declaredCommands.filter((value) => !handlerCommands.includes(value));
if (missingFromContract.length > 0 || missingFromHandler.length > 0) {
throw new Error([
"wire command catalog and agent dispatcher have drifted",
missingFromContract.length > 0
? `missing from contract: ${missingFromContract.join(", ")}`
: "",
missingFromHandler.length > 0
? `missing from handler: ${missingFromHandler.join(", ")}`
: "",
].filter(Boolean).join("; "));
}
const allMessageTypes = [...new Set([
...contract.serverToAgentMessageTypes,
...contract.agentToServerMessageTypes,
])].sort();
const serverMessages = [...contract.serverToAgentMessageTypes].sort();
const agentMessages = [...contract.agentToServerMessageTypes].sort();
const commands = [...contract.commands].sort();
const commandVersionSupport = Object.fromEntries(
commands.map((command) => [
command,
{
min: 1,
max: contract.commandVersioning.overrides[command]?.latestVersion ?? 1,
},
]),
) as Record<string, { min: number; max: number }>;
function tsArray(name: string, values: string[]): string {
return `export const ${name} = ${JSON.stringify(values, null, 2)} as const;\n`;
}
const ts = `// Code generated by scripts/generate-wire-protocol.ts; DO NOT EDIT.
export const WIRE_SCHEMA_VERSION = ${contract.schemaVersion} as const;
export const WIRE_PROTOCOL_VERSION = ${contract.protocolVersion} as const;
${tsArray("WIRE_MESSAGE_TYPES", allMessageTypes)}
export type MessageKind = (typeof WIRE_MESSAGE_TYPES)[number];
${tsArray("SERVER_TO_AGENT_MESSAGE_TYPES", serverMessages)}
export type ServerToAgentMessageType = (typeof SERVER_TO_AGENT_MESSAGE_TYPES)[number];
${tsArray("AGENT_TO_SERVER_MESSAGE_TYPES", agentMessages)}
export type AgentToServerMessageType = (typeof AGENT_TO_SERVER_MESSAGE_TYPES)[number];
${tsArray("COMMAND_TYPES", commands)}
export type CommandType = (typeof COMMAND_TYPES)[number];
export type CommandVersionRange = Readonly<{ min: number; max: number }>;
export const COMMAND_VERSION_SUPPORT = ${JSON.stringify(commandVersionSupport, null, 2)} as const satisfies
Record<CommandType, CommandVersionRange>;
const commandTypeSet: ReadonlySet<string> = new Set(COMMAND_TYPES);
const agentMessageTypeSet: ReadonlySet<string> = new Set(AGENT_TO_SERVER_MESSAGE_TYPES);
export function isCommandType(value: string): value is CommandType {
return commandTypeSet.has(value);
}
export function getCommandVersionRange(command: CommandType): CommandVersionRange {
return COMMAND_VERSION_SUPPORT[command];
}
export function isSupportedCommandVersion(command: CommandType, version: number): boolean {
const range = getCommandVersionRange(command);
return Number.isInteger(version) && version >= range.min && version <= range.max;
}
export function getImplicitCommandVersion(command: CommandType): number {
const range = getCommandVersionRange(command);
if (range.min !== range.max) {
throw new Error(
\`Command \${command} supports versions \${range.min}-\${range.max}; negotiate an explicit commandVersion\`,
);
}
return range.min;
}
export function isAgentToServerMessageType(value: string): value is AgentToServerMessageType {
return agentMessageTypeSet.has(value);
}
`;
function goName(value: string): string {
return value.split("_").map((part) => part.charAt(0).toUpperCase() + part.slice(1)).join("");
}
function goConstants(prefix: string, typeName: string, values: string[]): string {
return values.map((value) => `\t${prefix}${goName(value)} ${typeName} = "${value}"`).join("\n");
}
function goSlice(name: string, typeName: string, prefix: string, values: string[]): string {
return `var ${name} = [...]${typeName}{\n${values.map((value) => `\t${prefix}${goName(value)},`).join("\n")}\n}`;
}
function goVersionMap(values: string[]): string {
return values
.map((value) => {
const range = commandVersionSupport[value];
return `\tCommand${goName(value)}: {Min: ${range.min}, Max: ${range.max}},`;
})
.join("\n");
}
const go = `// Code generated by scripts/generate-wire-protocol.ts; DO NOT EDIT.
package wire
const (
\tWireSchemaVersion = ${contract.schemaVersion}
\tWireProtocolVersion = ${contract.protocolVersion}
)
type MessageType string
const (
${goConstants("Message", "MessageType", allMessageTypes)}
)
type CommandType string
const (
${goConstants("Command", "CommandType", commands)}
)
${goSlice("AgentToServerMessageTypes", "MessageType", "Message", agentMessages)}
${goSlice("ServerToAgentMessageTypes", "MessageType", "Message", serverMessages)}
${goSlice("CommandTypes", "CommandType", "Command", commands)}
type CommandVersionRange struct {
\tMin int \`msgpack:"min" json:"min"\`
\tMax int \`msgpack:"max" json:"max"\`
}
var CommandVersionSupport = map[CommandType]CommandVersionRange{
${goVersionMap(commands)}
}
var commandTypeSet = func() map[CommandType]struct{} {
\tvalues := make(map[CommandType]struct{}, len(CommandTypes))
\tfor _, value := range CommandTypes {
\t\tvalues[value] = struct{}{}
\t}
\treturn values
}()
var serverToAgentMessageTypeSet = func() map[MessageType]struct{} {
\tvalues := make(map[MessageType]struct{}, len(ServerToAgentMessageTypes))
\tfor _, value := range ServerToAgentMessageTypes {
\t\tvalues[value] = struct{}{}
\t}
\treturn values
}()
func IsCommandType(value string) bool {
\t_, ok := commandTypeSet[CommandType(value)]
\treturn ok
}
func IsSupportedCommandVersion(command string, version int) bool {
\trangeValue, ok := CommandVersionSupport[CommandType(command)]
\treturn ok && version >= rangeValue.Min && version <= rangeValue.Max
}
func SupportedCommandVersionRanges() map[string]CommandVersionRange {
\tvalues := make(map[string]CommandVersionRange, len(CommandVersionSupport))
\tfor command, rangeValue := range CommandVersionSupport {
\t\tvalues[string(command)] = rangeValue
\t}
\treturn values
}
func IsServerToAgentMessageType(value string) bool {
\t_, ok := serverToAgentMessageTypeSet[MessageType(value)]
\treturn ok
}
`;
async function emit(path: string, content: string): Promise<void> {
if (checkOnly) {
const current = await Bun.file(path).text().catch(() => "");
if (current !== content) {
console.error(`Generated artifact is stale: ${path}`);
process.exitCode = 1;
}
return;
}
await mkdir(dirname(path), { recursive: true });
await Bun.write(path, content);
console.log(`Generated ${path}`);
}
await Promise.all([
emit(tsOutputPath, ts),
emit(goOutputPath, go),
]);