Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ codex exec --edit-mode trusted "update CHANGELOG and open a PR"
```

Safety guidance:

- Prefer running in a VM or container when using `--edit-mode trusted`.
- Pair with `--sandbox workspace-write` for guardrails. If you need `--sandbox danger-full-access`, only do so inside an isolated environment.
- You can control command timeouts via `-c command_timeout_ms=120000` or disable with `-c command_timeout_ms=\"none\"`.
Expand All @@ -73,6 +74,9 @@ You can also use Codex with an API key, but this requires [additional setup](./d

Codex CLI supports [MCP servers](./docs/advanced.md#model-context-protocol-mcp). Enable by adding an `mcp_servers` section to your `~/.codex/config.toml`.

### Remote server/client mode

Codex also includes a lightweight TCP server and client that let you run the agent on one machine and drive it from another. Launch the server with `cargo run -p codex-server` (or the `codex-server` binary) and connect using `cargo run -p codex-client` or the `codex-client` binary. The server boots the same `ConversationManager` used by the CLI, so plugins, MCP servers and other runtime features remain available when Codex runs in this remote mode.

### Configuration

Expand Down Expand Up @@ -110,4 +114,3 @@ Codex CLI supports a rich set of configuration options, with preferences stored
## License

This repository is licensed under the [Apache-2.0 License](LICENSE).

5 changes: 5 additions & 0 deletions codex-rs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,10 @@ It is still experimental, but you can also launch Codex as an MCP _server_ by ru
npx @modelcontextprotocol/inspector codex mcp
```

### Remote server and client

For remote workflows, the workspace includes `codex-server` and `codex-client` binaries. `codex-server` exposes a raw TCP interface that speaks the same JSON protocol used by the CLI, and `codex-client` forwards stdin to the server and prints events. Because the server spins up the full `ConversationManager`, all plugins, MCP servers, and other runtime features remain available when using Codex over the network.

### Notifications

You can enable notifications by configuring a script that is run whenever the agent finishes a turn. The [notify documentation](../docs/config.md#notify) includes a detailed example that explains how to get desktop notifications via [terminal-notifier](https://github.com/julienXX/terminal-notifier) on macOS.
Expand Down Expand Up @@ -135,6 +139,7 @@ codex --edit-mode trusted
```

Safety guidance for `trusted`:

- Prefer running in a VM or container.
- Consider pairing with `--sandbox workspace-write` for guardrails. If you need `--sandbox danger-full-access`, only do so inside an isolated environment.
- Control host command timeouts via `-c command_timeout_ms=120000` or disable with `-c command_timeout_ms=\"none\"`.
Expand Down
11 changes: 6 additions & 5 deletions codex-rs/ai/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,12 @@ use std::sync::Arc;

use anyhow::Result;
use futures::StreamExt;
use genai::{
Client as GenaiClient,
chat::{ChatMessage, ChatRequest, ChatStreamEvent},
};
use groqai::{AudioTranscriptionRequest, GroqClient};
use genai::Client as GenaiClient;
use genai::chat::ChatMessage;
use genai::chat::ChatRequest;
use genai::chat::ChatStreamEvent;
use groqai::AudioTranscriptionRequest;
use groqai::GroqClient;

/// Thin facade over GenAI and Groq clients.
#[derive(Clone)]
Expand Down
8 changes: 7 additions & 1 deletion codex-rs/client/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,13 @@ async fn main() -> anyhow::Result<()> {
}
match serde_json::from_str::<Submission>(&line) {
Ok(sub) => {
let s = serde_json::to_string(&sub).unwrap();
let s = match serde_json::to_string(&sub) {
Ok(s) => s,
Err(e) => {
eprintln!("invalid submission: {e}");
continue;
}
};
if write_half.write_all(s.as_bytes()).await.is_err() {
break;
}
Expand Down
1 change: 1 addition & 0 deletions codex-rs/core/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@ tree-sitter-bash = "0.25.0"
uuid = { version = "1", features = ["serde", "v4"] }
whoami = "1.6.1"
wildmatch = "2.4.0"
async-trait = "0.1"

# Optional speech dependencies for the offline voice stack.
kokoro-tts = { version = "0.2.8", optional = true }
Expand Down
15 changes: 9 additions & 6 deletions codex-rs/core/src/autogen.rs
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
use std::process::Command;

use anyhow::{Context, Result, anyhow};
use anyhow::Context;
use anyhow::Result;
use anyhow::anyhow;
use serde_json::json;

use crate::{ModelProviderInfo, create_oss_provider_with_base_url};
use crate::ModelProviderInfo;
use crate::create_oss_provider_with_base_url;

/// Integration helpers for invoking Python's AutoGen framework.
///
Expand Down Expand Up @@ -76,10 +79,10 @@ pub fn run_autogen_with_provider(
entry["base_url"] = json!(base_url);
}

if let Some(env_key) = &provider.env_key {
if let Ok(value) = std::env::var(env_key) {
entry["api_key"] = json!(value);
}
if let Some(env_key) = &provider.env_key
&& let Ok(value) = std::env::var(env_key)
{
entry["api_key"] = json!(value);
}

let config_list = json!([entry]);
Expand Down
77 changes: 66 additions & 11 deletions codex-rs/core/src/client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ use crate::error::CodexErr;
use crate::error::Result;
use crate::error::UsageLimitReachedError;
use crate::flags::CODEX_RS_SSE_FIXTURE;
use crate::model_adapter::ModelAdapter;
use crate::model_family::ModelFamily;
use crate::model_provider_info::ModelProviderInfo;
use crate::model_provider_info::WireApi;
Expand All @@ -43,7 +44,8 @@ use crate::util::backoff;
use codex_protocol::config_types::ReasoningEffort as ReasoningEffortConfig;
use codex_protocol::config_types::ReasoningSummary as ReasoningSummaryConfig;
use codex_protocol::models::ResponseItem;
use std::sync::{Arc, Mutex};
use std::sync::Arc;
use std::sync::Mutex;

#[derive(Debug, Deserialize)]
struct ErrorResponse {
Expand All @@ -69,6 +71,7 @@ pub struct ModelClient {
session_id: Uuid,
effort: ReasoningEffortConfig,
summary: ReasoningSummaryConfig,
adapter: Option<Arc<dyn ModelAdapter>>,
}

impl ModelClient {
Expand All @@ -79,15 +82,36 @@ impl ModelClient {
effort: ReasoningEffortConfig,
summary: ReasoningSummaryConfig,
session_id: Uuid,
) -> Self {
Self::new_with_adapter(
config,
auth_manager,
provider,
effort,
summary,
session_id,
None,
)
}

pub fn new_with_adapter(
config: Arc<Config>,
auth_manager: Option<Arc<AuthManager>>,
provider: ModelProviderInfo,
effort: ReasoningEffortConfig,
summary: ReasoningSummaryConfig,
session_id: Uuid,
adapter: Option<Arc<dyn ModelAdapter>>,
) -> Self {
Self {
config,
auth_manager,
client: reqwest::Client::new(),
provider: Arc::new(Mutex::new(provider)),
provider: Arc::new(Mutex::new(provider)),
session_id,
effort,
summary,
adapter,
}
}

Expand All @@ -105,11 +129,20 @@ impl ModelClient {
/// the provider config. Public callers always invoke `stream()` – the
/// specialised helpers are private to avoid accidental misuse.
pub async fn stream(&self, prompt: &Prompt) -> Result<ResponseStream> {
let wire_api = { self.provider.lock().unwrap().wire_api };
let wire_api = {
self.provider
.lock()
.unwrap_or_else(|e| e.into_inner())
.wire_api
};
match wire_api {
WireApi::Responses => self.stream_responses(prompt).await,
WireApi::Chat => {
let provider = self.provider.lock().unwrap().clone();
let provider = self
.provider
.lock()
.unwrap_or_else(|e| e.into_inner())
.clone();
// Create the raw streaming connection first.
let response_stream = stream_chat_completions(
prompt,
Expand Down Expand Up @@ -144,12 +177,27 @@ impl ModelClient {

Ok(ResponseStream { rx_event: rx })
}
WireApi::Custom => {
let adapter = self.adapter.as_ref().ok_or(CodexErr::NoModelAdapter)?;
let provider = self
.provider
.lock()
.unwrap_or_else(|e| e.into_inner())
.clone();
adapter
.stream(prompt, &self.config.model_family, &self.client, &provider)
.await
}
}
}

/// Implementation for the OpenAI *Responses* experimental API.
async fn stream_responses(&self, prompt: &Prompt) -> Result<ResponseStream> {
let provider = self.provider.lock().unwrap().clone();
let provider = self
.provider
.lock()
.unwrap_or_else(|e| e.into_inner())
.clone();
if let Some(path) = &*CODEX_RS_SSE_FIXTURE {
// short circuit for tests
warn!(path, "Streaming from fixture");
Expand Down Expand Up @@ -241,9 +289,7 @@ impl ModelClient {
serde_json::to_string(&payload)?
);

let mut req_builder = provider
.create_request_builder(&self.client, &auth)
.await?;
let mut req_builder = provider.create_request_builder(&self.client, &auth).await?;

req_builder = req_builder
.header("OpenAI-Beta", "responses=experimental")
Expand Down Expand Up @@ -367,7 +413,10 @@ impl ModelClient {
}

pub fn get_provider(&self) -> ModelProviderInfo {
self.provider.lock().unwrap().clone()
self.provider
.lock()
.unwrap_or_else(|e| e.into_inner())
.clone()
}

/// Returns the currently configured model slug.
Expand Down Expand Up @@ -397,12 +446,18 @@ impl ModelClient {
/// Rotate to the next API key for the current provider. Returns `true`
/// if a rotation occurred.
pub fn rotate_api_key(&self) -> bool {
self.provider.lock().unwrap().rotate_api_key()
self.provider
.lock()
.unwrap_or_else(|e| e.into_inner())
.rotate_api_key()
}

/// Reset provider API key rotation back to the first key.
pub fn reset_api_key_rotation(&self) {
self.provider.lock().unwrap().reset_api_key_rotation();
self.provider
.lock()
.unwrap_or_else(|e| e.into_inner())
.reset_api_key_rotation();
}
}

Expand Down
2 changes: 1 addition & 1 deletion codex-rs/core/src/client_common.rs
Original file line number Diff line number Diff line change
Expand Up @@ -175,7 +175,7 @@ pub(crate) fn create_text_param_for_request(
})
}

pub(crate) struct ResponseStream {
pub struct ResponseStream {
pub(crate) rx_event: mpsc::Receiver<Result<ResponseEvent>>,
}

Expand Down
10 changes: 7 additions & 3 deletions codex-rs/core/src/codex.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2951,16 +2951,20 @@ fn convert_call_tool_result_to_function_call_output_payload(
#[cfg(test)]
mod tests {
use super::*;
use crate::config::{Config, ConfigOverrides, ConfigToml};
use codex_login::{AuthManager, CodexAuth};
use crate::config::Config;
use crate::config::ConfigOverrides;
use crate::config::ConfigToml;
use codex_login::AuthManager;
use codex_login::CodexAuth;
use mcp_types::ContentBlock;
use mcp_types::TextContent;
use pretty_assertions::assert_eq;
use serde_json::json;
use std::collections::HashMap;
use std::sync::Arc;
use std::time::Duration as StdDuration;
use tempfile::{TempDir, tempdir};
use tempfile::TempDir;
use tempfile::tempdir;
use tokio::time;

fn text_block(s: &str) -> ContentBlock {
Expand Down
5 changes: 5 additions & 0 deletions codex-rs/core/src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,11 @@ pub enum CodexErr {
#[error("internal error; agent loop died unexpectedly")]
InternalAgentDied,

/// Returned when a model provider declares a custom wire protocol but no
/// adapter was supplied when constructing the [`ModelClient`].
#[error("no model adapter registered for provider")]
NoModelAdapter,

/// Sandbox error
#[error("sandbox error: {0}")]
Sandbox(#[from] SandboxErr),
Expand Down
3 changes: 3 additions & 0 deletions codex-rs/core/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -30,8 +30,11 @@ pub mod landlock;
mod mcp_connection_manager;
mod mcp_tool_call;
mod message_history;
mod model_adapter;
mod model_provider_info;
pub mod parse_command;
pub use model_adapter::ModelAdapter;
pub use model_adapter::OpenAiChatAdapter;
pub use model_provider_info::BUILT_IN_OSS_MODEL_PROVIDER_ID;
pub use model_provider_info::ModelProviderInfo;
pub use model_provider_info::WireApi;
Expand Down
Loading
Loading