mirror of
https://github.com/openai/codex.git
synced 2026-05-17 17:53:06 +00:00
## Why Desktop and mobile Codex clients need a machine-readable way to bootstrap and manage `codex app-server` on remote machines reached over SSH. The same flow is also useful for bringing up app-server with `remote_control` enabled on a fresh developer machine and keeping that managed install current without requiring a human session. ## What changed - add the new experimental `codex-app-server-daemon` crate and wire it into `codex app-server daemon` lifecycle commands: `start`, `restart`, `stop`, `version`, and `bootstrap` - add explicit `enable-remote-control` and `disable-remote-control` commands that persist the launch setting and restart a running managed daemon so the change takes effect immediately - emit JSON success responses for daemon commands so remote callers can consume them directly - support a Unix-only pidfile-backed detached backend for lifecycle management - assume the standalone `install.sh` layout for daemon-managed binaries and always launch `CODEX_HOME/packages/standalone/current/codex` - add bootstrap support for the standalone managed install plus a detached hourly updater loop - harden lifecycle management around concurrent operations, pidfile ownership, stale state cleanup, updater ownership, managed-binary preflight, Unix-only rejection, forced shutdown after the graceful window, and updater process-group tracking/cleanup - document the experimental Unix-only support boundary plus the standalone bootstrap/update flow in `codex-rs/app-server-daemon/README.md` ## Verification - `cargo test -p codex-app-server-daemon -p codex-cli` - live pid validation on `cb4`: `bootstrap --remote-control`, `restart`, `version`, `stop` ## Follow-up - Add updater self-refresh so the long-lived `pid-update-loop` can replace its own executable image after installing a newer managed Codex binary.
64 lines
1.9 KiB
Rust
64 lines
1.9 KiB
Rust
use std::path::Path;
|
|
|
|
use anyhow::Context;
|
|
use anyhow::Result;
|
|
use serde::Deserialize;
|
|
use serde::Serialize;
|
|
use tokio::fs;
|
|
|
|
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
|
|
#[serde(rename_all = "camelCase")]
|
|
pub(crate) struct DaemonSettings {
|
|
pub(crate) remote_control_enabled: bool,
|
|
}
|
|
|
|
impl DaemonSettings {
|
|
pub(crate) async fn load(path: &Path) -> Result<Self> {
|
|
let contents = match fs::read_to_string(path).await {
|
|
Ok(contents) => contents,
|
|
Err(err) if err.kind() == std::io::ErrorKind::NotFound => return Ok(Self::default()),
|
|
Err(err) => {
|
|
return Err(err)
|
|
.with_context(|| format!("failed to read daemon settings {}", path.display()));
|
|
}
|
|
};
|
|
|
|
serde_json::from_str(&contents)
|
|
.with_context(|| format!("failed to parse daemon settings {}", path.display()))
|
|
}
|
|
|
|
pub(crate) async fn save(&self, path: &Path) -> Result<()> {
|
|
if let Some(parent) = path.parent() {
|
|
fs::create_dir_all(parent).await.with_context(|| {
|
|
format!(
|
|
"failed to create daemon settings directory {}",
|
|
parent.display()
|
|
)
|
|
})?;
|
|
}
|
|
|
|
let contents = serde_json::to_vec_pretty(self).context("failed to serialize settings")?;
|
|
fs::write(path, contents)
|
|
.await
|
|
.with_context(|| format!("failed to write daemon settings {}", path.display()))
|
|
}
|
|
}
|
|
|
|
#[cfg(all(test, unix))]
|
|
mod tests {
|
|
use pretty_assertions::assert_eq;
|
|
|
|
use super::DaemonSettings;
|
|
|
|
#[test]
|
|
fn daemon_settings_use_camel_case_json() {
|
|
assert_eq!(
|
|
serde_json::to_string(&DaemonSettings {
|
|
remote_control_enabled: true,
|
|
})
|
|
.expect("serialize"),
|
|
r#"{"remoteControlEnabled":true}"#
|
|
);
|
|
}
|
|
}
|