Support the app-server daemon on Windows (#42405)

## Why

The managed app-server daemon and automatic TUI attachment were limited to
Unix, so Windows users could not share a background server across Codex
sessions.

## What changed

- Enable daemon lifecycle commands, `codex agents` startup, and AF_UNIX daemon
  discovery on Windows.
- Require Windows daemons to start non-elevated and verify the discovered
  socket directory and peer before connecting.
- Preflight detached process support, preserve path-based environment settings,
  and keep daemon processes out of the launching project's working directory.
- Fall back to an embedded server when an implicitly discovered daemon cannot
  connect, while preserving errors for explicit remote endpoints and bypassing
  daemon reuse when `CODEX_EXEC_SERVER_URL` is set.

## Testing

Add coverage for Windows launch restrictions and socket validation, implicit
connection fallback, executor selection, daemon discovery, and the agents
dashboard startup result.

GitOrigin-RevId: e7fd63cf8690bcca8b5155266416f31f2f792aa2
This commit is contained in:
Eric Traut
2026-09-03 01:26:35 +00:00
committed by copyberry
parent deb1471166
commit b7f710273e
25 changed files with 659 additions and 92 deletions
+34 -1
View File
@@ -169,8 +169,34 @@ pub struct RemoteAppServerRequestHandle {
command_tx: mpsc::Sender<RemoteClientCommand>,
}
enum SocketPeerPolicy {
ExplicitEndpoint,
#[cfg(windows)]
NonElevatedCurrentUser,
}
impl RemoteAppServerClient {
pub async fn connect(args: RemoteAppServerConnectArgs) -> IoResult<Self> {
Self::connect_with_policy(args, SocketPeerPolicy::ExplicitEndpoint).await
}
/// Connects to an implicitly discovered Windows daemon, verifying its peer
/// token before the WebSocket handshake or any session requests.
#[cfg(windows)]
pub async fn connect_local_daemon(args: RemoteAppServerConnectArgs) -> IoResult<Self> {
if !matches!(args.endpoint, RemoteAppServerEndpoint::UnixSocket { .. }) {
return Err(IoError::new(
ErrorKind::InvalidInput,
"local daemon requires a Unix socket",
));
}
Self::connect_with_policy(args, SocketPeerPolicy::NonElevatedCurrentUser).await
}
async fn connect_with_policy(
args: RemoteAppServerConnectArgs,
peer_policy: SocketPeerPolicy,
) -> IoResult<Self> {
let channel_capacity = args.channel_capacity.max(1);
let initialize_params = args.initialize_params();
match args.endpoint {
@@ -184,7 +210,8 @@ impl RemoteAppServerClient {
.await
}
RemoteAppServerEndpoint::UnixSocket { socket_path } => {
let (endpoint, stream) = connect_unix_socket_endpoint(socket_path).await?;
let (endpoint, stream) =
connect_unix_socket_endpoint(socket_path, peer_policy).await?;
Self::connect_with_stream(channel_capacity, endpoint, stream, initialize_params)
.await
}
@@ -757,6 +784,7 @@ async fn connect_websocket_endpoint(
async fn connect_unix_socket_endpoint(
socket_path: AbsolutePathBuf,
peer_policy: SocketPeerPolicy,
) -> IoResult<(String, WebSocketStream<UnixStream>)> {
let endpoint = format!("unix://{}", socket_path.display());
let request = UDS_WEBSOCKET_HANDSHAKE_URL
@@ -780,6 +808,11 @@ async fn connect_unix_socket_endpoint(
"failed to connect to remote app server at `{endpoint}`: {err}"
))
})?;
match peer_policy {
SocketPeerPolicy::ExplicitEndpoint => {}
#[cfg(windows)]
SocketPeerPolicy::NonElevatedCurrentUser => stream.ensure_non_elevated_peer()?,
}
let websocket_config = remote_websocket_config();
let stream = timeout(
CONNECT_TIMEOUT,