mirror of
https://github.com/openai/codex.git
synced 2026-10-19 17:28:46 +00:00
## Why Application network restrictions need to cover clients created by the TUI and `codex exec` before embedded app-server startup, and remain effective when configuration is rebuilt. ## What changed - Introduce `EmbeddedNetworkPolicy` to share live policy between caller-owned transports and the embedded app-server. - Apply loaded requirements before creating telemetry, background HTTP clients, and executor connections. Limit authentication and cloud configuration bootstrap requests to exact endpoints under local policy. - Preserve the shared policy across TUI session changes, permission-profile rebuilds, and worktree cloud loaders so existing clients observe policy reloads and failures. - Document application network policy behavior and transport coverage. ## Testing Add regression coverage for policy activation, reload failure and recovery, retained policy across TUI rebuilds, rejected OAuth and cloud-policy redirects, and workspace network restrictions with `CODEX_API_KEY`. Also cover retaining the caller-selected project during embedded startup. GitOrigin-RevId: 629009f087b8cb21fc34a1ee7ea6fe95d9e0f2b5
196 lines
7.2 KiB
Rust
196 lines
7.2 KiB
Rust
#![allow(clippy::unwrap_used)]
|
|
use core_test_support::responses::ev_completed;
|
|
use core_test_support::responses::mount_sse_once_match;
|
|
use core_test_support::responses::sse;
|
|
use core_test_support::responses::start_mock_server;
|
|
use core_test_support::test_codex_exec::test_codex_exec;
|
|
use serde_json::json;
|
|
use wiremock::Mock;
|
|
use wiremock::ResponseTemplate;
|
|
use wiremock::matchers::header;
|
|
use wiremock::matchers::method;
|
|
use wiremock::matchers::path;
|
|
|
|
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
|
async fn exec_uses_codex_api_key_env_var() -> anyhow::Result<()> {
|
|
let test = test_codex_exec();
|
|
let server = start_mock_server().await;
|
|
let repo_root = codex_utils_cargo_bin::repo_root()?;
|
|
|
|
mount_sse_once_match(
|
|
&server,
|
|
header("Authorization", "Bearer dummy"),
|
|
sse(vec![ev_completed("request_0")]),
|
|
)
|
|
.await;
|
|
|
|
test.cmd_with_server(&server)
|
|
.arg("--skip-git-repo-check")
|
|
.arg("-C")
|
|
.arg(&repo_root)
|
|
.arg("echo testing codex api key")
|
|
.assert()
|
|
.success();
|
|
|
|
Ok(())
|
|
}
|
|
|
|
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
|
async fn exec_api_key_cannot_discard_stored_workspace_network_policy() -> anyhow::Result<()> {
|
|
let test = test_codex_exec();
|
|
let policy_server = start_mock_server().await;
|
|
let inference_server = start_mock_server().await;
|
|
let token = concat!(
|
|
"eyJhbGciOiJub25lIiwidHlwIjoiSldUIn0.",
|
|
"eyJodHRwczovL2FwaS5vcGVuYWkuY29tL2F1dGgiOnsiY2hhdGdwdF9wbGFuX3R5cGUiOiJlbnRlcnByaXNlIiwiY2hhdGdwdF9hY2NvdW50X2lkIjoid29ya3NwYWNlIn19.",
|
|
"c2lnbmF0dXJl",
|
|
);
|
|
std::fs::write(
|
|
test.home_path().join("auth.json"),
|
|
serde_json::to_vec(&json!({
|
|
"auth_mode": "chatgpt",
|
|
"tokens": {"id_token": token, "access_token": "workspace-token",
|
|
"refresh_token": "refresh-token", "account_id": "workspace"},
|
|
"last_refresh": "2099-01-01T00:00:00Z",
|
|
}))?,
|
|
)?;
|
|
std::fs::write(
|
|
test.home_path().join("config.toml"),
|
|
format!(
|
|
"chatgpt_base_url = '{}/backend-api/'\ncli_auth_credentials_store = 'file'\n",
|
|
policy_server.uri()
|
|
),
|
|
)?;
|
|
Mock::given(method("GET"))
|
|
.and(path("/backend-api/wham/config/bundle"))
|
|
.and(header("authorization", "Bearer workspace-token"))
|
|
.and(header("chatgpt-account-id", "workspace"))
|
|
.respond_with(ResponseTemplate::new(200).set_body_json(json!({
|
|
"requirements_toml": {"enterprise_managed": [{
|
|
"id": "workspace-network", "name": "Workspace network",
|
|
"contents": "[application.network]\nenabled = true",
|
|
}]},
|
|
})))
|
|
.expect(1..)
|
|
.mount(&policy_server)
|
|
.await;
|
|
let inference = mount_sse_once_match(
|
|
&inference_server,
|
|
header("Authorization", "Bearer dummy"),
|
|
sse(vec![ev_completed("unexpected-request")]),
|
|
)
|
|
.await;
|
|
|
|
test.cmd_with_server(&inference_server)
|
|
.arg("--skip-git-repo-check")
|
|
.arg("echo testing workspace network policy")
|
|
.assert()
|
|
.failure();
|
|
|
|
assert!(
|
|
inference.requests().is_empty(),
|
|
"an environment API key sent model traffic despite workspace policy"
|
|
);
|
|
policy_server.verify().await;
|
|
Ok(())
|
|
}
|
|
|
|
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
|
async fn exec_bootstrap_rejects_redirected_oauth_and_cloud_policy() -> anyhow::Result<()> {
|
|
for redirect_oauth in [true, false] {
|
|
let test = test_codex_exec();
|
|
let origin = start_mock_server().await;
|
|
let destination = start_mock_server().await;
|
|
let inference = start_mock_server().await;
|
|
let token = concat!(
|
|
"eyJhbGciOiJub25lIiwidHlwIjoiSldUIn0.",
|
|
"eyJodHRwczovL2FwaS5vcGVuYWkuY29tL2F1dGgiOnsiY2hhdGdwdF9wbGFuX3R5cGUiOiJlbnRlcnByaXNlIiwiY2hhdGdwdF9hY2NvdW50X2lkIjoid29ya3NwYWNlIn19.",
|
|
"c2lnbmF0dXJl",
|
|
);
|
|
let last_refresh = if redirect_oauth {
|
|
"2000-01-01T00:00:00Z"
|
|
} else {
|
|
"2099-01-01T00:00:00Z"
|
|
};
|
|
std::fs::write(
|
|
test.home_path().join("auth.json"),
|
|
serde_json::to_vec(&json!({
|
|
"auth_mode": "chatgpt",
|
|
"tokens": {"id_token": token, "access_token": "workspace-token",
|
|
"refresh_token": "refresh-token", "account_id": "workspace"},
|
|
"last_refresh": last_refresh,
|
|
}))?,
|
|
)?;
|
|
std::fs::write(
|
|
test.home_path().join("config.toml"),
|
|
format!(
|
|
"chatgpt_base_url = '{}/backend-api/'\ncli_auth_credentials_store = 'file'\n",
|
|
origin.uri()
|
|
),
|
|
)?;
|
|
let cloud_response = if redirect_oauth {
|
|
Mock::given(method("POST"))
|
|
.and(path("/oauth/token"))
|
|
.and(wiremock::matchers::body_string_contains("refresh-token"))
|
|
.respond_with(
|
|
ResponseTemplate::new(307)
|
|
.insert_header("location", format!("{}/stolen", destination.uri())),
|
|
)
|
|
.expect(1..)
|
|
.mount(&origin)
|
|
.await;
|
|
Mock::given(method("POST"))
|
|
.and(path("/stolen"))
|
|
.respond_with(ResponseTemplate::new(200).set_body_json(json!({
|
|
"id_token": token, "access_token": "replacement", "refresh_token": "replacement",
|
|
})))
|
|
.mount(&destination)
|
|
.await;
|
|
ResponseTemplate::new(200).set_body_json(json!({}))
|
|
} else {
|
|
Mock::given(method("GET"))
|
|
.and(path("/policy"))
|
|
.respond_with(ResponseTemplate::new(200).set_body_json(json!({
|
|
"requirements_toml": {"enterprise_managed": [{
|
|
"id": "redirected", "name": "Redirected policy",
|
|
"contents": "[application.network.domains]\n'foreign.example' = 'allow'",
|
|
}]},
|
|
})))
|
|
.mount(&destination)
|
|
.await;
|
|
ResponseTemplate::new(308)
|
|
.insert_header("location", format!("{}/policy", destination.uri()))
|
|
};
|
|
let cloud = Mock::given(method("GET"))
|
|
.and(path("/backend-api/wham/config/bundle"))
|
|
.respond_with(cloud_response);
|
|
if redirect_oauth {
|
|
cloud.mount(&origin).await;
|
|
} else {
|
|
cloud.expect(1..).mount(&origin).await;
|
|
}
|
|
mount_sse_once_match(
|
|
&inference,
|
|
header("Authorization", "Bearer dummy"),
|
|
sse(vec![ev_completed("request")]),
|
|
)
|
|
.await;
|
|
|
|
let output = test
|
|
.cmd_with_server(&inference)
|
|
.env(
|
|
codex_login::REFRESH_TOKEN_URL_OVERRIDE_ENV_VAR,
|
|
format!("{}/oauth/token", origin.uri()),
|
|
)
|
|
.arg("--skip-git-repo-check")
|
|
.arg("exercise embedded discovery")
|
|
.output()?;
|
|
if !redirect_oauth {
|
|
assert!(!output.status.success());
|
|
}
|
|
origin.verify().await;
|
|
assert!(destination.received_requests().await.unwrap().is_empty());
|
|
}
|
|
Ok(())
|
|
}
|