mirror of
https://github.com/openai/codex.git
synced 2026-10-18 17:18:52 +00:00
Ignore project instructions for untrusted projects (#39837)
## What changed - Skip project-scoped `AGENTS.md` discovery when the active project is untrusted, while preserving user-level instructions. - Include the active project trust level in the instruction cache key so runtime trust changes reload the applicable instructions. - Preserve updated project trust metadata during runtime configuration refreshes. ## Testing Add coverage for initially untrusted projects and for switching between trusted and untrusted states at runtime. GitOrigin-RevId: 284bf7693943f585013fb5fe4b6aa3309bc50cf3
This commit is contained in:
@@ -61,6 +61,10 @@ pub(crate) async fn load_project_instructions(
|
||||
windows_sandbox_level: WindowsSandboxLevel,
|
||||
) -> io::Result<Option<LoadedAgentsMd>> {
|
||||
let mut loaded = LoadedAgentsMd::from_user_instructions(user_instructions);
|
||||
if config.active_project.is_untrusted() {
|
||||
return Ok((!loaded.is_empty()).then_some(loaded));
|
||||
}
|
||||
|
||||
let mut remaining = config.project_doc_max_bytes;
|
||||
for turn_environment in environments.turn_environments() {
|
||||
if remaining == 0 {
|
||||
|
||||
@@ -3,6 +3,7 @@ use crate::agents_md::load_project_instructions;
|
||||
use crate::config::Config;
|
||||
use crate::environment_selection::TurnEnvironmentSnapshot;
|
||||
use codex_extension_api::UserInstructions;
|
||||
use codex_protocol::config_types::TrustLevel;
|
||||
use codex_protocol::config_types::WindowsSandboxLevel;
|
||||
use codex_protocol::protocol::TurnEnvironmentSelection;
|
||||
use std::io;
|
||||
@@ -18,6 +19,7 @@ pub(crate) struct AgentsMdManager {
|
||||
#[derive(Default)]
|
||||
struct AgentsMdCache {
|
||||
selections: Option<Vec<TurnEnvironmentSelection>>,
|
||||
active_project_trust_level: Option<TrustLevel>,
|
||||
windows_sandbox_level: Option<WindowsSandboxLevel>,
|
||||
loaded: Option<Arc<LoadedAgentsMd>>,
|
||||
}
|
||||
@@ -42,14 +44,17 @@ impl AgentsMdManager {
|
||||
.turn_environments()
|
||||
.map(|environment| environment.selection.clone())
|
||||
.collect::<Vec<_>>();
|
||||
let active_project_trust_level = config.active_project.trust_level;
|
||||
{
|
||||
let mut cache = self.cache.lock().await;
|
||||
if cache.selections.as_ref() == Some(&selections)
|
||||
&& cache.active_project_trust_level == active_project_trust_level
|
||||
&& cache.windows_sandbox_level == Some(windows_sandbox_level)
|
||||
{
|
||||
return Ok(());
|
||||
}
|
||||
cache.selections = None;
|
||||
cache.active_project_trust_level = None;
|
||||
cache.windows_sandbox_level = None;
|
||||
cache.loaded = None;
|
||||
}
|
||||
@@ -64,6 +69,7 @@ impl AgentsMdManager {
|
||||
.map(Arc::new);
|
||||
let mut cache = self.cache.lock().await;
|
||||
cache.selections = Some(selections);
|
||||
cache.active_project_trust_level = active_project_trust_level;
|
||||
cache.windows_sandbox_level = Some(windows_sandbox_level);
|
||||
cache.loaded = loaded;
|
||||
Ok(())
|
||||
|
||||
@@ -1752,6 +1752,7 @@ impl Session {
|
||||
let previous_config = notify_config_contributors
|
||||
.then(|| self.build_effective_session_config(&state.session_configuration));
|
||||
let mut config = (*state.session_configuration.original_config_do_not_use).clone();
|
||||
config.active_project = next_config.active_project.clone();
|
||||
config.config_layer_stack = config
|
||||
.config_layer_stack
|
||||
.with_user_layer_from(&next_config.config_layer_stack);
|
||||
|
||||
@@ -10,6 +10,7 @@ use codex_features::Feature;
|
||||
use codex_history::RolloutItem;
|
||||
use codex_history::RolloutLine;
|
||||
use codex_home::CodexHomeUserInstructionsProvider;
|
||||
use codex_protocol::config_types::TrustLevel;
|
||||
use codex_protocol::models::PermissionProfile;
|
||||
use codex_protocol::permissions::FileSystemAccessMode;
|
||||
use codex_protocol::permissions::FileSystemSandboxEntry;
|
||||
@@ -466,6 +467,125 @@ async fn selected_environment_sources_match_model_visible_instructions() -> Resu
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn untrusted_project_excludes_project_instructions() -> Result<()> {
|
||||
let server = start_mock_server().await;
|
||||
let resp_mock = mount_sse_once(
|
||||
&server,
|
||||
sse(vec![ev_response_created("resp1"), ev_completed("resp1")]),
|
||||
)
|
||||
.await;
|
||||
let home = Arc::new(TempDir::new()?);
|
||||
let global_agents =
|
||||
write_global_file(home.as_ref(), GLOBAL_AGENTS_FILENAME, GLOBAL_INSTRUCTIONS)?;
|
||||
|
||||
let mut builder = test_codex()
|
||||
.with_home(home)
|
||||
.with_config(|config| {
|
||||
config.active_project.trust_level = Some(TrustLevel::Untrusted);
|
||||
})
|
||||
.with_workspace_setup(|cwd, fs| async move {
|
||||
fs.write_file(
|
||||
&executor_path_uri(cwd.join(GLOBAL_AGENTS_FILENAME))?,
|
||||
PROJECT_INSTRUCTIONS.as_bytes().to_vec(),
|
||||
Default::default(),
|
||||
/*sandbox*/ None,
|
||||
)
|
||||
.await?;
|
||||
Ok::<(), anyhow::Error>(())
|
||||
});
|
||||
let test = builder.build_with_auto_env(&server).await?;
|
||||
|
||||
assert_eq!(
|
||||
test.codex.instruction_sources().await,
|
||||
vec![PathUri::from_abs_path(&global_agents)]
|
||||
);
|
||||
|
||||
test.submit_turn("hello").await?;
|
||||
let instructions = resp_mock
|
||||
.single_request()
|
||||
.message_input_texts("user")
|
||||
.into_iter()
|
||||
.find(|text| text.starts_with("# AGENTS.md instructions"))
|
||||
.expect("global instructions message");
|
||||
assert!(instructions.contains(GLOBAL_INSTRUCTIONS));
|
||||
assert!(!instructions.contains(PROJECT_INSTRUCTIONS));
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn runtime_trust_reload_refreshes_project_instructions() -> Result<()> {
|
||||
let server = start_mock_server().await;
|
||||
let response_mock = responses::mount_sse_sequence(
|
||||
&server,
|
||||
vec![
|
||||
sse(vec![ev_response_created("resp1"), ev_completed("resp1")]),
|
||||
sse(vec![ev_response_created("resp2"), ev_completed("resp2")]),
|
||||
sse(vec![ev_response_created("resp3"), ev_completed("resp3")]),
|
||||
],
|
||||
)
|
||||
.await;
|
||||
let home = Arc::new(TempDir::new()?);
|
||||
let global_agents =
|
||||
write_global_file(home.as_ref(), GLOBAL_AGENTS_FILENAME, GLOBAL_INSTRUCTIONS)?;
|
||||
let mut builder = test_codex()
|
||||
.with_home(home)
|
||||
.with_config(|config| {
|
||||
config.active_project.trust_level = Some(TrustLevel::Trusted);
|
||||
})
|
||||
.with_workspace_setup(|cwd, fs| async move {
|
||||
fs.write_file(
|
||||
&executor_path_uri(cwd.join(GLOBAL_AGENTS_FILENAME))?,
|
||||
PROJECT_INSTRUCTIONS.as_bytes().to_vec(),
|
||||
Default::default(),
|
||||
/*sandbox*/ None,
|
||||
)
|
||||
.await?;
|
||||
Ok::<(), anyhow::Error>(())
|
||||
});
|
||||
let test = builder.build_with_auto_env(&server).await?;
|
||||
let project_agents = test.workspace_path_uri(GLOBAL_AGENTS_FILENAME)?;
|
||||
let global_agents = PathUri::from_abs_path(&global_agents);
|
||||
|
||||
test.submit_turn("trusted project").await?;
|
||||
assert_eq!(
|
||||
test.codex.instruction_sources().await,
|
||||
vec![global_agents.clone(), project_agents.clone()]
|
||||
);
|
||||
|
||||
let mut untrusted_config = (*test.codex.config().await).clone();
|
||||
untrusted_config.active_project.trust_level = Some(TrustLevel::Untrusted);
|
||||
test.codex.refresh_runtime_config(untrusted_config).await;
|
||||
test.submit_turn("untrusted project").await?;
|
||||
assert_eq!(
|
||||
test.codex.instruction_sources().await,
|
||||
vec![global_agents.clone()]
|
||||
);
|
||||
|
||||
let mut trusted_config = (*test.codex.config().await).clone();
|
||||
trusted_config.active_project.trust_level = Some(TrustLevel::Trusted);
|
||||
test.codex.refresh_runtime_config(trusted_config).await;
|
||||
test.submit_turn("trusted again").await?;
|
||||
assert_eq!(
|
||||
test.codex.instruction_sources().await,
|
||||
vec![global_agents, project_agents]
|
||||
);
|
||||
|
||||
let requests = response_mock.requests();
|
||||
assert_eq!(requests.len(), 3);
|
||||
let latest_instruction_fragments = requests
|
||||
.iter()
|
||||
.map(instruction_fragments)
|
||||
.map(|fragments| fragments.last().cloned().expect("instructions message"))
|
||||
.collect::<Vec<_>>();
|
||||
assert!(latest_instruction_fragments[0].contains(PROJECT_INSTRUCTIONS));
|
||||
assert!(!latest_instruction_fragments[1].contains(PROJECT_INSTRUCTIONS));
|
||||
assert!(latest_instruction_fragments[2].contains(PROJECT_INSTRUCTIONS));
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn restricted_project_without_instructions_starts_successfully() -> Result<()> {
|
||||
skip_if_target_windows!(
|
||||
|
||||
Reference in New Issue
Block a user