Skip to content
Merged
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
7 changes: 5 additions & 2 deletions codex-rs/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions codex-rs/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -185,6 +185,7 @@ codex-execpolicy = { path = "execpolicy" }
codex-extension-api = { path = "ext/extension-api" }
codex-extension-items = { path = "ext/items" }
codex-goal-extension = { path = "ext/goal" }
codex-git-attribution = { path = "ext/git-attribution" }
codex-guardian = { path = "ext/guardian" }
codex-image-generation-extension = { path = "ext/image-generation" }
codex-external-agent-migration = { path = "external-agent-migration" }
Expand Down
1 change: 1 addition & 0 deletions codex-rs/app-server/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ codex-extension-api = { workspace = true }
codex-external-agent-migration = { workspace = true }
codex-features = { workspace = true }
codex-goal-extension = { workspace = true }
codex-git-attribution = { workspace = true }
codex-guardian = { workspace = true }
codex-git-utils = { workspace = true }
codex-file-watcher = { workspace = true }
Expand Down
11 changes: 11 additions & 0 deletions codex-rs/app-server/src/extensions.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ use codex_extension_api::ExtensionEventSink;
use codex_extension_api::ExtensionRegistry;
use codex_extension_api::ExtensionRegistryBuilder;
use codex_goal_extension::GoalService;
use codex_http_client::HttpClientFactory;
use codex_login::AuthManager;
use codex_protocol::ThreadId;
use codex_protocol::error::CodexErr;
Expand All @@ -37,6 +38,8 @@ pub(crate) struct ThreadExtensionDependencies {
pub(crate) goal_service: Arc<GoalService>,
pub(crate) environment_manager: Arc<EnvironmentManager>,
pub(crate) executor_skill_provider: Arc<dyn codex_skills_extension::SkillProvider>,
pub(crate) git_attribution_base_url: String,
pub(crate) http_client_factory: HttpClientFactory,
/// Process-scoped persistence backend for extensions that need stored thread history.
pub(crate) thread_store: Arc<dyn ThreadStore>,
}
Expand All @@ -57,6 +60,8 @@ where
goal_service,
environment_manager,
executor_skill_provider,
git_attribution_base_url,
http_client_factory,
thread_store: _thread_store,
} = dependencies;
let mut builder = ExtensionRegistryBuilder::<Config>::with_event_sink(event_sink);
Expand All @@ -71,6 +76,12 @@ where
|config: &Config| config.features.enabled(codex_features::Feature::Goals),
);
}
codex_git_attribution::install(
&mut builder,
auth_manager.clone(),
git_attribution_base_url,
http_client_factory,
);
codex_guardian::install(&mut builder, guardian_agent_spawner);
codex_memories_extension::install(&mut builder, codex_otel::global());
codex_mcp_extension::install(&mut builder);
Expand Down
2 changes: 2 additions & 0 deletions codex-rs/app-server/src/mcp_refresh.rs
Original file line number Diff line number Diff line change
Expand Up @@ -326,6 +326,8 @@ enabled = false
goal_service: Arc::new(codex_goal_extension::GoalService::new()),
environment_manager: Arc::clone(&environment_manager),
executor_skill_provider: Arc::clone(&executor_skill_provider),
git_attribution_base_url: good_config.chatgpt_base_url.clone(),
http_client_factory: good_config.http_client_factory(),
thread_store: Arc::clone(&thread_store),
},
),
Expand Down
2 changes: 2 additions & 0 deletions codex-rs/app-server/src/message_processor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -274,6 +274,8 @@ impl MessageProcessor {
goal_service: Arc::clone(&goal_service),
environment_manager: Arc::clone(&environment_manager_for_extensions),
executor_skill_provider: Arc::clone(&executor_skill_provider),
git_attribution_base_url: config.chatgpt_base_url.clone(),
http_client_factory: config.http_client_factory(),
thread_store: Arc::clone(&thread_store),
},
),
Expand Down
37 changes: 37 additions & 0 deletions codex-rs/app-server/tests/common/test_app_server.rs
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,11 @@ use core_test_support::test_codex::test_env;
use serde::de::DeserializeOwned;
use tempfile::TempDir;
use tokio::process::Command;
use wiremock::Mock;
use wiremock::MockServer;
use wiremock::ResponseTemplate;
use wiremock::matchers::method;
use wiremock::matchers::path;

