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
4 changes: 4 additions & 0 deletions codex-rs/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion codex-rs/app-server/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ codex-analytics = { workspace = true }
codex-agent-extension = { workspace = true }
codex-arg0 = { workspace = true }
codex-cloud-config = { workspace = true }
codex-code-mode = { workspace = true }
codex-config = { workspace = true }
codex-connectors = { workspace = true }
codex-core = { workspace = true }
Expand Down Expand Up @@ -105,6 +106,7 @@ tokio = { workspace = true, features = [
tokio-util = { workspace = true }
tracing = { workspace = true, features = ["log"] }
tracing-subscriber = { workspace = true, features = ["env-filter", "fmt", "json"] }
url = { workspace = true }
uuid = { workspace = true, features = ["serde", "v7"] }

[target.'cfg(windows)'.dependencies]
Expand Down Expand Up @@ -138,5 +140,4 @@ shlex = { workspace = true }
tar = { workspace = true }
tokio-tungstenite = { workspace = true }
tracing-opentelemetry = { workspace = true }
url = { workspace = true }
wiremock = { workspace = true }
2 changes: 2 additions & 0 deletions codex-rs/app-server/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,8 @@ When running with `--listen ws://IP:PORT`, the same listener also serves basic H

Websocket transport is currently experimental and unsupported. Do not rely on it for production workloads.

Pass `--code-mode-host wss://HOST/PATH` to connect this app-server process to a remote code-mode host instead of starting a local host. This outbound connection is independent of `--listen` and is shared by the process's threads. Use `ws://` for a local code-mode host.

The unix socket transport is intended for local app-server control-plane clients. `codex app-server proxy`
opens exactly one raw stream connection to `$CODEX_HOME/app-server-control/app-server-control.sock`
by default, or to `--sock PATH` when provided, and proxies bytes between that socket and stdin/stdout.
Expand Down
48 changes: 48 additions & 0 deletions codex-rs/app-server/src/code_mode_host.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
use clap::Args;
use url::Url;

/// Selects the code-mode host for a single app-server process.
#[derive(Args, Debug, Clone, Default, PartialEq, Eq)]
pub struct AppServerCodeModeHostArgs {
/// Connect to a remote code-mode host instead of starting a local host.
#[arg(
long = "code-mode-host",
value_name = "WS_URL",
value_parser = parse_websocket_url
)]
pub code_mode_host: Option<Url>,
}

/// Process-scoped transport used to reach the code-mode host.
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub enum CodeModeHostTransport {
/// Start and own the default local code-mode host.
#[default]
Local,
/// Share a connection to the specified remote code-mode host.
WebSocket(Url),
}

impl From<AppServerCodeModeHostArgs> for CodeModeHostTransport {
fn from(args: AppServerCodeModeHostArgs) -> Self {
match args.code_mode_host {
Some(url) => Self::WebSocket(url),
None => Self::Local,
}
}
}

fn parse_websocket_url(value: &str) -> Result<Url, String> {
let url = Url::parse(value).map_err(|error| format!("invalid websocket URL: {error}"))?;
if !matches!(url.scheme(), "ws" | "wss") || url.host_str().is_none() {
return Err("code-mode host URL must use ws:// or wss:// with a host".to_string());
}
if url.fragment().is_some() {
return Err("code-mode host URL must not contain a fragment".to_string());
}
Ok(url)
}

#[cfg(test)]
#[path = "code_mode_host_tests.rs"]
mod tests;
51 changes: 51 additions & 0 deletions codex-rs/app-server/src/code_mode_host_tests.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
use super::AppServerCodeModeHostArgs;
use super::CodeModeHostTransport;
use super::parse_websocket_url;
use pretty_assertions::assert_eq;
use url::Url;

#[test]
fn websocket_host_accepts_local_and_secure_endpoints() {
for endpoint in ["ws://127.0.0.1:8765", "wss://example.test/code-mode"] {
assert_eq!(
parse_websocket_url(endpoint),
Ok(Url::parse(endpoint).expect("test endpoint should parse"))
);
}
}

#[test]
fn websocket_host_rejects_invalid_endpoints() {
for endpoint in [
"http://127.0.0.1:8765",
"https://example.test/code-mode",
"ws://",
"not a websocket",
"wss://example.test/code-mode#fragment",
] {
assert!(
parse_websocket_url(endpoint).is_err(),
"invalid code-mode host endpoint should be rejected: {endpoint}"
);
}
}

#[test]
fn omitted_websocket_host_selects_local_transport() {
assert_eq!(
CodeModeHostTransport::from(AppServerCodeModeHostArgs::default()),
CodeModeHostTransport::Local
);
}

