enable/disable remote control at runtime, not via features (#22578)

## Why
reapplies https://github.com/openai/codex/pull/22386 which was
previously reverted

Also, introduce `remoteControl/enable` and `remoteControl/disable`
app-server APIs to toggle on/off remote control at runtime for a given
running app-server instance.

## What Changed

- Adds experimental v2 RPCs:
  - `remoteControl/enable`
  - `remoteControl/disable`
- Adds `RemoteControlRequestProcessor` and routes the new RPCs through
it instead of `ConfigRequestProcessor`.
- Adds named `RemoteControlHandle::enable`, `disable`, and `status`
methods.
- Makes `remoteControl/enable` return an error when sqlite state DB is
unavailable, while keeping enrollment/websocket failures as async status
updates.
- Adds `AppServerRuntimeOptions.remote_control_enabled` and hidden
`--remote-control` flags for `codex app-server` and `codex-app-server`.
- Updates managed daemon startup to use `codex app-server
--remote-control --listen unix://`.
- Marks `Feature::RemoteControl` as removed and ignores
`[features].remote_control`.
- Updates app-server README entries for the new remote-control methods.
This commit is contained in:
Owen Lin authored and GitHub committed 2026-05-14 01:07:46 +00:00
1 parent 512f8f8012
commit 4e368aa2e9
22 files changed
+346 -52

No files matched your search

+1
View File
@@ -12,6 +12,7 @@ pub use transport::CHANNEL_CAPACITY;
pub use transport::ConnectionOrigin;
pub use transport::RemoteControlHandle;
pub use transport::RemoteControlStartConfig;
pub use transport::RemoteControlUnavailable;
pub use transport::TransportEvent;
pub use transport::app_server_control_socket_path;
pub use transport::auth;
@@ -32,6 +32,7 @@ mod websocket;
pub use remote_control::RemoteControlHandle;
pub use remote_control::RemoteControlStartConfig;
pub use remote_control::RemoteControlUnavailable;
pub use remote_control::start_remote_control;
pub use stdio::start_stdio_connection;
pub use unix_socket::start_control_socket_acceptor;
@@ -19,6 +19,8 @@ use codex_app_server_protocol::RemoteControlConnectionStatus;
use codex_app_server_protocol::RemoteControlStatusChangedNotification;
use codex_login::AuthManager;
use codex_state::StateRuntime;
use std::error::Error;
use std::fmt;
use std::io;
use std::sync::Arc;
use tokio::sync::mpsc;
@@ -47,23 +49,87 @@ pub struct RemoteControlHandle {
state_db_available: bool,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct RemoteControlUnavailable;
impl fmt::Display for RemoteControlUnavailable {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
f,
"remote control cannot be enabled because sqlite state db is unavailable"
)
}
}
impl Error for RemoteControlUnavailable {}
impl RemoteControlHandle {
pub fn set_enabled(&self, enabled: bool) {
let requested_enabled = enabled;
let enabled = enabled && self.state_db_available;
if requested_enabled && !self.state_db_available {
pub fn enable(
&self,
) -> Result<RemoteControlStatusChangedNotification, RemoteControlUnavailable> {
if !self.state_db_available {
warn!("remote control cannot be enabled because sqlite state db is unavailable");
return Err(RemoteControlUnavailable);
}
self.enabled_tx.send_if_modified(|state| {
let changed = *state != enabled;
*state = enabled;
let changed = !*state;
*state = true;
changed
});
let status = self.status();
if matches!(
status.status,
RemoteControlConnectionStatus::Connected | RemoteControlConnectionStatus::Connecting
) {
return Ok(status);
}
Ok(self.publish_status(RemoteControlConnectionStatus::Connecting))
}
pub fn disable(&self) -> RemoteControlStatusChangedNotification {
self.enabled_tx.send_if_modified(|state| {
let changed = *state;
*state = false;
changed
});
self.publish_status(RemoteControlConnectionStatus::Disabled)
}
pub fn status(&self) -> RemoteControlStatusChangedNotification {
self.status_tx.borrow().clone()
}
pub fn status_receiver(&self) -> watch::Receiver<RemoteControlStatusChangedNotification> {
self.status_tx.subscribe()
}
fn publish_status(
&self,
connection_status: RemoteControlConnectionStatus,
) -> RemoteControlStatusChangedNotification {
self.status_tx.send_if_modified(|status| {
let next_status = RemoteControlStatusChangedNotification {
status: connection_status,
installation_id: status.installation_id.clone(),
environment_id: if connection_status == RemoteControlConnectionStatus::Disabled {
None
} else {
status.environment_id.clone()
},
};
if *status == next_status {
return false;
}
*status = next_status;
true
});
self.status()
}
}
pub async fn start_remote_control(
@@ -639,7 +639,10 @@ async fn remote_control_start_reports_missing_state_db_as_disabled_when_enabled(
.await
.expect_err("remote control should not connect without sqlite state db");
remote_handle.set_enabled(/*enabled*/ true);
assert_eq!(
remote_handle.enable().expect_err("enable should fail"),
super::RemoteControlUnavailable
);
timeout(Duration::from_millis(100), listener.accept())
.await
.expect_err("remote control should remain disabled without sqlite state db");
@@ -655,7 +658,7 @@ async fn remote_control_start_reports_missing_state_db_as_disabled_when_enabled(
}
#[tokio::test]
async fn remote_control_handle_set_enabled_stops_and_restarts_connections() {
async fn remote_control_handle_enable_disable_stops_and_restarts_connections() {
let listener = TcpListener::bind("127.0.0.1:0")
.await
.expect("listener should bind");
@@ -701,7 +704,14 @@ async fn remote_control_handle_set_enabled_stops_and_restarts_connections() {
)
.await;
remote_handle.set_enabled(/*enabled*/ false);
assert_eq!(
remote_handle.disable(),
RemoteControlStatusChangedNotification {
status: RemoteControlConnectionStatus::Disabled,
installation_id: TEST_INSTALLATION_ID.to_string(),
environment_id: None,
}
);
expect_remote_control_status_snapshot(
&mut status_rx,
RemoteControlStatusChangedNotification {
@@ -718,13 +728,20 @@ async fn remote_control_handle_set_enabled_stops_and_restarts_connections() {
.await
.expect_err("disabled remote control should not reconnect");
remote_handle.set_enabled(/*enabled*/ true);
assert_eq!(
remote_handle.enable().expect("enable should succeed"),
RemoteControlStatusChangedNotification {
status: RemoteControlConnectionStatus::Connecting,
installation_id: TEST_INSTALLATION_ID.to_string(),
environment_id: None,
}
);
expect_remote_control_status_snapshot(
&mut status_rx,
RemoteControlStatusChangedNotification {
status: RemoteControlConnectionStatus::Connecting,
installation_id: TEST_INSTALLATION_ID.to_string(),
environment_id: Some("env_test".to_string()),
environment_id: None,
},
)
.await;