Enforce filesystem permissions when loading AGENTS.md (#39653)

## Why

Project instructions must respect the selected environment's filesystem read
permissions. Tightening those permissions for a later turn must not allow
previously cached instructions to reach the model.

## What changed

- Apply each environment's filesystem sandbox while discovering and reading
  `AGENTS.md` files.
- Fail thread or turn setup when sandboxing blocks a discovered instruction
  file, while allowing a restricted project with no instructions to start.
- Clear cached instructions before refresh and include the Windows sandbox
  level in the cache key.
- Ignore inaccessible ancestor marker probes so readable instructions in the
  selected working directory can still load.

## Testing

Added coverage for restricted projects, denied instruction files, cache
invalidation after permissions tighten, and unreadable ancestor markers.

GitOrigin-RevId: 6ea1a27b9c873a6260e4f87d42ae1317a6a4ae4b
This commit is contained in:
Adam Perry @ OpenAI authored and copyberry committed 2026-08-20 07:48:46 +00:00
1 parent ab82cddd04
commit 7ece061767
12 files changed
+407 -60

No files matched your search

+43 -9
View File
@@ -18,14 +18,17 @@
use crate::config::Config;
use crate::context::UserInstructions as ContextUserInstructions;
use crate::environment_selection::TurnEnvironmentSnapshot;
use crate::tools::sandboxing::executor_windows_sandbox_level;
use codex_config::ConfigLayerSource;
use codex_config::default_project_root_markers;
use codex_config::merge_toml_values;
use codex_config::project_root_markers_from_config;
use codex_exec_server::ExecutorFileSystem;
use codex_extension_api::UserInstructions;
use codex_file_system::FileSystemSandboxContext;
use codex_file_system::FindUpErrorPolicy;
use codex_file_system::find_nearest_ancestor_with_markers;
use codex_protocol::config_types::WindowsSandboxLevel;
use codex_utils_absolute_path::AbsolutePathBuf;
use codex_utils_path_uri::PathUri;
use futures::StreamExt;
@@ -53,7 +56,8 @@ pub(crate) async fn load_project_instructions(
config: &Config,
user_instructions: Option<UserInstructions>,
environments: &TurnEnvironmentSnapshot,
) -> Option<LoadedAgentsMd> {
windows_sandbox_level: WindowsSandboxLevel,
) -> io::Result<Option<LoadedAgentsMd>> {
let mut loaded = LoadedAgentsMd::from_user_instructions(user_instructions);
let mut remaining = config.project_doc_max_bytes;
for turn_environment in environments.turn_environments() {
@@ -62,12 +66,31 @@ pub(crate) async fn load_project_instructions(
}
let filesystem = turn_environment.environment.get_filesystem();
let sandbox = (!turn_environment
.permission_profile()
.file_system_sandbox_policy()
.has_full_disk_read_access())
.then(|| {
// TODO(anp): Move sandbox context construction to a method on TurnEnvironment.
let mut sandbox = FileSystemSandboxContext::from_permission_profile_with_cwd(
turn_environment.permission_profile().clone(),
turn_environment.cwd().clone(),
);
sandbox.workspace_roots = turn_environment.workspace_roots().to_vec();
sandbox.windows_sandbox_level =
executor_windows_sandbox_level(windows_sandbox_level, turn_environment.cwd());
sandbox.windows_sandbox_private_desktop =
config.permissions.windows_sandbox_private_desktop;
sandbox.use_legacy_landlock = config.features.use_legacy_landlock();
sandbox
});
match read_agents_md(
config,
filesystem.as_ref(),
&turn_environment.selection.environment_id,
turn_environment.cwd(),
remaining,
sandbox.as_ref(),
)
.await
{
@@ -78,16 +101,25 @@ pub(crate) async fn load_project_instructions(
}
}
Ok(None) => {}
Err(e) => {
Err(error) if sandbox.is_none() => {
error!(
environment_id = turn_environment.selection.environment_id,
"error trying to find AGENTS.md docs: {e:#}"
"error trying to find AGENTS.md docs: {error:#}"
);
}
Err(error) => {
return Err(io::Error::new(
error.kind(),
format!(
"failed to load AGENTS.md instructions for environment `{}`: {error}",
turn_environment.selection.environment_id
),
));
}
}
}
(!loaded.is_empty()).then_some(loaded)
Ok((!loaded.is_empty()).then_some(loaded))
}
/// Attempt to locate and load AGENTS.md documentation.
@@ -102,12 +134,13 @@ async fn read_agents_md(
environment_id: &str,
cwd: &PathUri,
max_total: usize,
sandbox: Option<&FileSystemSandboxContext>,
) -> io::Result<Option<LoadedAgentsMd>> {
if max_total == 0 {
return Ok(None);
}
let paths = agents_md_paths(config, cwd, fs).await?;
let paths = agents_md_paths(config, cwd, fs, sandbox).await?;
if paths.is_empty() {
return Ok(None);
}
@@ -120,7 +153,7 @@ async fn read_agents_md(
break;
}
let mut data = match fs.read_file(&p, /*sandbox*/ None).await {
let mut data = match fs.read_file(&p, sandbox).await {
Ok(data) => data,
Err(err) if err.kind() == io::ErrorKind::NotFound => continue,
Err(err) => return Err(err),
@@ -165,6 +198,7 @@ async fn agents_md_paths(
config: &Config,
cwd: &PathUri,
fs: &dyn ExecutorFileSystem,
sandbox: Option<&FileSystemSandboxContext>,
) -> io::Result<Vec<PathUri>> {
let dir = cwd.clone();
@@ -187,8 +221,8 @@ async fn agents_md_paths(
fs,
&dir,
project_root_markers,
FindUpErrorPolicy::Propagate,
/*sandbox*/ None,
FindUpErrorPolicy::Ignore,
sandbox,
)
.await?;
let search_dirs = if let Some(root) = project_root {
@@ -218,7 +252,7 @@ async fn agents_md_paths(
let candidate = directory
.join(name)
.map_err(|err| io::Error::new(io::ErrorKind::InvalidInput, err))?;
match fs.get_metadata(&candidate, /*sandbox*/ None).await {
match fs.get_metadata(&candidate, sandbox).await {
Ok(metadata) if metadata.is_file => return Ok(Some(candidate)),
Ok(_) => {}
Err(err) if err.kind() == io::ErrorKind::NotFound => {}