feat(remote-control): add pairing start (#25675)

## Why

Remote control enrollment authorizes a desktop server, but app-server v2
did not expose the follow-up pairing operation needed to mint a
short-lived controller pairing artifact from that enrolled server.
Clients need a narrow RPC that starts pairing without exposing the
backend `serverId` or conflating pairing with websocket connection
state.

Issue: N/A; internal remote-control pairing API change.

## What Changed

Added experimental app-server v2 `remoteControl/pairing/start` with
`manualCode` input and `pairingCode`, nullable `manualPairingCode`,
`environmentId`, and Unix-seconds `expiresAt` output. The method
serializes under its own `global("remote-control-pairing")` scope and is
documented in `app-server/README.md`.

Extended the remote-control transport with private `/server/pair`
request/response types and normalized `pair_url` handling. Pairing uses
the current enrolled server bearer, refreshes that bearer when needed,
keeps backend `server_id` private, validates returned `server_id` and
`environment_id` against the current enrollment, and preserves backend
status/header/body context for failures and malformed responses.

Wired the request through `RemoteControlRequestProcessor` and
`MessageProcessor`, mapping unavailable/disabled pairing to
`invalid_request` and backend failures to internal errors.

## Verification

- `just test -p codex-app-server-transport`
- `just test -p codex-app-server
remote_control_pairing_start_returns_pairing_artifacts`
This commit is contained in:
Anton Panasenko authored and GitHub committed 2026-06-02 01:05:50 +00:00
1 parent 1ad0d7aa4b
commit 0002316687
15 files changed
+1302 -48

No files matched your search

@@ -5,7 +5,10 @@ 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::RemoteControlPairingStartParams;
use codex_app_server_protocol::RemoteControlPairingStartResponse;
use codex_app_server_protocol::RemoteControlStatusReadResponse;
use std::io;
#[derive(Clone)]
pub(crate) struct RemoteControlRequestProcessor {
@@ -42,6 +45,16 @@ impl RemoteControlRequestProcessor {
})
}
pub(crate) async fn pairing_start(
&self,
params: RemoteControlPairingStartParams,
) -> Result<RemoteControlPairingStartResponse, JSONRPCErrorError> {
self.handle()?
.start_pairing(params)
.await
.map_err(map_pairing_start_error)
}
fn handle(&self) -> Result<&RemoteControlHandle, JSONRPCErrorError> {
self.remote_control_handle
.as_ref()
@@ -52,3 +65,14 @@ impl RemoteControlRequestProcessor {
fn map_unavailable(err: RemoteControlUnavailable) -> JSONRPCErrorError {
invalid_request(err.to_string())
}
fn map_pairing_start_error(err: io::Error) -> JSONRPCErrorError {
if err.kind() == io::ErrorKind::InvalidInput {
invalid_request(err.to_string())
} else {
internal_error(err.to_string())
}
}
#[cfg(test)]
mod remote_control_processor_tests;
@@ -0,0 +1,48 @@
use super::*;
use crate::error_code::INTERNAL_ERROR_CODE;
use crate::error_code::INVALID_REQUEST_ERROR_CODE;
use pretty_assertions::assert_eq;
#[tokio::test]
async fn pairing_start_returns_internal_error_when_remote_control_is_unavailable() {
let err = RemoteControlRequestProcessor::new(/*remote_control_handle*/ None)
.pairing_start(RemoteControlPairingStartParams::default())
.await
.expect_err("missing remote control should fail pairing");
assert_eq!(
err,
JSONRPCErrorError {
code: INTERNAL_ERROR_CODE,
data: None,
message: "remote control is unavailable for this app-server".to_string(),
}
);
}
#[test]
fn pairing_start_maps_invalid_input_to_invalid_request() {
assert_eq!(
map_pairing_start_error(io::Error::new(
io::ErrorKind::InvalidInput,
"remote control pairing is unavailable",
)),
JSONRPCErrorError {
code: INVALID_REQUEST_ERROR_CODE,
data: None,
message: "remote control pairing is unavailable".to_string(),
}
);
}
#[test]
fn pairing_start_maps_backend_failures_to_internal_error() {
assert_eq!(
map_pairing_start_error(io::Error::other("remote control pairing failed")),
JSONRPCErrorError {
code: INTERNAL_ERROR_CODE,
data: None,
message: "remote control pairing failed".to_string(),
}
);
}