mirror of
https://github.com/openai/codex.git
synced 2026-11-05 20:10:33 +00:00
## What changed - Use the shared runtime and effective configuration for account API clients and remote plugin requests, including standalone plugin commands. - Support dual-header and assertion-only authentication for remote plugins and MCP file uploads when runtime agent instances are enabled. - Keep plugin credentials and caches scoped to the thread's account when resuming with different credentials. Bind request pools and multi-step plugin storage operations to the selected account so account replacement revokes stale operations. - Protect runtime credentials on cross-origin redirects while preserving configured routing policies and legacy authentication for custom backends. - Apply server-provided `upload_headers` to file storage uploads. ## Testing Add coverage for shared runtime registration, both authentication modes, configuration reloads, resumed-thread account selection, account replacement during plugin uploads and downloads, and credential stripping on redirects. Extend file upload tests to verify runtime assertions and storage-only upload headers. GitOrigin-RevId: 68aef81051a6f9bf919fffda52f372b108fce805
195 lines
7.6 KiB
Rust
195 lines
7.6 KiB
Rust
//! Cover direct plugin mutations when runtime authentication is enabled after startup.
|
|
|
|
use super::*;
|
|
use anyhow::Context;
|
|
use base64::Engine;
|
|
use codex_app_server_protocol::PluginUninstallParams;
|
|
use codex_app_server_protocol::PluginUninstallResponse;
|
|
use core_test_support::runtime_auth::PROBE_SERVER;
|
|
use core_test_support::runtime_auth::run_harness_through_proxy;
|
|
use pretty_assertions::assert_eq;
|
|
use test_case::test_case;
|
|
|
|
#[test_case("dual_header", "install"; "install_dual_header")]
|
|
#[test_case("assertion_only", "install"; "install_assertion_only")]
|
|
#[test_case("dual_header", "uninstall"; "uninstall_dual_header")]
|
|
#[test_case("assertion_only", "uninstall"; "uninstall_assertion_only")]
|
|
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
|
async fn direct_mutation_initializes_runtime_after_config_reload(
|
|
mode: &str,
|
|
operation: &str,
|
|
) -> Result<()> {
|
|
core_test_support::skip_if_no_network!(Ok(()));
|
|
let token = format!(
|
|
"e30.{}.c2ln",
|
|
base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(
|
|
json!({
|
|
"exp": 4_102_444_800_u64,
|
|
"session_id": "session-a",
|
|
"iss": "https://auth.openai.com",
|
|
"https://api.openai.com/auth": {
|
|
"chatgpt_user_id": "user-123", "chatgpt_account_id": "account-123",
|
|
},
|
|
})
|
|
.to_string()
|
|
)
|
|
);
|
|
if std::env::var(PROBE_SERVER).is_ok() {
|
|
let home = TempDir::new()?;
|
|
write_chatgpt_auth(
|
|
home.path(),
|
|
ChatGptAuthFixture::new(&token)
|
|
.account_id("account-123")
|
|
.chatgpt_user_id("user-123")
|
|
.chatgpt_account_id("account-123"),
|
|
AuthCredentialsStoreMode::File,
|
|
)?;
|
|
write_remote_plugin_catalog_config(home.path(), "https://chatgpt.com/backend-api")?;
|
|
let config_path = home.path().join("config.toml");
|
|
let initial = std::fs::read_to_string(&config_path)?;
|
|
std::fs::write(
|
|
&config_path,
|
|
format!(
|
|
"{initial}\n[features.runtime_agent_instances]\nenabled = false\nmode = {mode:?}\n"
|
|
),
|
|
)?;
|
|
let mut app = TestAppServer::builder()
|
|
.with_codex_home(home.path())
|
|
.without_auto_env()
|
|
.with_env_overrides(&[
|
|
("OPENAI_API_KEY", None),
|
|
("CODEX_ACCESS_TOKEN", None),
|
|
(TEST_ALLOW_HTTP_REMOTE_PLUGIN_BUNDLE_DOWNLOADS, Some("1")),
|
|
])
|
|
.build_initialized_with_timeout(DEFAULT_TIMEOUT)
|
|
.await?;
|
|
std::fs::write(
|
|
&config_path,
|
|
format!(
|
|
"{initial}\n[features.runtime_agent_instances]\nenabled = true\nmode = {mode:?}\n"
|
|
),
|
|
)?;
|
|
let cache = home
|
|
.path()
|
|
.join("plugins/cache/openai-curated-remote/linear");
|
|
match operation {
|
|
"install" => {
|
|
let id = send_remote_plugin_install_request(&mut app, REMOTE_PLUGIN_ID).await?;
|
|
let response: PluginInstallResponse =
|
|
timeout(DEFAULT_TIMEOUT, app.read_response(id)).await??;
|
|
assert_eq!(
|
|
response,
|
|
PluginInstallResponse {
|
|
auth_policy: PluginAuthPolicy::OnUse,
|
|
apps_needing_auth: Vec::new(),
|
|
}
|
|
);
|
|
assert!(cache.join("1.2.3/.codex-plugin/plugin.json").is_file());
|
|
}
|
|
"uninstall" => {
|
|
std::fs::create_dir_all(cache.join("1.2.3/.codex-plugin"))?;
|
|
std::fs::write(
|
|
cache.join("1.2.3/.codex-plugin/plugin.json"),
|
|
r#"{"name":"linear","version":"1.2.3"}"#,
|
|
)?;
|
|
let id = app
|
|
.send_plugin_uninstall_request(PluginUninstallParams {
|
|
plugin_id: REMOTE_PLUGIN_ID.into(),
|
|
})
|
|
.await?;
|
|
let response: PluginUninstallResponse =
|
|
timeout(DEFAULT_TIMEOUT, app.read_response(id)).await??;
|
|
assert_eq!(response, PluginUninstallResponse {});
|
|
assert!(!cache.exists());
|
|
}
|
|
_ => unreachable!("test operation"),
|
|
}
|
|
return Ok(());
|
|
}
|
|
|
|
let server = MockServer::start().await;
|
|
let bundle = mount_remote_plugin_bundle(
|
|
&server,
|
|
/*status_code*/ 200,
|
|
remote_plugin_bundle_tar_gz_bytes("linear")?,
|
|
)
|
|
.await;
|
|
Mock::given(method("POST"))
|
|
.and(path("/agent/register"))
|
|
.respond_with(ResponseTemplate::new(200).set_body_json(json!({
|
|
"runtime_agent_instance_id": "runtime-a", "agent_host_id": "host-a",
|
|
"actor": {"kind": "chatgpt", "user_id": "user-123", "account_id": "account-123"},
|
|
})))
|
|
.expect(1)
|
|
.mount(&server)
|
|
.await;
|
|
Mock::given(method("GET"))
|
|
.and(path(format!("/backend-api/ps/plugins/{REMOTE_PLUGIN_ID}")))
|
|
.respond_with(ResponseTemplate::new(200).set_body_json(json!({
|
|
"id": REMOTE_PLUGIN_ID, "name": "linear", "scope": "GLOBAL",
|
|
"installation_policy": "AVAILABLE", "authentication_policy": "ON_USE", "status": "ENABLED",
|
|
"release": {
|
|
"version": "1.2.3", "bundle_download_url": bundle, "display_name": "Linear",
|
|
"description": "Track work in Linear.",
|
|
"app_ids": [], "interface": {}, "skills": [],
|
|
},
|
|
})))
|
|
.expect(1)
|
|
.mount(&server).await;
|
|
Mock::given(method("POST"))
|
|
.and(path(format!(
|
|
"/backend-api/ps/plugins/{REMOTE_PLUGIN_ID}/{operation}"
|
|
)))
|
|
.respond_with(ResponseTemplate::new(200).set_body_json(json!({
|
|
"id": REMOTE_PLUGIN_ID, "enabled": operation == "install",
|
|
})))
|
|
.expect(1)
|
|
.mount(&server)
|
|
.await;
|
|
Mock::given(method("GET"))
|
|
.and(path("/backend-api/ps/plugins/installed"))
|
|
.respond_with(ResponseTemplate::new(200).set_body_json(json!({
|
|
"plugins": [], "pagination": {"limit": 50, "next_page_token": null},
|
|
})))
|
|
.mount(&server)
|
|
.await;
|
|
run_harness_through_proxy(
|
|
&server,
|
|
&format!("suite::v2::plugin_install::runtime_auth_tests::direct_mutation_initializes_runtime_after_config_reload::{operation}_{mode}"),
|
|
/*realtime_target*/ None,
|
|
).await?;
|
|
let requests = server
|
|
.received_requests()
|
|
.await
|
|
.context("recorded requests")?;
|
|
let plugin_requests = requests
|
|
.iter()
|
|
.filter(|request| {
|
|
request
|
|
.url
|
|
.path()
|
|
.starts_with(&format!("/backend-api/ps/plugins/{REMOTE_PLUGIN_ID}"))
|
|
})
|
|
.collect::<Vec<_>>();
|
|
assert_eq!(plugin_requests.len(), 2);
|
|
for request in plugin_requests {
|
|
let proof = if mode == "dual_header" {
|
|
assert_eq!(request.headers["authorization"], format!("Bearer {token}"));
|
|
request.headers["x-openai-agent-assertion"].to_str()?
|
|
} else {
|
|
assert!(!request.headers.contains_key("x-openai-agent-assertion"));
|
|
request.headers["authorization"]
|
|
.to_str()?
|
|
.strip_prefix("AgentAssertion ")
|
|
.context("assertion auth")?
|
|
};
|
|
let claims: serde_json::Value = serde_json::from_slice(
|
|
&base64::engine::general_purpose::URL_SAFE_NO_PAD
|
|
.decode(proof.split('.').nth(2).context("assertion payload")?)?,
|
|
)?;
|
|
assert_eq!(claims["runtime_agent_instance"], "runtime-a");
|
|
}
|
|
server.verify().await;
|
|
Ok(())
|
|
}
|