Refactor cloud requirements error and surface in JSON-RPC error (#14504)

Refactors cloud requirements error handling to carry structured error
metadata and surfaces that metadata through JSON-RPC config-load
failures, including:
* adds typed CloudRequirementsLoadErrorCode values plus optional
statusCode
* marks thread/start, thread/resume, and thread/fork config failures
with structured cloud-requirements error data
This commit is contained in:
alexsong-oai
2026-03-12 20:30:51 -07:00
committed by GitHub
Unverified
parent 0daffe667a
commit 650beb177e
9 changed files with 562 additions and 29 deletions
@@ -201,6 +201,8 @@ use codex_core::config::NetworkProxyAuditMetadata;
use codex_core::config::edit::ConfigEdit;
use codex_core::config::edit::ConfigEditsBuilder;
use codex_core::config::types::McpServerTransportConfig;
use codex_core::config_loader::CloudRequirementsLoadError;
use codex_core::config_loader::CloudRequirementsLoadErrorCode;
use codex_core::config_loader::CloudRequirementsLoader;
use codex_core::default_client::set_default_client_residency_requirement;
use codex_core::error::CodexErr;
@@ -1959,11 +1961,7 @@ impl CodexMessageProcessor {
{
Ok(config) => config,
Err(err) => {
let error = JSONRPCErrorError {
code: INVALID_REQUEST_ERROR_CODE,
message: format!("error deriving config: {err}"),
data: None,
};
let error = config_load_error(&err);
listener_task_context
.outgoing
.send_error(request_id, error)
@@ -3366,11 +3364,7 @@ impl CodexMessageProcessor {
{
Ok(config) => config,
Err(err) => {
let error = JSONRPCErrorError {
code: INVALID_REQUEST_ERROR_CODE,
message: format!("error deriving config: {err}"),
data: None,
};
let error = config_load_error(&err);
self.outgoing.send_error(request_id, error).await;
return;
}
@@ -3889,11 +3883,9 @@ impl CodexMessageProcessor {
{
Ok(config) => config,
Err(err) => {
self.send_invalid_request_error(
request_id,
format!("error deriving config: {err}"),
)
.await;
self.outgoing
.send_error(request_id, config_load_error(&err))
.await;
return;
}
};
@@ -7464,6 +7456,42 @@ fn errors_to_info(
.collect()
}
fn cloud_requirements_load_error(err: &std::io::Error) -> Option<&CloudRequirementsLoadError> {
let mut current: Option<&(dyn std::error::Error + 'static)> = err
.get_ref()
.map(|source| source as &(dyn std::error::Error + 'static));
while let Some(source) = current {
if let Some(cloud_error) = source.downcast_ref::<CloudRequirementsLoadError>() {
return Some(cloud_error);
}
current = source.source();
}
None
}
fn config_load_error(err: &std::io::Error) -> JSONRPCErrorError {
let data = cloud_requirements_load_error(err).map(|cloud_error| {
let mut data = serde_json::json!({
"reason": "cloudRequirements",
"errorCode": format!("{:?}", cloud_error.code()),
"detail": cloud_error.to_string(),
});
if let Some(status_code) = cloud_error.status_code() {
data["statusCode"] = serde_json::json!(status_code);
}
if cloud_error.code() == CloudRequirementsLoadErrorCode::Auth {
data["action"] = serde_json::json!("relogin");
}
data
});
JSONRPCErrorError {
code: INVALID_REQUEST_ERROR_CODE,
message: format!("failed to load configuration: {err}"),
data,
}
}
fn validate_dynamic_tools(tools: &[ApiDynamicToolSpec]) -> Result<(), String> {
let mut seen = HashSet::new();
for tool in tools {
@@ -8099,6 +8127,67 @@ mod tests {
validate_dynamic_tools(&tools).expect("valid schema");
}
#[test]
fn config_load_error_marks_cloud_requirements_failures_for_relogin() {
let err = std::io::Error::other(CloudRequirementsLoadError::new(
CloudRequirementsLoadErrorCode::Auth,
Some(401),
"Your authentication session could not be refreshed automatically. Please log out and sign in again.",
));
let error = config_load_error(&err);
assert_eq!(
error.data,
Some(json!({
"reason": "cloudRequirements",
"errorCode": "Auth",
"action": "relogin",
"statusCode": 401,
"detail": "Your authentication session could not be refreshed automatically. Please log out and sign in again.",
}))
);
assert!(
error.message.contains("failed to load configuration"),
"unexpected error message: {}",
error.message
);
}
#[test]
fn config_load_error_leaves_non_cloud_requirements_failures_unmarked() {
let err = std::io::Error::other("required MCP servers failed to initialize");
let error = config_load_error(&err);
assert_eq!(error.data, None);
assert!(
error.message.contains("failed to load configuration"),
"unexpected error message: {}",
error.message
);
}
#[test]
fn config_load_error_marks_non_auth_cloud_requirements_failures_without_relogin() {
let err = std::io::Error::other(CloudRequirementsLoadError::new(
CloudRequirementsLoadErrorCode::RequestFailed,
None,
"failed to load your workspace-managed config",
));
let error = config_load_error(&err);
assert_eq!(
error.data,
Some(json!({
"reason": "cloudRequirements",
"errorCode": "RequestFailed",
"detail": "failed to load your workspace-managed config",
}))
);
}
#[test]
fn collect_resume_override_mismatches_includes_service_tier() {
let request = ThreadResumeParams {