wire protocol is generated now instead of being hard coded with versioning for commands.
This commit is contained in:
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
@@ -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",
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 @@
|
||||
export const SERVER_VERSION = "2.6.1";
|
||||
export const SERVER_VERSION = "3.0.0";
|
||||
|
||||
@@ -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, {
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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.
|
||||
@@ -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"
|
||||
]
|
||||
}
|
||||
@@ -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]+)*$"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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),
|
||||
]);
|
||||
Reference in New Issue
Block a user