mirror of
https://github.com/pchuan98/codex.git
synced 2026-07-01 00:31:56 +08:00
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:
committed by
GitHub
Unverified
parent
512f8f8012
commit
4e368aa2e9
@@ -349,13 +349,7 @@ impl PidBackend {
|
||||
match self.command_kind {
|
||||
PidCommandKind::AppServer {
|
||||
remote_control_enabled: true,
|
||||
} => vec![
|
||||
"--enable",
|
||||
"remote_control",
|
||||
"app-server",
|
||||
"--listen",
|
||||
"unix://",
|
||||
],
|
||||
} => vec!["app-server", "--remote-control", "--listen", "unix://"],
|
||||
PidCommandKind::AppServer {
|
||||
remote_control_enabled: false,
|
||||
} => vec!["app-server", "--listen", "unix://"],
|
||||
|
||||
@@ -156,3 +156,17 @@ fn update_loop_uses_hidden_app_server_subcommand() {
|
||||
vec!["app-server", "daemon", "pid-update-loop"]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn app_server_remote_control_uses_runtime_flag() {
|
||||
let backend = PidBackend::new(
|
||||
"codex".into(),
|
||||
"app-server.pid".into(),
|
||||
/*remote_control_enabled*/ true,
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
backend.command_args(),
|
||||
vec!["app-server", "--remote-control", "--listen", "unix://"]
|
||||
);
|
||||
}
|
||||
|
||||
@@ -799,6 +799,18 @@ client_request_definitions! {
|
||||
serialization: global("config"),
|
||||
response: v2::ExperimentalFeatureEnablementSetResponse,
|
||||
},
|
||||
#[experimental("remoteControl/enable")]
|
||||
RemoteControlEnable => "remoteControl/enable" {
|
||||
params: #[ts(type = "undefined")] #[serde(skip_serializing_if = "Option::is_none")] Option<()>,
|
||||
serialization: global("remote-control"),
|
||||
response: v2::RemoteControlEnableResponse,
|
||||
},
|
||||
#[experimental("remoteControl/disable")]
|
||||
RemoteControlDisable => "remoteControl/disable" {
|
||||
params: #[ts(type = "undefined")] #[serde(skip_serializing_if = "Option::is_none")] Option<()>,
|
||||
serialization: global("remote-control"),
|
||||
response: v2::RemoteControlDisableResponse,
|
||||
},
|
||||
#[experimental("collaborationMode/list")]
|
||||
/// Lists collaboration mode presets.
|
||||
CollaborationModeList => "collaborationMode/list" {
|
||||
|
||||
@@ -13,6 +13,24 @@ pub struct RemoteControlStatusChangedNotification {
|
||||
pub environment_id: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(export_to = "v2/")]
|
||||
pub struct RemoteControlEnableResponse {
|
||||
pub status: RemoteControlConnectionStatus,
|
||||
pub installation_id: String,
|
||||
pub environment_id: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(export_to = "v2/")]
|
||||
pub struct RemoteControlDisableResponse {
|
||||
pub status: RemoteControlConnectionStatus,
|
||||
pub installation_id: String,
|
||||
pub environment_id: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq, JsonSchema, TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(rename_all = "camelCase", export_to = "v2/")]
|
||||
@@ -22,3 +40,33 @@ pub enum RemoteControlConnectionStatus {
|
||||
Connected,
|
||||
Errored,
|
||||
}
|
||||
|
||||
impl From<RemoteControlStatusChangedNotification> for RemoteControlEnableResponse {
|
||||
fn from(notification: RemoteControlStatusChangedNotification) -> Self {
|
||||
let RemoteControlStatusChangedNotification {
|
||||
status,
|
||||
installation_id,
|
||||
environment_id,
|
||||
} = notification;
|
||||
Self {
|
||||
status,
|
||||
installation_id,
|
||||
environment_id,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<RemoteControlStatusChangedNotification> for RemoteControlDisableResponse {
|
||||
fn from(notification: RemoteControlStatusChangedNotification) -> Self {
|
||||
let RemoteControlStatusChangedNotification {
|
||||
status,
|
||||
installation_id,
|
||||
environment_id,
|
||||
} = notification;
|
||||
Self {
|
||||
status,
|
||||
installation_id,
|
||||
environment_id,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -189,7 +189,7 @@ Example with notification opt-out:
|
||||
- `model/list` — list available models (set `includeHidden: true` to include entries with `hidden: true`), with reasoning effort options, `additionalSpeedTiers`, optional legacy `upgrade` model ids, optional `upgradeInfo` metadata (`model`, `upgradeCopy`, `modelLink`, `migrationMarkdown`), and optional `availabilityNux` metadata.
|
||||
- `modelProvider/capabilities/read` — read provider-level capabilities for the currently configured model provider.
|
||||
- `experimentalFeature/list` — list feature flags with stage metadata (`beta`, `underDevelopment`, `stable`, etc.), enabled/default-enabled state, and cursor pagination. For non-beta flags, `displayName`/`description`/`announcement` are `null`.
|
||||
- `experimentalFeature/enablement/set` — patch the in-memory process-wide runtime feature enablement for the currently supported feature keys (`apps`, `memories`, `plugins`, `remote_control`, `tool_search`, `tool_suggest`, `tool_call_mcp_elicitation`). For each feature, precedence is: cloud requirements > --enable <feature_name> > config.toml > experimentalFeature/enablement/set (new) > code default.
|
||||
- `experimentalFeature/enablement/set` — patch the in-memory process-wide runtime feature enablement for the currently supported feature keys (`apps`, `memories`, `plugins`, `tool_search`, `tool_suggest`, `tool_call_mcp_elicitation`). For each feature, precedence is: cloud requirements > --enable <feature_name> > config.toml > experimentalFeature/enablement/set (new) > code default.
|
||||
- `environment/add` — experimental; add or replace a named remote environment by `environmentId` and `execServerUrl` for later selection by `thread/start` or `turn/start`; returns `{}` and does not change the default environment.
|
||||
- `collaborationMode/list` — list available collaboration mode presets (experimental, no pagination). Built-in presets do not select a model; the Plan preset selects medium reasoning effort. This response omits built-in developer instructions; clients should either pass `settings.developer_instructions: null` when setting a mode to use Codex's built-in instructions, or provide their own instructions explicitly.
|
||||
- `skills/list` — list skills for one or more `cwd` values (optional `forceReload`).
|
||||
@@ -202,6 +202,8 @@ Example with notification opt-out:
|
||||
- `plugin/skill/read` — read remote plugin skill markdown on demand by `remoteMarketplaceName`, `remotePluginId`, and `skillName`. This lets clients preview uninstalled remote plugin skills without downloading the plugin bundle.
|
||||
- `skills/changed` — notification emitted when watched local skill files change.
|
||||
- `app/list` — list available apps.
|
||||
- `remoteControl/enable` — experimental; enable remote control for the current app-server process and return the current remote-control status snapshot. The caller is responsible for persisting the desired setting outside app-server.
|
||||
- `remoteControl/disable` — experimental; disable remote control for the current app-server process and return the current remote-control status snapshot. This does not revoke already enrolled controller devices.
|
||||
- `remoteControl/status/changed` — notification emitted when the remote-control status or client-visible environment id changes. `status` is one of `disabled`, `connecting`, `connected`, or `errored`; `environmentId` is a string when the app-server has a current enrollment and `null` when that enrollment is cleared, invalidated, or remote control is disabled. Newly initialized app-server clients always receive the current status snapshot.
|
||||
- `skills/config/write` — write user-level skill config by name or absolute path.
|
||||
- `plugin/install` — install a plugin from a discovered marketplace entry, rejecting marketplace entries marked unavailable for install, install MCPs if any, and return the effective plugin auth policy plus any apps that still need auth (**under development; do not call from production clients yet**).
|
||||
|
||||
@@ -8,7 +8,6 @@ use codex_config::RemoteThreadConfigLoader;
|
||||
use codex_config::ThreadConfigLoader;
|
||||
use codex_core::config::Config;
|
||||
use codex_core::resolve_installation_id;
|
||||
use codex_features::Feature;
|
||||
use codex_login::AuthManager;
|
||||
use codex_utils_cli::CliConfigOverrides;
|
||||
use std::collections::HashMap;
|
||||
@@ -401,12 +400,14 @@ pub enum PluginStartupTasks {
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub struct AppServerRuntimeOptions {
|
||||
pub plugin_startup_tasks: PluginStartupTasks,
|
||||
pub remote_control_enabled: bool,
|
||||
}
|
||||
|
||||
impl Default for AppServerRuntimeOptions {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
plugin_startup_tasks: PluginStartupTasks::Start,
|
||||
remote_control_enabled: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -684,15 +685,15 @@ pub async fn run_main_with_transport_options(
|
||||
let auth_manager =
|
||||
AuthManager::shared_from_config(&config, /*enable_codex_api_key_env*/ false).await;
|
||||
|
||||
let remote_control_config_enabled = config.features.enabled(Feature::RemoteControl);
|
||||
let remote_control_enabled = remote_control_config_enabled && state_db.is_some();
|
||||
if remote_control_config_enabled && state_db.is_none() {
|
||||
let remote_control_requested = runtime_options.remote_control_enabled;
|
||||
let remote_control_enabled = remote_control_requested && state_db.is_some();
|
||||
if remote_control_requested && state_db.is_none() {
|
||||
error!("remote control disabled because sqlite state db is unavailable");
|
||||
}
|
||||
if transport_accept_handles.is_empty() && !remote_control_enabled {
|
||||
return Err(std::io::Error::new(
|
||||
ErrorKind::InvalidInput,
|
||||
if remote_control_config_enabled && state_db.is_none() {
|
||||
if remote_control_requested && state_db.is_none() {
|
||||
"no transport configured; remote control disabled because sqlite state db is unavailable"
|
||||
} else {
|
||||
"no transport configured; use --listen or enable remote control"
|
||||
|
||||
@@ -48,6 +48,10 @@ struct AppServerArgs {
|
||||
#[cfg(debug_assertions)]
|
||||
#[arg(long = "disable-plugin-startup-tasks-for-tests", hide = true)]
|
||||
disable_plugin_startup_tasks_for_tests: bool,
|
||||
|
||||
/// Enable remote control for this app-server process.
|
||||
#[arg(long = "remote-control", hide = true)]
|
||||
remote_control: bool,
|
||||
}
|
||||
|
||||
fn main() -> anyhow::Result<()> {
|
||||
@@ -59,6 +63,7 @@ fn main() -> anyhow::Result<()> {
|
||||
strict_config,
|
||||
#[cfg(debug_assertions)]
|
||||
disable_plugin_startup_tasks_for_tests,
|
||||
remote_control,
|
||||
} = AppServerArgs::parse();
|
||||
let loader_overrides = if disable_managed_config_from_debug_env() {
|
||||
LoaderOverrides::without_managed_config_for_tests()
|
||||
@@ -74,6 +79,7 @@ fn main() -> anyhow::Result<()> {
|
||||
if disable_plugin_startup_tasks_for_tests {
|
||||
runtime_options.plugin_startup_tasks = PluginStartupTasks::Skip;
|
||||
}
|
||||
runtime_options.remote_control_enabled = remote_control;
|
||||
|
||||
run_main_with_transport_options(
|
||||
arg0_paths,
|
||||
|
||||
@@ -30,6 +30,7 @@ use crate::request_processors::MarketplaceRequestProcessor;
|
||||
use crate::request_processors::McpRequestProcessor;
|
||||
use crate::request_processors::PluginRequestProcessor;
|
||||
use crate::request_processors::ProcessExecRequestProcessor;
|
||||
use crate::request_processors::RemoteControlRequestProcessor;
|
||||
use crate::request_processors::SearchRequestProcessor;
|
||||
use crate::request_processors::ThreadGoalRequestProcessor;
|
||||
use crate::request_processors::ThreadRequestProcessor;
|
||||
@@ -173,6 +174,7 @@ pub(crate) struct MessageProcessor {
|
||||
marketplace_processor: MarketplaceRequestProcessor,
|
||||
mcp_processor: McpRequestProcessor,
|
||||
plugin_processor: PluginRequestProcessor,
|
||||
remote_control_processor: RemoteControlRequestProcessor,
|
||||
search_processor: SearchRequestProcessor,
|
||||
thread_goal_processor: ThreadGoalRequestProcessor,
|
||||
thread_processor: ThreadRequestProcessor,
|
||||
@@ -389,6 +391,7 @@ impl MessageProcessor {
|
||||
config_manager.clone(),
|
||||
workspace_settings_cache,
|
||||
);
|
||||
let remote_control_processor = RemoteControlRequestProcessor::new(remote_control_handle);
|
||||
let search_processor = SearchRequestProcessor::new(outgoing.clone());
|
||||
let thread_goal_processor = ThreadGoalRequestProcessor::new(
|
||||
Arc::clone(&thread_manager),
|
||||
@@ -446,7 +449,6 @@ impl MessageProcessor {
|
||||
auth_manager,
|
||||
thread_manager.clone(),
|
||||
analytics_events_client,
|
||||
remote_control_handle,
|
||||
);
|
||||
let external_agent_config_processor = ExternalAgentConfigRequestProcessor::new(
|
||||
outgoing.clone(),
|
||||
@@ -488,6 +490,7 @@ impl MessageProcessor {
|
||||
marketplace_processor,
|
||||
mcp_processor,
|
||||
plugin_processor,
|
||||
remote_control_processor,
|
||||
search_processor,
|
||||
thread_goal_processor,
|
||||
thread_processor,
|
||||
@@ -886,6 +889,14 @@ impl MessageProcessor {
|
||||
.experimental_feature_enablement_set(request_id.clone(), params)
|
||||
.await
|
||||
}
|
||||
ClientRequest::RemoteControlEnable { .. } => self
|
||||
.remote_control_processor
|
||||
.enable()
|
||||
.map(|response| Some(response.into())),
|
||||
ClientRequest::RemoteControlDisable { .. } => self
|
||||
.remote_control_processor
|
||||
.disable()
|
||||
.map(|response| Some(response.into())),
|
||||
ClientRequest::ConfigRequirementsRead { params: _, .. } => self
|
||||
.config_processor
|
||||
.config_requirements_read()
|
||||
|
||||
@@ -448,6 +448,7 @@ mod marketplace_processor;
|
||||
mod mcp_processor;
|
||||
mod plugins;
|
||||
mod process_exec_processor;
|
||||
mod remote_control_processor;
|
||||
mod search;
|
||||
mod thread_processor;
|
||||
mod token_usage_replay;
|
||||
@@ -469,6 +470,7 @@ pub(crate) use marketplace_processor::MarketplaceRequestProcessor;
|
||||
pub(crate) use mcp_processor::McpRequestProcessor;
|
||||
pub(crate) use plugins::PluginRequestProcessor;
|
||||
pub(crate) use process_exec_processor::ProcessExecRequestProcessor;
|
||||
pub(crate) use remote_control_processor::RemoteControlRequestProcessor;
|
||||
pub(crate) use search::SearchRequestProcessor;
|
||||
pub(crate) use thread_goal_processor::ThreadGoalRequestProcessor;
|
||||
pub(crate) use thread_processor::ThreadRequestProcessor;
|
||||
|
||||
@@ -6,7 +6,6 @@ use crate::error_code::internal_error;
|
||||
use crate::error_code::invalid_request;
|
||||
use crate::outgoing_message::ConnectionRequestId;
|
||||
use crate::outgoing_message::OutgoingMessageSender;
|
||||
use crate::transport::RemoteControlHandle;
|
||||
use codex_analytics::AnalyticsEventsClient;
|
||||
use codex_app_server_protocol::AppListUpdatedNotification;
|
||||
use codex_app_server_protocol::ClientResponsePayload;
|
||||
@@ -39,7 +38,6 @@ use codex_config::MatcherGroup as CoreMatcherGroup;
|
||||
use codex_config::ResidencyRequirement as CoreResidencyRequirement;
|
||||
use codex_config::SandboxModeRequirement as CoreSandboxModeRequirement;
|
||||
use codex_core::ThreadManager;
|
||||
use codex_features::Feature;
|
||||
use codex_features::canonical_feature_for_key;
|
||||
use codex_features::feature_for_key;
|
||||
use codex_login::AuthManager;
|
||||
@@ -67,7 +65,6 @@ pub(crate) struct ConfigRequestProcessor {
|
||||
auth_manager: Arc<AuthManager>,
|
||||
thread_manager: Arc<ThreadManager>,
|
||||
analytics_events_client: AnalyticsEventsClient,
|
||||
remote_control_handle: Option<RemoteControlHandle>,
|
||||
}
|
||||
|
||||
impl ConfigRequestProcessor {
|
||||
@@ -77,7 +74,6 @@ impl ConfigRequestProcessor {
|
||||
auth_manager: Arc<AuthManager>,
|
||||
thread_manager: Arc<ThreadManager>,
|
||||
analytics_events_client: AnalyticsEventsClient,
|
||||
remote_control_handle: Option<RemoteControlHandle>,
|
||||
) -> Self {
|
||||
Self {
|
||||
outgoing,
|
||||
@@ -85,7 +81,6 @@ impl ConfigRequestProcessor {
|
||||
auth_manager,
|
||||
thread_manager,
|
||||
analytics_events_client,
|
||||
remote_control_handle,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -187,21 +182,6 @@ impl ConfigRequestProcessor {
|
||||
pub(crate) async fn handle_config_mutation(&self) {
|
||||
self.thread_manager.plugins_manager().clear_cache();
|
||||
self.thread_manager.skills_manager().clear_cache();
|
||||
let Some(remote_control_handle) = &self.remote_control_handle else {
|
||||
return;
|
||||
};
|
||||
|
||||
match self.load_latest_config(/*fallback_cwd*/ None).await {
|
||||
Ok(config) => {
|
||||
remote_control_handle.set_enabled(config.features.enabled(Feature::RemoteControl));
|
||||
}
|
||||
Err(error) => {
|
||||
tracing::warn!(
|
||||
"failed to load config for remote control enablement refresh after config mutation: {}",
|
||||
error.message
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn handle_config_mutation_result<T>(
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
use crate::error_code::internal_error;
|
||||
use crate::error_code::invalid_request;
|
||||
use crate::transport::RemoteControlHandle;
|
||||
use crate::transport::RemoteControlUnavailable;
|
||||
use codex_app_server_protocol::JSONRPCErrorError;
|
||||
use codex_app_server_protocol::RemoteControlDisableResponse;
|
||||
use codex_app_server_protocol::RemoteControlEnableResponse;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub(crate) struct RemoteControlRequestProcessor {
|
||||
remote_control_handle: Option<RemoteControlHandle>,
|
||||
}
|
||||
|
||||
impl RemoteControlRequestProcessor {
|
||||
pub(crate) fn new(remote_control_handle: Option<RemoteControlHandle>) -> Self {
|
||||
Self {
|
||||
remote_control_handle,
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn enable(&self) -> Result<RemoteControlEnableResponse, JSONRPCErrorError> {
|
||||
let handle = self.handle()?;
|
||||
handle
|
||||
.enable()
|
||||
.map(RemoteControlEnableResponse::from)
|
||||
.map_err(map_unavailable)
|
||||
}
|
||||
|
||||
pub(crate) fn disable(&self) -> Result<RemoteControlDisableResponse, JSONRPCErrorError> {
|
||||
let handle = self.handle()?;
|
||||
Ok(RemoteControlDisableResponse::from(handle.disable()))
|
||||
}
|
||||
|
||||
fn handle(&self) -> Result<&RemoteControlHandle, JSONRPCErrorError> {
|
||||
self.remote_control_handle
|
||||
.as_ref()
|
||||
.ok_or_else(|| internal_error("remote control is unavailable for this app-server"))
|
||||
}
|
||||
}
|
||||
|
||||
fn map_unavailable(err: RemoteControlUnavailable) -> JSONRPCErrorError {
|
||||
invalid_request(err.to_string())
|
||||
}
|
||||
@@ -20,6 +20,7 @@ pub(crate) use codex_app_server_transport::OutgoingMessage;
|
||||
pub(crate) use codex_app_server_transport::QueuedOutgoingMessage;
|
||||
pub(crate) use codex_app_server_transport::RemoteControlHandle;
|
||||
pub(crate) use codex_app_server_transport::RemoteControlStartConfig;
|
||||
pub(crate) use codex_app_server_transport::RemoteControlUnavailable;
|
||||
pub(crate) use codex_app_server_transport::TransportEvent;
|
||||
pub use codex_app_server_transport::app_server_control_socket_path;
|
||||
pub use codex_app_server_transport::auth;
|
||||
|
||||
@@ -570,6 +570,18 @@ impl McpProcess {
|
||||
.await
|
||||
}
|
||||
|
||||
/// Send a `remoteControl/enable` JSON-RPC request.
|
||||
pub async fn send_remote_control_enable_request(&mut self) -> anyhow::Result<i64> {
|
||||
self.send_request("remoteControl/enable", /*params*/ None)
|
||||
.await
|
||||
}
|
||||
|
||||
/// Send a `remoteControl/disable` JSON-RPC request.
|
||||
pub async fn send_remote_control_disable_request(&mut self) -> anyhow::Result<i64> {
|
||||
self.send_request("remoteControl/disable", /*params*/ None)
|
||||
.await
|
||||
}
|
||||
|
||||
/// Send an `app/list` JSON-RPC request.
|
||||
pub async fn send_apps_list_request(&mut self, params: AppsListParams) -> anyhow::Result<i64> {
|
||||
let params = Some(serde_json::to_value(params)?);
|
||||
|
||||
@@ -38,6 +38,7 @@ mod plugin_uninstall;
|
||||
mod process_exec;
|
||||
mod rate_limits;
|
||||
mod realtime_conversation;
|
||||
mod remote_control;
|
||||
#[cfg(debug_assertions)]
|
||||
mod remote_thread_store;
|
||||
mod request_permissions;
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
use std::time::Duration;
|
||||
|
||||
use anyhow::Result;
|
||||
use app_test_support::McpProcess;
|
||||
use app_test_support::to_response;
|
||||
use codex_app_server_protocol::JSONRPCResponse;
|
||||
use codex_app_server_protocol::RemoteControlConnectionStatus;
|
||||
use codex_app_server_protocol::RemoteControlDisableResponse;
|
||||
use codex_app_server_protocol::RemoteControlEnableResponse;
|
||||
use codex_app_server_protocol::RequestId;
|
||||
use tempfile::TempDir;
|
||||
use tokio::time::timeout;
|
||||
|
||||
const DEFAULT_TIMEOUT: Duration = Duration::from_secs(10);
|
||||
|
||||
#[tokio::test]
|
||||
async fn remote_control_disable_returns_disabled_status() -> Result<()> {
|
||||
let codex_home = TempDir::new()?;
|
||||
let mut mcp = McpProcess::new(codex_home.path()).await?;
|
||||
timeout(DEFAULT_TIMEOUT, mcp.initialize()).await??;
|
||||
|
||||
let request_id = mcp.send_remote_control_disable_request().await?;
|
||||
let response: JSONRPCResponse = timeout(
|
||||
DEFAULT_TIMEOUT,
|
||||
mcp.read_stream_until_response_message(RequestId::Integer(request_id)),
|
||||
)
|
||||
.await??;
|
||||
let received: RemoteControlDisableResponse = to_response(response)?;
|
||||
|
||||
assert_eq!(received.status, RemoteControlConnectionStatus::Disabled);
|
||||
assert_eq!(received.environment_id, None);
|
||||
assert!(!received.installation_id.is_empty());
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn remote_control_enable_returns_connecting_status() -> Result<()> {
|
||||
let codex_home = TempDir::new()?;
|
||||
let mut mcp = McpProcess::new(codex_home.path()).await?;
|
||||
timeout(DEFAULT_TIMEOUT, mcp.initialize()).await??;
|
||||
|
||||
let request_id = mcp.send_remote_control_enable_request().await?;
|
||||
let response: JSONRPCResponse = timeout(
|
||||
DEFAULT_TIMEOUT,
|
||||
mcp.read_stream_until_response_message(RequestId::Integer(request_id)),
|
||||
)
|
||||
.await??;
|
||||
let received: RemoteControlEnableResponse = to_response(response)?;
|
||||
|
||||
assert_eq!(received.status, RemoteControlConnectionStatus::Connecting);
|
||||
assert_eq!(received.environment_id, None);
|
||||
assert!(!received.installation_id.is_empty());
|
||||
Ok(())
|
||||
}
|
||||
@@ -454,6 +454,10 @@ struct AppServerCommand {
|
||||
)]
|
||||
listen: codex_app_server::AppServerTransport,
|
||||
|
||||
/// Enable remote control for this app-server process.
|
||||
#[arg(long = "remote-control", hide = true)]
|
||||
remote_control: bool,
|
||||
|
||||
/// Controls whether analytics are enabled by default.
|
||||
///
|
||||
/// Analytics are disabled by default for app-server. Users have to explicitly opt in
|
||||
@@ -532,10 +536,10 @@ enum AppServerDaemonSubcommand {
|
||||
/// Restart the local app server daemon.
|
||||
Restart,
|
||||
|
||||
/// Enable remote_control for future starts and a currently running managed daemon.
|
||||
/// 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.
|
||||
/// Disable remote control for future starts and a currently running managed daemon.
|
||||
DisableRemoteControl,
|
||||
|
||||
/// Stop the local app server daemon.
|
||||
@@ -558,7 +562,7 @@ struct AppServerProxyCommand {
|
||||
|
||||
#[derive(Debug, Args)]
|
||||
struct AppServerBootstrapCommand {
|
||||
/// Launch the managed app-server with remote_control enabled.
|
||||
/// Launch the managed app-server with remote control enabled.
|
||||
#[arg(long = "remote-control")]
|
||||
remote_control: bool,
|
||||
}
|
||||
@@ -945,6 +949,7 @@ async fn cli_main(arg0_paths: Arg0DispatchPaths) -> anyhow::Result<()> {
|
||||
subcommand,
|
||||
strict_config: app_server_strict_config,
|
||||
listen,
|
||||
remote_control,
|
||||
analytics_default_enabled,
|
||||
auth,
|
||||
} = app_server_cli;
|
||||
@@ -959,6 +964,10 @@ async fn cli_main(arg0_paths: Arg0DispatchPaths) -> anyhow::Result<()> {
|
||||
None => {
|
||||
let transport = listen;
|
||||
let auth = auth.try_into_settings()?;
|
||||
let runtime_options = codex_app_server::AppServerRuntimeOptions {
|
||||
remote_control_enabled: remote_control,
|
||||
..Default::default()
|
||||
};
|
||||
codex_app_server::run_main_with_transport_options(
|
||||
arg0_paths.clone(),
|
||||
root_config_overrides,
|
||||
@@ -968,7 +977,7 @@ async fn cli_main(arg0_paths: Arg0DispatchPaths) -> anyhow::Result<()> {
|
||||
transport,
|
||||
codex_protocol::protocol::SessionSource::VSCode,
|
||||
auth,
|
||||
codex_app_server::AppServerRuntimeOptions::default(),
|
||||
runtime_options,
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
@@ -2607,6 +2616,7 @@ mod tests {
|
||||
fn app_server_analytics_default_disabled_without_flag() {
|
||||
let app_server = app_server_from_args(["codex", "app-server"].as_ref());
|
||||
assert!(!app_server.analytics_default_enabled);
|
||||
assert!(!app_server.remote_control);
|
||||
assert_eq!(
|
||||
app_server.listen,
|
||||
codex_app_server::AppServerTransport::Stdio
|
||||
|
||||
@@ -421,6 +421,9 @@ impl Features {
|
||||
"js_repl_tools_only" => {
|
||||
continue;
|
||||
}
|
||||
"remote_control" => {
|
||||
continue;
|
||||
}
|
||||
"image_detail_original" => {
|
||||
continue;
|
||||
}
|
||||
@@ -1117,7 +1120,7 @@ pub const FEATURES: &[FeatureSpec] = &[
|
||||
FeatureSpec {
|
||||
id: Feature::RemoteControl,
|
||||
key: "remote_control",
|
||||
stage: Stage::UnderDevelopment,
|
||||
stage: Stage::Removed,
|
||||
default_enabled: false,
|
||||
},
|
||||
FeatureSpec {
|
||||
|
||||
@@ -293,9 +293,24 @@ fn auth_elicitation_is_under_development() {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn remote_control_is_under_development() {
|
||||
assert_eq!(Feature::RemoteControl.stage(), Stage::UnderDevelopment);
|
||||
fn remote_control_is_removed_and_disabled_by_default() {
|
||||
assert_eq!(Feature::RemoteControl.stage(), Stage::Removed);
|
||||
assert_eq!(Feature::RemoteControl.default_enabled(), false);
|
||||
assert_eq!(
|
||||
feature_for_key("remote_control"),
|
||||
Some(Feature::RemoteControl)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn remote_control_config_is_ignored() {
|
||||
let mut entries = BTreeMap::new();
|
||||
entries.insert("remote_control".to_string(), true);
|
||||
|
||||
let mut features = Features::with_defaults();
|
||||
features.apply_map(&entries);
|
||||
|
||||
assert_eq!(features.enabled(Feature::RemoteControl), false);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
Reference in New Issue
Block a user