use crate::json_logging::JsonLogCapture;
use crate::local_websocket_exec_server::LocalWebsocketExecServer;
Expand All @@ -150,6 +155,7 @@ pub struct TestAppServer {
// Fields drop in declaration order. Tear down the delayed child before
// removing an owned CODEX_HOME that may still be its cwd on Windows.
_delayed_exec_server: Option<(LocalWebsocketExecServer, WebsocketDelayInterposer)>,
_attribution_settings_server: Option<MockServer>,
_owned_codex_home: Option<TempDir>,
}

Expand Down Expand Up @@ -285,6 +291,7 @@ impl TestAppServer {
auto_env: None,
json_logs,
_delayed_exec_server: None,
_attribution_settings_server: None,
_owned_codex_home: None,
})
}
Expand Down Expand Up @@ -1879,6 +1886,35 @@ impl TestAppServerBuilder {
)
}
};
let attribution_settings_server = if codex_home.join("auth.json").is_file() {
let config_path = codex_home.join("config.toml");
let config = std::fs::read_to_string(&config_path)?;
if config
.lines()
.any(|line| line.trim_start().starts_with("chatgpt_base_url"))
{
None
} else {
let settings_server = MockServer::start().await;
Mock::given(method("GET"))
.and(path("/backend-api/wham/settings/user"))
.respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
"commit_attribution_enabled": false,
})))
.mount(&settings_server)
.await;
std::fs::write(
&config_path,
format!(
"chatgpt_base_url = \"{}/backend-api\"\n{config}",
settings_server.uri()
),
)?;
Some(settings_server)
}
} else {
None
};
let (auto_env, delayed_exec_server) = match environment {
TestAppServerEnvironment::Auto => {
let environments_toml = codex_home.join("environments.toml");
Expand Down Expand Up @@ -1985,6 +2021,7 @@ impl TestAppServerBuilder {
app_server.auto_env = auto_env;
app_server._owned_codex_home = owned_codex_home;
app_server._delayed_exec_server = delayed_exec_server;
app_server._attribution_settings_server = attribution_settings_server;
Ok(app_server)
}
}
Expand Down
28 changes: 26 additions & 2 deletions codex-rs/app-server/tests/suite/v2/account.rs
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,7 @@ struct CreateConfigTomlParams {
forced_workspace_ids: Option<Vec<String>>,
requires_openai_auth: Option<bool>,
base_url: Option<String>,
chatgpt_base_url: Option<String>,
model_provider_id: Option<String>,
extra_provider_config: Option<String>,
}
Expand Down Expand Up @@ -115,6 +116,10 @@ fn create_config_toml(codex_home: &Path, params: CreateConfigTomlParams) -> std:
Some(false) => String::new(),
None => String::new(),
};
let chatgpt_base_url_line = params
.chatgpt_base_url
.map(|url| format!("chatgpt_base_url = \"{url}\"\n"))
.unwrap_or_default();
let model_provider_id = params
.model_provider_id
.unwrap_or_else(|| "mock_provider".to_string());
Expand All @@ -137,6 +142,7 @@ stream_max_retries = 0
model = "mock-model"
approval_policy = "never"
sandbox_mode = "danger-full-access"
{chatgpt_base_url_line}
{forced_line}
{forced_workspace_line}

Expand Down Expand Up @@ -532,6 +538,16 @@ async fn respond_to_refresh_request(
Ok(())
}

async fn mount_disabled_attribution_settings(mock_server: &MockServer) {
Mock::given(method("GET"))
.and(path("/backend-api/wham/settings/user"))
.respond_with(ResponseTemplate::new(200).set_body_json(json!({
"commit_attribution_enabled": false,
})))
.mount(mock_server)
.await;
}

