permissions: make runtime config profile-backed

This commit is contained in:
Michael Bolin
2026-04-25 22:39:53 -07:00
parent 9881dc7306
commit b7e4eadb35
67 changed files with 1641 additions and 681 deletions
+1
View File
@@ -26,6 +26,7 @@ pub use core_test_support::test_tmp_path;
pub use core_test_support::test_tmp_path_buf;
pub use mcp_process::DEFAULT_CLIENT_NAME;
pub use mcp_process::McpProcess;
pub use mcp_process::TEST_DISABLE_PLUGIN_STARTUP_TASKS_ENV_VAR;
pub use mock_model_server::create_mock_responses_server_repeating_assistant;
pub use mock_model_server::create_mock_responses_server_sequence;
pub use mock_model_server::create_mock_responses_server_sequence_unchecked;
@@ -106,6 +106,8 @@ pub struct McpProcess {
}
pub const DEFAULT_CLIENT_NAME: &str = "codex-app-server-tests";
pub const TEST_DISABLE_PLUGIN_STARTUP_TASKS_ENV_VAR: &str =
"CODEX_APP_SERVER_TEST_DISABLE_PLUGIN_STARTUP_TASKS";
const DISABLE_MANAGED_CONFIG_ENV_VAR: &str = "CODEX_APP_SERVER_DISABLE_MANAGED_CONFIG";
impl McpProcess {
@@ -147,7 +149,8 @@ impl McpProcess {
cmd.stderr(Stdio::piped());
cmd.current_dir(codex_home);
cmd.env("CODEX_HOME", codex_home);
cmd.env("RUST_LOG", "info");
cmd.env("RUST_LOG", "warn");
cmd.env(TEST_DISABLE_PLUGIN_STARTUP_TASKS_ENV_VAR, "1");
// Keep integration tests isolated from host managed configuration.
cmd.env(
"CODEX_APP_SERVER_MANAGED_CONFIG_PATH",
+10 -1
View File
@@ -33,6 +33,7 @@ use std::process::Command;
const DEFAULT_READ_TIMEOUT: Duration = Duration::from_secs(60);
#[cfg(not(any(target_os = "macos", windows)))]
const DEFAULT_READ_TIMEOUT: Duration = Duration::from_secs(10);
const OPTIONAL_FS_CHANGE_TIMEOUT: Duration = Duration::from_secs(2);
async fn initialized_mcp(codex_home: &TempDir) -> Result<McpProcess> {
let mut mcp = McpProcess::new(codex_home.path()).await?;
@@ -832,7 +833,7 @@ async fn maybe_fs_changed_notification(
mcp: &mut McpProcess,
) -> Result<Option<FsChangedNotification>> {
match timeout(
DEFAULT_READ_TIMEOUT,
OPTIONAL_FS_CHANGE_TIMEOUT,
mcp.read_stream_until_notification_message("fs/changed"),
)
.await
@@ -845,6 +846,14 @@ async fn maybe_fs_changed_notification(
fn replace_file_atomically(path: &PathBuf, contents: &str) -> Result<()> {
let temp_path = path.with_extension("lock");
std::fs::write(&temp_path, contents)?;
#[cfg(windows)]
match std::fs::remove_file(path) {
Ok(()) => {}
Err(err) if err.kind() == std::io::ErrorKind::NotFound => {}
Err(err) => return Err(err.into()),
}
std::fs::rename(temp_path, path)?;
Ok(())
}
@@ -17,6 +17,9 @@ use pretty_assertions::assert_eq;
use tempfile::TempDir;
use tokio::time::timeout;
#[cfg(windows)]
const DEFAULT_TIMEOUT: Duration = Duration::from_secs(25);
#[cfg(not(windows))]
const DEFAULT_TIMEOUT: Duration = Duration::from_secs(10);
const INSTALLED_MARKETPLACES_DIR: &str = ".tmp/marketplaces";
@@ -63,13 +66,14 @@ fn commit_marketplace_marker(root: &Path, marker: &str) -> Result<String> {
fn configured_git_marketplace_update<'a>(
source: &'a str,
last_revision: Option<&'a str>,
ref_name: Option<&'a str>,
) -> MarketplaceConfigUpdate<'a> {
MarketplaceConfigUpdate {
last_updated: "2026-04-13T00:00:00Z",
last_revision,
source_type: "git",
source,
ref_name: None,
ref_name,
sparse_paths: &[],
}
}
@@ -90,12 +94,13 @@ fn record_git_marketplace(
marketplace_name: &str,
source: &Path,
last_revision: &str,
ref_name: Option<&str>,
) -> Result<()> {
let source = source.display().to_string();
record_user_marketplace(
codex_home,
marketplace_name,
&configured_git_marketplace_update(&source, Some(last_revision)),
&configured_git_marketplace_update(&source, Some(last_revision), ref_name),
)?;
Ok(())
}
@@ -153,12 +158,14 @@ async fn marketplace_upgrade_all_configured_git_marketplaces() -> Result<()> {
"debug",
debug_source.path(),
&debug_old_revision,
Some(&debug_new_revision),
)?;
record_git_marketplace(
codex_home.path(),
"tools",
tools_source.path(),
&tools_old_revision,
Some(&tools_new_revision),
)?;
disable_plugin_startup_tasks(codex_home.path())?;
@@ -205,12 +212,14 @@ async fn marketplace_upgrade_named_marketplace_only() -> Result<()> {
"debug",
debug_source.path(),
&debug_old_revision,
/*ref_name*/ None,
)?;
record_git_marketplace(
codex_home.path(),
"tools",
tools_source.path(),
&tools_old_revision,
/*ref_name*/ None,
)?;
disable_plugin_startup_tasks(codex_home.path())?;
@@ -246,7 +255,13 @@ async fn marketplace_upgrade_returns_empty_roots_when_already_up_to_date() -> Re
let source = TempDir::new()?;
let old_revision = init_marketplace_repo(source.path(), "debug", "debug old")?;
commit_marketplace_marker(source.path(), "debug new")?;
record_git_marketplace(codex_home.path(), "debug", source.path(), &old_revision)?;
record_git_marketplace(
codex_home.path(),
"debug",
source.path(),
&old_revision,
/*ref_name*/ None,
)?;
disable_plugin_startup_tasks(codex_home.path())?;
let mut mcp = McpProcess::new(codex_home.path()).await?;
@@ -4,6 +4,7 @@ use anyhow::Result;
use anyhow::bail;
use app_test_support::ChatGptAuthFixture;
use app_test_support::McpProcess;
use app_test_support::TEST_DISABLE_PLUGIN_STARTUP_TASKS_ENV_VAR;
use app_test_support::to_response;
use app_test_support::write_chatgpt_auth;
use codex_app_server_protocol::JSONRPCResponse;
@@ -1066,7 +1067,11 @@ async fn app_server_startup_remote_plugin_sync_runs_once() -> Result<()> {
.join(STARTUP_REMOTE_PLUGIN_SYNC_MARKER_FILE);
{
let mut mcp = McpProcess::new(codex_home.path()).await?;
let mut mcp = McpProcess::new_with_env(
codex_home.path(),
&[(TEST_DISABLE_PLUGIN_STARTUP_TASKS_ENV_VAR, None)],
)
.await?;
timeout(DEFAULT_TIMEOUT, mcp.initialize()).await??;
wait_for_path_exists(&marker_path).await?;
@@ -1102,7 +1107,11 @@ async fn app_server_startup_remote_plugin_sync_runs_once() -> Result<()> {
assert!(config.contains(r#"[plugins."linear@openai-curated"]"#));
{
let mut mcp = McpProcess::new(codex_home.path()).await?;
let mut mcp = McpProcess::new_with_env(
codex_home.path(),
&[(TEST_DISABLE_PLUGIN_STARTUP_TASKS_ENV_VAR, None)],
)
.await?;
timeout(DEFAULT_TIMEOUT, mcp.initialize()).await??;
}
@@ -1490,7 +1499,11 @@ async fn plugin_list_uses_warmed_featured_plugin_ids_cache_on_first_request() ->
.mount(&server)
.await;
let mut mcp = McpProcess::new(codex_home.path()).await?;
let mut mcp = McpProcess::new_with_env(
codex_home.path(),
&[(TEST_DISABLE_PLUGIN_STARTUP_TASKS_ENV_VAR, None)],
)
.await?;
timeout(DEFAULT_TIMEOUT, mcp.initialize()).await??;
wait_for_featured_plugin_request_count(&server, /*expected_count*/ 1).await?;
@@ -749,13 +749,17 @@ async fn turn_start_rejects_combined_oversized_text_input() -> Result<()> {
#[tokio::test]
async fn turn_start_rejects_invalid_permission_profile_before_starting_turn() -> Result<()> {
let codex_home = TempDir::new()?;
let unsupported_write_root = TempDir::new()?;
let disallowed_write_root = TempDir::new()?;
create_config_toml(
codex_home.path(),
"http://localhost/unused",
"never",
&BTreeMap::from([(Feature::Personality, true)]),
)?;
std::fs::write(
codex_home.path().join("managed_config.toml"),
"sandbox_mode = \"read-only\"\n",
)?;
let mut mcp = McpProcess::new(codex_home.path()).await?;
timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??;
@@ -772,7 +776,7 @@ async fn turn_start_rejects_invalid_permission_profile_before_starting_turn() ->
)
.await??;
let ThreadStartResponse { thread, .. } = to_response::<ThreadStartResponse>(thread_resp)?;
let unsupported_write_root = AbsolutePathBuf::from_absolute_path(unsupported_write_root.path())
let disallowed_write_root = AbsolutePathBuf::from_absolute_path(disallowed_write_root.path())
.expect("tempdir path should be absolute");
let turn_req = mcp
@@ -787,7 +791,7 @@ async fn turn_start_rejects_invalid_permission_profile_before_starting_turn() ->
file_system: PermissionProfileFileSystemPermissions::Restricted {
entries: vec![FileSystemSandboxEntry {
path: FileSystemPath::Path {
path: unsupported_write_root,
path: disallowed_write_root,
},
access: FileSystemAccessMode::Write,
}],
@@ -806,9 +810,9 @@ async fn turn_start_rejects_invalid_permission_profile_before_starting_turn() ->
assert_eq!(err.error.code, INVALID_REQUEST_ERROR_CODE);
assert!(err.error.message.contains("invalid turn context override"));
assert!(
err.error
.message
.contains("filesystem writes outside the workspace root")
err.error.message.contains("allowed set [ReadOnly]"),
"unexpected error message: {}",
err.error.message
);
let turn_started = tokio::time::timeout(
std::time::Duration::from_millis(250),