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