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

@@ -917,6 +917,11 @@ impl MessageProcessor {
.remote_control_processor
.status_read()
.map(|response| Some(response.into())),
ClientRequest::RemoteControlPairingStart { params, .. } => self
.remote_control_processor
.pairing_start(params)
.await
.map(|response| Some(response.into())),
ClientRequest::ConfigRequirementsRead { params: _, .. } => self
.config_processor
.config_requirements_read()