mirror of
https://github.com/pchuan98/codex.git
synced 2026-07-01 00:31:56 +08:00
feat(app-server): add remote control client management RPCs (#25785)
## Why
Remote-control clients need to list and revoke controller-device grants
without enabling or enrolling the local relay. These are signed-in
account-management operations, so coupling them to websocket, pairing,
enrollment, or persisted relay state would prevent clients from managing
stale grants from the picker.
Related enhancement request: N/A. This adds the Codex app-server surface
for the planned upstream environment-scoped revoke endpoint.
## What Changed
- Added experimental app-server v2 RPCs:
- `remoteControl/client/list`
- `remoteControl/client/revoke`
- Added picker-oriented protocol types and standard generated schema
fixtures. The list response intentionally omits backend account id,
enrollment status, and location fields.
- Added `app-server-transport/src/transport/remote_control/clients.rs`
for environment-scoped GET and DELETE requests. It builds escaped URL
path segments, forwards optional pagination query fields, sends ChatGPT
auth plus `chatgpt-account-id`, converts RFC3339 `last_seen_at` values
to Unix seconds, accepts `204 No Content` revoke responses, and retries
once after a `401`.
- Extracted shared ChatGPT auth loading and recovery into
`app-server-transport/src/transport/remote_control/auth.rs` so
websocket, pairing, and client management use the same account-auth
boundary.
- Retained the configured remote-control base URL on
`RemoteControlHandle` and resolve management URLs lazily, preserving
deferred validation while relay startup is disabled.
- Registered list as `global_shared_read("remote-control-clients")` and
revoke as `global("remote-control-clients")`.
## Verification
- Added transport coverage proving list and revoke work while relay
state is disabled, IDs are escaped, picker-only fields are returned,
timestamps are converted, revoke accepts `204`, auth headers are
forwarded, `401` retries exactly once, `403` is not retried, and
malformed list payloads retain decode context.
- Added an app-server integration test proving both JSON-RPC methods
work before relay enablement and successful revoke returns `{}`.
- Regenerated and validated experimental and standard app-server schema
fixtures.
This commit is contained in:
@@ -849,6 +849,18 @@ client_request_definitions! {
|
||||
serialization: global("remote-control-pairing"),
|
||||
response: v2::RemoteControlPairingStartResponse,
|
||||
},
|
||||
#[experimental("remoteControl/client/list")]
|
||||
RemoteControlClientsList => "remoteControl/client/list" {
|
||||
params: v2::RemoteControlClientsListParams,
|
||||
serialization: global_shared_read("remote-control-clients"),
|
||||
response: v2::RemoteControlClientsListResponse,
|
||||
},
|
||||
#[experimental("remoteControl/client/revoke")]
|
||||
RemoteControlClientsRevoke => "remoteControl/client/revoke" {
|
||||
params: v2::RemoteControlClientsRevokeParams,
|
||||
serialization: global("remote-control-clients"),
|
||||
response: v2::RemoteControlClientsRevokeResponse,
|
||||
},
|
||||
#[experimental("collaborationMode/list")]
|
||||
/// Lists collaboration mode presets.
|
||||
CollaborationModeList => "collaborationMode/list" {
|
||||
@@ -1994,6 +2006,29 @@ mod tests {
|
||||
"remote-control-pairing"
|
||||
))
|
||||
);
|
||||
let remote_control_clients_list = ClientRequest::RemoteControlClientsList {
|
||||
request_id: request_id(),
|
||||
params: v2::RemoteControlClientsListParams::default(),
|
||||
};
|
||||
assert_eq!(
|
||||
remote_control_clients_list.serialization_scope(),
|
||||
Some(ClientRequestSerializationScope::GlobalSharedRead(
|
||||
"remote-control-clients"
|
||||
))
|
||||
);
|
||||
let remote_control_clients_revoke = ClientRequest::RemoteControlClientsRevoke {
|
||||
request_id: request_id(),
|
||||
params: v2::RemoteControlClientsRevokeParams {
|
||||
environment_id: "environment-id".to_string(),
|
||||
client_id: "client-id".to_string(),
|
||||
},
|
||||
};
|
||||
assert_eq!(
|
||||
remote_control_clients_revoke.serialization_scope(),
|
||||
Some(ClientRequestSerializationScope::Global(
|
||||
"remote-control-clients"
|
||||
))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -62,6 +62,62 @@ pub struct RemoteControlPairingStartResponse {
|
||||
pub expires_at: i64,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, Default, PartialEq, Eq, JsonSchema, TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(export_to = "v2/")]
|
||||
pub struct RemoteControlClientsListParams {
|
||||
pub environment_id: String,
|
||||
#[ts(optional = nullable)]
|
||||
pub cursor: Option<String>,
|
||||
#[ts(optional = nullable)]
|
||||
pub limit: Option<u32>,
|
||||
#[ts(optional = nullable)]
|
||||
pub order: Option<RemoteControlClientsListOrder>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq, JsonSchema, TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(rename_all = "camelCase", export_to = "v2/")]
|
||||
pub enum RemoteControlClientsListOrder {
|
||||
Asc,
|
||||
Desc,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(export_to = "v2/")]
|
||||
pub struct RemoteControlClientsListResponse {
|
||||
pub data: Vec<RemoteControlClient>,
|
||||
pub next_cursor: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(export_to = "v2/")]
|
||||
pub struct RemoteControlClient {
|
||||
pub client_id: String,
|
||||
pub display_name: Option<String>,
|
||||
pub device_type: Option<String>,
|
||||
pub platform: Option<String>,
|
||||
pub os_version: Option<String>,
|
||||
pub device_model: Option<String>,
|
||||
pub app_version: Option<String>,
|
||||
pub last_seen_at: Option<i64>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(export_to = "v2/")]
|
||||
pub struct RemoteControlClientsRevokeParams {
|
||||
pub environment_id: String,
|
||||
pub client_id: String,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(export_to = "v2/")]
|
||||
pub struct RemoteControlClientsRevokeResponse {}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq, JsonSchema, TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(rename_all = "camelCase", export_to = "v2/")]
|
||||
@@ -105,3 +161,7 @@ impl From<RemoteControlStatusChangedNotification> for RemoteControlDisableRespon
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "remote_control_tests.rs"]
|
||||
mod tests;
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
use super::*;
|
||||
use pretty_assertions::assert_eq;
|
||||
use serde_json::json;
|
||||
|
||||
#[test]
|
||||
fn remote_control_clients_list_params_serialize_nullable_optional_fields() {
|
||||
assert_eq!(
|
||||
serde_json::to_value(RemoteControlClientsListParams {
|
||||
environment_id: "env-123".to_string(),
|
||||
cursor: None,
|
||||
limit: None,
|
||||
order: None,
|
||||
})
|
||||
.expect("params should serialize"),
|
||||
json!({
|
||||
"environmentId": "env-123",
|
||||
"cursor": null,
|
||||
"limit": null,
|
||||
"order": null,
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn remote_control_clients_list_params_deserialize_camel_case_fields() {
|
||||
assert_eq!(
|
||||
serde_json::from_value::<RemoteControlClientsListParams>(json!({
|
||||
"environmentId": "env-123",
|
||||
"cursor": "cursor-123",
|
||||
"limit": 10,
|
||||
"order": "asc",
|
||||
}))
|
||||
.expect("params should deserialize"),
|
||||
RemoteControlClientsListParams {
|
||||
environment_id: "env-123".to_string(),
|
||||
cursor: Some("cursor-123".to_string()),
|
||||
limit: Some(10),
|
||||
order: Some(RemoteControlClientsListOrder::Asc),
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn remote_control_clients_revoke_response_serializes_as_empty_object() {
|
||||
assert_eq!(
|
||||
serde_json::to_value(RemoteControlClientsRevokeResponse {})
|
||||
.expect("response should serialize"),
|
||||
json!({})
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user