codex-mobile-bridge 0.3.10

Remote bridge and service manager for codex-mobile.
Documentation
mod handshake;
mod manager;
mod running;
mod streams;

#[cfg(test)]
mod tests;

use std::collections::HashMap;
use std::path::PathBuf;
use std::sync::{
    Arc,
    atomic::{AtomicBool, AtomicU64},
};

use anyhow::Result;
use serde::{Deserialize, Serialize};
use serde_json::Value;
use tokio::io::BufWriter;
use tokio::process::{Child, ChildStdin};
use tokio::sync::{Mutex, mpsc, oneshot};

const APP_SERVER_EXPERIMENTAL_API_ENABLED: bool = true;
const APP_SERVER_OPT_OUT_NOTIFICATION_METHODS: &[&str] = &[
    "account/login/completed",
    "account/rateLimits/updated",
    "account/updated",
    "app/list/updated",
    "fs/changed",
    "fuzzyFileSearch/sessionCompleted",
    "fuzzyFileSearch/sessionUpdated",
    "mcpServer/oauthLogin/completed",
    "mcpServer/startupStatus/updated",
    "skills/changed",
    "thread/realtime/closed",
    "thread/realtime/error",
    "thread/realtime/itemAdded",
    "thread/realtime/outputAudio/delta",
    "thread/realtime/sdp",
    "thread/realtime/started",
    "thread/realtime/transcriptUpdated",
    "windows/worldWritableWarning",
    "windowsSandbox/setupCompleted",
];

#[derive(Debug, Clone)]
pub struct AppServerLaunchConfig {
    pub runtime_id: String,
    pub codex_binary: String,
    pub codex_home: Option<PathBuf>,
}

#[derive(Debug, Clone)]
pub struct InitializeInfo {
    pub user_agent: String,
    pub codex_home: String,
    pub platform_family: String,
    pub platform_os: String,
}

#[derive(Debug, Clone)]
pub enum AppServerInbound {
    Starting {
        runtime_id: String,
    },
    ProcessChanged {
        runtime_id: String,
        pid: Option<u32>,
        running: bool,
    },
    Initializing {
        runtime_id: String,
        experimental_api_enabled: bool,
        opt_out_notification_methods: Vec<String>,
    },
    Initialized {
        runtime_id: String,
        info: InitializeInfo,
        experimental_api_enabled: bool,
        opt_out_notification_methods: Vec<String>,
    },
    HandshakeFailed {
        runtime_id: String,
        message: String,
        experimental_api_enabled: bool,
        opt_out_notification_methods: Vec<String>,
    },
    Notification {
        runtime_id: String,
        method: String,
        params: Value,
    },
    ServerRequest {
        runtime_id: String,
        id: Value,
        method: String,
        params: Value,
    },
    Exited {
        runtime_id: String,
        message: String,
        expected: bool,
    },
    LogChunk {
        runtime_id: String,
        stream: String,
        level: String,
        source: String,
        message: String,
        detail: Option<Value>,
        occurred_at_ms: i64,
    },
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RpcErrorPayload {
    pub code: i64,
    pub message: String,
    #[serde(default)]
    pub data: Option<Value>,
}

impl std::fmt::Display for RpcErrorPayload {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        write!(f, "[{}] {}", self.code, self.message)
    }
}

pub struct AppServerManager {
    launch_config: AppServerLaunchConfig,
    inbound_tx: mpsc::UnboundedSender<AppServerInbound>,
    inner: Mutex<Option<Arc<RunningAppServer>>>,
}

struct RunningAppServer {
    runtime_id: String,
    stdin: Arc<Mutex<BufWriter<ChildStdin>>>,
    child: Arc<Mutex<Option<Child>>>,
    pending: Arc<Mutex<HashMap<String, oneshot::Sender<Result<Value, RpcErrorPayload>>>>>,
    next_id: AtomicU64,
    alive: Arc<AtomicBool>,
    stopping: Arc<AtomicBool>,
}