Route AGENTS.md loading through environment filesystems (#26205)

## Why

Workspace-specific `AGENTS.md` loading needs to use the selected
environment filesystem so remote workspaces and child agents read
instructions from their actual environment instead of the host
filesystem. The app-server should report the same instruction sources
the initialized thread actually loaded, rather than independently
rescanning configuration and filesystem state.

## What changed

- Introduce `LoadedAgentsMd` to retain ordered user, project, and
internal instructions with their provenance.
- Load and canonicalize workspace `AGENTS.md` paths through the primary
`EnvironmentManager` environment, then render the loaded instructions
when constructing turn context.
- Expose cached loaded instruction sources from initialized threads and
use them for app-server start, resume, and fork responses.
- Preserve global `CODEX_HOME` loading and separator behavior while
excluding empty project files that did not supply model-visible
instructions.
- Add integration coverage for CLI injection, selected-environment
provenance and rendering, empty environment selection, and cached
sources on loaded-thread resume.

## Validation

- `just test -p codex-core agents_md`
- `just test -p codex-core
selected_environment_sources_match_model_visible_instructions`
- `just test -p codex-exec agents_md`
- `just test -p codex-app-server instruction_sources`
- `just test -p codex-app-server --status-level fail`
This commit is contained in:
Adam Perry @ OpenAI
2026-06-04 12:43:07 -07:00
committed by GitHub
Unverified
parent c3fcb0e745
commit e64b469bbc
22 changed files with 740 additions and 137 deletions
+53
View File
@@ -1,5 +1,6 @@
use anyhow::Result;
use codex_exec_server::CreateDirectoryOptions;
use codex_utils_absolute_path::AbsolutePathBuf;
use core_test_support::responses::ev_completed;
use core_test_support::responses::ev_response_created;
use core_test_support::responses::mount_sse_once;
@@ -7,6 +8,8 @@ use core_test_support::responses::sse;
use core_test_support::responses::start_mock_server;
use core_test_support::test_codex::TestCodexBuilder;
use core_test_support::test_codex::test_codex;
use std::sync::Arc;
use tempfile::TempDir;
async fn agents_instructions(mut builder: TestCodexBuilder) -> Result<String> {
let server = start_mock_server().await;
@@ -139,3 +142,53 @@ async fn agents_docs_are_concatenated_from_project_root_to_cwd() -> Result<()> {
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn selected_environment_sources_match_model_visible_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 = home.path().join("AGENTS.md");
std::fs::write(&global_agents, "global doc")?;
let mut builder = test_codex()
.with_home(home)
.with_workspace_setup(|cwd, fs| async move {
fs.write_file(
&cwd.join("AGENTS.md"),
b"project doc".to_vec(),
/*sandbox*/ None,
)
.await?;
Ok::<(), anyhow::Error>(())
});
let test = builder.build_with_remote_env(&server).await?;
let project_agents = test
.fs()
.canonicalize(
&test.executor_environment().cwd().join("AGENTS.md"),
/*sandbox*/ None,
)
.await?;
let global_agents = AbsolutePathBuf::try_from(global_agents).expect("absolute path");
assert_eq!(
test.codex.instruction_sources().await,
vec![global_agents, project_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 for "))
.expect("instructions message");
assert!(instructions.contains("global doc\n\n--- project-doc ---\n\nproject doc"));
Ok(())
}
+4 -3
View File
@@ -1,5 +1,6 @@
use codex_config::ConfigLayerStack;
use codex_config::types::AuthCredentialsStoreMode;
use codex_core::LoadedAgentsMd;
use codex_core::ModelClient;
use codex_core::NewThread;
use codex_core::Prompt;
@@ -364,7 +365,7 @@ async fn resume_includes_initial_messages_and_sends_prior_items() {
.with_home(codex_home.clone())
.with_config(|config| {
// Ensure user instructions are NOT delivered on resume.
config.user_instructions = Some("be nice".to_string());
config.user_instructions = Some(LoadedAgentsMd::from_text_for_testing("be nice"));
});
let test = builder
.resume(&server, codex_home, session_path.clone())
@@ -1180,7 +1181,7 @@ async fn includes_user_instructions_message_in_request() {
let mut builder = test_codex()
.with_auth(CodexAuth::from_api_key("Test API Key"))
.with_config(|config| {
config.user_instructions = Some("be nice".to_string());
config.user_instructions = Some(LoadedAgentsMd::from_text_for_testing("be nice"));
});
let codex = builder
.build(&server)
@@ -2212,7 +2213,7 @@ async fn includes_developer_instructions_message_in_request() {
let mut builder = test_codex()
.with_auth(CodexAuth::from_api_key("Test API Key"))
.with_config(|config| {
config.user_instructions = Some("be nice".to_string());
config.user_instructions = Some(LoadedAgentsMd::from_text_for_testing("be nice"));
config.developer_instructions = Some("be useful".to_string());
});
let codex = builder
@@ -5,6 +5,7 @@ use std::path::Path;
use std::path::PathBuf;
use anyhow::Result;
use codex_core::LoadedAgentsMd;
use codex_features::Feature;
use codex_login::CodexAuth;
use codex_protocol::config_types::ServiceTier;
@@ -517,7 +518,9 @@ async fn build_harness_inner(
FIXED_CWD,
))
.expect("fixed cwd should be absolute");
config.user_instructions = Some("PARITY_USER_INSTRUCTIONS".to_string());
config.user_instructions = Some(LoadedAgentsMd::from_text_for_testing(
"PARITY_USER_INSTRUCTIONS",
));
config.developer_instructions = Some("PARITY_DEVELOPER_INSTRUCTIONS".to_string());
if settings.service_tier_fast {
config.service_tier = Some(ServiceTier::Fast.request_value().to_string());
+22 -7
View File
@@ -1,5 +1,6 @@
#![allow(clippy::unwrap_used)]
use codex_core::LoadedAgentsMd;
use codex_core::shell::default_user_shell;
use codex_features::Feature;
use codex_prompts::APPLY_PATCH_TOOL_INSTRUCTIONS;
@@ -122,7 +123,9 @@ async fn prompt_tools_are_consistent_across_requests() -> anyhow::Result<()> {
..
} = test_codex()
.with_config(|config| {
config.user_instructions = Some("be consistent and helpful".to_string());
config.user_instructions = Some(LoadedAgentsMd::from_text_for_testing(
"be consistent and helpful",
));
config.model = Some("gpt-5.2".to_string());
// Keep tool expectations stable when the default web_search mode changes.
config
@@ -237,7 +240,9 @@ async fn gpt_5_tools_without_apply_patch_append_apply_patch_instructions() -> an
let TestCodex { codex, .. } = test_codex()
.with_config(|config| {
config.user_instructions = Some("be consistent and helpful".to_string());
config.user_instructions = Some(LoadedAgentsMd::from_text_for_testing(
"be consistent and helpful",
));
config
.features
.enable(Feature::CollaborationModes)
@@ -319,7 +324,9 @@ async fn prefixes_context_and_instructions_once_and_consistently_across_requests
let TestCodex { codex, config, .. } = test_codex()
.with_config(|config| {
config.user_instructions = Some("be consistent and helpful".to_string());
config.user_instructions = Some(LoadedAgentsMd::from_text_for_testing(
"be consistent and helpful",
));
config
.features
.enable(Feature::CollaborationModes)
@@ -417,7 +424,9 @@ async fn overrides_turn_context_but_keeps_cached_prefix_and_key_constant() -> an
let TestCodex { codex, config, .. } = test_codex()
.with_config(|config| {
config.user_instructions = Some("be consistent and helpful".to_string());
config.user_instructions = Some(LoadedAgentsMd::from_text_for_testing(
"be consistent and helpful",
));
config
.features
.enable(Feature::CollaborationModes)
@@ -709,7 +718,9 @@ async fn per_turn_overrides_keep_cached_prefix_and_key_constant() -> anyhow::Res
let TestCodex { codex, .. } = test_codex()
.with_config(|config| {
config.user_instructions = Some("be consistent and helpful".to_string());
config.user_instructions = Some(LoadedAgentsMd::from_text_for_testing(
"be consistent and helpful",
));
config
.features
.enable(Feature::CollaborationModes)
@@ -844,7 +855,9 @@ async fn send_user_turn_with_no_changes_does_not_send_environment_context() -> a
..
} = test_codex()
.with_config(|config| {
config.user_instructions = Some("be consistent and helpful".to_string());
config.user_instructions = Some(LoadedAgentsMd::from_text_for_testing(
"be consistent and helpful",
));
config
.features
.enable(Feature::CollaborationModes)
@@ -985,7 +998,9 @@ async fn send_user_turn_with_changes_sends_environment_context() -> anyhow::Resu
..
} = test_codex()
.with_config(|config| {
config.user_instructions = Some("be consistent and helpful".to_string());
config.user_instructions = Some(LoadedAgentsMd::from_text_for_testing(
"be consistent and helpful",
));
config
.features
.enable(Feature::CollaborationModes)
@@ -1,4 +1,5 @@
use anyhow::Result;
use codex_core::LoadedAgentsMd;
use codex_core::build_prompt_input;
use codex_core::config::ConfigBuilder;
use codex_core::config::ConfigOverrides;
@@ -21,7 +22,9 @@ async fn build_prompt_input_includes_context_and_user_message() -> Result<()> {
})
.build()
.await?;
config.user_instructions = Some("Project-specific test instructions".to_string());
config.user_instructions = Some(LoadedAgentsMd::from_text_for_testing(
"Project-specific test instructions",
));
let input = build_prompt_input(
config,