xworkmate-app/rust/src/runtime.rs
Haitao Pan 6b6a586f8d fix: resolve Rust FFI compilation errors and simplify build
- Fix duplicate Clone implementation conflict in CodexConfig
- Add Default derive to CodexConfigRust for unwrap_or_default()
- Fix temporary lifetime issues in find_codex_binary()
- Remove unused imports (CString, CodexEvent)
- Update Makefile rust-build-release to target arm64 only (no x86_64/lipo)
- Add gitignore rules for Rust build artifacts
2026-03-14 09:52:13 +08:00

292 lines
7.4 KiB
Rust

//! Core runtime for Codex FFI.
use std::ffi::CString;
use std::os::raw::c_char;
use std::path::PathBuf;
use crate::error::CodexError;
/// Configuration for Codex runtime.
#[derive(Debug, Clone)]
#[repr(C)]
pub struct CodexConfig {
/// Path to Codex binary.
pub codex_path: *const c_char,
/// Working directory.
pub working_directory: *const c_char,
/// Sandbox mode: 0=read-only, 1=workspace-write, 2=danger-full-access.
pub sandbox_mode: i32,
/// Approval policy: 0=suggest, 1=auto-edit, 2=full-auto.
pub approval_policy: i32,
/// Model to use.
pub model: *const c_char,
/// API key for gateway.
pub api_key: *const c_char,
/// Gateway URL.
pub gateway_url: *const c_char,
/// Enable debug logging.
pub debug: bool,
}
impl Default for CodexConfig {
fn default() -> Self {
CodexConfig {
codex_path: std::ptr::null(),
working_directory: std::ptr::null(),
sandbox_mode: 1, // workspace-write
approval_policy: 0, // suggest
model: std::ptr::null(),
api_key: std::ptr::null(),
gateway_url: std::ptr::null(),
debug: false,
}
}
}
impl CodexConfig {
/// Convert FFI config to Rust types.
pub unsafe fn to_rust(&self) -> Result<CodexConfigRust, CodexError> {
let codex_path = if self.codex_path.is_null() {
None
} else {
Some(std::ffi::CStr::from_ptr(self.codex_path)
.to_string_lossy()
.into_owned())
};
let working_directory = if self.working_directory.is_null() {
None
} else {
Some(std::ffi::CStr::from_ptr(self.working_directory)
.to_string_lossy()
.into_owned())
};
let model = if self.model.is_null() {
None
} else {
Some(std::ffi::CStr::from_ptr(self.model)
.to_string_lossy()
.into_owned())
};
let api_key = if self.api_key.is_null() {
None
} else {
Some(std::ffi::CStr::from_ptr(self.api_key)
.to_string_lossy()
.into_owned())
};
let gateway_url = if self.gateway_url.is_null() {
None
} else {
Some(std::ffi::CStr::from_ptr(self.gateway_url)
.to_string_lossy()
.into_owned())
};
Ok(CodexConfigRust {
codex_path,
working_directory,
sandbox_mode: self.sandbox_mode,
approval_policy: self.approval_policy,
model,
api_key,
gateway_url,
debug: self.debug,
})
}
}
/// Rust-native config type.
#[derive(Debug, Clone, Default)]
pub struct CodexConfigRust {
pub codex_path: Option<String>,
pub working_directory: Option<String>,
pub sandbox_mode: i32,
pub approval_policy: i32,
pub model: Option<String>,
pub api_key: Option<String>,
pub gateway_url: Option<String>,
pub debug: bool,
}
/// Opaque handle to a thread.
#[repr(C)]
#[derive(Debug, Clone, Copy)]
pub struct ThreadHandle {
pub id: u64,
}
impl ThreadHandle {
pub fn new(id: u64) -> Self {
ThreadHandle { id }
}
pub fn null() -> Self {
ThreadHandle { id: 0 }
}
pub fn is_null(&self) -> bool {
self.id == 0
}
}
/// Codex runtime state.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum RuntimeState {
Disconnected,
Connecting,
Connected,
Ready,
Error,
}
/// Core runtime for managing Codex process.
pub struct CodexRuntime {
config: CodexConfigRust,
state: RuntimeState,
pub last_error: CString,
}
impl CodexRuntime {
/// Create a new runtime with the given configuration.
pub fn new(config: CodexConfig) -> Self {
let rust_config = unsafe { config.to_rust().unwrap_or_default() };
CodexRuntime {
config: rust_config,
state: RuntimeState::Disconnected,
last_error: CString::new("").unwrap_or_default(),
}
}
/// Create from Rust config.
pub fn with_config(config: CodexConfigRust) -> Self {
CodexRuntime {
config,
state: RuntimeState::Disconnected,
last_error: CString::new("").unwrap_or_default(),
}
}
/// Get the current state.
pub fn state(&self) -> RuntimeState {
self.state
}
/// Set error message.
pub fn set_error(&mut self, message: &str) {
self.last_error = CString::new(message).unwrap_or_default();
self.state = RuntimeState::Error;
}
/// Find the Codex binary.
pub fn find_codex_binary(&self) -> Option<PathBuf> {
// Check config path
if let Some(ref path) = self.config.codex_path {
let path = PathBuf::from(path);
if path.exists() {
return Some(path);
}
}
// Check environment
if let Ok(path) = std::env::var("CODEX_PATH") {
let path = PathBuf::from(path);
if path.exists() {
return Some(path);
}
}
// Check common locations
let home = std::env::var("HOME").unwrap_or_default();
let cargo_path = format!("{}/.cargo/bin/codex", home);
let local_path = format!("{}/.local/bin/codex", home);
let paths = [
"/usr/local/bin/codex",
"/opt/homebrew/bin/codex",
cargo_path.as_str(),
local_path.as_str(),
];
for path in paths {
let path = PathBuf::from(path);
if path.exists() {
return Some(path);
}
}
None
}
/// Start the runtime.
pub async fn start(&mut self) -> Result<(), CodexError> {
if self.state == RuntimeState::Ready {
return Err(CodexError::AlreadyInitialized);
}
self.state = RuntimeState::Connecting;
// Find binary
let _binary = self.find_codex_binary()
.ok_or_else(|| CodexError::Process("Codex binary not found".into()))?;
// TODO: Start process
self.state = RuntimeState::Ready;
Ok(())
}
/// Stop the runtime.
pub async fn stop(&mut self) -> Result<(), CodexError> {
if self.state == RuntimeState::Disconnected {
return Ok(());
}
// TODO: Stop process
self.state = RuntimeState::Disconnected;
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_config_default() {
let config = CodexConfig::default();
assert!(config.codex_path.is_null());
assert_eq!(config.sandbox_mode, 1);
}
#[test]
fn test_thread_handle() {
let handle = ThreadHandle::new(42);
assert_eq!(handle.id, 42);
assert!(!handle.is_null());
let null_handle = ThreadHandle::null();
assert!(null_handle.is_null());
}
#[test]
fn test_runtime_state() {
let config = CodexConfigRust {
codex_path: None,
working_directory: None,
sandbox_mode: 1,
approval_policy: 0,
model: None,
api_key: None,
gateway_url: None,
debug: false,
};
let runtime = CodexRuntime::with_config(config);
assert_eq!(runtime.state(), RuntimeState::Disconnected);
}
}