mirror of
https://github.com/pchuan98/codex.git
synced 2026-07-01 00:31:56 +08:00
[app-server] expose environment info RPC (#30291)
## Why
App-server clients that configure named execution environments need to
discover an environment's shell and working directory before selecting
it for a thread or turn. Because the environment can run on a different
operating system than app-server, its working directory is represented
as a canonical `file:` URI rather than a host-local path string. The
probe also needs a bounded response time: an exec-server that completes
initialization but never answers `environment/info` must not hold the
environment serialization queue indefinitely.
## What changed
- Add an experimental `environment/info` app-server RPC for named
environments.
- Route the probe through the managed environment connection and return
target-native shell metadata plus the default working directory as a
`PathUri`.
- Return connection and protocol failures as JSON-RPC errors.
- Bound the exec-server probe response to 30 seconds and remove
timed-out calls from the pending-request table so later environment
mutations can proceed.
- Cover successful responses, omitted working directories, unknown
environments, connection failures, and pending-call cleanup.
## Protocol examples
Request:
```json
{
"id": 42,
"method": "environment/info",
"params": {
"environmentId": "remote-a"
}
}
```
Successful response:
```json
{
"id": 42,
"result": {
"shell": {
"name": "zsh",
"path": "/bin/zsh"
},
"cwd": "file:///workspace"
}
}
```
If the exec-server initializes but does not answer the probe within 30
seconds:
```json
{
"id": 42,
"error": {
"code": -32603,
"message": "failed to get info for environment `remote-a`: exec-server protocol error: timed out waiting for exec-server `environment/info` response after 30s"
}
}
```
## Testing
- App-server integration coverage for successful info (including omitted
`cwd`), unknown environments, and connection failures.
- Exec-server RPC coverage verifying a timed-out call is removed from
the pending-request table.
---------
Co-authored-by: Michael Bolin <mbolin@openai.com>
This commit is contained in:
committed by
GitHub
Unverified
parent
d2885dc3cd
commit
e2398d0b16
@@ -110,6 +110,7 @@ mod recovery;
|
||||
|
||||
const CONNECT_TIMEOUT: Duration = Duration::from_secs(10);
|
||||
const INITIALIZE_TIMEOUT: Duration = Duration::from_secs(10);
|
||||
const ENVIRONMENT_INFO_TIMEOUT: Duration = Duration::from_secs(30);
|
||||
const PROCESS_EVENT_CHANNEL_CAPACITY: usize = 256;
|
||||
const PROCESS_EVENT_RETAINED_BYTES: usize = 1024 * 1024;
|
||||
|
||||
@@ -510,7 +511,12 @@ impl ExecServerClient {
|
||||
}
|
||||
|
||||
pub async fn environment_info(&self) -> Result<EnvironmentInfo, ExecServerError> {
|
||||
self.call(ENVIRONMENT_INFO_METHOD, &()).await
|
||||
let rpc_client = self.inner.rpc_client().await?;
|
||||
map_rpc_call_result(
|
||||
rpc_client
|
||||
.call_with_timeout(ENVIRONMENT_INFO_METHOD, &(), ENVIRONMENT_INFO_TIMEOUT)
|
||||
.await,
|
||||
)
|
||||
}
|
||||
|
||||
pub async fn read(&self, params: ReadParams) -> Result<ReadResponse, ExecServerError> {
|
||||
@@ -780,22 +786,21 @@ impl ExecServerClient {
|
||||
P: serde::Serialize,
|
||||
T: serde::de::DeserializeOwned,
|
||||
{
|
||||
match rpc_client.call(method, params).await {
|
||||
Ok(response) => Ok(response),
|
||||
Err(error) => {
|
||||
let error = ExecServerError::from(error);
|
||||
if is_transport_closed_error(&error) {
|
||||
Err(ExecServerError::Disconnected(disconnected_message(
|
||||
/*reason*/ None,
|
||||
)))
|
||||
} else {
|
||||
Err(error)
|
||||
}
|
||||
}
|
||||
}
|
||||
map_rpc_call_result(rpc_client.call(method, params).await)
|
||||
}
|
||||
}
|
||||
|
||||
fn map_rpc_call_result<T>(result: Result<T, RpcCallError>) -> Result<T, ExecServerError> {
|
||||
result.map_err(|error| {
|
||||
let error = ExecServerError::from(error);
|
||||
if is_transport_closed_error(&error) {
|
||||
ExecServerError::Disconnected(disconnected_message(/*reason*/ None))
|
||||
} else {
|
||||
error
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
async fn cleanup_process_start(
|
||||
client: &ExecServerClient,
|
||||
process_id: &ProcessId,
|
||||
@@ -822,6 +827,9 @@ impl From<RpcCallError> for ExecServerError {
|
||||
code: error.code,
|
||||
message: error.message,
|
||||
},
|
||||
RpcCallError::TimedOut { method, timeout } => Self::Protocol(format!(
|
||||
"timed out waiting for exec-server `{method}` response after {timeout:?}"
|
||||
)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ use std::sync::Arc;
|
||||
use std::sync::atomic::AtomicBool;
|
||||
use std::sync::atomic::AtomicI64;
|
||||
use std::sync::atomic::Ordering;
|
||||
use std::time::Duration;
|
||||
|
||||
use codex_exec_server_protocol::JSONRPCError;
|
||||
use codex_exec_server_protocol::JSONRPCErrorError;
|
||||
@@ -21,6 +22,7 @@ use tokio::sync::mpsc;
|
||||
use tokio::sync::oneshot;
|
||||
use tokio::sync::watch;
|
||||
use tokio::task::JoinHandle;
|
||||
use tokio::time::timeout;
|
||||
|
||||
use crate::connection::JsonRpcConnection;
|
||||
use crate::connection::JsonRpcConnectionEvent;
|
||||
@@ -36,6 +38,8 @@ pub(crate) enum RpcCallError {
|
||||
Json(serde_json::Error),
|
||||
/// The executor returned a JSON-RPC error response for this call.
|
||||
Server(JSONRPCErrorError),
|
||||
/// The executor did not return a response before the caller's deadline.
|
||||
TimedOut { method: String, timeout: Duration },
|
||||
}
|
||||
|
||||
type PendingRequest = oneshot::Sender<Result<Value, RpcCallError>>;
|
||||
@@ -46,6 +50,11 @@ type RequestRoute<S> = Box<
|
||||
type NotificationRoute<S> =
|
||||
Box<dyn Fn(Arc<S>, JSONRPCNotification) -> BoxFuture<Result<(), String>> + Send + Sync>;
|
||||
|
||||
enum RpcCallTimeout {
|
||||
None,
|
||||
After(Duration),
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub(crate) enum RpcClientEvent {
|
||||
Notification(JSONRPCNotification),
|
||||
@@ -334,6 +343,33 @@ impl RpcClient {
|
||||
}
|
||||
|
||||
pub(crate) async fn call<P, T>(&self, method: &str, params: &P) -> Result<T, RpcCallError>
|
||||
where
|
||||
P: Serialize,
|
||||
T: DeserializeOwned,
|
||||
{
|
||||
self.call_inner(method, params, RpcCallTimeout::None).await
|
||||
}
|
||||
|
||||
pub(crate) async fn call_with_timeout<P, T>(
|
||||
&self,
|
||||
method: &str,
|
||||
params: &P,
|
||||
call_timeout: Duration,
|
||||
) -> Result<T, RpcCallError>
|
||||
where
|
||||
P: Serialize,
|
||||
T: DeserializeOwned,
|
||||
{
|
||||
self.call_inner(method, params, RpcCallTimeout::After(call_timeout))
|
||||
.await
|
||||
}
|
||||
|
||||
async fn call_inner<P, T>(
|
||||
&self,
|
||||
method: &str,
|
||||
params: &P,
|
||||
call_timeout: RpcCallTimeout,
|
||||
) -> Result<T, RpcCallError>
|
||||
where
|
||||
P: Serialize,
|
||||
T: DeserializeOwned,
|
||||
@@ -379,8 +415,20 @@ impl RpcClient {
|
||||
// still-pending requests. Awaiting this receiver preserves that order:
|
||||
// responses already read before EOF still win, and truly pending calls
|
||||
// are failed once the reader observes the disconnect.
|
||||
let result: Result<Value, RpcCallError> =
|
||||
response_rx.await.map_err(|_| RpcCallError::Closed)?;
|
||||
let response = match call_timeout {
|
||||
RpcCallTimeout::None => response_rx.await,
|
||||
RpcCallTimeout::After(call_timeout) => match timeout(call_timeout, response_rx).await {
|
||||
Ok(response) => response,
|
||||
Err(_) => {
|
||||
self.pending.lock().await.remove(&request_id);
|
||||
return Err(RpcCallError::TimedOut {
|
||||
method: method.to_string(),
|
||||
timeout: call_timeout,
|
||||
});
|
||||
}
|
||||
},
|
||||
};
|
||||
let result: Result<Value, RpcCallError> = response.map_err(|_| RpcCallError::Closed)?;
|
||||
let response = match result {
|
||||
Ok(response) => response,
|
||||
Err(error) => return Err(error),
|
||||
@@ -673,6 +721,40 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn rpc_client_timeout_removes_pending_request() {
|
||||
let (client_stdin, server_reader) = tokio::io::duplex(4096);
|
||||
let (server_writer, client_stdout) = tokio::io::duplex(4096);
|
||||
let (release_server_tx, release_server_rx) = tokio::sync::oneshot::channel();
|
||||
let connection =
|
||||
JsonRpcConnection::from_stdio(client_stdout, client_stdin, "test-rpc".to_string());
|
||||
let (client, _events_rx) = RpcClient::new(connection);
|
||||
|
||||
let server = tokio::spawn(async move {
|
||||
let mut lines = BufReader::new(server_reader).lines();
|
||||
let request = read_jsonrpc_line(&mut lines).await;
|
||||
assert!(matches!(request, JSONRPCMessage::Request(_)));
|
||||
let _server_writer = server_writer;
|
||||
let _ = release_server_rx.await;
|
||||
});
|
||||
|
||||
let call_timeout = Duration::from_millis(10);
|
||||
let result = client
|
||||
.call_with_timeout::<_, serde_json::Value>("slow", &serde_json::json!({}), call_timeout)
|
||||
.await;
|
||||
assert!(matches!(
|
||||
result,
|
||||
Err(super::RpcCallError::TimedOut { method, timeout })
|
||||
if method == "slow" && timeout == call_timeout
|
||||
));
|
||||
assert_eq!(client.pending_request_count().await, 0);
|
||||
|
||||
let _ = release_server_tx.send(());
|
||||
if let Err(err) = server.await {
|
||||
panic!("server task failed: {err}");
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "current_thread")]
|
||||
async fn rpc_client_propagates_current_trace_context() {
|
||||
let span_exporter = InMemorySpanExporter::default();
|
||||
|
||||
Reference in New Issue
Block a user