mirror of
https://github.com/openai/codex.git
synced 2026-09-03 14:59:03 +00:00
app-server-daemon: route updater download through HTTP client factory
This commit is contained in:
3
codex-rs/Cargo.lock
generated
3
codex-rs/Cargo.lock
generated
@@ -2092,12 +2092,12 @@ dependencies = [
|
||||
"anyhow",
|
||||
"codex-app-server-protocol",
|
||||
"codex-app-server-transport",
|
||||
"codex-http-client",
|
||||
"codex-uds",
|
||||
"codex-utils-home-dir",
|
||||
"futures",
|
||||
"libc",
|
||||
"pretty_assertions",
|
||||
"reqwest 0.12.28",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"sha2 0.10.9",
|
||||
@@ -2347,6 +2347,7 @@ dependencies = [
|
||||
"codex-features",
|
||||
"codex-git-utils",
|
||||
"codex-home",
|
||||
"codex-http-client",
|
||||
"codex-install-context",
|
||||
"codex-login",
|
||||
"codex-mcp",
|
||||
|
||||
@@ -16,11 +16,11 @@ workspace = true
|
||||
anyhow = { workspace = true }
|
||||
codex-app-server-protocol = { workspace = true }
|
||||
codex-app-server-transport = { workspace = true }
|
||||
codex-http-client = { workspace = true }
|
||||
codex-utils-home-dir = { workspace = true }
|
||||
codex-uds = { workspace = true }
|
||||
futures = { workspace = true }
|
||||
libc = { workspace = true }
|
||||
reqwest = { workspace = true, features = ["rustls-tls"] }
|
||||
serde = { workspace = true, features = ["derive"] }
|
||||
serde_json = { workspace = true }
|
||||
sha2 = { workspace = true }
|
||||
|
||||
@@ -238,9 +238,11 @@ pub async fn set_remote_control(mode: RemoteControlMode) -> Result<RemoteControl
|
||||
Daemon::from_environment()?.set_remote_control(mode).await
|
||||
}
|
||||
|
||||
pub async fn run_pid_update_loop() -> Result<()> {
|
||||
pub async fn run_pid_update_loop(
|
||||
http_client_factory: codex_http_client::HttpClientFactory,
|
||||
) -> Result<()> {
|
||||
ensure_supported_platform()?;
|
||||
update_loop::run().await
|
||||
update_loop::run(http_client_factory).await
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
|
||||
@@ -11,6 +11,11 @@ use anyhow::Result;
|
||||
#[cfg(not(unix))]
|
||||
use anyhow::bail;
|
||||
#[cfg(unix)]
|
||||
use codex_http_client::ClientRouteClass;
|
||||
use codex_http_client::HttpClientFactory;
|
||||
#[cfg(unix)]
|
||||
use codex_http_client::RouteAwareClientPool;
|
||||
#[cfg(unix)]
|
||||
use futures::FutureExt;
|
||||
#[cfg(unix)]
|
||||
use std::os::unix::process::CommandExt;
|
||||
@@ -48,17 +53,20 @@ const INITIAL_UPDATE_DELAY: Duration = Duration::from_secs(5 * 60);
|
||||
const RESTART_RETRY_INTERVAL: Duration = Duration::from_millis(50);
|
||||
#[cfg(unix)]
|
||||
const UPDATE_INTERVAL: Duration = Duration::from_secs(60 * 60);
|
||||
#[cfg(unix)]
|
||||
const INSTALL_URL: &str = "https://chatgpt.com/codex/install.sh";
|
||||
|
||||
#[cfg(unix)]
|
||||
pub(crate) async fn run() -> Result<()> {
|
||||
pub(crate) async fn run(http_client_factory: HttpClientFactory) -> Result<()> {
|
||||
let mut terminate =
|
||||
signal(SignalKind::terminate()).context("failed to install updater shutdown handler")?;
|
||||
let running_updater_identity = current_updater_identity().await?;
|
||||
let http = RouteAwareClientPool::new(http_client_factory, ClientRouteClass::Other);
|
||||
if sleep_or_terminate(INITIAL_UPDATE_DELAY, &mut terminate).await {
|
||||
return Ok(());
|
||||
}
|
||||
loop {
|
||||
match update_once(&running_updater_identity, &mut terminate).await {
|
||||
match update_once(&http, &running_updater_identity, &mut terminate).await {
|
||||
Ok(UpdateLoopControl::Continue) | Err(_) => {}
|
||||
Ok(UpdateLoopControl::Stop) => return Ok(()),
|
||||
}
|
||||
@@ -69,7 +77,7 @@ pub(crate) async fn run() -> Result<()> {
|
||||
}
|
||||
|
||||
#[cfg(not(unix))]
|
||||
pub(crate) async fn run() -> Result<()> {
|
||||
pub(crate) async fn run(_http_client_factory: HttpClientFactory) -> Result<()> {
|
||||
bail!("pid-managed updater loop is unsupported on this platform")
|
||||
}
|
||||
|
||||
@@ -89,10 +97,11 @@ enum UpdateLoopControl {
|
||||
|
||||
#[cfg(unix)]
|
||||
async fn update_once(
|
||||
http: &RouteAwareClientPool,
|
||||
running_updater_identity: &ExecutableIdentity,
|
||||
terminate: &mut Signal,
|
||||
) -> Result<UpdateLoopControl> {
|
||||
install_latest_standalone().await?;
|
||||
install_latest_standalone(http).await?;
|
||||
|
||||
let daemon = Daemon::from_environment()?;
|
||||
let managed_codex_bin = resolved_managed_codex_bin(&daemon.managed_codex_bin).await?;
|
||||
@@ -154,15 +163,8 @@ pub(crate) fn reexec_managed_updater(managed_codex_bin: &std::path::Path) -> Res
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
async fn install_latest_standalone() -> Result<()> {
|
||||
let script = reqwest::get("https://chatgpt.com/codex/install.sh")
|
||||
.await
|
||||
.context("failed to fetch standalone Codex updater")?
|
||||
.error_for_status()
|
||||
.context("standalone Codex updater request failed")?
|
||||
.bytes()
|
||||
.await
|
||||
.context("failed to read standalone Codex updater")?;
|
||||
async fn install_latest_standalone(http: &RouteAwareClientPool) -> Result<()> {
|
||||
let script = fetch_installer_script(http).await?;
|
||||
|
||||
let mut child = Command::new("/bin/sh")
|
||||
.arg("-s")
|
||||
@@ -192,6 +194,54 @@ async fn install_latest_standalone() -> Result<()> {
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
async fn fetch_installer_script(http: &impl InstallerHttp) -> Result<Vec<u8>> {
|
||||
let response = http.get(INSTALL_URL).await?;
|
||||
if !(200..300).contains(&response.status) {
|
||||
anyhow::bail!(
|
||||
"standalone Codex updater request failed with status {}",
|
||||
response.status
|
||||
);
|
||||
}
|
||||
Ok(response.body)
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
struct InstallerResponse {
|
||||
status: u16,
|
||||
body: Vec<u8>,
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
trait InstallerHttp: Send + Sync {
|
||||
fn get<'a>(
|
||||
&'a self,
|
||||
url: &'a str,
|
||||
) -> impl std::future::Future<Output = Result<InstallerResponse>> + Send + 'a;
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
impl InstallerHttp for RouteAwareClientPool {
|
||||
async fn get(&self, url: &str) -> Result<InstallerResponse> {
|
||||
let response = self
|
||||
.client_for_url(url)
|
||||
.await
|
||||
.context("failed to configure standalone Codex updater client")?
|
||||
.get(url)
|
||||
.send()
|
||||
.await
|
||||
.context("failed to fetch standalone Codex updater")?;
|
||||
let status = response.status().as_u16();
|
||||
let body = response
|
||||
.bytes()
|
||||
.await
|
||||
.context("failed to read standalone Codex updater")?
|
||||
.to_vec();
|
||||
Ok(InstallerResponse { status, body })
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(all(test, unix))]
|
||||
#[path = "update_loop_tests.rs"]
|
||||
mod tests;
|
||||
|
||||
@@ -1,5 +1,11 @@
|
||||
use std::sync::Mutex;
|
||||
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
use super::INSTALL_URL;
|
||||
use super::InstallerHttp;
|
||||
use super::InstallerResponse;
|
||||
use super::fetch_installer_script;
|
||||
use super::update_modes_for_identities;
|
||||
use crate::RestartMode;
|
||||
use crate::UpdaterRefreshMode;
|
||||
@@ -29,3 +35,66 @@ fn changed_updater_forces_refresh_even_when_version_may_match() {
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn installer_fetch_uses_exact_url_and_preserves_bytes() {
|
||||
let script = b"#!/bin/sh\nprintf 'update bytes'\n".to_vec();
|
||||
let http = FakeInstallerHttp::new(InstallerResponse {
|
||||
status: 200,
|
||||
body: script.clone(),
|
||||
});
|
||||
|
||||
assert_eq!(
|
||||
fetch_installer_script(&http)
|
||||
.await
|
||||
.expect("installer fetch should succeed"),
|
||||
script
|
||||
);
|
||||
assert_eq!(http.requested_urls(), vec![INSTALL_URL.to_string()]);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn installer_fetch_rejects_non_success_status() {
|
||||
let http = FakeInstallerHttp::new(InstallerResponse {
|
||||
status: 503,
|
||||
body: b"unavailable".to_vec(),
|
||||
});
|
||||
|
||||
let error = fetch_installer_script(&http)
|
||||
.await
|
||||
.expect_err("non-success response should fail");
|
||||
|
||||
assert!(error.to_string().contains("503"));
|
||||
assert_eq!(http.requested_urls(), vec![INSTALL_URL.to_string()]);
|
||||
}
|
||||
|
||||
struct FakeInstallerHttp {
|
||||
response: InstallerResponse,
|
||||
requested_urls: Mutex<Vec<String>>,
|
||||
}
|
||||
|
||||
impl FakeInstallerHttp {
|
||||
fn new(response: InstallerResponse) -> Self {
|
||||
Self {
|
||||
response,
|
||||
requested_urls: Mutex::new(Vec::new()),
|
||||
}
|
||||
}
|
||||
|
||||
fn requested_urls(&self) -> Vec<String> {
|
||||
self.requested_urls
|
||||
.lock()
|
||||
.unwrap_or_else(std::sync::PoisonError::into_inner)
|
||||
.clone()
|
||||
}
|
||||
}
|
||||
|
||||
impl InstallerHttp for FakeInstallerHttp {
|
||||
async fn get(&self, url: &str) -> anyhow::Result<InstallerResponse> {
|
||||
self.requested_urls
|
||||
.lock()
|
||||
.unwrap_or_else(std::sync::PoisonError::into_inner)
|
||||
.push(url.to_string());
|
||||
Ok(self.response.clone())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -34,6 +34,7 @@ codex-config = { workspace = true }
|
||||
codex-core = { workspace = true }
|
||||
codex-core-plugins = { workspace = true }
|
||||
codex-home = { workspace = true }
|
||||
codex-http-client = { workspace = true }
|
||||
codex-exec = { workspace = true }
|
||||
codex-exec-server = { workspace = true }
|
||||
codex-execpolicy = { workspace = true }
|
||||
|
||||
@@ -1182,7 +1182,16 @@ async fn cli_main(
|
||||
print_app_server_daemon_output(AppServerLifecycleCommand::Version).await?;
|
||||
}
|
||||
AppServerDaemonSubcommand::PidUpdateLoop => {
|
||||
codex_app_server_daemon::run_pid_update_loop().await?;
|
||||
let cli_overrides = root_config_overrides
|
||||
.parse_overrides()
|
||||
.map_err(anyhow::Error::msg)?;
|
||||
let config = ConfigBuilder::default()
|
||||
.cli_overrides(cli_overrides)
|
||||
.build()
|
||||
.await
|
||||
.map_err(anyhow::Error::from);
|
||||
let http_client_factory = updater_http_client_factory(config);
|
||||
codex_app_server_daemon::run_pid_update_loop(http_client_factory).await?;
|
||||
}
|
||||
},
|
||||
Some(AppServerSubcommand::Proxy(proxy_cli)) => {
|
||||
@@ -2205,6 +2214,20 @@ async fn print_app_server_daemon_output(command: AppServerLifecycleCommand) -> a
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn updater_http_client_factory(
|
||||
config: anyhow::Result<codex_core::config::Config>,
|
||||
) -> codex_http_client::HttpClientFactory {
|
||||
match config {
|
||||
Ok(config) => config.http_client_factory(),
|
||||
Err(error) => {
|
||||
eprintln!("warning: failed to load updater network configuration: {error}");
|
||||
codex_http_client::HttpClientFactory::new(
|
||||
codex_http_client::OutboundProxyPolicy::ReqwestDefault,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn print_app_server_remote_control_output(
|
||||
mode: AppServerRemoteControlMode,
|
||||
) -> anyhow::Result<()> {
|
||||
@@ -2501,6 +2524,34 @@ mod tests {
|
||||
use codex_tui::TokenUsage;
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
#[tokio::test]
|
||||
async fn updater_http_client_factory_honors_respect_system_proxy() {
|
||||
let codex_home = tempfile::tempdir().expect("temporary Codex home");
|
||||
let config = ConfigBuilder::default()
|
||||
.codex_home(codex_home.path().to_path_buf())
|
||||
.cli_overrides(vec![(
|
||||
"features.respect_system_proxy".to_string(),
|
||||
toml::Value::Boolean(true),
|
||||
)])
|
||||
.build()
|
||||
.await
|
||||
.expect("config should load");
|
||||
|
||||
assert_eq!(
|
||||
updater_http_client_factory(Ok(config)).outbound_proxy_policy(),
|
||||
codex_http_client::OutboundProxyPolicy::RespectSystemProxy
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn updater_http_client_factory_falls_back_when_config_load_fails() {
|
||||
assert_eq!(
|
||||
updater_http_client_factory(Err(anyhow::anyhow!("invalid config")))
|
||||
.outbound_proxy_policy(),
|
||||
codex_http_client::OutboundProxyPolicy::ReqwestDefault
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn exec_server_remote_auth_accepts_api_key_auth() {
|
||||
let auth = CodexAuth::from_api_key("sk-test");
|
||||
|
||||
@@ -242,7 +242,6 @@ deny = [
|
||||
"codex-agent-identity",
|
||||
"codex-api",
|
||||
"codex-app-server",
|
||||
"codex-app-server-daemon",
|
||||
"codex-core",
|
||||
"codex-core-plugins",
|
||||
"codex-exec-server",
|
||||
|
||||
Reference in New Issue
Block a user