Skip to main content

mobius_gateway/wire/
messages.rs

1use super::*;
2
3const MAX_REQUEST_ID_BYTES: usize = 256;
4
5/// One client-to-gateway frame.
6#[derive(Debug, Clone, PartialEq, Serialize)]
7pub struct ClientFrame {
8    pub version: u16,
9    #[serde(flatten)]
10    pub message: ClientMessage,
11}
12
13impl<'de> Deserialize<'de> for ClientFrame {
14    fn deserialize<D>(deserializer: D) -> std::result::Result<Self, D::Error>
15    where
16        D: serde::Deserializer<'de>,
17    {
18        let (version, message) = deserialize_frame(deserializer)?;
19        if let Some(request_id) = message.get("request_id")
20            && !request_id
21                .as_str()
22                .is_some_and(|id| !id.is_empty() && id.len() <= MAX_REQUEST_ID_BYTES)
23        {
24            return Err(D::Error::custom("request ID must be 1–256 bytes"));
25        }
26        let message = serde_json::from_value(message).map_err(D::Error::custom)?;
27        Ok(Self { version, message })
28    }
29}
30
31impl ClientFrame {
32    /// Wraps a message in the current protocol version.
33    #[must_use]
34    pub const fn new(message: ClientMessage) -> Self {
35        Self {
36            version: PROTOCOL_VERSION,
37            message,
38        }
39    }
40}
41
42/// Authenticated operations accepted by the gateway.
43#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
44#[serde(tag = "type", rename_all = "snake_case")]
45#[non_exhaustive]
46pub enum ClientMessage {
47    SetDesktopRuntime {
48        request_id: String,
49        enabled: bool,
50    },
51    DesktopControlReply {
52        request_id: String,
53        response: Value,
54    },
55    Pair {
56        code: String,
57        client_label: String,
58        client_kind: ClientKind,
59    },
60    Authenticate {
61        token: String,
62        client_kind: ClientKind,
63    },
64    ListClients {
65        request_id: String,
66    },
67    UnpairClient {
68        request_id: String,
69        client_id: String,
70    },
71    ListSessions {
72        request_id: String,
73    },
74    ListBotSessions {
75        request_id: String,
76        bot_id: String,
77    },
78    CreateSession {
79        request_id: String,
80        workspace: PathBuf,
81        bot_ids: Vec<String>,
82    },
83    CreateWorkspaceDirectory {
84        request_id: String,
85        parent: PathBuf,
86        name: String,
87    },
88    OpenSession {
89        request_id: String,
90        session_id: String,
91        last_sequence: Option<u64>,
92    },
93    GetSessionHistory {
94        request_id: String,
95        session_id: String,
96        before_sequence: Option<u64>,
97    },
98    ReassignSession {
99        request_id: String,
100        session_id: String,
101        bot_id: String,
102    },
103    RenameSession {
104        request_id: String,
105        session_id: String,
106        title: String,
107    },
108    AttachSessionFolder {
109        request_id: String,
110        session_id: String,
111        folder: PathBuf,
112    },
113    SetSessionPinned {
114        request_id: String,
115        session_id: String,
116        pinned: bool,
117    },
118    DeleteSessions {
119        request_id: String,
120        session_ids: Vec<String>,
121    },
122    Submit {
123        session_id: String,
124        submission: Submission,
125    },
126    StartRealtimeVoice {
127        request_id: String,
128        session_id: String,
129        offer_sdp: String,
130    },
131    EndRealtimeVoice {
132        session_id: String,
133        voice_id: String,
134    },
135    GetContributions {
136        request_id: String,
137    },
138    SubmitContribution {
139        request_id: String,
140        operation: Op,
141    },
142    BeginSessionFileUpload {
143        request_id: String,
144        session_id: String,
145        name: String,
146        size: u64,
147        media_type: String,
148    },
149    UploadSessionFileChunk {
150        request_id: String,
151        session_id: String,
152        upload_id: String,
153        offset: u64,
154        #[serde(with = "base64_bytes")]
155        data: Vec<u8>,
156    },
157    FinishSessionFileUpload {
158        request_id: String,
159        session_id: String,
160        upload_id: String,
161    },
162    DeleteSessionFile {
163        request_id: String,
164        session_id: String,
165        file_id: String,
166    },
167    ListSessionFiles {
168        request_id: String,
169        session_id: String,
170    },
171    ReadSessionFile {
172        request_id: String,
173        session_id: String,
174        file_id: String,
175        offset: u64,
176        max_bytes: usize,
177    },
178    CreateBot {
179        request_id: String,
180        name: String,
181        description: String,
182    },
183    ListBots {
184        request_id: String,
185    },
186    UpdateBot {
187        request_id: String,
188        id: String,
189        expected_revision: u64,
190        name: String,
191        description: String,
192        tint: ProviderTint,
193        config: AgentComposition,
194    },
195    DeleteBot {
196        request_id: String,
197        id: String,
198        expected_revision: u64,
199    },
200    ConfigureBotDefaults {
201        request_id: String,
202        expected_revision: u64,
203        config: AgentComposition,
204    },
205    InstallExtension {
206        request_id: String,
207        source: String,
208        reference: Option<String>,
209        subdirectory: Option<String>,
210    },
211    UpdateExtension {
212        request_id: String,
213        id: String,
214    },
215    UninstallExtension {
216        request_id: String,
217        id: String,
218    },
219    TrustExtensionHooks {
220        request_id: String,
221        id: String,
222        expected_digest: String,
223    },
224    RevokeExtensionHooksTrust {
225        request_id: String,
226        id: String,
227        expected_digest: String,
228    },
229    ProbeGitCredential {
230        request_id: String,
231        target: String,
232    },
233    ApproveGitCredential {
234        request_id: String,
235        target: String,
236        username: String,
237        token: String,
238    },
239    ListSshIdentities {
240        request_id: String,
241    },
242    GenerateSshIdentity {
243        request_id: String,
244    },
245    GetGitDiff {
246        request_id: String,
247        session_id: String,
248        scope: GitDiffScope,
249    },
250    SwitchGitBranch {
251        request_id: String,
252        session_id: String,
253        branch: String,
254    },
255    ListDirectories {
256        request_id: String,
257        path: PathBuf,
258        include_files: bool,
259    },
260    ListWorkspaceFiles {
261        request_id: String,
262        session_id: String,
263        scope: WorkspaceFileScope,
264    },
265    ReadWorkspaceFile {
266        request_id: String,
267        session_id: String,
268        path: String,
269        offset: u64,
270        max_bytes: usize,
271    },
272    WriteWorkspaceFile {
273        request_id: String,
274        session_id: String,
275        path: String,
276        content: String,
277    },
278    ClearProviderCredential {
279        request_id: String,
280        instance: String,
281    },
282    SetProviderCredential {
283        request_id: String,
284        instance: String,
285        provider: String,
286        api_key: String,
287        expires_at: Option<u64>,
288    },
289    SetProviderEndpointCredential {
290        request_id: String,
291        instance: String,
292        provider: String,
293        base_url: String,
294        api_key: String,
295        expires_at: Option<u64>,
296    },
297    RegisterProvider {
298        request_id: String,
299        config: ProviderConfig,
300        label: String,
301        tint: ProviderTint,
302        model_ids: Vec<String>,
303        reasoning_efforts: Vec<String>,
304    },
305    RemoveProvider {
306        request_id: String,
307        instance: String,
308    },
309    CreatePairingCode {
310        request_id: String,
311    },
312    StartProviderLogin {
313        request_id: String,
314        provider: String,
315    },
316    GetProfile {
317        request_id: String,
318        include_provider_usage: bool,
319    },
320    CreateRoutine {
321        request_id: String,
322        bot_id: String,
323        workspace: PathBuf,
324        instructions: String,
325        schedule: RoutineSchedule,
326        ends_at: Option<i64>,
327    },
328    ListRoutines {
329        request_id: String,
330        bot_id: Option<String>,
331    },
332    UpdateRoutine {
333        request_id: String,
334        id: String,
335        bot_id: String,
336        workspace: PathBuf,
337        instructions: String,
338        schedule: RoutineSchedule,
339        ends_at: Option<i64>,
340        enabled: bool,
341    },
342    DeleteRoutine {
343        request_id: String,
344        id: String,
345    },
346    RunRoutine {
347        request_id: String,
348        id: String,
349    },
350    ListRoutineHistory {
351        request_id: String,
352        id: Option<String>,
353    },
354    DeleteRoutineRun {
355        request_id: String,
356        id: String,
357    },
358    GetRoutineRunPreview {
359        request_id: String,
360        id: String,
361        before_sequence: Option<u64>,
362    },
363}
364
365/// One gateway-to-client frame.
366#[derive(Debug, Clone, PartialEq, Serialize)]
367pub struct ServerFrame {
368    pub version: u16,
369    #[serde(flatten)]
370    pub message: ServerMessage,
371}
372
373impl<'de> Deserialize<'de> for ServerFrame {
374    fn deserialize<D>(deserializer: D) -> std::result::Result<Self, D::Error>
375    where
376        D: serde::Deserializer<'de>,
377    {
378        let (version, message) = deserialize_frame(deserializer)?;
379        let message = serde_json::from_value(message).map_err(D::Error::custom)?;
380        Ok(Self { version, message })
381    }
382}
383
384impl ServerFrame {
385    /// Wraps a message in the current protocol version.
386    #[must_use]
387    pub const fn new(message: ServerMessage) -> Self {
388        Self {
389            version: PROTOCOL_VERSION,
390            message,
391        }
392    }
393}
394
395/// Results and broadcasts emitted by the gateway.
396#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
397#[serde(tag = "type", rename_all = "snake_case")]
398#[non_exhaustive]
399pub enum ServerMessage {
400    DesktopControlRequested {
401        request_id: String,
402        execution_id: String,
403        session_id: String,
404        request: Value,
405    },
406    DesktopControlEnded {
407        execution_id: String,
408    },
409    Paired {
410        client_id: String,
411        token: String,
412    },
413    Authenticated,
414    Ready {
415        payload: ReadyPayload,
416    },
417    SessionOpened {
418        request_id: String,
419        payload: SessionReadyPayload,
420    },
421    SessionReplayComplete {
422        request_id: String,
423        session_id: String,
424    },
425    SessionHistory {
426        request_id: String,
427        session_id: String,
428        records: Vec<RecordedEvent>,
429        next_before_sequence: Option<u64>,
430    },
431    SessionChanged {
432        payload: SessionReadyPayload,
433    },
434    RealtimeVoiceStarted {
435        request_id: String,
436        session_id: String,
437        voice_id: String,
438        answer_sdp: String,
439    },
440    RealtimeVoiceFailed {
441        request_id: String,
442        session_id: String,
443        message: String,
444    },
445    RealtimeVoiceEnded {
446        session_id: String,
447        voice_id: String,
448        reason: Option<String>,
449    },
450    GatewayConfigured {
451        request_id: String,
452        payload: ReadyPayload,
453    },
454    Contributions {
455        request_id: String,
456        contributions: Vec<FrontendContribution>,
457    },
458    Accepted {
459        request_id: String,
460    },
461    SessionFileUploadReady {
462        request_id: String,
463        session_id: String,
464        upload_id: String,
465        max_chunk_bytes: usize,
466    },
467    SessionFileUploadChunkAccepted {
468        request_id: String,
469        session_id: String,
470        upload_id: String,
471        next_offset: u64,
472    },
473    SessionFileUploadCompleted {
474        request_id: String,
475        session_id: String,
476        file: SessionFileReference,
477    },
478    SessionFiles {
479        request_id: String,
480        session_id: String,
481        files: Vec<SessionFileRecord>,
482    },
483    SessionFileChunk {
484        request_id: String,
485        session_id: String,
486        file_id: String,
487        offset: u64,
488        #[serde(with = "base64_bytes")]
489        data: Vec<u8>,
490        next_offset: Option<u64>,
491    },
492    Rejected {
493        request_id: String,
494        code: String,
495        message: String,
496        fatal: bool,
497    },
498    AgentEvent {
499        session_id: String,
500        record: RecordedEvent,
501    },
502    Sessions {
503        #[serde(default, skip_serializing_if = "Option::is_none")]
504        request_id: Option<String>,
505        sessions: Vec<SessionRecord>,
506    },
507    BackgroundApprovals {
508        approvals: Vec<BackgroundApproval>,
509    },
510
511    BotSessions {
512        request_id: String,
513        bot_id: String,
514        sessions: Vec<SessionRecord>,
515    },
516    Bots {
517        #[serde(default, skip_serializing_if = "Option::is_none")]
518        request_id: Option<String>,
519        bots: Vec<BotRecord>,
520    },
521
522    Clients {
523        request_id: String,
524        current_client_id: String,
525        clients: Vec<ClientStatus>,
526    },
527    ProviderCredentialCleared {
528        request_id: String,
529        instance: String,
530    },
531    ProviderCredentialSaved {
532        request_id: String,
533        instance: String,
534        provider: String,
535    },
536    PairingCode {
537        request_id: String,
538        code: String,
539        expires_at: i64,
540    },
541    ProviderLoginStarted {
542        request_id: String,
543        login_id: String,
544        provider: String,
545        verification_url: String,
546        user_code: String,
547    },
548    ProviderLoginFinished {
549        request_id: String,
550        login_id: String,
551        provider: String,
552    },
553    GitCredentialStatus {
554        request_id: String,
555        available: bool,
556        #[serde(skip_serializing_if = "Option::is_none")]
557        username: Option<String>,
558    },
559    SshIdentities {
560        request_id: String,
561        identities: Vec<SshIdentityRecord>,
562    },
563    SshIdentityGenerated {
564        request_id: String,
565        identity: SshIdentityRecord,
566        public_key: String,
567    },
568    Profile {
569        request_id: String,
570        profile: ProfileSnapshot,
571    },
572    GitDiff {
573        request_id: String,
574        session_id: String,
575        scope: GitDiffScope,
576        diff: String,
577    },
578    Directories {
579        request_id: String,
580        listing: DirectoryListing,
581    },
582    WorkspaceFiles {
583        request_id: String,
584        session_id: String,
585        files: Vec<WorkspaceFileRecord>,
586        truncated: bool,
587    },
588    WorkspaceFileChunk {
589        request_id: String,
590        session_id: String,
591        path: String,
592        offset: u64,
593        #[serde(with = "base64_bytes")]
594        data: Vec<u8>,
595        next_offset: Option<u64>,
596    },
597    Routines {
598        request_id: String,
599        routines: Vec<Routine>,
600    },
601    RoutineHistory {
602        request_id: String,
603        runs: Vec<RoutineRun>,
604    },
605    RoutineRunPreview {
606        request_id: String,
607        preview: RoutineRunPreview,
608    },
609    Error {
610        code: String,
611        message: String,
612        fatal: bool,
613    },
614}