mirror of
https://github.com/openai/codex.git
synced 2026-05-24 04:54:52 +00:00
[daemon] Add app-server daemon lifecycle management (#20718)
## 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.
This commit is contained in:
committed by
GitHub
parent
faa5d4a5e2
commit
0c8d42525e
@@ -3,6 +3,9 @@ use clap::CommandFactory;
|
||||
use clap::Parser;
|
||||
use clap_complete::Shell;
|
||||
use clap_complete::generate;
|
||||
use codex_app_server_daemon::BootstrapOptions as AppServerBootstrapOptions;
|
||||
use codex_app_server_daemon::LifecycleCommand as AppServerLifecycleCommand;
|
||||
use codex_app_server_daemon::RemoteControlMode as AppServerRemoteControlMode;
|
||||
use codex_arg0::Arg0DispatchPaths;
|
||||
use codex_arg0::arg0_dispatch_or_else;
|
||||
use codex_chatgpt::apply_command::ApplyCommand;
|
||||
@@ -469,6 +472,9 @@ struct ExecServerCommand {
|
||||
#[derive(Debug, clap::Subcommand)]
|
||||
#[allow(clippy::enum_variant_names)]
|
||||
enum AppServerSubcommand {
|
||||
/// Manage the local app-server daemon.
|
||||
Daemon(AppServerDaemonCommand),
|
||||
|
||||
/// Proxy stdio bytes to the running app-server control socket.
|
||||
Proxy(AppServerProxyCommand),
|
||||
|
||||
@@ -483,6 +489,40 @@ enum AppServerSubcommand {
|
||||
GenerateInternalJsonSchema(GenerateInternalJsonSchemaCommand),
|
||||
}
|
||||
|
||||
#[derive(Debug, Args)]
|
||||
struct AppServerDaemonCommand {
|
||||
#[command(subcommand)]
|
||||
subcommand: AppServerDaemonSubcommand,
|
||||
}
|
||||
|
||||
#[derive(Debug, clap::Subcommand)]
|
||||
enum AppServerDaemonSubcommand {
|
||||
/// Install durable local app-server management for SSH-driven use.
|
||||
Bootstrap(AppServerBootstrapCommand),
|
||||
|
||||
/// Start the local app server daemon if it is not already running.
|
||||
Start,
|
||||
|
||||
/// Restart the local app server daemon.
|
||||
Restart,
|
||||
|
||||
/// Enable remote_control for future starts and a currently running managed daemon.
|
||||
EnableRemoteControl,
|
||||
|
||||
/// Disable remote_control for future starts and a currently running managed daemon.
|
||||
DisableRemoteControl,
|
||||
|
||||
/// Stop the local app server daemon.
|
||||
Stop,
|
||||
|
||||
/// Print local CLI and running app-server versions as JSON.
|
||||
Version,
|
||||
|
||||
/// [internal] Run the detached pid-backed standalone updater loop.
|
||||
#[clap(hide = true)]
|
||||
PidUpdateLoop,
|
||||
}
|
||||
|
||||
#[derive(Debug, Args)]
|
||||
struct AppServerProxyCommand {
|
||||
/// Path to the app-server Unix domain socket to connect to.
|
||||
@@ -490,6 +530,13 @@ struct AppServerProxyCommand {
|
||||
socket_path: Option<AbsolutePathBuf>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Args)]
|
||||
struct AppServerBootstrapCommand {
|
||||
/// Launch the managed app-server with remote_control enabled.
|
||||
#[arg(long = "remote-control")]
|
||||
remote_control: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Args)]
|
||||
struct GenerateTsCommand {
|
||||
/// Output directory where .ts files will be written
|
||||
@@ -875,6 +922,41 @@ async fn cli_main(arg0_paths: Arg0DispatchPaths) -> anyhow::Result<()> {
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
Some(AppServerSubcommand::Daemon(daemon_cli)) => match daemon_cli.subcommand {
|
||||
AppServerDaemonSubcommand::Start => {
|
||||
print_app_server_daemon_output(AppServerLifecycleCommand::Start).await?;
|
||||
}
|
||||
AppServerDaemonSubcommand::Bootstrap(bootstrap_cli) => {
|
||||
let output =
|
||||
codex_app_server_daemon::bootstrap(AppServerBootstrapOptions {
|
||||
remote_control_enabled: bootstrap_cli.remote_control,
|
||||
})
|
||||
.await?;
|
||||
println!("{}", serde_json::to_string(&output)?);
|
||||
}
|
||||
AppServerDaemonSubcommand::Restart => {
|
||||
print_app_server_daemon_output(AppServerLifecycleCommand::Restart).await?;
|
||||
}
|
||||
AppServerDaemonSubcommand::EnableRemoteControl => {
|
||||
print_app_server_remote_control_output(AppServerRemoteControlMode::Enabled)
|
||||
.await?;
|
||||
}
|
||||
AppServerDaemonSubcommand::DisableRemoteControl => {
|
||||
print_app_server_remote_control_output(
|
||||
AppServerRemoteControlMode::Disabled,
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
AppServerDaemonSubcommand::Stop => {
|
||||
print_app_server_daemon_output(AppServerLifecycleCommand::Stop).await?;
|
||||
}
|
||||
AppServerDaemonSubcommand::Version => {
|
||||
print_app_server_daemon_output(AppServerLifecycleCommand::Version).await?;
|
||||
}
|
||||
AppServerDaemonSubcommand::PidUpdateLoop => {
|
||||
codex_app_server_daemon::run_pid_update_loop().await?;
|
||||
}
|
||||
},
|
||||
Some(AppServerSubcommand::Proxy(proxy_cli)) => {
|
||||
let socket_path = match proxy_cli.socket_path {
|
||||
Some(socket_path) => socket_path,
|
||||
@@ -1547,6 +1629,20 @@ fn reject_remote_mode_for_app_server_subcommand(
|
||||
) -> anyhow::Result<()> {
|
||||
let subcommand_name = match subcommand {
|
||||
None => "app-server",
|
||||
Some(AppServerSubcommand::Daemon(daemon)) => match daemon.subcommand {
|
||||
AppServerDaemonSubcommand::Bootstrap(_) => "app-server daemon bootstrap",
|
||||
AppServerDaemonSubcommand::Start => "app-server daemon start",
|
||||
AppServerDaemonSubcommand::Restart => "app-server daemon restart",
|
||||
AppServerDaemonSubcommand::EnableRemoteControl => {
|
||||
"app-server daemon enable-remote-control"
|
||||
}
|
||||
AppServerDaemonSubcommand::DisableRemoteControl => {
|
||||
"app-server daemon disable-remote-control"
|
||||
}
|
||||
AppServerDaemonSubcommand::Stop => "app-server daemon stop",
|
||||
AppServerDaemonSubcommand::Version => "app-server daemon version",
|
||||
AppServerDaemonSubcommand::PidUpdateLoop => "app-server daemon pid-update-loop",
|
||||
},
|
||||
Some(AppServerSubcommand::Proxy(_)) => "app-server proxy",
|
||||
Some(AppServerSubcommand::GenerateTs(_)) => "app-server generate-ts",
|
||||
Some(AppServerSubcommand::GenerateJsonSchema(_)) => "app-server generate-json-schema",
|
||||
@@ -1557,6 +1653,20 @@ fn reject_remote_mode_for_app_server_subcommand(
|
||||
reject_remote_mode_for_subcommand(remote, remote_auth_token_env, subcommand_name)
|
||||
}
|
||||
|
||||
async fn print_app_server_daemon_output(command: AppServerLifecycleCommand) -> anyhow::Result<()> {
|
||||
let output = codex_app_server_daemon::run(command).await?;
|
||||
println!("{}", serde_json::to_string(&output)?);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn print_app_server_remote_control_output(
|
||||
mode: AppServerRemoteControlMode,
|
||||
) -> anyhow::Result<()> {
|
||||
let output = codex_app_server_daemon::set_remote_control(mode).await?;
|
||||
println!("{}", serde_json::to_string(&output)?);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn read_remote_auth_token_from_env_var_with<F>(
|
||||
env_var_name: &str,
|
||||
get_var: F,
|
||||
@@ -2524,6 +2634,70 @@ mod tests {
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn app_server_daemon_subcommands_parse() {
|
||||
assert!(matches!(
|
||||
app_server_from_args(
|
||||
[
|
||||
"codex",
|
||||
"app-server",
|
||||
"daemon",
|
||||
"bootstrap",
|
||||
"--remote-control"
|
||||
]
|
||||
.as_ref()
|
||||
)
|
||||
.subcommand,
|
||||
Some(AppServerSubcommand::Daemon(AppServerDaemonCommand {
|
||||
subcommand: AppServerDaemonSubcommand::Bootstrap(AppServerBootstrapCommand {
|
||||
remote_control: true
|
||||
})
|
||||
}))
|
||||
));
|
||||
assert!(matches!(
|
||||
app_server_from_args(["codex", "app-server", "daemon", "start"].as_ref()).subcommand,
|
||||
Some(AppServerSubcommand::Daemon(AppServerDaemonCommand {
|
||||
subcommand: AppServerDaemonSubcommand::Start
|
||||
}))
|
||||
));
|
||||
assert!(matches!(
|
||||
app_server_from_args(["codex", "app-server", "daemon", "restart"].as_ref()).subcommand,
|
||||
Some(AppServerSubcommand::Daemon(AppServerDaemonCommand {
|
||||
subcommand: AppServerDaemonSubcommand::Restart
|
||||
}))
|
||||
));
|
||||
assert!(matches!(
|
||||
app_server_from_args(
|
||||
["codex", "app-server", "daemon", "enable-remote-control"].as_ref()
|
||||
)
|
||||
.subcommand,
|
||||
Some(AppServerSubcommand::Daemon(AppServerDaemonCommand {
|
||||
subcommand: AppServerDaemonSubcommand::EnableRemoteControl
|
||||
}))
|
||||
));
|
||||
assert!(matches!(
|
||||
app_server_from_args(
|
||||
["codex", "app-server", "daemon", "disable-remote-control"].as_ref()
|
||||
)
|
||||
.subcommand,
|
||||
Some(AppServerSubcommand::Daemon(AppServerDaemonCommand {
|
||||
subcommand: AppServerDaemonSubcommand::DisableRemoteControl
|
||||
}))
|
||||
));
|
||||
assert!(matches!(
|
||||
app_server_from_args(["codex", "app-server", "daemon", "stop"].as_ref()).subcommand,
|
||||
Some(AppServerSubcommand::Daemon(AppServerDaemonCommand {
|
||||
subcommand: AppServerDaemonSubcommand::Stop
|
||||
}))
|
||||
));
|
||||
assert!(matches!(
|
||||
app_server_from_args(["codex", "app-server", "daemon", "version"].as_ref()).subcommand,
|
||||
Some(AppServerSubcommand::Daemon(AppServerDaemonCommand {
|
||||
subcommand: AppServerDaemonSubcommand::Version
|
||||
}))
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn app_server_proxy_sock_path_parses() {
|
||||
let app_server =
|
||||
@@ -2552,6 +2726,20 @@ mod tests {
|
||||
assert!(err.to_string().contains("app-server proxy"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reject_remote_auth_token_env_for_app_server_version() {
|
||||
let subcommand = AppServerSubcommand::Daemon(AppServerDaemonCommand {
|
||||
subcommand: AppServerDaemonSubcommand::Version,
|
||||
});
|
||||
let err = reject_remote_mode_for_app_server_subcommand(
|
||||
/*remote*/ None,
|
||||
Some("CODEX_REMOTE_AUTH_TOKEN"),
|
||||
Some(&subcommand),
|
||||
)
|
||||
.expect_err("app-server daemon version should reject --remote-auth-token-env");
|
||||
assert!(err.to_string().contains("app-server daemon version"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn app_server_capability_token_flags_parse() {
|
||||
let app_server = app_server_from_args(
|
||||
|
||||
Reference in New Issue
Block a user