Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions codex-rs/app-server/src/in_process.rs
Original file line number Diff line number Diff line change
Expand Up @@ -372,6 +372,7 @@ pub async fn start(args: InProcessStartArgs) -> IoResult<InProcessClientHandle>
}

async fn start_uninitialized(args: InProcessStartArgs) -> IoResult<InProcessClientHandle> {
crate::enforce_auth_restrictions(&args.config).await;
let channel_capacity = args.channel_capacity.max(1);
let installation_id = resolve_installation_id(&args.config.codex_home).await?;
let (client_tx, mut client_rx) = mpsc::channel::<InProcessClientMessage>(channel_capacity);
Expand Down
7 changes: 7 additions & 0 deletions codex-rs/app-server/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -739,6 +739,7 @@ pub async fn run_main_with_transport_options(
}
drop(unix_socket_startup_lock);

enforce_auth_restrictions(&config).await;
let auth_manager =
AuthManager::shared_from_config(&config, /*enable_codex_api_key_env*/ false).await;

Expand Down Expand Up @@ -1171,6 +1172,12 @@ pub async fn run_main_with_transport_options(
Ok(())
}

async fn enforce_auth_restrictions(config: &Config) {
if let Err(err) = codex_login::enforce_login_restrictions(&config.auth_config()).await {
tracing::warn!("applied auth restrictions after logging out: {err}");
}
}

struct SqliteRecoveryNotice {
details: String,
}
Expand Down
1 change: 1 addition & 0 deletions codex-rs/app-server/src/message_processor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -509,6 +509,7 @@ impl MessageProcessor {
outgoing.clone(),
config_manager.clone(),
thread_manager.clone(),
account_processor.clone(),
analytics_events_client,
);
let external_agent_config_processor = ExternalAgentConfigRequestProcessor::new(
Expand Down
1 change: 1 addition & 0 deletions codex-rs/app-server/src/request_processors.rs
Original file line number Diff line number Diff line change
Expand Up @@ -365,6 +365,7 @@ use codex_login::ServerOptions as LoginServerOptions;
use codex_login::ShutdownHandle;
use codex_login::auth::login_with_chatgpt_auth_tokens;
use codex_login::complete_device_code_login;
use codex_login::enforce_login_restrictions;
use codex_login::login_with_api_key;
use codex_login::oauth_client_id;
use codex_login::request_device_code;
Expand Down
51 changes: 30 additions & 21 deletions codex-rs/app-server/src/request_processors/account_processor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,21 @@ impl AccountRequestProcessor {
}
}

async fn load_latest_config(&self) -> Result<Config, JSONRPCErrorError> {
self.config_manager
.load_latest_config(/*fallback_cwd*/ None)
.await
.map_err(|err| internal_error(format!("failed to reload auth configuration: {err}")))
}

pub(crate) async fn refresh_auth_restrictions(&self, config: &Config) -> std::io::Result<()> {
let result = enforce_login_restrictions(&config.auth_config()).await;
self.auth_manager
.set_forced_chatgpt_workspace_id(config.forced_chatgpt_workspace_id.clone());
self.auth_manager.reload().await;
result
}

pub(crate) async fn login_account(
&self,
request_id: ConnectionRequestId,
Expand Down Expand Up @@ -270,14 +285,12 @@ impl AccountRequestProcessor {
&self,
params: &LoginApiKeyParams,
) -> std::result::Result<(), JSONRPCErrorError> {
let config = self.load_latest_config().await?;
if self.auth_manager.is_external_chatgpt_auth_active() {
return Err(self.external_auth_active_error());
}

if matches!(
self.config.forced_login_method,
Some(ForcedLoginMethod::Chatgpt)
) {
if matches!(config.forced_login_method, Some(ForcedLoginMethod::Chatgpt)) {
return Err(invalid_request(
"API key login is disabled. Use ChatGPT login instead.",
));
Expand All @@ -292,10 +305,10 @@ impl AccountRequestProcessor {
}

match login_with_api_key(
&self.config.codex_home,
&config.codex_home,
&params.api_key,
self.config.cli_auth_credentials_store_mode,
self.config.auth_keyring_backend_kind(),
config.cli_auth_credentials_store_mode,
config.auth_keyring_backend_kind(),
) {
Ok(()) => {
self.auth_manager.reload().await;
Expand Down Expand Up @@ -323,8 +336,8 @@ impl AccountRequestProcessor {
async fn login_chatgpt_common(
&self,
codex_streamlined_login: bool,
) -> std::result::Result<LoginServerOptions, JSONRPCErrorError> {
let config = self.config.as_ref();
) -> std::result::Result<(LoginServerOptions, String), JSONRPCErrorError> {
let config = self.load_latest_config().await?;

if self.auth_manager.is_external_chatgpt_auth_active() {
return Err(self.external_auth_active_error());
Expand Down Expand Up @@ -358,7 +371,7 @@ impl AccountRequestProcessor {
opts
};

Ok(opts)
Ok((opts, config.chatgpt_base_url))
}

fn login_chatgpt_device_code_start_error(err: IoError) -> JSONRPCErrorError {
Expand All @@ -383,7 +396,7 @@ impl AccountRequestProcessor {
&self,
codex_streamlined_login: bool,
) -> Result<LoginAccountResponse, JSONRPCErrorError> {
let opts = self.login_chatgpt_common(codex_streamlined_login).await?;
let (opts, chatgpt_base_url) = self.login_chatgpt_common(codex_streamlined_login).await?;
let server = run_login_server(opts)
.map_err(|err| internal_error(format!("failed to start login server: {err}")))?;
let login_id = Uuid::new_v4();
Expand All @@ -404,7 +417,6 @@ impl AccountRequestProcessor {
let outgoing_clone = self.outgoing.clone();
let config_manager = self.config_manager.clone();
let thread_manager = Arc::clone(&self.thread_manager);
let chatgpt_base_url = self.config.chatgpt_base_url.clone();
let active_login = self.active_login.clone();
let auth_url = server.auth_url.clone();
tokio::spawn(async move {
Expand Down Expand Up @@ -454,7 +466,7 @@ impl AccountRequestProcessor {
async fn login_chatgpt_device_code_response(
&self,
) -> Result<LoginAccountResponse, JSONRPCErrorError> {
let opts = self
let (opts, chatgpt_base_url) = self
.login_chatgpt_common(/*codex_streamlined_login*/ false)
.await?;
let device_code = request_device_code(&opts)
Expand All @@ -480,7 +492,6 @@ impl AccountRequestProcessor {
let outgoing_clone = self.outgoing.clone();
let config_manager = self.config_manager.clone();
let thread_manager = Arc::clone(&self.thread_manager);
let chatgpt_base_url = self.config.chatgpt_base_url.clone();
let active_login = self.active_login.clone();
tokio::spawn(async move {
let (success, error_msg) = tokio::select! {
Expand Down Expand Up @@ -573,10 +584,8 @@ impl AccountRequestProcessor {
chatgpt_account_id: String,
chatgpt_plan_type: Option<String>,
) -> Result<LoginAccountResponse, JSONRPCErrorError> {
if matches!(
self.config.forced_login_method,
Some(ForcedLoginMethod::Api)
) {
let config = self.load_latest_config().await?;
if matches!(config.forced_login_method, Some(ForcedLoginMethod::Api)) {
return Err(invalid_request(
"External ChatGPT auth is disabled. Use API key login instead.",
));
Expand All @@ -590,7 +599,7 @@ impl AccountRequestProcessor {
}
}

if let Some(expected_workspaces) = self.config.forced_chatgpt_workspace_id.as_deref()
if let Some(expected_workspaces) = config.forced_chatgpt_workspace_id.as_deref()
&& !expected_workspaces.contains(&chatgpt_account_id)
{
return Err(invalid_request(format!(
Expand All @@ -599,7 +608,7 @@ impl AccountRequestProcessor {
}

login_with_chatgpt_auth_tokens(
&self.config.codex_home,
&config.codex_home,
&access_token,
&chatgpt_account_id,
chatgpt_plan_type.as_deref(),
Expand All @@ -608,7 +617,7 @@ impl AccountRequestProcessor {
self.auth_manager.reload().await;
self.config_manager.replace_cloud_config_bundle_loader(
self.auth_manager.clone(),
self.config.chatgpt_base_url.clone(),
config.chatgpt_base_url.clone(),
);
self.config_manager
.sync_default_client_residency_requirement()
Expand Down
11 changes: 11 additions & 0 deletions codex-rs/app-server/src/request_processors/config_processor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ use crate::error_code::internal_error;
use crate::error_code::invalid_request;
use crate::outgoing_message::ConnectionRequestId;
use crate::outgoing_message::OutgoingMessageSender;
use crate::request_processors::AccountRequestProcessor;
use codex_analytics::AnalyticsEventsClient;
use codex_app_server_protocol::ClientResponsePayload;
use codex_app_server_protocol::ComputerUseRequirements;
Expand Down Expand Up @@ -59,6 +60,7 @@ pub(crate) struct ConfigRequestProcessor {
outgoing: Arc<OutgoingMessageSender>,
config_manager: ConfigManager,
thread_manager: Arc<ThreadManager>,
account_processor: AccountRequestProcessor,
analytics_events_client: AnalyticsEventsClient,
}

Expand All @@ -67,12 +69,14 @@ impl ConfigRequestProcessor {
outgoing: Arc<OutgoingMessageSender>,
config_manager: ConfigManager,
thread_manager: Arc<ThreadManager>,
account_processor: AccountRequestProcessor,
analytics_events_client: AnalyticsEventsClient,
) -> Self {
Self {
outgoing,
config_manager,
thread_manager,
account_processor,
analytics_events_client,
}
}
Expand Down Expand Up @@ -282,6 +286,13 @@ impl ConfigRequestProcessor {
return;
}
};
if let Err(err) = self
.account_processor
.refresh_auth_restrictions(&next_config)
.await
{
tracing::warn!("applied updated auth restrictions after logging out: {err}");
}
let thread_ids = self.thread_manager.list_thread_ids().await;
for thread_id in thread_ids {
let Ok(thread) = self.thread_manager.get_thread(thread_id).await else {
Expand Down
96 changes: 96 additions & 0 deletions codex-rs/app-server/tests/suite/v2/account.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,9 @@ use codex_app_server_protocol::CancelLoginAccountResponse;
use codex_app_server_protocol::CancelLoginAccountStatus;
use codex_app_server_protocol::ChatgptAuthTokensRefreshReason;
use codex_app_server_protocol::ChatgptAuthTokensRefreshResponse;
use codex_app_server_protocol::ConfigBatchWriteParams;
use codex_app_server_protocol::ConfigEdit;
use codex_app_server_protocol::ConfigWriteResponse;
use codex_app_server_protocol::GetAccountParams;
use codex_app_server_protocol::GetAccountResponse;
use codex_app_server_protocol::GetAuthStatusParams;
Expand All @@ -27,11 +30,13 @@ use codex_app_server_protocol::JSONRPCNotification;
use codex_app_server_protocol::JSONRPCResponse;
use codex_app_server_protocol::LoginAccountResponse;
use codex_app_server_protocol::LogoutAccountResponse;
use codex_app_server_protocol::MergeStrategy;
use codex_app_server_protocol::RequestId;
use codex_app_server_protocol::ServerNotification;
use codex_app_server_protocol::ServerRequest;
use codex_app_server_protocol::TurnCompletedNotification;
use codex_app_server_protocol::TurnStatus;
use codex_app_server_protocol::WriteStatus;
use codex_config::types::AuthCredentialsStoreMode;
use codex_login::AuthKeyringBackendKind;
use codex_login::CLIENT_ID_OVERRIDE_ENV_VAR;
Expand Down Expand Up @@ -251,6 +256,97 @@ async fn logout_account_removes_auth_and_notifies() -> Result<()> {
Ok(())
}

#[tokio::test]
async fn startup_removes_disallowed_stored_auth() -> Result<()> {
let codex_home = TempDir::new()?;
create_config_toml(
codex_home.path(),
CreateConfigTomlParams {
forced_method: Some("chatgpt".to_string()),
..Default::default()
},
)?;
login_with_api_key(
codex_home.path(),
"sk-test-key",
AuthCredentialsStoreMode::File,
AuthKeyringBackendKind::default(),
)?;

let mut mcp =
TestAppServer::new_with_env(codex_home.path(), &[("OPENAI_API_KEY", None)]).await?;
timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??;

assert!(!codex_home.path().join("auth.json").exists());

let request_id = mcp
.send_get_account_request(GetAccountParams {
refresh_token: false,
})
.await?;
let response = timeout(
DEFAULT_READ_TIMEOUT,
mcp.read_stream_until_response_message(RequestId::Integer(request_id)),
)
.await??;
let response: GetAccountResponse = to_response(response)?;
assert_eq!(response.account, None);

Ok(())
}

#[tokio::test]
async fn config_reload_removes_newly_disallowed_auth() -> Result<()> {
let codex_home = TempDir::new()?;
create_config_toml(codex_home.path(), CreateConfigTomlParams::default())?;
login_with_api_key(
codex_home.path(),
"sk-test-key",
AuthCredentialsStoreMode::File,
AuthKeyringBackendKind::default(),
)?;

let mut mcp =
TestAppServer::new_with_env(codex_home.path(), &[("OPENAI_API_KEY", None)]).await?;
timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??;

let request_id = mcp
.send_config_batch_write_request(ConfigBatchWriteParams {
file_path: None,
edits: vec![ConfigEdit {
key_path: "forced_login_method".to_string(),
value: json!("chatgpt"),
merge_strategy: MergeStrategy::Replace,
}],
expected_version: None,
reload_user_config: true,
})
.await?;
let response = timeout(
DEFAULT_READ_TIMEOUT,
mcp.read_stream_until_response_message(RequestId::Integer(request_id)),
)
.await??;
let response: ConfigWriteResponse = to_response(response)?;
assert_eq!(response.status, WriteStatus::Ok);
assert!(!codex_home.path().join("auth.json").exists());

let request_id = mcp
.send_get_account_request(GetAccountParams {
refresh_token: false,
})
.await?;
let response = timeout(
DEFAULT_READ_TIMEOUT,
mcp.read_stream_until_response_message(RequestId::Integer(request_id)),
)
.await??;
let response: GetAccountResponse = to_response(response)?;
assert_eq!(response.account, None);

Ok(())
}

#[tokio::test]
async fn set_auth_token_updates_account_and_notifies() -> Result<()> {
let codex_home = TempDir::new()?;
Expand Down
Loading
Loading