mirror of
https://github.com/pchuan98/codex.git
synced 2026-07-01 00:31:56 +08:00
feat(app-server): update remote control APIs for better UX (#22877)
## Why To help improve `codex remote-control` CLI UX which I plan to do in a followup, this PR adds `server-name` to the various remote control APIs: - `remoteControl/enable` - `remoteControl/disable` - `remoteControl/status/changed` Also, add a `remoteControl/status/read` API. This will be helpful in the Codex App.
This commit is contained in:
committed by
GitHub
Unverified
parent
98129fb9c5
commit
6a331a66eb
@@ -2748,12 +2748,16 @@
|
||||
"installationId": {
|
||||
"type": "string"
|
||||
},
|
||||
"serverName": {
|
||||
"type": "string"
|
||||
},
|
||||
"status": {
|
||||
"$ref": "#/definitions/RemoteControlConnectionStatus"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"installationId",
|
||||
"serverName",
|
||||
"status"
|
||||
],
|
||||
"type": "object"
|
||||
|
||||
+4
@@ -13446,12 +13446,16 @@
|
||||
"installationId": {
|
||||
"type": "string"
|
||||
},
|
||||
"serverName": {
|
||||
"type": "string"
|
||||
},
|
||||
"status": {
|
||||
"$ref": "#/definitions/v2/RemoteControlConnectionStatus"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"installationId",
|
||||
"serverName",
|
||||
"status"
|
||||
],
|
||||
"title": "RemoteControlStatusChangedNotification",
|
||||
|
||||
+4
@@ -9995,12 +9995,16 @@
|
||||
"installationId": {
|
||||
"type": "string"
|
||||
},
|
||||
"serverName": {
|
||||
"type": "string"
|
||||
},
|
||||
"status": {
|
||||
"$ref": "#/definitions/RemoteControlConnectionStatus"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"installationId",
|
||||
"serverName",
|
||||
"status"
|
||||
],
|
||||
"title": "RemoteControlStatusChangedNotification",
|
||||
|
||||
+4
@@ -22,12 +22,16 @@
|
||||
"installationId": {
|
||||
"type": "string"
|
||||
},
|
||||
"serverName": {
|
||||
"type": "string"
|
||||
},
|
||||
"status": {
|
||||
"$ref": "#/definitions/RemoteControlConnectionStatus"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"installationId",
|
||||
"serverName",
|
||||
"status"
|
||||
],
|
||||
"title": "RemoteControlStatusChangedNotification",
|
||||
|
||||
Generated
+1
-1
@@ -6,4 +6,4 @@ import type { RemoteControlConnectionStatus } from "./RemoteControlConnectionSta
|
||||
/**
|
||||
* Current remote-control connection status and remote identity exposed to clients.
|
||||
*/
|
||||
export type RemoteControlStatusChangedNotification = { status: RemoteControlConnectionStatus, installationId: string, environmentId: string | null, };
|
||||
export type RemoteControlStatusChangedNotification = { status: RemoteControlConnectionStatus, serverName: string, installationId: string, environmentId: string | null, };
|
||||
|
||||
@@ -812,6 +812,12 @@ client_request_definitions! {
|
||||
serialization: global("remote-control"),
|
||||
response: v2::RemoteControlDisableResponse,
|
||||
},
|
||||
#[experimental("remoteControl/status/read")]
|
||||
RemoteControlStatusRead => "remoteControl/status/read" {
|
||||
params: #[ts(type = "undefined")] #[serde(skip_serializing_if = "Option::is_none")] Option<()>,
|
||||
serialization: global_shared_read("remote-control"),
|
||||
response: v2::RemoteControlStatusReadResponse,
|
||||
},
|
||||
#[experimental("collaborationMode/list")]
|
||||
/// Lists collaboration mode presets.
|
||||
CollaborationModeList => "collaborationMode/list" {
|
||||
|
||||
@@ -9,6 +9,7 @@ use ts_rs::TS;
|
||||
#[ts(export_to = "v2/")]
|
||||
pub struct RemoteControlStatusChangedNotification {
|
||||
pub status: RemoteControlConnectionStatus,
|
||||
pub server_name: String,
|
||||
pub installation_id: String,
|
||||
pub environment_id: Option<String>,
|
||||
}
|
||||
@@ -18,6 +19,7 @@ pub struct RemoteControlStatusChangedNotification {
|
||||
#[ts(export_to = "v2/")]
|
||||
pub struct RemoteControlEnableResponse {
|
||||
pub status: RemoteControlConnectionStatus,
|
||||
pub server_name: String,
|
||||
pub installation_id: String,
|
||||
pub environment_id: Option<String>,
|
||||
}
|
||||
@@ -27,6 +29,17 @@ pub struct RemoteControlEnableResponse {
|
||||
#[ts(export_to = "v2/")]
|
||||
pub struct RemoteControlDisableResponse {
|
||||
pub status: RemoteControlConnectionStatus,
|
||||
pub server_name: String,
|
||||
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 RemoteControlStatusReadResponse {
|
||||
pub status: RemoteControlConnectionStatus,
|
||||
pub server_name: String,
|
||||
pub installation_id: String,
|
||||
pub environment_id: Option<String>,
|
||||
}
|
||||
@@ -45,11 +58,13 @@ impl From<RemoteControlStatusChangedNotification> for RemoteControlEnableRespons
|
||||
fn from(notification: RemoteControlStatusChangedNotification) -> Self {
|
||||
let RemoteControlStatusChangedNotification {
|
||||
status,
|
||||
server_name,
|
||||
installation_id,
|
||||
environment_id,
|
||||
} = notification;
|
||||
Self {
|
||||
status,
|
||||
server_name,
|
||||
installation_id,
|
||||
environment_id,
|
||||
}
|
||||
@@ -60,11 +75,13 @@ impl From<RemoteControlStatusChangedNotification> for RemoteControlDisableRespon
|
||||
fn from(notification: RemoteControlStatusChangedNotification) -> Self {
|
||||
let RemoteControlStatusChangedNotification {
|
||||
status,
|
||||
server_name,
|
||||
installation_id,
|
||||
environment_id,
|
||||
} = notification;
|
||||
Self {
|
||||
status,
|
||||
server_name,
|
||||
installation_id,
|
||||
environment_id,
|
||||
}
|
||||
|
||||
@@ -6,7 +6,6 @@ use codex_api::SharedAuthProvider;
|
||||
use codex_login::default_client::build_reqwest_client;
|
||||
use codex_state::RemoteControlEnrollmentRecord;
|
||||
use codex_state::StateRuntime;
|
||||
use gethostname::gethostname;
|
||||
use std::io;
|
||||
use std::io::ErrorKind;
|
||||
use tracing::info;
|
||||
@@ -195,11 +194,11 @@ pub(super) async fn enroll_remote_control_server(
|
||||
remote_control_target: &RemoteControlTarget,
|
||||
auth: &RemoteControlConnectionAuth,
|
||||
installation_id: &str,
|
||||
server_name: &str,
|
||||
) -> io::Result<RemoteControlEnrollment> {
|
||||
let enroll_url = &remote_control_target.enroll_url;
|
||||
let server_name = gethostname().to_string_lossy().trim().to_string();
|
||||
let request = EnrollRemoteServerRequest {
|
||||
name: server_name.clone(),
|
||||
name: server_name.to_string(),
|
||||
os: std::env::consts::OS,
|
||||
arch: std::env::consts::ARCH,
|
||||
app_server_version: env!("CARGO_PKG_VERSION"),
|
||||
@@ -255,7 +254,7 @@ pub(super) async fn enroll_remote_control_server(
|
||||
account_id: auth.account_id.clone(),
|
||||
environment_id: enrollment.environment_id,
|
||||
server_id: enrollment.server_id,
|
||||
server_name,
|
||||
server_name: server_name.to_string(),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -464,6 +463,7 @@ mod tests {
|
||||
account_id: "account_id".to_string(),
|
||||
},
|
||||
"11111111-1111-4111-8111-111111111111",
|
||||
"test-server",
|
||||
)
|
||||
.await
|
||||
.expect_err("invalid response should fail to parse");
|
||||
|
||||
@@ -19,6 +19,7 @@ use codex_app_server_protocol::RemoteControlConnectionStatus;
|
||||
use codex_app_server_protocol::RemoteControlStatusChangedNotification;
|
||||
use codex_login::AuthManager;
|
||||
use codex_state::StateRuntime;
|
||||
use gethostname::gethostname;
|
||||
use std::error::Error;
|
||||
use std::fmt;
|
||||
use std::io;
|
||||
@@ -112,15 +113,8 @@ impl RemoteControlHandle {
|
||||
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()
|
||||
},
|
||||
};
|
||||
let next_status =
|
||||
remote_control_status_with_connection_status(status, connection_status);
|
||||
if *status == next_status {
|
||||
return false;
|
||||
}
|
||||
@@ -132,6 +126,22 @@ impl RemoteControlHandle {
|
||||
}
|
||||
}
|
||||
|
||||
fn remote_control_status_with_connection_status(
|
||||
status: &RemoteControlStatusChangedNotification,
|
||||
connection_status: RemoteControlConnectionStatus,
|
||||
) -> RemoteControlStatusChangedNotification {
|
||||
RemoteControlStatusChangedNotification {
|
||||
status: connection_status,
|
||||
server_name: status.server_name.clone(),
|
||||
installation_id: status.installation_id.clone(),
|
||||
environment_id: if connection_status == RemoteControlConnectionStatus::Disabled {
|
||||
None
|
||||
} else {
|
||||
status.environment_id.clone()
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn start_remote_control(
|
||||
config: RemoteControlStartConfig,
|
||||
state_db: Option<Arc<StateRuntime>>,
|
||||
@@ -154,12 +164,14 @@ pub async fn start_remote_control(
|
||||
};
|
||||
|
||||
let (enabled_tx, enabled_rx) = watch::channel(initial_enabled);
|
||||
let server_name = gethostname().to_string_lossy().trim().to_string();
|
||||
let initial_status = RemoteControlStatusChangedNotification {
|
||||
status: if initial_enabled {
|
||||
RemoteControlConnectionStatus::Connecting
|
||||
} else {
|
||||
RemoteControlConnectionStatus::Disabled
|
||||
},
|
||||
server_name: server_name.clone(),
|
||||
installation_id: config.installation_id.clone(),
|
||||
environment_id: None,
|
||||
};
|
||||
@@ -171,6 +183,7 @@ pub async fn start_remote_control(
|
||||
remote_control_url: config.remote_control_url,
|
||||
installation_id: config.installation_id,
|
||||
remote_control_target,
|
||||
server_name,
|
||||
},
|
||||
state_db,
|
||||
auth_manager,
|
||||
|
||||
@@ -121,6 +121,10 @@ fn remote_control_url_for_listener(listener: &TcpListener) -> String {
|
||||
format!("http://{addr}/backend-api/")
|
||||
}
|
||||
|
||||
fn test_server_name() -> String {
|
||||
gethostname().to_string_lossy().trim().to_string()
|
||||
}
|
||||
|
||||
async fn expect_remote_control_status(
|
||||
status_rx: &mut watch::Receiver<RemoteControlStatusChangedNotification>,
|
||||
expected_status: Option<RemoteControlConnectionStatus>,
|
||||
@@ -134,6 +138,7 @@ async fn expect_remote_control_status(
|
||||
if let Some(expected_status) = expected_status {
|
||||
assert_eq!(status.status, expected_status);
|
||||
}
|
||||
assert_eq!(status.server_name, test_server_name());
|
||||
assert_eq!(status.installation_id, TEST_INSTALLATION_ID);
|
||||
assert_eq!(status.environment_id.as_deref(), expected_environment_id);
|
||||
}
|
||||
@@ -630,6 +635,7 @@ async fn remote_control_start_reports_missing_state_db_as_disabled_when_enabled(
|
||||
status_rx.borrow().clone(),
|
||||
RemoteControlStatusChangedNotification {
|
||||
status: RemoteControlConnectionStatus::Disabled,
|
||||
server_name: test_server_name(),
|
||||
installation_id: TEST_INSTALLATION_ID.to_string(),
|
||||
environment_id: None,
|
||||
}
|
||||
@@ -698,6 +704,7 @@ async fn remote_control_handle_enable_disable_stops_and_restarts_connections() {
|
||||
&mut status_rx,
|
||||
RemoteControlStatusChangedNotification {
|
||||
status: RemoteControlConnectionStatus::Connected,
|
||||
server_name: test_server_name(),
|
||||
installation_id: TEST_INSTALLATION_ID.to_string(),
|
||||
environment_id: Some("env_test".to_string()),
|
||||
},
|
||||
@@ -708,6 +715,7 @@ async fn remote_control_handle_enable_disable_stops_and_restarts_connections() {
|
||||
remote_handle.disable(),
|
||||
RemoteControlStatusChangedNotification {
|
||||
status: RemoteControlConnectionStatus::Disabled,
|
||||
server_name: test_server_name(),
|
||||
installation_id: TEST_INSTALLATION_ID.to_string(),
|
||||
environment_id: None,
|
||||
}
|
||||
@@ -716,6 +724,7 @@ async fn remote_control_handle_enable_disable_stops_and_restarts_connections() {
|
||||
&mut status_rx,
|
||||
RemoteControlStatusChangedNotification {
|
||||
status: RemoteControlConnectionStatus::Disabled,
|
||||
server_name: test_server_name(),
|
||||
installation_id: TEST_INSTALLATION_ID.to_string(),
|
||||
environment_id: None,
|
||||
},
|
||||
@@ -732,6 +741,7 @@ async fn remote_control_handle_enable_disable_stops_and_restarts_connections() {
|
||||
remote_handle.enable().expect("enable should succeed"),
|
||||
RemoteControlStatusChangedNotification {
|
||||
status: RemoteControlConnectionStatus::Connecting,
|
||||
server_name: test_server_name(),
|
||||
installation_id: TEST_INSTALLATION_ID.to_string(),
|
||||
environment_id: None,
|
||||
}
|
||||
@@ -740,6 +750,7 @@ async fn remote_control_handle_enable_disable_stops_and_restarts_connections() {
|
||||
&mut status_rx,
|
||||
RemoteControlStatusChangedNotification {
|
||||
status: RemoteControlConnectionStatus::Connecting,
|
||||
server_name: test_server_name(),
|
||||
installation_id: TEST_INSTALLATION_ID.to_string(),
|
||||
environment_id: None,
|
||||
},
|
||||
|
||||
@@ -15,6 +15,7 @@ use super::protocol::ClientId;
|
||||
use super::protocol::RemoteControlTarget;
|
||||
use super::protocol::ServerEnvelope;
|
||||
use super::protocol::StreamId;
|
||||
use super::remote_control_status_with_connection_status;
|
||||
use super::segment::ClientSegmentObservation;
|
||||
use super::segment::ClientSegmentReassembler;
|
||||
use super::segment::REMOTE_CONTROL_SEGMENT_MAX_BYTES;
|
||||
@@ -216,6 +217,7 @@ impl WebsocketState {
|
||||
pub(crate) struct RemoteControlWebsocket {
|
||||
remote_control_url: String,
|
||||
installation_id: String,
|
||||
server_name: String,
|
||||
remote_control_target: Option<RemoteControlTarget>,
|
||||
state_db: Option<Arc<StateRuntime>>,
|
||||
auth_manager: Arc<AuthManager>,
|
||||
@@ -235,6 +237,7 @@ pub(crate) struct RemoteControlWebsocketConfig {
|
||||
pub(crate) remote_control_url: String,
|
||||
pub(crate) installation_id: String,
|
||||
pub(crate) remote_control_target: Option<RemoteControlTarget>,
|
||||
pub(crate) server_name: String,
|
||||
}
|
||||
|
||||
enum ConnectOutcome {
|
||||
@@ -260,15 +263,8 @@ impl RemoteControlStatusPublisher {
|
||||
|
||||
fn publish_status(&self, connection_status: RemoteControlConnectionStatus) {
|
||||
self.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()
|
||||
},
|
||||
};
|
||||
let next_status =
|
||||
remote_control_status_with_connection_status(status, connection_status);
|
||||
if *status == next_status {
|
||||
return false;
|
||||
}
|
||||
@@ -285,6 +281,7 @@ impl RemoteControlStatusPublisher {
|
||||
}
|
||||
let next_status = RemoteControlStatusChangedNotification {
|
||||
status: status.status,
|
||||
server_name: status.server_name.clone(),
|
||||
installation_id: status.installation_id.clone(),
|
||||
environment_id,
|
||||
};
|
||||
@@ -301,6 +298,7 @@ impl RemoteControlStatusPublisher {
|
||||
#[derive(Clone, Copy)]
|
||||
pub(super) struct RemoteControlConnectOptions<'a> {
|
||||
installation_id: &'a str,
|
||||
server_name: &'a str,
|
||||
subscribe_cursor: Option<&'a str>,
|
||||
app_server_client_name: Option<&'a str>,
|
||||
}
|
||||
@@ -327,6 +325,7 @@ impl RemoteControlWebsocket {
|
||||
Self {
|
||||
remote_control_url: config.remote_control_url,
|
||||
installation_id: config.installation_id,
|
||||
server_name: config.server_name,
|
||||
remote_control_target: config.remote_control_target,
|
||||
state_db,
|
||||
auth_manager,
|
||||
@@ -454,6 +453,7 @@ impl RemoteControlWebsocket {
|
||||
let subscribe_cursor = self.state.lock().await.subscribe_cursor.clone();
|
||||
let connect_options = RemoteControlConnectOptions {
|
||||
installation_id: &self.installation_id,
|
||||
server_name: &self.server_name,
|
||||
subscribe_cursor: subscribe_cursor.as_deref(),
|
||||
app_server_client_name,
|
||||
};
|
||||
@@ -1076,7 +1076,10 @@ pub(super) async fn connect_remote_control_websocket(
|
||||
if let Some(loaded_enrollment) = loaded_enrollment.as_ref() {
|
||||
status_publisher.publish_environment_id(Some(loaded_enrollment.environment_id.clone()));
|
||||
}
|
||||
*enrollment = loaded_enrollment;
|
||||
*enrollment = loaded_enrollment.map(|mut enrollment| {
|
||||
enrollment.server_name = connect_options.server_name.to_string();
|
||||
enrollment
|
||||
});
|
||||
}
|
||||
|
||||
if enrollment.is_none() {
|
||||
@@ -1088,6 +1091,7 @@ pub(super) async fn connect_remote_control_websocket(
|
||||
remote_control_target,
|
||||
&auth,
|
||||
connect_options.installation_id,
|
||||
connect_options.server_name,
|
||||
)
|
||||
.await
|
||||
{
|
||||
@@ -1279,6 +1283,7 @@ mod tests {
|
||||
) {
|
||||
let (status_tx, status_rx) = watch::channel(RemoteControlStatusChangedNotification {
|
||||
status: RemoteControlConnectionStatus::Connecting,
|
||||
server_name: "test-server".to_string(),
|
||||
installation_id: TEST_INSTALLATION_ID.to_string(),
|
||||
environment_id: None,
|
||||
});
|
||||
@@ -1386,6 +1391,7 @@ mod tests {
|
||||
&mut enrollment,
|
||||
RemoteControlConnectOptions {
|
||||
installation_id: TEST_INSTALLATION_ID,
|
||||
server_name: "test-server",
|
||||
subscribe_cursor: None,
|
||||
app_server_client_name: None,
|
||||
},
|
||||
@@ -1403,6 +1409,7 @@ mod tests {
|
||||
status_rx.borrow().clone(),
|
||||
RemoteControlStatusChangedNotification {
|
||||
status: RemoteControlConnectionStatus::Connecting,
|
||||
server_name: "test-server".to_string(),
|
||||
installation_id: TEST_INSTALLATION_ID.to_string(),
|
||||
environment_id: Some("env_test".to_string()),
|
||||
}
|
||||
@@ -1464,6 +1471,7 @@ mod tests {
|
||||
&mut enrollment,
|
||||
RemoteControlConnectOptions {
|
||||
installation_id: TEST_INSTALLATION_ID,
|
||||
server_name: "test-server",
|
||||
subscribe_cursor: None,
|
||||
app_server_client_name: None,
|
||||
},
|
||||
@@ -1477,6 +1485,7 @@ mod tests {
|
||||
status_rx.borrow().clone(),
|
||||
RemoteControlStatusChangedNotification {
|
||||
status: RemoteControlConnectionStatus::Connecting,
|
||||
server_name: "test-server".to_string(),
|
||||
installation_id: TEST_INSTALLATION_ID.to_string(),
|
||||
environment_id: Some("env_test".to_string()),
|
||||
}
|
||||
@@ -1546,6 +1555,7 @@ mod tests {
|
||||
&mut enrollment,
|
||||
RemoteControlConnectOptions {
|
||||
installation_id: TEST_INSTALLATION_ID,
|
||||
server_name: "test-server",
|
||||
subscribe_cursor: None,
|
||||
app_server_client_name: None,
|
||||
},
|
||||
@@ -1599,6 +1609,7 @@ mod tests {
|
||||
&mut enrollment,
|
||||
RemoteControlConnectOptions {
|
||||
installation_id: TEST_INSTALLATION_ID,
|
||||
server_name: "test-server",
|
||||
subscribe_cursor: None,
|
||||
app_server_client_name: None,
|
||||
},
|
||||
@@ -1647,6 +1658,7 @@ mod tests {
|
||||
&mut enrollment,
|
||||
RemoteControlConnectOptions {
|
||||
installation_id: TEST_INSTALLATION_ID,
|
||||
server_name: "test-server",
|
||||
subscribe_cursor: None,
|
||||
app_server_client_name: None,
|
||||
},
|
||||
@@ -1665,6 +1677,7 @@ mod tests {
|
||||
status_rx.borrow().clone(),
|
||||
RemoteControlStatusChangedNotification {
|
||||
status: RemoteControlConnectionStatus::Connecting,
|
||||
server_name: "test-server".to_string(),
|
||||
installation_id: TEST_INSTALLATION_ID.to_string(),
|
||||
environment_id: None,
|
||||
}
|
||||
@@ -1694,6 +1707,7 @@ mod tests {
|
||||
remote_control_url,
|
||||
installation_id: TEST_INSTALLATION_ID.to_string(),
|
||||
remote_control_target: Some(remote_control_target),
|
||||
server_name: "test-server".to_string(),
|
||||
},
|
||||
/*state_db*/ None,
|
||||
remote_control_auth_manager(),
|
||||
@@ -1738,6 +1752,7 @@ mod tests {
|
||||
status_rx.borrow().clone(),
|
||||
RemoteControlStatusChangedNotification {
|
||||
status: RemoteControlConnectionStatus::Connecting,
|
||||
server_name: "test-server".to_string(),
|
||||
installation_id: TEST_INSTALLATION_ID.to_string(),
|
||||
environment_id: Some("env_first".to_string()),
|
||||
}
|
||||
@@ -1759,6 +1774,7 @@ mod tests {
|
||||
status_rx.borrow().clone(),
|
||||
RemoteControlStatusChangedNotification {
|
||||
status: RemoteControlConnectionStatus::Connected,
|
||||
server_name: "test-server".to_string(),
|
||||
installation_id: TEST_INSTALLATION_ID.to_string(),
|
||||
environment_id: Some("env_first".to_string()),
|
||||
}
|
||||
@@ -1773,6 +1789,7 @@ mod tests {
|
||||
status_rx.borrow().clone(),
|
||||
RemoteControlStatusChangedNotification {
|
||||
status: RemoteControlConnectionStatus::Connected,
|
||||
server_name: "test-server".to_string(),
|
||||
installation_id: TEST_INSTALLATION_ID.to_string(),
|
||||
environment_id: None,
|
||||
}
|
||||
@@ -1788,6 +1805,7 @@ mod tests {
|
||||
status_rx.borrow().clone(),
|
||||
RemoteControlStatusChangedNotification {
|
||||
status: RemoteControlConnectionStatus::Disabled,
|
||||
server_name: "test-server".to_string(),
|
||||
installation_id: TEST_INSTALLATION_ID.to_string(),
|
||||
environment_id: None,
|
||||
}
|
||||
|
||||
@@ -204,7 +204,8 @@ Example with notification opt-out:
|
||||
- `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.
|
||||
- `remoteControl/status/read` — experimental; read the current remote-control status snapshot. `status` is one of `disabled`, `connecting`, `connected`, or `errored`; `serverName` is the local machine name used by this app-server process; `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.
|
||||
- `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`; `serverName` is the local machine name used by this app-server process; `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**).
|
||||
- `plugin/uninstall` — uninstall a local plugin by `pluginId` in `<plugin>@<marketplace>` form by removing its cached files and clearing its user-level config entry, or uninstall a remote ChatGPT plugin by backend `pluginId` by forwarding the uninstall to the ChatGPT plugin backend and removing any downloaded remote-plugin cache (**under development; do not call from production clients yet**).
|
||||
|
||||
@@ -41,7 +41,6 @@ use codex_analytics::AppServerRpcTransport;
|
||||
use codex_app_server_protocol::ConfigLayerSource;
|
||||
use codex_app_server_protocol::ConfigWarningNotification;
|
||||
use codex_app_server_protocol::JSONRPCMessage;
|
||||
use codex_app_server_protocol::RemoteControlStatusChangedNotification;
|
||||
use codex_app_server_protocol::ServerNotification;
|
||||
use codex_app_server_protocol::TextPosition as AppTextPosition;
|
||||
use codex_app_server_protocol::TextRange as AppTextRange;
|
||||
@@ -1003,14 +1002,9 @@ pub async fn run_main_with_transport_options(
|
||||
continue;
|
||||
}
|
||||
remote_control_status = status.clone();
|
||||
let notification = ServerNotification::RemoteControlStatusChanged(status);
|
||||
initialize_notification_sender
|
||||
.send_server_notification(ServerNotification::RemoteControlStatusChanged(
|
||||
RemoteControlStatusChangedNotification {
|
||||
status: status.status,
|
||||
installation_id: status.installation_id,
|
||||
environment_id: status.environment_id,
|
||||
},
|
||||
))
|
||||
.send_server_notification(notification)
|
||||
.await;
|
||||
}
|
||||
created = thread_created_rx.recv(), if listen_for_threads => {
|
||||
|
||||
@@ -897,6 +897,10 @@ impl MessageProcessor {
|
||||
.remote_control_processor
|
||||
.disable()
|
||||
.map(|response| Some(response.into())),
|
||||
ClientRequest::RemoteControlStatusRead { .. } => self
|
||||
.remote_control_processor
|
||||
.status_read()
|
||||
.map(|response| Some(response.into())),
|
||||
ClientRequest::ConfigRequirementsRead { params: _, .. } => self
|
||||
.config_processor
|
||||
.config_requirements_read()
|
||||
|
||||
@@ -5,6 +5,7 @@ use crate::transport::RemoteControlUnavailable;
|
||||
use codex_app_server_protocol::JSONRPCErrorError;
|
||||
use codex_app_server_protocol::RemoteControlDisableResponse;
|
||||
use codex_app_server_protocol::RemoteControlEnableResponse;
|
||||
use codex_app_server_protocol::RemoteControlStatusReadResponse;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub(crate) struct RemoteControlRequestProcessor {
|
||||
@@ -31,6 +32,16 @@ impl RemoteControlRequestProcessor {
|
||||
Ok(RemoteControlDisableResponse::from(handle.disable()))
|
||||
}
|
||||
|
||||
pub(crate) fn status_read(&self) -> Result<RemoteControlStatusReadResponse, JSONRPCErrorError> {
|
||||
let status = self.handle()?.status();
|
||||
Ok(RemoteControlStatusReadResponse {
|
||||
status: status.status,
|
||||
server_name: status.server_name,
|
||||
installation_id: status.installation_id,
|
||||
environment_id: status.environment_id,
|
||||
})
|
||||
}
|
||||
|
||||
fn handle(&self) -> Result<&RemoteControlHandle, JSONRPCErrorError> {
|
||||
self.remote_control_handle
|
||||
.as_ref()
|
||||
|
||||
@@ -582,6 +582,12 @@ impl McpProcess {
|
||||
.await
|
||||
}
|
||||
|
||||
/// Send a `remoteControl/status/read` JSON-RPC request.
|
||||
pub async fn send_remote_control_status_read_request(&mut self) -> anyhow::Result<i64> {
|
||||
self.send_request("remoteControl/status/read", /*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)?);
|
||||
|
||||
@@ -1,14 +1,27 @@
|
||||
use std::time::Duration;
|
||||
|
||||
use anyhow::Context;
|
||||
use anyhow::Result;
|
||||
use app_test_support::ChatGptAuthFixture;
|
||||
use app_test_support::McpProcess;
|
||||
use app_test_support::to_response;
|
||||
use app_test_support::write_chatgpt_auth;
|
||||
use app_test_support::write_mock_responses_config_toml_with_chatgpt_base_url;
|
||||
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::RemoteControlStatusReadResponse;
|
||||
use codex_app_server_protocol::RequestId;
|
||||
use codex_config::types::AuthCredentialsStoreMode;
|
||||
use pretty_assertions::assert_eq;
|
||||
use tempfile::TempDir;
|
||||
use tokio::io::AsyncBufReadExt;
|
||||
use tokio::io::BufReader;
|
||||
use tokio::net::TcpListener;
|
||||
use tokio::net::TcpStream;
|
||||
use tokio::sync::oneshot;
|
||||
use tokio::task::JoinHandle;
|
||||
use tokio::time::timeout;
|
||||
|
||||
const DEFAULT_TIMEOUT: Duration = Duration::from_secs(10);
|
||||
@@ -28,6 +41,28 @@ async fn remote_control_disable_returns_disabled_status() -> Result<()> {
|
||||
let received: RemoteControlDisableResponse = to_response(response)?;
|
||||
|
||||
assert_eq!(received.status, RemoteControlConnectionStatus::Disabled);
|
||||
assert!(!received.server_name.is_empty());
|
||||
assert_eq!(received.environment_id, None);
|
||||
assert!(!received.installation_id.is_empty());
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn remote_control_status_read_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_status_read_request().await?;
|
||||
let response: JSONRPCResponse = timeout(
|
||||
DEFAULT_TIMEOUT,
|
||||
mcp.read_stream_until_response_message(RequestId::Integer(request_id)),
|
||||
)
|
||||
.await??;
|
||||
let received: RemoteControlStatusReadResponse = to_response(response)?;
|
||||
|
||||
assert_eq!(received.status, RemoteControlConnectionStatus::Disabled);
|
||||
assert!(!received.server_name.is_empty());
|
||||
assert_eq!(received.environment_id, None);
|
||||
assert!(!received.installation_id.is_empty());
|
||||
Ok(())
|
||||
@@ -36,6 +71,7 @@ async fn remote_control_disable_returns_disabled_status() -> Result<()> {
|
||||
#[tokio::test]
|
||||
async fn remote_control_enable_returns_connecting_status() -> Result<()> {
|
||||
let codex_home = TempDir::new()?;
|
||||
let _backend = BlockingRemoteControlBackend::start(codex_home.path()).await?;
|
||||
let mut mcp = McpProcess::new(codex_home.path()).await?;
|
||||
timeout(DEFAULT_TIMEOUT, mcp.initialize()).await??;
|
||||
|
||||
@@ -48,7 +84,117 @@ async fn remote_control_enable_returns_connecting_status() -> Result<()> {
|
||||
let received: RemoteControlEnableResponse = to_response(response)?;
|
||||
|
||||
assert_eq!(received.status, RemoteControlConnectionStatus::Connecting);
|
||||
assert!(!received.server_name.is_empty());
|
||||
assert_eq!(received.environment_id, None);
|
||||
assert!(!received.installation_id.is_empty());
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn remote_control_status_read_returns_connecting_status_after_enable() -> Result<()> {
|
||||
let codex_home = TempDir::new()?;
|
||||
let mut backend = BlockingRemoteControlBackend::start(codex_home.path()).await?;
|
||||
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 _: JSONRPCResponse = timeout(
|
||||
DEFAULT_TIMEOUT,
|
||||
mcp.read_stream_until_response_message(RequestId::Integer(request_id)),
|
||||
)
|
||||
.await??;
|
||||
|
||||
let enroll_request = timeout(DEFAULT_TIMEOUT, backend.wait_for_enroll_request()).await??;
|
||||
assert_eq!(
|
||||
enroll_request,
|
||||
"POST /backend-api/wham/remote/control/server/enroll HTTP/1.1"
|
||||
);
|
||||
|
||||
let request_id = mcp.send_remote_control_status_read_request().await?;
|
||||
let response: JSONRPCResponse = timeout(
|
||||
DEFAULT_TIMEOUT,
|
||||
mcp.read_stream_until_response_message(RequestId::Integer(request_id)),
|
||||
)
|
||||
.await??;
|
||||
let received: RemoteControlStatusReadResponse = to_response(response)?;
|
||||
|
||||
assert_eq!(received.status, RemoteControlConnectionStatus::Connecting);
|
||||
assert!(!received.server_name.is_empty());
|
||||
assert_eq!(received.environment_id, None);
|
||||
assert!(!received.installation_id.is_empty());
|
||||
Ok(())
|
||||
}
|
||||
|
||||
struct BlockingRemoteControlBackend {
|
||||
enroll_request_rx: Option<oneshot::Receiver<Result<String>>>,
|
||||
server_task: JoinHandle<()>,
|
||||
}
|
||||
|
||||
impl BlockingRemoteControlBackend {
|
||||
async fn start(codex_home: &std::path::Path) -> Result<Self> {
|
||||
let listener = TcpListener::bind("127.0.0.1:0").await?;
|
||||
let remote_control_url = format!("http://{}/backend-api/", listener.local_addr()?);
|
||||
write_mock_responses_config_toml_with_chatgpt_base_url(
|
||||
codex_home,
|
||||
&remote_control_url,
|
||||
&remote_control_url,
|
||||
)?;
|
||||
write_chatgpt_auth(
|
||||
codex_home,
|
||||
ChatGptAuthFixture::new("chatgpt-token")
|
||||
.account_id("account_id")
|
||||
.chatgpt_account_id("account_id"),
|
||||
AuthCredentialsStoreMode::File,
|
||||
)?;
|
||||
|
||||
let (enroll_request_tx, enroll_request_rx) = oneshot::channel();
|
||||
let server_task = tokio::spawn(async move {
|
||||
match read_enroll_request(listener).await {
|
||||
Ok((request_line, _reader)) => {
|
||||
let _ = enroll_request_tx.send(Ok(request_line));
|
||||
std::future::pending::<()>().await;
|
||||
}
|
||||
Err(err) => {
|
||||
let _ = enroll_request_tx.send(Err(err));
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
Ok(Self {
|
||||
enroll_request_rx: Some(enroll_request_rx),
|
||||
server_task,
|
||||
})
|
||||
}
|
||||
|
||||
async fn wait_for_enroll_request(&mut self) -> Result<String> {
|
||||
let rx = self
|
||||
.enroll_request_rx
|
||||
.take()
|
||||
.context("enroll request should only be awaited once")?;
|
||||
rx.await?
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for BlockingRemoteControlBackend {
|
||||
fn drop(&mut self) {
|
||||
self.server_task.abort();
|
||||
}
|
||||
}
|
||||
|
||||
async fn read_enroll_request(listener: TcpListener) -> Result<(String, BufReader<TcpStream>)> {
|
||||
let (stream, _) = listener.accept().await?;
|
||||
let mut reader = BufReader::new(stream);
|
||||
|
||||
let mut request_line = String::new();
|
||||
reader.read_line(&mut request_line).await?;
|
||||
|
||||
loop {
|
||||
let mut line = String::new();
|
||||
reader.read_line(&mut line).await?;
|
||||
if line == "\r\n" {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
Ok((request_line.trim_end().to_string(), reader))
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user