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:
Dylan Hurd
2026-08-21 02:33:50 +00:00
committed by copyberry
parent d8ec270183
commit bd19459358
4 changed files with 131 additions and 0 deletions
+4
View File
@@ -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 {
+6
View File
@@ -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(())
+1
View File
@@ -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);
+120
View File
@@ -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!(