Skip to main content

codex_mobile_bridge/
bridge_protocol.rs

1use anyhow::Result;
2use serde::{Deserialize, Serialize};
3use serde_json::{Value, json};
4
5#[derive(Debug, Clone, Serialize, Deserialize)]
6pub struct ApiError {
7    pub code: String,
8    pub message: String,
9}
10
11impl ApiError {
12    pub fn new(code: impl Into<String>, message: impl Into<String>) -> Self {
13        Self {
14            code: code.into(),
15            message: message.into(),
16        }
17    }
18}
19
20#[derive(Debug, Clone, Serialize, Deserialize)]
21#[serde(rename_all = "camelCase")]
22pub struct RuntimeRecord {
23    pub runtime_id: String,
24    pub display_name: String,
25    pub codex_home: Option<String>,
26    pub codex_binary: String,
27    pub is_primary: bool,
28    pub auto_start: bool,
29    pub created_at_ms: i64,
30    pub updated_at_ms: i64,
31}
32
33#[derive(Debug, Clone, Serialize, Deserialize)]
34#[serde(rename_all = "camelCase")]
35pub struct AppServerHandshakeSummary {
36    pub state: String,
37    pub protocol: String,
38    pub transport: String,
39    pub experimental_api_enabled: bool,
40    pub notification_mode: String,
41    #[serde(default)]
42    pub opt_out_notification_methods: Vec<String>,
43    pub detail: Option<String>,
44    pub updated_at_ms: i64,
45}
46
47impl Default for AppServerHandshakeSummary {
48    fn default() -> Self {
49        Self::inactive()
50    }
51}
52
53impl AppServerHandshakeSummary {
54    pub fn new(
55        state: impl Into<String>,
56        experimental_api_enabled: bool,
57        opt_out_notification_methods: Vec<String>,
58        detail: Option<String>,
59    ) -> Self {
60        let notification_mode = if opt_out_notification_methods.is_empty() {
61            "full".to_string()
62        } else {
63            "optimized".to_string()
64        };
65        Self {
66            state: state.into(),
67            protocol: "jsonrpc2.0".to_string(),
68            transport: "stdio".to_string(),
69            experimental_api_enabled,
70            notification_mode,
71            opt_out_notification_methods,
72            detail,
73            updated_at_ms: now_millis(),
74        }
75    }
76
77    pub fn inactive() -> Self {
78        Self::new("inactive", false, Vec::new(), None)
79    }
80}
81
82#[derive(Debug, Clone, Serialize, Deserialize)]
83#[serde(rename_all = "camelCase")]
84pub struct RuntimeStatusSnapshot {
85    pub runtime_id: String,
86    pub status: String,
87    pub codex_home: Option<String>,
88    pub user_agent: Option<String>,
89    pub platform_family: Option<String>,
90    pub platform_os: Option<String>,
91    pub last_error: Option<String>,
92    pub pid: Option<u32>,
93    #[serde(default)]
94    pub app_server_handshake: AppServerHandshakeSummary,
95    pub updated_at_ms: i64,
96}
97
98impl RuntimeStatusSnapshot {
99    pub fn stopped(runtime_id: impl Into<String>) -> Self {
100        Self {
101            runtime_id: runtime_id.into(),
102            status: "stopped".to_string(),
103            codex_home: None,
104            user_agent: None,
105            platform_family: None,
106            platform_os: None,
107            last_error: None,
108            pid: None,
109            app_server_handshake: AppServerHandshakeSummary::inactive(),
110            updated_at_ms: now_millis(),
111        }
112    }
113}
114
115#[derive(Debug, Clone, Serialize, Deserialize)]
116#[serde(rename_all = "camelCase")]
117pub struct RuntimeSummary {
118    pub runtime_id: String,
119    pub display_name: String,
120    pub codex_home: Option<String>,
121    pub codex_binary: String,
122    pub is_primary: bool,
123    pub auto_start: bool,
124    pub created_at_ms: i64,
125    pub updated_at_ms: i64,
126    pub status: RuntimeStatusSnapshot,
127}
128
129impl RuntimeSummary {
130    pub fn from_parts(record: &RuntimeRecord, status: RuntimeStatusSnapshot) -> Self {
131        Self {
132            runtime_id: record.runtime_id.clone(),
133            display_name: record.display_name.clone(),
134            codex_home: record.codex_home.clone(),
135            codex_binary: record.codex_binary.clone(),
136            is_primary: record.is_primary,
137            auto_start: record.auto_start,
138            created_at_ms: record.created_at_ms,
139            updated_at_ms: record.updated_at_ms,
140            status,
141        }
142    }
143}
144
145#[derive(Debug, Clone, Serialize, Deserialize)]
146#[serde(rename_all = "camelCase")]
147pub struct DirectoryBookmarkRecord {
148    pub path: String,
149    pub display_name: String,
150    pub created_at_ms: i64,
151    pub updated_at_ms: i64,
152}
153
154#[derive(Debug, Clone, Serialize, Deserialize)]
155#[serde(rename_all = "camelCase")]
156pub struct DirectoryHistoryRecord {
157    pub path: String,
158    pub display_name: String,
159    pub last_used_at_ms: i64,
160    pub use_count: i64,
161}
162
163#[derive(Debug, Clone, Serialize, Deserialize)]
164#[serde(rename_all = "camelCase")]
165pub struct DirectoryEntry {
166    pub name: String,
167    pub path: String,
168    pub is_directory: bool,
169}
170
171#[derive(Debug, Clone, Serialize, Deserialize)]
172#[serde(rename_all = "camelCase")]
173pub struct DirectoryListing {
174    pub path: String,
175    pub parent_path: Option<String>,
176    pub entries: Vec<DirectoryEntry>,
177}
178
179#[derive(Debug, Clone, Default, Serialize, Deserialize)]
180#[serde(rename_all = "camelCase")]
181#[serde(default)]
182pub struct ThreadStatusInfo {
183    pub kind: String,
184    pub reason: Option<String>,
185    pub raw: Value,
186}
187
188#[derive(Debug, Clone, Default, Serialize, Deserialize)]
189#[serde(rename_all = "camelCase")]
190#[serde(default)]
191pub struct ThreadTokenUsage {
192    pub input_tokens: Option<i64>,
193    pub cached_input_tokens: Option<i64>,
194    pub output_tokens: Option<i64>,
195    pub reasoning_tokens: Option<i64>,
196    pub total_tokens: Option<i64>,
197    pub raw: Value,
198    pub updated_at_ms: i64,
199}
200
201#[derive(Debug, Clone, Default, Serialize, Deserialize)]
202#[serde(rename_all = "camelCase")]
203#[serde(default)]
204pub struct ThreadSummary {
205    pub id: String,
206    pub runtime_id: String,
207    pub name: Option<String>,
208    pub note: Option<String>,
209    pub preview: String,
210    pub cwd: String,
211    pub status: String,
212    pub status_info: ThreadStatusInfo,
213    pub token_usage: Option<ThreadTokenUsage>,
214    pub model_provider: String,
215    pub source: String,
216    pub created_at: i64,
217    pub updated_at: i64,
218    pub is_loaded: bool,
219    pub is_active: bool,
220    pub archived: bool,
221}
222
223#[derive(Debug, Clone, Serialize, Deserialize)]
224#[serde(rename_all = "camelCase")]
225pub struct TimelineEntry {
226    pub id: String,
227    pub runtime_id: String,
228    pub thread_id: String,
229    pub turn_id: Option<String>,
230    pub item_id: Option<String>,
231    pub entry_type: String,
232    pub title: Option<String>,
233    pub text: String,
234    pub status: Option<String>,
235    #[serde(default)]
236    pub metadata: Value,
237}
238
239#[derive(Debug, Clone, Default, Serialize, Deserialize)]
240#[serde(rename_all = "camelCase")]
241#[serde(default)]
242pub struct PendingServerRequestOption {
243    pub label: String,
244    pub description: Option<String>,
245    pub value: Option<Value>,
246    pub is_other: bool,
247    pub raw: Value,
248}
249
250#[derive(Debug, Clone, Default, Serialize, Deserialize)]
251#[serde(rename_all = "camelCase")]
252#[serde(default)]
253pub struct PendingServerRequestQuestion {
254    pub id: String,
255    pub header: Option<String>,
256    pub question: Option<String>,
257    pub required: bool,
258    pub options: Vec<PendingServerRequestOption>,
259    pub raw: Value,
260}
261
262#[derive(Debug, Clone, Default, Serialize, Deserialize)]
263#[serde(rename_all = "camelCase")]
264#[serde(default)]
265pub struct PendingServerRequestRecord {
266    pub request_id: String,
267    pub runtime_id: String,
268    pub rpc_request_id: Value,
269    pub request_type: String,
270    pub thread_id: Option<String>,
271    pub turn_id: Option<String>,
272    pub item_id: Option<String>,
273    pub title: Option<String>,
274    pub reason: Option<String>,
275    pub command: Option<String>,
276    pub cwd: Option<String>,
277    pub grant_root: Option<String>,
278    pub tool_name: Option<String>,
279    pub arguments: Option<Value>,
280    #[serde(default)]
281    pub questions: Vec<PendingServerRequestQuestion>,
282    pub proposed_execpolicy_amendment: Option<Value>,
283    pub network_approval_context: Option<Value>,
284    pub schema: Option<Value>,
285    #[serde(default)]
286    pub available_decisions: Vec<String>,
287    pub raw_payload: Value,
288    pub created_at_ms: i64,
289}
290
291#[derive(Debug, Clone, Serialize, Deserialize)]
292#[serde(rename_all = "camelCase")]
293pub struct PersistedEvent {
294    pub seq: i64,
295    pub event_type: String,
296    pub runtime_id: Option<String>,
297    pub thread_id: Option<String>,
298    pub payload: Value,
299    pub created_at_ms: i64,
300}
301
302#[derive(Debug, Deserialize)]
303#[serde(tag = "kind", rename_all = "snake_case")]
304pub enum ClientEnvelope {
305    Hello {
306        device_id: String,
307        last_ack_seq: Option<i64>,
308    },
309    Request {
310        request_id: String,
311        action: String,
312        #[serde(default)]
313        payload: Value,
314    },
315    AckEvents {
316        last_seq: i64,
317    },
318    Ping,
319}
320
321#[derive(Debug, Serialize)]
322#[serde(tag = "kind", rename_all = "snake_case")]
323pub enum ServerEnvelope {
324    Hello {
325        bridge_version: String,
326        protocol_version: u32,
327        runtime: RuntimeStatusSnapshot,
328        runtimes: Vec<RuntimeSummary>,
329        directory_bookmarks: Vec<DirectoryBookmarkRecord>,
330        directory_history: Vec<DirectoryHistoryRecord>,
331        pending_requests: Vec<PendingServerRequestRecord>,
332    },
333    Response {
334        request_id: String,
335        success: bool,
336        #[serde(skip_serializing_if = "Option::is_none")]
337        data: Option<Value>,
338        #[serde(skip_serializing_if = "Option::is_none")]
339        error: Option<ApiError>,
340    },
341    Event {
342        seq: i64,
343        event_type: String,
344        #[serde(skip_serializing_if = "Option::is_none")]
345        runtime_id: Option<String>,
346        #[serde(skip_serializing_if = "Option::is_none")]
347        thread_id: Option<String>,
348        payload: Value,
349    },
350    Pong {
351        server_time_ms: i64,
352    },
353}
354
355#[derive(Debug, Deserialize)]
356#[serde(rename_all = "camelCase")]
357pub struct GetRuntimeStatusRequest {
358    pub runtime_id: Option<String>,
359}
360
361#[derive(Debug, Deserialize)]
362#[serde(rename_all = "camelCase")]
363pub struct StartRuntimeRequest {
364    pub runtime_id: Option<String>,
365    pub display_name: Option<String>,
366    pub codex_home: Option<String>,
367    pub codex_binary: Option<String>,
368    pub auto_start: Option<bool>,
369}
370
371#[derive(Debug, Deserialize)]
372#[serde(rename_all = "camelCase")]
373pub struct StopRuntimeRequest {
374    pub runtime_id: String,
375}
376
377#[derive(Debug, Deserialize)]
378#[serde(rename_all = "camelCase")]
379pub struct RestartRuntimeRequest {
380    pub runtime_id: String,
381}
382
383#[derive(Debug, Deserialize)]
384#[serde(rename_all = "camelCase")]
385pub struct PruneRuntimeRequest {
386    pub runtime_id: String,
387}
388
389#[derive(Debug, Deserialize)]
390#[serde(rename_all = "camelCase")]
391pub struct CreateDirectoryBookmarkRequest {
392    pub path: String,
393    pub display_name: Option<String>,
394}
395
396#[derive(Debug, Deserialize)]
397#[serde(rename_all = "camelCase")]
398pub struct RemoveDirectoryBookmarkRequest {
399    pub path: String,
400}
401
402#[derive(Debug, Deserialize)]
403#[serde(rename_all = "camelCase")]
404pub struct ReadDirectoryRequest {
405    pub runtime_id: Option<String>,
406    pub path: String,
407}
408
409#[derive(Debug, Deserialize)]
410#[serde(rename_all = "camelCase")]
411pub struct ListThreadsRequest {
412    pub directory_prefix: Option<String>,
413    pub runtime_id: Option<String>,
414    pub limit: Option<usize>,
415    pub cursor: Option<String>,
416    pub archived: Option<bool>,
417    pub search_term: Option<String>,
418}
419
420#[derive(Debug, Deserialize)]
421#[serde(rename_all = "camelCase")]
422pub struct StartThreadRequest {
423    pub runtime_id: Option<String>,
424    pub cwd: String,
425    pub model: Option<String>,
426    pub name: Option<String>,
427    pub note: Option<String>,
428}
429
430#[derive(Debug, Deserialize)]
431#[serde(rename_all = "camelCase")]
432pub struct ResumeThreadRequest {
433    pub thread_id: String,
434}
435
436#[derive(Debug, Deserialize)]
437#[serde(rename_all = "camelCase")]
438pub struct ReadThreadRequest {
439    pub thread_id: String,
440}
441
442#[derive(Debug, Deserialize)]
443#[serde(rename_all = "camelCase")]
444pub struct StageInputImageRequest {
445    pub file_name: Option<String>,
446    pub mime_type: Option<String>,
447    pub base64_data: String,
448}
449
450#[derive(Debug, Clone, Serialize, Deserialize)]
451#[serde(rename_all = "camelCase")]
452pub struct StagedInputImage {
453    pub local_path: String,
454    pub display_name: Option<String>,
455    pub mime_type: Option<String>,
456    pub size_bytes: i64,
457}
458
459#[derive(Debug, Clone, Serialize, Deserialize)]
460#[serde(tag = "type", rename_all = "camelCase")]
461pub enum SendTurnInputItem {
462    Text { text: String },
463    LocalImage { path: String },
464}
465
466#[derive(Debug, Deserialize)]
467#[serde(rename_all = "camelCase")]
468pub struct SendTurnRequest {
469    pub thread_id: String,
470    #[serde(default)]
471    pub text: String,
472    pub input_items: Option<Vec<SendTurnInputItem>>,
473}
474
475#[derive(Debug, Deserialize)]
476#[serde(rename_all = "camelCase")]
477pub struct InterruptTurnRequest {
478    pub thread_id: String,
479    pub turn_id: String,
480}
481
482#[derive(Debug, Deserialize)]
483#[serde(rename_all = "camelCase")]
484pub struct UpdateThreadRequest {
485    pub thread_id: String,
486    pub name: Option<String>,
487    pub note: Option<String>,
488}
489
490#[derive(Debug, Deserialize)]
491#[serde(rename_all = "camelCase")]
492pub struct ArchiveThreadRequest {
493    pub thread_id: String,
494}
495
496#[derive(Debug, Deserialize)]
497#[serde(rename_all = "camelCase")]
498pub struct UnarchiveThreadRequest {
499    pub thread_id: String,
500}
501
502#[derive(Debug, Deserialize)]
503#[serde(rename_all = "camelCase")]
504pub struct RespondPendingRequestRequest {
505    pub request_id: String,
506    pub response: Value,
507}
508
509pub fn ok_response(request_id: String, data: Value) -> ServerEnvelope {
510    ServerEnvelope::Response {
511        request_id,
512        success: true,
513        data: Some(data),
514        error: None,
515    }
516}
517
518pub fn error_response(request_id: String, error: ApiError) -> ServerEnvelope {
519    ServerEnvelope::Response {
520        request_id,
521        success: false,
522        data: None,
523        error: Some(error),
524    }
525}
526
527pub fn event_envelope(event: PersistedEvent) -> ServerEnvelope {
528    ServerEnvelope::Event {
529        seq: event.seq,
530        event_type: event.event_type,
531        runtime_id: event.runtime_id,
532        thread_id: event.thread_id,
533        payload: event.payload,
534    }
535}
536
537pub fn now_millis() -> i64 {
538    let now = std::time::SystemTime::now();
539    let since_epoch = now
540        .duration_since(std::time::UNIX_EPOCH)
541        .unwrap_or_default();
542    since_epoch.as_millis() as i64
543}
544
545pub fn json_string(value: &Value) -> String {
546    match value {
547        Value::String(inner) => inner.clone(),
548        _ => value.to_string(),
549    }
550}
551
552pub fn require_payload<T: for<'de> Deserialize<'de>>(payload: Value) -> Result<T> {
553    Ok(serde_json::from_value(payload)?)
554}
555
556pub fn status_payload(runtime: &RuntimeSummary) -> Value {
557    json!({ "runtime": runtime })
558}
559
560pub fn runtime_list_payload(runtimes: &[RuntimeSummary]) -> Value {
561    json!({ "runtimes": runtimes })
562}