#[test]
fn explicit_websocket_host_selects_remote_transport() {
let url = Url::parse("wss://example.test/code-mode").expect("test endpoint should parse");

assert_eq!(
CodeModeHostTransport::from(AppServerCodeModeHostArgs {
code_mode_host: Some(url.clone()),
}),
CodeModeHostTransport::WebSocket(url)
);
}
1 change: 1 addition & 0 deletions codex-rs/app-server/src/in_process.rs
Original file line number Diff line number Diff line change
Expand Up @@ -450,6 +450,7 @@ async fn start_uninitialized(args: InProcessStartArgs) -> IoResult<InProcessClie
session_source: args.session_source,
auth_manager,
installation_id,
code_mode_session_provider: None,
rpc_transport: AppServerRpcTransport::InProcess,
remote_control_handle: None,
plugin_startup_tasks: crate::PluginStartupTasks::Start,
Expand Down
29 changes: 28 additions & 1 deletion codex-rs/app-server/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@
#![deny(clippy::print_stdout, clippy::print_stderr)]

use codex_arg0::Arg0DispatchPaths;
use codex_code_mode::CodeModeSessionProvider;
use codex_code_mode::WebSocketCodeModeSessionProvider;
use codex_config::ConfigLayerStackOrdering;
use codex_config::LoaderOverrides;
use codex_config::NoopThreadConfigLoader;
Expand Down Expand Up @@ -60,6 +62,7 @@ use codex_core::check_execpolicy_for_warnings;
use codex_core::config::find_codex_home;
use codex_exec_server::EnvironmentManager;
use codex_exec_server::ExecServerRuntimePaths;
use codex_features::Feature;
use codex_feedback::CodexFeedback;
use codex_protocol::protocol::SessionSource;
use codex_rollout::state_db as rollout_state_db;
Expand All @@ -85,6 +88,7 @@ mod app_server_tracing;
mod attestation;
mod auth_mode;
mod bespoke_event_handling;
mod code_mode_host;
mod command_exec;
mod config_layer;
mod config_manager;
Expand Down Expand Up @@ -116,6 +120,8 @@ mod thread_state;
mod thread_status;
mod transport;