#[tokio::test]
// 401 response triggers account/chatgptAuthTokens/refresh and retries with new tokens.
async fn external_auth_refreshes_on_unauthorized() -> Result<()> {
Expand All @@ -542,6 +558,7 @@ async fn external_auth_refreshes_on_unauthorized() -> Result<()> {
CreateConfigTomlParams {
requires_openai_auth: Some(true),
base_url: Some(format!("{}/v1", mock_server.uri())),
chatgpt_base_url: Some(format!("{}/backend-api", mock_server.uri())),
..Default::default()
},
)?;
Expand All @@ -560,6 +577,7 @@ async fn external_auth_refreshes_on_unauthorized() -> Result<()> {
vec![unauthorized, responses::sse_response(success_sse)],
)
.await;
mount_disabled_attribution_settings(&mock_server).await;

let initial_access_token = encode_id_token(
&ChatGptIdTokenClaims::new()
Expand Down Expand Up @@ -655,6 +673,7 @@ async fn external_auth_refresh_error_fails_turn() -> Result<()> {
CreateConfigTomlParams {
requires_openai_auth: Some(true),
base_url: Some(format!("{}/v1", mock_server.uri())),
chatgpt_base_url: Some(format!("{}/backend-api", mock_server.uri())),
..Default::default()
},
)?;
Expand All @@ -665,6 +684,7 @@ async fn external_auth_refresh_error_fails_turn() -> Result<()> {
}));
let _responses_mock =
responses::mount_response_sequence(&mock_server, vec![unauthorized]).await;
mount_disabled_attribution_settings(&mock_server).await;

let initial_access_token = encode_id_token(
&ChatGptIdTokenClaims::new()
Expand Down Expand Up @@ -764,6 +784,7 @@ async fn external_auth_refresh_mismatched_workspace_fails_turn() -> Result<()> {
forced_workspace_id: Some(WORKSPACE_ID_ALLOWED.to_string()),
requires_openai_auth: Some(true),
base_url: Some(format!("{}/v1", mock_server.uri())),
chatgpt_base_url: Some(format!("{}/backend-api", mock_server.uri())),
..Default::default()
},
)?;
Expand All @@ -774,6 +795,7 @@ async fn external_auth_refresh_mismatched_workspace_fails_turn() -> Result<()> {
}));
let _responses_mock =
responses::mount_response_sequence(&mock_server, vec![unauthorized]).await;
mount_disabled_attribution_settings(&mock_server).await;

let initial_access_token = encode_id_token(
&ChatGptIdTokenClaims::new()
Expand Down Expand Up @@ -878,6 +900,7 @@ async fn external_auth_refresh_invalid_access_token_fails_turn() -> Result<()> {
CreateConfigTomlParams {
requires_openai_auth: Some(true),
base_url: Some(format!("{}/v1", mock_server.uri())),
chatgpt_base_url: Some(format!("{}/backend-api", mock_server.uri())),
..Default::default()
},
)?;
Expand All @@ -888,6 +911,7 @@ async fn external_auth_refresh_invalid_access_token_fails_turn() -> Result<()> {
}));
let _responses_mock =
responses::mount_response_sequence(&mock_server, vec![unauthorized]).await;
mount_disabled_attribution_settings(&mock_server).await;

let initial_access_token = encode_id_token(
&ChatGptIdTokenClaims::new()
Expand Down Expand Up @@ -1110,7 +1134,6 @@ async fn login_amazon_bedrock_rejects_non_bedrock_provider_override_without_chan
AuthKeyringBackendKind::default(),
)?;
let expected_auth = load_file_auth(codex_home.path())?;
let expected_config = read_config_toml(codex_home.path())?;

let mut mcp = TestAppServer::builder()
.with_codex_home(codex_home.path())
Expand All @@ -1119,6 +1142,7 @@ async fn login_amazon_bedrock_rejects_non_bedrock_provider_override_without_chan
.with_args(&["-c", "model_provider=\"mock_provider\""])
.build_initialized_with_timeout(DEFAULT_READ_TIMEOUT)
.await?;
let expected_config = read_config_toml(codex_home.path())?;

let request_id = mcp
.send_login_account_amazon_bedrock_request("managed-bedrock-api-key", "us-west-2")
Expand Down Expand Up @@ -1294,14 +1318,14 @@ async fn logout_aws_managed_bedrock_errors_without_changing_auth_or_config() ->
AuthKeyringBackendKind::default(),
)?;
let expected_auth = load_file_auth(codex_home.path())?;
let expected_config = read_config_toml(codex_home.path())?;

let mut mcp = TestAppServer::builder()
.with_codex_home(codex_home.path())
.without_auto_env()
.with_env_overrides(&[("OPENAI_API_KEY", None)])
.build_initialized_with_timeout(DEFAULT_READ_TIMEOUT)
.await?;
let expected_config = read_config_toml(codex_home.path())?;
let request_id = mcp.send_logout_account_request().await?;
let error = timeout(
DEFAULT_READ_TIMEOUT,
Expand Down
Loading
Loading