mirror of
https://github.com/openai/codex.git
synced 2026-11-06 20:20:34 +00:00
[codex] cover app-owned HTTP requests [ci changed_files]
This commit is contained in:
@@ -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]
|
||||
|
||||
@@ -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?;
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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!();
|
||||
|
||||
@@ -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(())
|
||||
|
||||
Reference in New Issue
Block a user