pub use crate::code_mode_host::AppServerCodeModeHostArgs;
pub use crate::code_mode_host::CodeModeHostTransport;
pub use crate::error_code::INPUT_TOO_LARGE_ERROR_CODE;
pub use crate::error_code::INVALID_PARAMS_ERROR_CODE;
pub use crate::transport::AppServerTransport;
Expand Down Expand Up @@ -428,8 +434,9 @@ pub enum PluginStartupTasks {
Skip,
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct AppServerRuntimeOptions {
pub code_mode_host_transport: CodeModeHostTransport,
pub plugin_startup_tasks: PluginStartupTasks,
pub remote_control_startup_mode: RemoteControlStartupMode,
pub install_shutdown_signal_handler: bool,
Expand All @@ -438,6 +445,7 @@ pub struct AppServerRuntimeOptions {
impl Default for AppServerRuntimeOptions {
fn default() -> Self {
Self {
code_mode_host_transport: CodeModeHostTransport::Local,
plugin_startup_tasks: PluginStartupTasks::Start,
remote_control_startup_mode: RemoteControlStartupMode::ResolvePersisted,
install_shutdown_signal_handler: true,
Expand Down Expand Up @@ -534,6 +542,24 @@ pub async fn run_main_with_transport_options(
})?
}
};
let code_mode_session_provider: Option<Arc<dyn CodeModeSessionProvider>> =
match &runtime_options.code_mode_host_transport {
CodeModeHostTransport::Local => None,
CodeModeHostTransport::WebSocket(url) => {
if !config.features.enabled(Feature::CodeModeHost) {
return Err(std::io::Error::new(
ErrorKind::InvalidInput,
"remote code-mode host requires the code_mode_host feature to be enabled",
));
}
Some(Arc::new(
WebSocketCodeModeSessionProvider::with_http_client_factory(
url.to_string(),
config.http_client_factory(),
),
))
}
};
let environment_manager = if ignore_user_config {
EnvironmentManager::from_env(Some(local_runtime_paths), config.http_client_factory()).await
} else {
Expand Down Expand Up @@ -869,6 +895,7 @@ pub async fn run_main_with_transport_options(
session_source,
auth_manager,
installation_id,
code_mode_session_provider,
rpc_transport: analytics_rpc_transport(&transport),
remote_control_handle: Some(remote_control_handle.clone()),
plugin_startup_tasks: runtime_options.plugin_startup_tasks,
Expand Down
10 changes: 9 additions & 1 deletion codex-rs/app-server/src/main.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
use clap::Parser;
use codex_app_server::AppServerCodeModeHostArgs;
use codex_app_server::AppServerRuntimeOptions;
use codex_app_server::AppServerTransport;
use codex_app_server::AppServerWebsocketAuthArgs;
Expand All @@ -22,6 +23,9 @@ struct AppServerArgs {
#[command(flatten)]
config_overrides: CliConfigOverrides,

#[command(flatten)]
code_mode_host: AppServerCodeModeHostArgs,

/// Transport endpoint URL. Supported values: `stdio://` (default),
/// `unix://`, `unix://PATH`, `ws://IP:PORT`, `off`.
#[arg(
Expand Down Expand Up @@ -63,6 +67,7 @@ fn main() -> anyhow::Result<()> {
arg0_dispatch_or_else(move |arg0_paths: Arg0DispatchPaths| async move {
let AppServerArgs {
config_overrides,
code_mode_host,
listen,
session_source,
auth,
Expand All @@ -80,7 +85,10 @@ fn main() -> anyhow::Result<()> {
};
let transport = listen;
let auth = auth.try_into_settings()?;
let mut runtime_options = AppServerRuntimeOptions::default();
let mut runtime_options = AppServerRuntimeOptions {
code_mode_host_transport: code_mode_host.into(),
..Default::default()
};
#[cfg(debug_assertions)]
if disable_plugin_startup_tasks_for_tests {
runtime_options.plugin_startup_tasks = PluginStartupTasks::Skip;
Expand Down
36 changes: 36 additions & 0 deletions codex-rs/app-server/src/main_tests.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
use super::AppServerArgs;
use clap::Parser;
use codex_app_server::AppServerTransport;
use pretty_assertions::assert_eq;
use toml::Value as TomlValue;
use url::Url;

#[test]
fn app_server_accepts_cli_config_overrides() {
Expand Down Expand Up @@ -35,3 +37,37 @@ fn app_server_accepts_cli_config_overrides() {
]
);
}

#[test]
fn app_server_accepts_process_scoped_code_mode_host() {
let args = AppServerArgs::try_parse_from([
"codex-app-server",
"--code-mode-host",
"wss://example.test/code-mode",
"--listen",
"off",
])
.expect("parse app-server args");

assert_eq!(
args.code_mode_host.code_mode_host,
Some(Url::parse("wss://example.test/code-mode").expect("test endpoint should parse"))
);
assert_eq!(args.listen, AppServerTransport::Off);
assert_eq!(args.config_overrides.raw_overrides, Vec::<String>::new());
}

#[test]
fn app_server_rejects_invalid_code_mode_host() {
for endpoint in [
"http://127.0.0.1:8765",
"ws://",
"wss://example.test/code-mode#fragment",
] {
let error =
AppServerArgs::try_parse_from(["codex-app-server", "--code-mode-host", endpoint])
.expect_err("invalid code-mode host endpoint should fail startup argument parsing");

assert_eq!(error.kind(), clap::error::ErrorKind::ValueValidation);
}
}
11 changes: 9 additions & 2 deletions codex-rs/app-server/src/message_processor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ use codex_app_server_protocol::JSONRPCResponse;
use codex_app_server_protocol::experimental_required_message;
use codex_arg0::Arg0DispatchPaths;
use codex_chatgpt::workspace_settings;
use codex_code_mode::CodeModeSessionProvider;
use codex_core::ThreadManager;
use codex_core::config::Config;
use codex_exec_server::EnvironmentManager;
Expand Down Expand Up @@ -210,6 +211,7 @@ pub(crate) struct MessageProcessorArgs {
pub(crate) session_source: SessionSource,
pub(crate) auth_manager: Arc<AuthManager>,
pub(crate) installation_id: String,
pub(crate) code_mode_session_provider: Option<Arc<dyn CodeModeSessionProvider>>,
pub(crate) rpc_transport: AppServerRpcTransport,
pub(crate) remote_control_handle: Option<RemoteControlHandle>,
pub(crate) plugin_startup_tasks: crate::PluginStartupTasks,
Expand All @@ -233,6 +235,7 @@ impl MessageProcessor {
session_source,
auth_manager,
installation_id,
code_mode_session_provider,
rpc_transport,
remote_control_handle,
plugin_startup_tasks,
Expand All @@ -253,7 +256,7 @@ impl MessageProcessor {
);
let goal_service = Arc::new(GoalService::new());
let thread_manager = Arc::new_cyclic(|thread_manager| {
ThreadManager::new(
let manager = ThreadManager::new(
config.as_ref(),
auth_manager.clone(),
codex_core::build_models_manager(config.as_ref(), auth_manager.clone()),
Expand Down Expand Up @@ -294,7 +297,11 @@ impl MessageProcessor {
outgoing.clone(),
thread_state_manager.clone(),
)),
)
);
match code_mode_session_provider {
Some(provider) => manager.with_code_mode_session_provider(provider),
None => manager,
}
});
let models_manager = thread_manager.get_models_manager();
let models_refresh_worker =
Expand Down
1 change: 1 addition & 0 deletions codex-rs/app-server/src/message_processor_tracing_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -264,6 +264,7 @@ async fn build_test_processor(
session_source: SessionSource::VSCode,
auth_manager,
installation_id: "11111111-1111-4111-8111-111111111111".to_string(),
code_mode_session_provider: None,
rpc_transport: AppServerRpcTransport::Stdio,
remote_control_handle: None,
plugin_startup_tasks: crate::PluginStartupTasks::Start,
Expand Down
Loading
Loading