[codex] cover app-owned HTTP requests [ci changed_files]

This commit is contained in:
Cooper Gamble
2026-06-03 23:44:25 +00:00
parent ec2e111e04
commit d766866d3c
52 changed files with 1415 additions and 358 deletions
+20
View File
@@ -1597,6 +1597,16 @@ async fn spawn_thread_subagent_inherits_parent_http_state_surface() {
)
.await
.expect("parent client info should update");
assert_eq!(
parent_thread
.codex
.session
.services
.thread_extension_data
.get::<HttpStateSurface>()
.as_deref(),
Some(&HttpStateSurface::CodexDesktop)
);
let child_thread_id = harness
.control
@@ -1628,6 +1638,16 @@ async fn spawn_thread_subagent_inherits_parent_http_state_surface() {
.http_state_surface(),
Some(HttpStateSurface::CodexDesktop)
);
assert_eq!(
child_thread
.codex
.session
.services
.thread_extension_data
.get::<HttpStateSurface>()
.as_deref(),
Some(&HttpStateSurface::CodexDesktop)
);
}
#[tokio::test]
+35 -7
View File
@@ -415,6 +415,16 @@ impl ModelClient {
self.state.http_state.clone()
}
pub(crate) fn http_state_context_for_surface(
&self,
surface: HttpStateSurface,
) -> Option<HttpStateContext> {
self.state
.http_state
.as_ref()
.map(|state| state.for_surface(surface))
}
pub(crate) fn set_http_state_surface(&self, surface: HttpStateSurface) {
if self
.state
@@ -869,7 +879,15 @@ impl ModelClient {
/// This centralizes setup used by both prewarm and normal request paths so they stay in
/// lockstep when auth/provider resolution changes.
async fn current_client_setup(&self) -> Result<CurrentClientSetup> {
let auth = self.state.provider.auth().await;
let auth = match (
self.state.provider.auth_manager(),
self.http_state_surface(),
) {
(Some(auth_manager), Some(http_state_surface)) => {
auth_manager.auth_for_surface(http_state_surface).await
}
_ => self.state.provider.auth().await,
};
let api_provider = self.state.provider.api_provider().await?;
let api_auth = with_native_integrity_state(
self.state.provider.api_auth().await?,
@@ -1316,9 +1334,14 @@ impl ModelClientSession {
inference_trace: &InferenceTraceContext,
) -> Result<ResponseStream> {
let auth_manager = self.client.state.provider.auth_manager();
let mut auth_recovery = auth_manager
.as_ref()
.map(AuthManager::unauthorized_recovery);
let http_state_surface = self.client.http_state_surface();
let mut auth_recovery = auth_manager.as_ref().map(|auth_manager| {
if let Some(http_state_surface) = http_state_surface {
auth_manager.unauthorized_recovery_for_surface(http_state_surface)
} else {
auth_manager.unauthorized_recovery()
}
});
let mut pending_retry = PendingUnauthorizedRetry::default();
loop {
let client_setup = self.client.current_client_setup().await?;
@@ -1431,9 +1454,14 @@ impl ModelClientSession {
inference_trace: &InferenceTraceContext,
) -> Result<WebsocketStreamOutcome> {
let auth_manager = self.client.state.provider.auth_manager();
let mut auth_recovery = auth_manager
.as_ref()
.map(AuthManager::unauthorized_recovery);
let http_state_surface = self.client.http_state_surface();
let mut auth_recovery = auth_manager.as_ref().map(|auth_manager| {
if let Some(http_state_surface) = http_state_surface {
auth_manager.unauthorized_recovery_for_surface(http_state_surface)
} else {
auth_manager.unauthorized_recovery()
}
});
let mut pending_retry = PendingUnauthorizedRetry::default();
loop {
let client_setup = self.client.current_client_setup().await?;
+59 -5
View File
@@ -31,6 +31,7 @@ use codex_config::types::AppsConfigToml;
use codex_config::types::ToolSuggestDiscoverableType;
use codex_core_plugins::PluginsManager;
use codex_features::Feature;
use codex_http_state::HttpStateContext;
use codex_login::AuthManager;
use codex_login::CodexAuth;
use codex_login::default_client::originator;
@@ -87,10 +88,17 @@ pub struct AccessibleConnectorsStatus {
pub async fn list_accessible_connectors_from_mcp_tools(
config: &Config,
) -> anyhow::Result<Vec<AppInfo>> {
list_accessible_connectors_from_mcp_tools_with_http_state(config, /*http_state*/ None).await
}
pub(crate) async fn list_accessible_connectors_from_mcp_tools_with_http_state(
config: &Config,
http_state: Option<HttpStateContext>,
) -> anyhow::Result<Vec<AppInfo>> {
Ok(
list_accessible_connectors_from_mcp_tools_with_options_and_status(
config, /*force_refetch*/ false,
list_accessible_connectors_from_mcp_tools_with_options_and_status_and_http_state(
config, /*force_refetch*/ false, http_state,
)
.await?
.connectors,
@@ -147,10 +155,23 @@ pub(crate) async fn list_tool_suggest_discoverable_tools_with_auth(
pub async fn list_cached_accessible_connectors_from_mcp_tools(
config: &Config,
) -> Option<Vec<AppInfo>> {
list_cached_accessible_connectors_from_mcp_tools_with_http_state(
config, /*http_state*/ None,
)
.await
}
pub async fn list_cached_accessible_connectors_from_mcp_tools_with_http_state(
config: &Config,
http_state: Option<&HttpStateContext>,
) -> Option<Vec<AppInfo>> {
let auth_manager =
AuthManager::shared_from_config(config, /*enable_codex_api_key_env*/ false).await;
let auth = auth_manager.auth().await;
let auth = match http_state {
Some(http_state) => auth_manager.auth_for_surface(http_state.surface()).await,
None => auth_manager.auth().await,
};
if !config
.features
.apps_enabled_for_auth(auth.as_ref().is_some_and(CodexAuth::uses_codex_backend))
@@ -197,6 +218,19 @@ pub async fn list_accessible_connectors_from_mcp_tools_with_options(
pub async fn list_accessible_connectors_from_mcp_tools_with_options_and_status(
config: &Config,
force_refetch: bool,
) -> anyhow::Result<AccessibleConnectorsStatus> {
list_accessible_connectors_from_mcp_tools_with_options_and_status_and_http_state(
config,
force_refetch,
/*http_state*/ None,
)
.await
}
async fn list_accessible_connectors_from_mcp_tools_with_options_and_status_and_http_state(
config: &Config,
force_refetch: bool,
http_state: Option<HttpStateContext>,
) -> anyhow::Result<AccessibleConnectorsStatus> {
// TODO: Wire callers that already own an EnvironmentManager into
// list_accessible_connectors_from_mcp_tools_with_environment_manager instead
@@ -208,10 +242,11 @@ pub async fn list_accessible_connectors_from_mcp_tools_with_options_and_status(
let environment_manager =
EnvironmentManager::from_codex_home(config.codex_home.clone(), Some(local_runtime_paths))
.await?;
list_accessible_connectors_from_mcp_tools_with_environment_manager(
list_accessible_connectors_from_mcp_tools_with_environment_manager_and_http_state(
config,
force_refetch,
Arc::new(environment_manager),
http_state,
)
.await
}
@@ -220,10 +255,28 @@ pub async fn list_accessible_connectors_from_mcp_tools_with_environment_manager(
config: &Config,
force_refetch: bool,
environment_manager: Arc<EnvironmentManager>,
) -> anyhow::Result<AccessibleConnectorsStatus> {
list_accessible_connectors_from_mcp_tools_with_environment_manager_and_http_state(
config,
force_refetch,
environment_manager,
/*http_state*/ None,
)
.await
}
pub async fn list_accessible_connectors_from_mcp_tools_with_environment_manager_and_http_state(
config: &Config,
force_refetch: bool,
environment_manager: Arc<EnvironmentManager>,
http_state: Option<HttpStateContext>,
) -> anyhow::Result<AccessibleConnectorsStatus> {
let auth_manager =
AuthManager::shared_from_config(config, /*enable_codex_api_key_env*/ false).await;
let auth = auth_manager.auth().await;
let auth = match http_state.as_ref() {
Some(http_state) => auth_manager.auth_for_surface(http_state.surface()).await,
None => auth_manager.auth().await,
};
if !config
.features
.apps_enabled_for_auth(auth.as_ref().is_some_and(CodexAuth::uses_codex_backend))
@@ -282,6 +335,7 @@ pub async fn list_accessible_connectors_from_mcp_tools_with_environment_manager(
// one exists, but do not reintroduce the old hidden-local fallback.
McpRuntimeContext::new(environment_manager, config.cwd.to_path_buf()),
config.codex_home.to_path_buf(),
http_state,
codex_apps_tools_cache_key(auth.as_ref()),
host_owned_codex_apps_enabled,
mcp_config.prefix_mcp_tool_names,
+9 -2
View File
@@ -33,8 +33,15 @@ pub(crate) async fn rewrite_mcp_tool_arguments_for_openai_files(
let Some(arguments) = arguments_value.as_object() else {
return Ok(Some(arguments_value));
};
let auth = sess.services.auth_manager.auth().await;
let http_state = sess.services.model_client.http_state_context();
let auth = sess
.services
.auth_manager
.auth_for_surface(turn_context.http_state_surface)
.await;
let http_state = sess
.services
.model_client
.http_state_context_for_surface(turn_context.http_state_surface);
let mut rewritten_arguments = arguments.clone();
for field_name in openai_file_input_params {
+25 -13
View File
@@ -35,6 +35,7 @@ use codex_config::types::AppToolApproval;
use codex_config::types::ApprovalsReviewer;
use codex_features::Feature;
use codex_hooks::PermissionRequestDecision;
use codex_http_state::HttpStateContext;
use codex_mcp::CODEX_APPS_MCP_SERVER_NAME;
use codex_mcp::MCP_TOOL_CODEX_APPS_META_KEY;
use codex_mcp::McpPermissionPromptAutoApproveContext;
@@ -682,7 +683,11 @@ async fn refresh_codex_apps_after_connector_auth(sess: &Session, turn_context: &
match mcp_tools_result {
Ok(mcp_tools) => {
let auth = sess.services.auth_manager.auth().await;
let auth = sess
.services
.auth_manager
.auth_for_surface(turn_context.http_state_surface)
.await;
connectors::refresh_accessible_connectors_cache_from_mcp_tools(
&turn_context.config,
auth.as_ref(),
@@ -1434,18 +1439,25 @@ pub(crate) async fn lookup_mcp_tool_metadata(
.into_iter()
.find(|tool_info| tool_info.server_name == server && tool_info.tool.name == tool_name)?;
let connector_description = if server == CODEX_APPS_MCP_SERVER_NAME {
let connectors = match connectors::list_cached_accessible_connectors_from_mcp_tools(
turn_context.config.as_ref(),
)
.await
{
Some(connectors) => Some(connectors),
None => {
connectors::list_accessible_connectors_from_mcp_tools(turn_context.config.as_ref())
.await
.ok()
}
};
let http_state = HttpStateContext::new(
turn_context.config.codex_home.to_path_buf(),
turn_context.http_state_surface,
);
let connectors =
match connectors::list_cached_accessible_connectors_from_mcp_tools_with_http_state(
turn_context.config.as_ref(),
Some(&http_state),
)
.await
{
Some(connectors) => Some(connectors),
None => connectors::list_accessible_connectors_from_mcp_tools_with_http_state(
turn_context.config.as_ref(),
Some(http_state),
)
.await
.ok(),
};
connectors.and_then(|connectors| {
let connector_id = tool_info.connector_id.as_deref()?;
connectors
+1
View File
@@ -1269,6 +1269,7 @@ async fn install_host_owned_codex_apps_manager(session: &Session, turn_context:
turn_context.cwd.to_path_buf()
}),
turn_context.config.codex_home.to_path_buf(),
/*http_state*/ None,
codex_mcp::codex_apps_tools_cache_key(auth.as_ref()),
/*host_owned_codex_apps_enabled*/ true,
turn_context.config.prefix_mcp_tool_names(),
@@ -320,11 +320,11 @@ remote_plugin = true
let mut config = load_plugins_config(codex_home.path()).await;
config.chatgpt_base_url = format!("{}/backend-api", server.uri());
let plugins_manager = PluginsManager::new(config.codex_home.to_path_buf());
let remote_plugin_service_config =
RemotePluginServiceConfig::new(config.chatgpt_base_url.clone());
fetch_and_cache_global_remote_plugin_catalog(
codex_home.path(),
&RemotePluginServiceConfig {
chatgpt_base_url: config.chatgpt_base_url.clone(),
},
&remote_plugin_service_config,
Some(&auth),
)
.await
+18 -2
View File
@@ -27,6 +27,8 @@ use codex_config::config_toml::RealtimeWsVersion;
use codex_login::CodexAuth;
use codex_login::default_client::default_headers;
use codex_login::read_openai_api_key_from_env;
use codex_model_provider::unauthenticated_auth_provider;
use codex_model_provider::with_native_integrity_state;
use codex_model_provider_info::ModelProviderInfo;
use codex_protocol::error::CodexErr;
use codex_protocol::error::Result as CodexResult;
@@ -227,6 +229,7 @@ struct ConversationState {
struct RealtimeStart {
api_provider: ApiProvider,
websocket_auth: SharedAuthProvider,
extra_headers: Option<HeaderMap>,
session_config: RealtimeSessionConfig,
model_client: ModelClient,
@@ -279,6 +282,7 @@ impl RealtimeConversationManager {
async fn start_inner(&self, start: RealtimeStart) -> CodexResult<RealtimeStartOutput> {
let RealtimeStart {
api_provider,
websocket_auth,
extra_headers,
session_config,
model_client,
@@ -307,7 +311,7 @@ impl RealtimeConversationManager {
audio_rx,
};
let client = RealtimeWebsocketClient::new(api_provider);
let client = RealtimeWebsocketClient::new(api_provider).with_auth(websocket_auth);
let (task, sdp) = if let Some(sdp) = sdp {
let call = model_client
.create_realtime_call_with_headers(
@@ -601,6 +605,7 @@ pub(crate) async fn handle_start(
struct PreparedRealtimeConversationStart {
api_provider: ApiProvider,
websocket_auth: SharedAuthProvider,
extra_headers: Option<HeaderMap>,
requested_realtime_session_id: Option<String>,
version: RealtimeWsVersion,
@@ -618,7 +623,15 @@ async fn prepare_realtime_start(
.model_client
.auth_manager()
.unwrap_or_else(|| Arc::clone(&sess.services.auth_manager));
let auth = auth_manager.auth().await;
let auth = match sess.services.model_client.http_state_surface() {
Some(http_state_surface) => auth_manager.auth_for_surface(http_state_surface).await,
None => auth_manager.auth().await,
};
let websocket_auth = with_native_integrity_state(
unauthenticated_auth_provider(),
auth.as_ref(),
sess.services.model_client.http_state_context(),
);
let config = sess.get_config().await;
let transport = params
.transport
@@ -656,6 +669,7 @@ async fn prepare_realtime_start(
};
Ok(PreparedRealtimeConversationStart {
api_provider,
websocket_auth,
extra_headers,
requested_realtime_session_id,
version,
@@ -777,6 +791,7 @@ async fn handle_start_inner(
) -> CodexResult<()> {
let PreparedRealtimeConversationStart {
api_provider,
websocket_auth,
extra_headers,
requested_realtime_session_id,
version,
@@ -790,6 +805,7 @@ async fn handle_start_inner(
};
let start = RealtimeStart {
api_provider,
websocket_auth,
extra_headers,
session_config,
model_client: sess.services.model_client.clone(),
+8 -1
View File
@@ -309,7 +309,11 @@ impl Session {
store_mode: OAuthCredentialsStoreMode,
elicitation_reviewer: Option<ElicitationReviewerHandle>,
) {
let auth = self.services.auth_manager.auth().await;
let auth = self
.services
.auth_manager
.auth_for_surface(turn_context.http_state_surface)
.await;
let config = self.get_config().await;
let mcp_config = config
.to_mcp_config(self.services.plugins_manager.as_ref())
@@ -351,6 +355,9 @@ impl Session {
turn_context.permission_profile(),
mcp_runtime_context,
config.codex_home.to_path_buf(),
self.services
.model_client
.http_state_context_for_surface(turn_context.http_state_surface),
codex_apps_tools_cache_key(auth.as_ref()),
host_owned_codex_apps_enabled,
mcp_config.prefix_mcp_tool_names,
+6
View File
@@ -796,6 +796,12 @@ impl Codex {
.services
.model_client
.set_app_server_client_name(app_server_client_name.as_deref());
if let Some(http_state_surface) = self.session.services.model_client.http_state_surface() {
self.session
.services
.thread_extension_data
.insert(http_state_surface);
}
self.session
.update_settings(SessionSettingsUpdate {
app_server_client_name,
+2
View File
@@ -973,6 +973,7 @@ impl Session {
codex_extension_api::ExtensionData::new(session_id.to_string());
let thread_extension_data =
codex_extension_api::ExtensionData::new(thread_id.to_string());
thread_extension_data.insert(http_state_surface);
for contributor in extensions.thread_lifecycle_contributors() {
contributor.on_thread_start(codex_extension_api::ThreadStartInput {
config: config.as_ref(),
@@ -1184,6 +1185,7 @@ impl Session {
session_configuration.permission_profile(),
mcp_runtime_context,
config.codex_home.to_path_buf(),
sess.services.model_client.http_state_context(),
codex_apps_tools_cache_key(auth),
host_owned_codex_apps_enabled,
config.prefix_mcp_tool_names(),
+5 -1
View File
@@ -1126,7 +1126,11 @@ pub(crate) async fn built_tools(
} else {
None
};
let auth = sess.services.auth_manager.auth().await;
let auth = sess
.services
.auth_manager
.auth_for_surface(turn_context.http_state_surface)
.await;
let loaded_plugin_app_connector_ids = loaded_plugins
.effective_apps()
.into_iter()
@@ -138,7 +138,11 @@ impl ToolExecutor<ToolInvocation> for RequestPluginInstallHandler {
.as_ref()
.is_some_and(|response| response.action == ElicitationAction::Accept);
let auth = session.services.auth_manager.auth().await;
let auth = session
.services
.auth_manager
.auth_for_surface(turn.http_state_surface)
.await;
let completed = if user_confirmed {
verify_request_plugin_install_completed(&session, &turn, &tool, auth.as_ref()).await
} else {
+38 -30
View File
@@ -24,6 +24,7 @@ use codex_exec_server::CreateDirectoryOptions;
use codex_exec_server::ExecutorFileSystem;
use codex_exec_server::RemoveOptions;
use codex_extension_api::empty_extension_registry;
use codex_login::AuthManager;
use codex_login::CodexAuth;
use codex_model_provider_info::ModelProviderInfo;
use codex_model_provider_info::built_in_model_providers;
@@ -210,6 +211,7 @@ pub fn turn_permission_fields(
pub struct TestCodexBuilder {
config_mutators: Vec<Box<ConfigMutator>>,
auth: CodexAuth,
auth_manager: Option<Arc<AuthManager>>,
pre_build_hooks: Vec<Box<PreBuildHook>>,
workspace_setups: Vec<Box<WorkspaceSetup>>,
home: Option<Arc<TempDir>>,
@@ -232,6 +234,11 @@ impl TestCodexBuilder {
self
}
pub fn with_auth_manager(mut self, auth_manager: Arc<AuthManager>) -> Self {
self.auth_manager = Some(auth_manager);
self
}
pub fn with_model(self, model: &str) -> Self {
let new_model = model.to_string();
self.with_config(move |config| {
@@ -464,13 +471,16 @@ impl TestCodexBuilder {
test_env: TestEnv,
environment_manager: Arc<codex_exec_server::EnvironmentManager>,
) -> anyhow::Result<TestCodex> {
let auth = self.auth.clone();
let auth_manager = self
.auth_manager
.clone()
.unwrap_or_else(|| codex_core::test_support::auth_manager_from_auth(self.auth.clone()));
let state_db = codex_core::init_state_db(&config).await;
let thread_store = thread_store_from_config(&config, state_db.clone());
let installation_id = resolve_installation_id(&config.codex_home).await?;
let thread_manager = ThreadManager::new(
&config,
codex_core::test_support::auth_manager_from_auth(auth.clone()),
Arc::clone(&auth_manager),
SessionSource::Exec,
Arc::clone(&environment_manager),
empty_extension_registry(),
@@ -483,42 +493,39 @@ impl TestCodexBuilder {
let thread_manager = Arc::new(thread_manager);
let user_shell_override = self.user_shell_override.clone();
let new_conversation = match (resume_from, user_shell_override) {
(Some(path), Some(user_shell_override)) => {
let auth_manager = codex_core::test_support::auth_manager_from_auth(auth);
Box::pin(
let new_conversation =
match (resume_from, user_shell_override) {
(Some(path), Some(user_shell_override)) => Box::pin(
codex_core::test_support::resume_thread_from_rollout_with_user_shell_override(
thread_manager.as_ref(),
config.clone(),
path,
auth_manager,
Arc::clone(&auth_manager),
user_shell_override,
),
)
.await?
}
(Some(path), None) => {
let auth_manager = codex_core::test_support::auth_manager_from_auth(auth);
Box::pin(thread_manager.resume_thread_from_rollout(
config.clone(),
path,
auth_manager,
/*parent_trace*/ None,
))
.await?
}
(None, Some(user_shell_override)) => {
Box::pin(
codex_core::test_support::start_thread_with_user_shell_override(
thread_manager.as_ref(),
.await?,
(Some(path), None) => {
Box::pin(thread_manager.resume_thread_from_rollout(
config.clone(),
user_shell_override,
),
)
.await?
}
(None, None) => Box::pin(thread_manager.start_thread(config.clone())).await?,
};
path,
Arc::clone(&auth_manager),
/*parent_trace*/ None,
))
.await?
}
(None, Some(user_shell_override)) => {
Box::pin(
codex_core::test_support::start_thread_with_user_shell_override(
thread_manager.as_ref(),
config.clone(),
user_shell_override,
),
)
.await?
}
(None, None) => Box::pin(thread_manager.start_thread(config.clone())).await?,
};
Ok(TestCodex {
home,
@@ -1031,6 +1038,7 @@ pub fn test_codex() -> TestCodexBuilder {
TestCodexBuilder {
config_mutators: vec![],
auth: CodexAuth::from_api_key("dummy"),
auth_manager: None,
pre_build_hooks: vec![],
workspace_setups: vec![],
home: None,
+129
View File
@@ -9,8 +9,11 @@ use codex_core::resolve_installation_id;
use codex_core::thread_store_from_config;
use codex_extension_api::empty_extension_registry;
use codex_features::Feature;
use codex_http_state::HttpStateStore;
use codex_http_state::HttpStateSurface;
use codex_login::AuthManager;
use codex_login::CodexAuth;
use codex_login::REFRESH_TOKEN_URL_OVERRIDE_ENV_VAR;
use codex_login::default_client::originator;
use codex_model_provider_info::ModelProviderInfo;
use codex_model_provider_info::WireApi;
@@ -58,6 +61,7 @@ use core_test_support::responses::ev_completed_with_tokens;
use core_test_support::responses::ev_message_item_added;
use core_test_support::responses::ev_output_text_delta;
use core_test_support::responses::ev_response_created;
use core_test_support::responses::mount_response_sequence;
use core_test_support::responses::mount_sse_once;
use core_test_support::responses::mount_sse_once_match;
use core_test_support::responses::mount_sse_sequence;
@@ -71,6 +75,7 @@ use dunce::canonicalize as normalize_path;
use futures::StreamExt;
use pretty_assertions::assert_eq;
use serde_json::json;
use std::ffi::OsString;
use std::io::Write;
use std::num::NonZeroU64;
use std::sync::Arc;
@@ -943,6 +948,130 @@ async fn send_provider_auth_request(server: &MockServer, auth: ModelProviderAuth
}
}
struct EnvGuard {
key: &'static str,
original: Option<OsString>,
}
impl EnvGuard {
fn set(key: &'static str, value: String) -> Self {
let original = std::env::var_os(key);
// SAFETY: the test is serialized with the other auth-refresh tests.
unsafe {
std::env::set_var(key, &value);
}
Self { key, original }
}
}
impl Drop for EnvGuard {
fn drop(&mut self) {
// SAFETY: the guard restores the original environment value before other tests run.
unsafe {
match &self.original {
Some(value) => std::env::set_var(self.key, value),
None => std::env::remove_var(self.key),
}
}
}
}
#[serial_test::serial(auth_refresh)]
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn response_401_refresh_preserves_native_http_state_surface() {
skip_if_no_network!();
let server = MockServer::start().await;
Mock::given(method("POST"))
.and(path("/oauth/token"))
.respond_with(ResponseTemplate::new(200).set_body_json(json!({
"access_token": "new-access-token",
"refresh_token": "new-refresh-token",
"state": "new-state",
})))
.expect(1)
.mount(&server)
.await;
let responses_mock = mount_response_sequence(
&server,
vec![
ResponseTemplate::new(401).set_body_string("unauthorized"),
ResponseTemplate::new(401).set_body_string("unauthorized"),
ResponseTemplate::new(200)
.insert_header("content-type", "text/event-stream")
.set_body_raw(
sse(vec![ev_response_created("resp1"), ev_completed("resp1")]),
"text/event-stream",
),
],
)
.await;
let home = Arc::new(TempDir::new().unwrap());
write_auth_json(
home.as_ref(),
/*openai_api_key*/ None,
"pro",
"old-access-token",
Some("acc-123"),
);
let store = HttpStateStore::new(home.path().to_path_buf());
store
.set(HttpStateSurface::CodexExec, "old-state".to_string())
.unwrap();
let _refresh_endpoint = EnvGuard::set(
REFRESH_TOKEN_URL_OVERRIDE_ENV_VAR,
format!("{}/oauth/token", server.uri()),
);
let auth_manager = AuthManager::shared(
home.path().to_path_buf(),
/*enable_codex_api_key_env*/ false,
AuthCredentialsStoreMode::File,
/*chatgpt_base_url*/ None,
)
.await;
let mut builder = test_codex()
.with_home(home)
.with_auth_manager(auth_manager)
.with_config(|config| {
config.model_catalog =
Some(bundled_models_response().expect("bundled models should parse"));
});
let test = builder.build(&server).await.unwrap();
test.submit_turn("hello").await.unwrap();
assert_eq!(
store.get(HttpStateSurface::CodexExec).unwrap(),
Some("new-state".to_string())
);
let requests = responses_mock.requests();
assert_eq!(requests.len(), 3);
assert_eq!(
requests[0].header("authorization").as_deref(),
Some("Bearer old-access-token")
);
assert_eq!(
requests[1].header("authorization").as_deref(),
Some("Bearer old-access-token")
);
assert_eq!(
requests[2].header("authorization").as_deref(),
Some("Bearer new-access-token")
);
let refresh_request = server
.received_requests()
.await
.unwrap_or_default()
.into_iter()
.find(|request| request.url.path() == "/oauth/token")
.expect("refresh request");
let refresh_body: serde_json::Value =
serde_json::from_slice(&refresh_request.body).expect("refresh JSON body");
assert_eq!(refresh_body["surface"], "codex_exec");
server.verify().await;
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn includes_base_instructions_override_in_request() {
skip_if_no_network!();
+41 -4
View File
@@ -5,6 +5,8 @@ use std::path::Path;
use anyhow::Context;
use anyhow::Result;
use codex_http_state::HttpStateStore;
use codex_http_state::HttpStateSurface;
use codex_protocol::models::PermissionProfile;
use codex_protocol::protocol::AskForApproval;
use core_test_support::apps_test_server::AppsTestServer;
@@ -33,6 +35,11 @@ use wiremock::matchers::header;
use wiremock::matchers::method;
use wiremock::matchers::path;
const INITIAL_INTEGRITY_STATE: &str = "state-n";
const INTEGRITY_STATE_HEADER_NAME: &str = "x-oai-is";
const INTEGRITY_STATE_UPDATE_HEADER_NAME: &str = "x-oai-is-update";
const UNTRUSTED_INTEGRITY_STATE_UPDATE: &str = "state-n-plus-one";
fn write_post_tool_use_hook(home: &Path) -> Result<()> {
let script_path = home.join("post_tool_use_hook.py");
let log_path = home.join("post_tool_use_hook_log.jsonl");
@@ -95,10 +102,17 @@ async fn codex_apps_file_params_upload_local_paths_before_mcp_tool_call() -> Res
"file_size": 11,
"use_case": "codex",
})))
.respond_with(ResponseTemplate::new(200).set_body_json(json!({
"file_id": "file_123",
"upload_url": format!("{}/upload/file_123", server.uri()),
})))
.respond_with(
ResponseTemplate::new(200)
.insert_header(
INTEGRITY_STATE_UPDATE_HEADER_NAME,
UNTRUSTED_INTEGRITY_STATE_UPDATE,
)
.set_body_json(json!({
"file_id": "file_123",
"upload_url": format!("{}/upload/file_123", server.uri()),
})),
)
.expect(1)
.mount(&server)
.await;
@@ -150,6 +164,12 @@ async fn codex_apps_file_params_upload_local_paths_before_mcp_tool_call() -> Res
if let Err(error) = write_post_tool_use_hook(home) {
panic!("failed to write apps file post tool use hook fixture: {error}");
}
HttpStateStore::new(home.to_path_buf())
.set(
HttpStateSurface::CodexExec,
INITIAL_INTEGRITY_STATE.to_string(),
)
.expect("initial HTTP state should store");
})
.with_config(move |config| {
trust_discovered_hooks(config);
@@ -218,6 +238,23 @@ async fn codex_apps_file_params_upload_local_paths_before_mcp_tool_call() -> Res
"file_size_bytes": 11,
})
);
assert_eq!(
HttpStateStore::new(test.codex_home_path().to_path_buf())
.get(HttpStateSurface::CodexExec)?,
Some(INITIAL_INTEGRITY_STATE.to_string())
);
let file_create_request = server
.received_requests()
.await
.context("capture file upload requests")?
.into_iter()
.find(|request| request.url.path() == "/files")
.context("find file create request")?;
assert!(
!file_create_request
.headers
.contains_key(INTEGRITY_STATE_HEADER_NAME)
);
server.verify().await;
Ok(())