core: load AGENTS.md from foreign environments (#28958)

## Why

Make it possible to load AGENTS.md from remote exec-servers whose OS is
different than app-server.

## What

- keep `AGENTS.md` discovery and provenance as `PathUri`, with
root-aware parent and ancestor traversal
- expose lifecycle instruction sources as legacy app-server path strings
in events while retaining `PathUri` internally
- preserve and test mixed POSIX and Windows paths in model context and
TUI status output
- cover remote Windows loading end to end by seeding the Wine prefix
through host filesystem APIs
- fix bug in `PathUri`'s parent() implementation that would erase
Windows drive letters
This commit is contained in:
Adam Perry @ OpenAI
2026-06-18 15:06:23 -07:00
committed by GitHub
parent 406062c3af
commit dce673905a
38 changed files with 550 additions and 203 deletions
+29 -34
View File
@@ -53,16 +53,11 @@ pub(crate) async fn load_project_instructions(
let mut loaded = LoadedAgentsMd::from_user_instructions(user_instructions);
for turn_environment in &environments.turn_environments {
let filesystem = turn_environment.environment.get_filesystem();
// TODO(anp): Migrate AGENTS.md discovery to PathUri so instructions can be loaded from
// environment-native foreign working directories.
let Ok(cwd) = turn_environment.cwd().to_abs_path() else {
continue;
};
match read_agents_md(
config,
filesystem.as_ref(),
&turn_environment.environment_id,
&cwd,
turn_environment.cwd(),
)
.await
{
@@ -97,7 +92,7 @@ async fn read_agents_md(
config: &Config,
fs: &dyn ExecutorFileSystem,
environment_id: &str,
cwd: &AbsolutePathBuf,
cwd: &PathUri,
) -> io::Result<Option<LoadedAgentsMd>> {
let max_total = config.project_doc_max_bytes;
@@ -118,15 +113,14 @@ async fn read_agents_md(
break;
}
let path_uri = PathUri::from_abs_path(&p);
match fs.get_metadata(&path_uri, /*sandbox*/ None).await {
match fs.get_metadata(&p, /*sandbox*/ None).await {
Ok(metadata) if !metadata.is_file => continue,
Ok(_) => {}
Err(err) if err.kind() == io::ErrorKind::NotFound => continue,
Err(err) => return Err(err),
}
let mut data = match fs.read_file(&path_uri, /*sandbox*/ None).await {
let mut data = match fs.read_file(&p, /*sandbox*/ None).await {
Ok(data) => data,
Err(err) if err.kind() == io::ErrorKind::NotFound => continue,
Err(err) => return Err(err),
@@ -139,7 +133,7 @@ async fn read_agents_md(
if size > remaining {
tracing::warn!(
"Project doc `{}` exceeds remaining budget ({} bytes) - truncating.",
p.display(),
p.inferred_native_path_string(),
remaining,
);
}
@@ -169,9 +163,9 @@ async fn read_agents_md(
/// directory, inclusive. Symlinks are allowed.
async fn agents_md_paths(
config: &Config,
cwd: &AbsolutePathBuf,
cwd: &PathUri,
fs: &dyn ExecutorFileSystem,
) -> io::Result<Vec<AbsolutePathBuf>> {
) -> io::Result<Vec<PathUri>> {
let dir = cwd.clone();
let mut merged = TomlValue::Table(toml::map::Map::new());
@@ -194,18 +188,18 @@ async fn agents_md_paths(
};
let mut project_root = None;
if !project_root_markers.is_empty() {
for ancestor in dir.ancestors() {
for current in dir.ancestors() {
for marker in &project_root_markers {
let marker_path = ancestor.join(marker);
let marker_path_uri = PathUri::from_abs_path(&marker_path);
let marker_exists = match fs.get_metadata(&marker_path_uri, /*sandbox*/ None).await
{
let marker_path = current
.join(marker)
.map_err(|err| io::Error::new(io::ErrorKind::InvalidInput, err))?;
let marker_exists = match fs.get_metadata(&marker_path, /*sandbox*/ None).await {
Ok(_) => true,
Err(err) if err.kind() == io::ErrorKind::NotFound => false,
Err(err) => return Err(err),
};
if marker_exists {
project_root = Some(ancestor.clone());
project_root = Some(current.clone());
break;
}
}
@@ -215,7 +209,7 @@ async fn agents_md_paths(
}
}
let search_dirs: Vec<AbsolutePathBuf> = if let Some(root) = project_root {
let search_dirs: Vec<PathUri> = if let Some(root) = project_root {
let mut dirs = Vec::new();
let mut cursor = dir.clone();
loop {
@@ -234,13 +228,14 @@ async fn agents_md_paths(
vec![dir]
};
let mut found: Vec<AbsolutePathBuf> = Vec::new();
let mut found: Vec<PathUri> = Vec::new();
let candidate_filenames = candidate_filenames(config);
for d in search_dirs {
for name in &candidate_filenames {
let candidate = d.join(name);
let candidate_uri = PathUri::from_abs_path(&candidate);
match fs.get_metadata(&candidate_uri, /*sandbox*/ None).await {
let candidate = d
.join(name)
.map_err(|err| io::Error::new(io::ErrorKind::InvalidInput, err))?;
match fs.get_metadata(&candidate, /*sandbox*/ None).await {
Ok(md) if md.is_file => {
found.push(candidate);
break;
@@ -371,7 +366,7 @@ impl LoadedAgentsMd {
fn environment_labeled_text(&self) -> String {
let mut output = String::new();
let mut has_previous = false;
let mut previous_environment: Option<(&str, &AbsolutePathBuf)> = None;
let mut previous_environment: Option<(&str, &PathUri)> = None;
if let Some(instructions) = &self.user_instructions {
output.push_str(&instructions.text);
has_previous = true;
@@ -394,7 +389,7 @@ impl LoadedAgentsMd {
output.push_str(&format!(
"for `{}` with root {}\n\n",
environment_id,
cwd.display()
cwd.inferred_native_path_string()
));
}
output.push_str(&entry.contents);
@@ -421,7 +416,7 @@ impl LoadedAgentsMd {
None
} else {
self.single_project_cwd()
.map(|cwd| cwd.to_string_lossy().into_owned())
.map(PathUri::inferred_native_path_string)
};
ContextUserInstructions {
directory,
@@ -436,10 +431,10 @@ impl LoadedAgentsMd {
}
/// Returns the AGENTS.md files that supplied instruction entries.
pub fn sources(&self) -> impl Iterator<Item = &AbsolutePathBuf> {
pub fn sources(&self) -> impl Iterator<Item = PathUri> + '_ {
self.user_instructions
.iter()
.map(|instructions| &instructions.source)
.map(|instructions| PathUri::from_abs_path(&instructions.source))
.chain(
self.entries
.iter()
@@ -463,7 +458,7 @@ impl LoadedAgentsMd {
})
}
fn single_project_cwd(&self) -> Option<&AbsolutePathBuf> {
fn single_project_cwd(&self) -> Option<&PathUri> {
self.entries
.iter()
.find_map(|entry| match &entry.provenance {
@@ -488,9 +483,9 @@ enum InstructionProvenance {
/// Workspace instructions discovered from project AGENTS.md files.
Project {
/// Exact AGENTS.md file, distinct from the environment's selected cwd.
source_path: AbsolutePathBuf,
source_path: PathUri,
environment_id: String,
cwd: AbsolutePathBuf,
cwd: PathUri,
},
/// Instructions without a file source, including internally defined guidance.
@@ -498,9 +493,9 @@ enum InstructionProvenance {
}
impl InstructionProvenance {
fn path(&self) -> Option<&AbsolutePathBuf> {
fn path(&self) -> Option<PathUri> {
match self {
Self::Project { source_path, .. } => Some(source_path),
Self::Project { source_path, .. } => Some(source_path.clone()),
Self::Internal => None,
}
}
+141 -40
View File
@@ -250,8 +250,13 @@ async fn load_agents_md(config: &TestConfig) -> Option<LoadedAgentsMd> {
.await
}
async fn agents_md_paths(config: &TestConfig) -> std::io::Result<Vec<AbsolutePathBuf>> {
super::agents_md_paths(&config.config, &config.cwd, LOCAL_FS.as_ref()).await
async fn agents_md_paths(config: &TestConfig) -> std::io::Result<Vec<PathUri>> {
super::agents_md_paths(
&config.config,
&PathUri::from_abs_path(&config.cwd),
LOCAL_FS.as_ref(),
)
.await
}
fn resolved_local_environments<const N: usize>(
@@ -277,12 +282,101 @@ fn resolved_local_environments<const N: usize>(
fn project_provenance(path: AbsolutePathBuf, cwd: AbsolutePathBuf) -> InstructionProvenance {
InstructionProvenance::Project {
source_path: path,
source_path: PathUri::from_abs_path(&path),
environment_id: "local".to_string(),
cwd,
cwd: PathUri::from_abs_path(&cwd),
}
}
#[test]
fn foreign_agents_md_uses_environment_native_paths() {
let (cwd, rendered_cwd) = if cfg!(windows) {
(
PathUri::parse("file:///codex%20runtime").expect("POSIX cwd URI"),
"/codex runtime",
)
} else {
(
PathUri::parse("file:///C:/codex%20runtime").expect("Windows cwd URI"),
r"C:\codex runtime",
)
};
let source_path = cwd.join("AGENTS.md").expect("AGENTS.md URI");
let loaded = LoadedAgentsMd {
user_instructions: None,
entries: vec![InstructionEntry {
contents: "remote instructions".to_string(),
provenance: InstructionProvenance::Project {
source_path: source_path.clone(),
environment_id: "remote".to_string(),
cwd,
},
}],
};
assert_eq!(
loaded.render(),
format!(
"# AGENTS.md instructions for {rendered_cwd}
<INSTRUCTIONS>
remote instructions
</INSTRUCTIONS>"
)
);
assert_eq!(loaded.sources().collect::<Vec<_>>(), vec![source_path]);
}
#[test]
fn multi_environment_agents_md_renders_mixed_path_conventions() {
let posix_cwd = PathUri::parse("file:///srv/project").expect("POSIX cwd URI");
let windows_cwd = PathUri::parse("file:///C:/workspace").expect("Windows cwd URI");
let posix_source = posix_cwd.join("AGENTS.md").expect("POSIX AGENTS.md URI");
let windows_source = windows_cwd
.join("AGENTS.md")
.expect("Windows AGENTS.md URI");
let loaded = LoadedAgentsMd {
user_instructions: None,
entries: vec![
InstructionEntry {
contents: "POSIX instructions".to_string(),
provenance: InstructionProvenance::Project {
source_path: posix_source.clone(),
environment_id: "posix".to_string(),
cwd: posix_cwd,
},
},
InstructionEntry {
contents: "Windows instructions".to_string(),
provenance: InstructionProvenance::Project {
source_path: windows_source.clone(),
environment_id: "windows".to_string(),
cwd: windows_cwd,
},
},
],
};
assert_eq!(
loaded.render(),
r#"# AGENTS.md instructions
<INSTRUCTIONS>
for `posix` with root /srv/project
POSIX instructions
for `windows` with root C:\workspace
Windows instructions
</INSTRUCTIONS>"#
);
assert_eq!(
loaded.sources().collect::<Vec<_>>(),
vec![posix_source, windows_source]
);
}
/// Helper that returns a `Config` pointing at `root` and using `limit` as
/// the maximum number of bytes to embed from AGENTS.md. The caller can
/// optionally specify a custom `instructions` string when `None` the
@@ -508,7 +602,7 @@ async fn read_agents_md_propagates_metadata_errors() {
};
let cwd = config.cwd.clone();
let err = read_agents_md(&config.config, &fs, "local", &cwd)
let err = read_agents_md(&config.config, &fs, "local", &PathUri::from_abs_path(&cwd))
.await
.expect_err("metadata error");
@@ -526,7 +620,7 @@ async fn read_agents_md_propagates_read_errors() {
};
let cwd = config.cwd.clone();
let err = read_agents_md(&config.config, &fs, "local", &cwd)
let err = read_agents_md(&config.config, &fs, "local", &PathUri::from_abs_path(&cwd))
.await
.expect_err("read error");
@@ -544,7 +638,7 @@ async fn read_agents_md_ignores_files_removed_after_discovery() {
};
let cwd = config.cwd.clone();
let loaded = read_agents_md(&config.config, &fs, "local", &cwd)
let loaded = read_agents_md(&config.config, &fs, "local", &PathUri::from_abs_path(&cwd))
.await
.expect("removed file is recoverable");
@@ -659,17 +753,18 @@ secondary doc"#,
);
assert_eq!(loaded.render(), expected_fragment);
assert_eq!(
loaded.sources().cloned().collect::<Vec<_>>(),
loaded.sources().collect::<Vec<_>>(),
vec![
config
.user_instructions
.as_ref()
.expect("global instructions")
.source
.clone(),
primary.path().join("AGENTS.md").abs(),
primary_nested.join("AGENTS.md").abs(),
secondary.path().join("AGENTS.md").abs(),
PathUri::from_abs_path(
&config
.user_instructions
.as_ref()
.expect("global instructions")
.source,
),
PathUri::from_abs_path(&primary.path().join("AGENTS.md").abs()),
PathUri::from_abs_path(&primary_nested.join("AGENTS.md").abs()),
PathUri::from_abs_path(&secondary.path().join("AGENTS.md").abs()),
]
);
}
@@ -898,7 +993,10 @@ async fn concatenates_root_and_cwd_docs() {
assert_eq!(loaded.text(), "root doc\n\ncrate doc");
assert_eq!(
loaded.sources().collect::<Vec<_>>(),
vec![&root_agents, &crate_agents]
vec![
PathUri::from_abs_path(&root_agents),
PathUri::from_abs_path(&crate_agents),
]
);
}
@@ -925,8 +1023,8 @@ async fn project_root_markers_are_honored_for_agents_discovery() {
let expected_parent = root.path().join("AGENTS.md").abs();
let expected_child = cfg.cwd.join("AGENTS.md");
assert_eq!(discovery.len(), 2);
assert_eq!(discovery[0], expected_parent);
assert_eq!(discovery[1], expected_child);
assert_eq!(discovery[0], PathUri::from_abs_path(&expected_parent));
assert_eq!(discovery[1], PathUri::from_abs_path(&expected_child));
let res = get_user_instructions(&cfg).await.expect("doc expected");
assert_eq!(res, "parent doc\n\nchild doc");
@@ -971,8 +1069,8 @@ async fn project_layers_do_not_override_project_root_markers() {
assert_eq!(
discovery,
vec![
root.path().join("AGENTS.md").abs(),
config.cwd.join("AGENTS.md"),
PathUri::from_abs_path(&root.path().join("AGENTS.md").abs()),
PathUri::from_abs_path(&config.cwd.join("AGENTS.md")),
]
);
}
@@ -991,7 +1089,10 @@ async fn agents_md_paths_preserve_symlinked_cwd() {
cfg.cwd = linked_cwd.abs();
let discovery = agents_md_paths(&cfg).await.expect("discover paths");
assert_eq!(discovery, vec![cfg.cwd.join("AGENTS.md")]);
assert_eq!(
discovery,
vec![PathUri::from_abs_path(&cfg.cwd.join("AGENTS.md"))]
);
let res = get_user_instructions(&cfg).await.expect("doc expected");
assert_eq!(res, "project doc");
@@ -1050,7 +1151,10 @@ async fn instruction_sources_include_global_before_agents_md_docs() {
assert_eq!(loaded.user_instructions(), cfg.user_instructions.as_ref());
assert_eq!(
loaded.sources().collect::<Vec<_>>(),
vec![&global_agents, &project_agents]
vec![
PathUri::from_abs_path(&global_agents),
PathUri::from_abs_path(&project_agents),
]
);
assert_eq!(
loaded.text(),
@@ -1091,7 +1195,10 @@ async fn child_agents_message_after_project_docs_is_not_an_instruction_source()
assert_eq!(loaded, expected);
assert_eq!(
loaded.sources().collect::<Vec<_>>(),
vec![&global_agents, &project_agents]
vec![
PathUri::from_abs_path(&global_agents),
PathUri::from_abs_path(&project_agents),
]
);
assert_eq!(
loaded.text(),
@@ -1117,8 +1224,8 @@ async fn agents_local_md_preferred() {
let discovery = agents_md_paths(&cfg).await.expect("discover paths");
assert_eq!(discovery.len(), 1);
assert_eq!(
discovery[0].file_name().unwrap().to_string_lossy(),
LOCAL_AGENTS_MD_FILENAME
discovery[0].basename().as_deref(),
Some(LOCAL_AGENTS_MD_FILENAME)
);
}
@@ -1166,12 +1273,9 @@ async fn agents_md_preferred_over_fallbacks() {
let discovery = agents_md_paths(&cfg).await.expect("discover paths");
assert_eq!(discovery.len(), 1);
assert!(
discovery[0]
.file_name()
.unwrap()
.to_string_lossy()
.eq(DEFAULT_AGENTS_MD_FILENAME)
assert_eq!(
discovery[0].basename().as_deref(),
Some(DEFAULT_AGENTS_MD_FILENAME)
);
}
@@ -1186,7 +1290,7 @@ async fn agents_md_directory_is_ignored() {
assert_eq!(res, None);
let discovery = agents_md_paths(&cfg).await.expect("discover paths");
assert_eq!(discovery, Vec::<AbsolutePathBuf>::new());
assert_eq!(discovery, Vec::<PathUri>::new());
}
#[cfg(unix)]
@@ -1209,7 +1313,7 @@ async fn agents_md_special_file_is_ignored() {
assert_eq!(res, None);
let discovery = agents_md_paths(&cfg).await.expect("discover paths");
assert_eq!(discovery, Vec::<AbsolutePathBuf>::new());
assert_eq!(discovery, Vec::<PathUri>::new());
}
#[tokio::test]
@@ -1228,11 +1332,8 @@ async fn override_directory_falls_back_to_agents_md_file() {
let discovery = agents_md_paths(&cfg).await.expect("discover paths");
assert_eq!(discovery.len(), 1);
assert_eq!(
discovery[0]
.file_name()
.expect("file name")
.to_string_lossy(),
DEFAULT_AGENTS_MD_FILENAME
discovery[0].basename().as_deref(),
Some(DEFAULT_AGENTS_MD_FILENAME)
);
}
+11 -1
View File
@@ -42,6 +42,7 @@ use codex_thread_store::ThreadMetadataPatch;
use codex_thread_store::ThreadStoreError;
use codex_thread_store::ThreadStoreResult;
use codex_utils_absolute_path::AbsolutePathBuf;
use codex_utils_path_uri::LegacyAppPathString;
use codex_utils_path_uri::PathUri;
use rmcp::model::ReadResourceRequestParams;
use std::collections::BTreeMap;
@@ -564,10 +565,19 @@ impl CodexThread {
}
/// Returns the files that supplied the thread's loaded model instructions.
pub async fn instruction_sources(&self) -> Vec<AbsolutePathBuf> {
pub async fn instruction_sources(&self) -> Vec<PathUri> {
self.codex.instruction_sources().await
}
/// Returns loaded instruction sources rendered as legacy app-server path strings.
pub async fn legacy_instruction_sources(&self) -> Vec<LegacyAppPathString> {
self.instruction_sources()
.await
.into_iter()
.map(Into::into)
.collect()
}
pub async fn config(&self) -> Arc<crate::config::Config> {
self.codex.session.get_config().await
}
+2 -4
View File
@@ -830,15 +830,13 @@ impl Codex {
state.session_configuration.thread_config_snapshot()
}
pub(crate) async fn instruction_sources(&self) -> Vec<AbsolutePathBuf> {
pub(crate) async fn instruction_sources(&self) -> Vec<PathUri> {
let state = self.session.state.lock().await;
state
.session_configuration
.loaded_agents_md
.as_ref()
.map_or_else(Vec::new, |instructions| {
instructions.sources().cloned().collect()
})
.map_or_else(Vec::new, |instructions| instructions.sources().collect())
}
pub(crate) async fn thread_environment_selections(&self) -> Vec<TurnEnvironmentSelection> {