mirror of
https://github.com/pchuan98/codex.git
synced 2026-07-01 00:31:56 +08:00
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:
committed by
GitHub
Unverified
parent
c3fcb0e745
commit
e64b469bbc
@@ -659,12 +659,6 @@ impl ThreadRequestProcessor {
|
||||
.map(|response| Some(response.into()))
|
||||
}
|
||||
|
||||
async fn instruction_sources_from_config(config: &Config) -> Vec<AbsolutePathBuf> {
|
||||
codex_core::AgentsMdManager::new(config)
|
||||
.instruction_sources(LOCAL_FS.as_ref())
|
||||
.await
|
||||
}
|
||||
|
||||
async fn load_thread(
|
||||
&self,
|
||||
thread_id: &str,
|
||||
@@ -1043,7 +1037,6 @@ impl ThreadRequestProcessor {
|
||||
.map_err(|err| config_load_error(&err))?;
|
||||
}
|
||||
|
||||
let instruction_sources = Self::instruction_sources_from_config(&config).await;
|
||||
let environments = environments.unwrap_or_else(|| {
|
||||
listener_task_context
|
||||
.thread_manager
|
||||
@@ -1113,6 +1106,7 @@ impl ThreadRequestProcessor {
|
||||
)
|
||||
.await?;
|
||||
|
||||
let instruction_sources = thread.instruction_sources().await;
|
||||
let config_snapshot = thread
|
||||
.config_snapshot()
|
||||
.instrument(tracing::info_span!(
|
||||
@@ -2524,13 +2518,12 @@ impl ThreadRequestProcessor {
|
||||
}
|
||||
};
|
||||
|
||||
let instruction_sources = Self::instruction_sources_from_config(&config).await;
|
||||
let response_history = thread_history.clone();
|
||||
|
||||
match self
|
||||
.thread_manager
|
||||
.resume_thread_with_history(
|
||||
config.clone(),
|
||||
config,
|
||||
thread_history,
|
||||
self.auth_manager.clone(),
|
||||
self.request_trace_context(&request_id).await,
|
||||
@@ -2553,6 +2546,7 @@ impl ThreadRequestProcessor {
|
||||
self.outgoing.send_error(request_id, err).await;
|
||||
return Ok(());
|
||||
}
|
||||
let instruction_sources = codex_thread.instruction_sources().await;
|
||||
let SessionConfiguredEvent { rollout_path, .. } = session_configured;
|
||||
let Some(rollout_path) = rollout_path else {
|
||||
let error =
|
||||
@@ -2853,10 +2847,7 @@ impl ThreadRequestProcessor {
|
||||
/*include_turns*/ false,
|
||||
);
|
||||
thread_summary.session_id = existing_thread.session_configured().session_id.to_string();
|
||||
let mut config_for_instruction_sources = self.config.as_ref().clone();
|
||||
config_for_instruction_sources.cwd = config_snapshot.cwd.clone();
|
||||
let instruction_sources =
|
||||
Self::instruction_sources_from_config(&config_for_instruction_sources).await;
|
||||
let instruction_sources = existing_thread.instruction_sources().await;
|
||||
|
||||
let listener_command_tx = {
|
||||
let thread_state = thread_state.lock().await;
|
||||
@@ -3233,7 +3224,6 @@ impl ThreadRequestProcessor {
|
||||
.map_err(|err| config_load_error(&err))?;
|
||||
|
||||
let fallback_model_provider = config.model_provider_id.clone();
|
||||
let instruction_sources = Self::instruction_sources_from_config(&config).await;
|
||||
|
||||
let NewThread {
|
||||
thread_id,
|
||||
@@ -3284,6 +3274,8 @@ impl ThreadRequestProcessor {
|
||||
.map_err(|err| core_thread_write_error("inherit source thread name", err))?;
|
||||
}
|
||||
|
||||
let instruction_sources = forked_thread.instruction_sources().await;
|
||||
|
||||
// Auto-attach a conversation listener when forking a thread.
|
||||
log_listener_attach_result(
|
||||
self.ensure_conversation_listener(
|
||||
|
||||
@@ -256,6 +256,82 @@ async fn thread_resume_with_empty_path_uses_running_thread_id() -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn thread_resume_running_thread_uses_cached_instruction_sources() -> Result<()> {
|
||||
let server = create_mock_responses_server_repeating_assistant("Done").await;
|
||||
let codex_home = TempDir::new()?;
|
||||
create_config_toml(codex_home.path(), &server.uri())?;
|
||||
let workspace = TempDir::new()?;
|
||||
let project_agents = workspace.path().join("AGENTS.md");
|
||||
std::fs::write(&project_agents, "project instructions")?;
|
||||
|
||||
let mut mcp = TestAppServer::new(codex_home.path()).await?;
|
||||
timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??;
|
||||
|
||||
let start_id = mcp
|
||||
.send_thread_start_request(ThreadStartParams {
|
||||
cwd: Some(workspace.path().display().to_string()),
|
||||
..Default::default()
|
||||
})
|
||||
.await?;
|
||||
let start_resp: JSONRPCResponse = timeout(
|
||||
DEFAULT_READ_TIMEOUT,
|
||||
mcp.read_stream_until_response_message(RequestId::Integer(start_id)),
|
||||
)
|
||||
.await??;
|
||||
let ThreadStartResponse {
|
||||
thread,
|
||||
instruction_sources,
|
||||
..
|
||||
} = to_response::<ThreadStartResponse>(start_resp)?;
|
||||
let project_agents = AbsolutePathBuf::try_from(std::fs::canonicalize(project_agents)?)?;
|
||||
assert_eq!(instruction_sources, vec![project_agents.clone()]);
|
||||
|
||||
let turn_id = mcp
|
||||
.send_turn_start_request(TurnStartParams {
|
||||
thread_id: thread.id.clone(),
|
||||
client_user_message_id: None,
|
||||
input: vec![UserInput::Text {
|
||||
text: "materialize rollout".to_string(),
|
||||
text_elements: Vec::new(),
|
||||
}],
|
||||
..Default::default()
|
||||
})
|
||||
.await?;
|
||||
timeout(
|
||||
DEFAULT_READ_TIMEOUT,
|
||||
mcp.read_stream_until_response_message(RequestId::Integer(turn_id)),
|
||||
)
|
||||
.await??;
|
||||
timeout(
|
||||
DEFAULT_READ_TIMEOUT,
|
||||
mcp.read_stream_until_notification_message("turn/completed"),
|
||||
)
|
||||
.await??;
|
||||
|
||||
std::fs::remove_file(project_agents.as_path())?;
|
||||
|
||||
let resume_id = mcp
|
||||
.send_thread_resume_request(ThreadResumeParams {
|
||||
thread_id: thread.id,
|
||||
..Default::default()
|
||||
})
|
||||
.await?;
|
||||
let resume_resp: JSONRPCResponse = timeout(
|
||||
DEFAULT_READ_TIMEOUT,
|
||||
mcp.read_stream_until_response_message(RequestId::Integer(resume_id)),
|
||||
)
|
||||
.await??;
|
||||
let ThreadResumeResponse {
|
||||
instruction_sources,
|
||||
..
|
||||
} = to_response::<ThreadResumeResponse>(resume_resp)?;
|
||||
|
||||
assert_eq!(instruction_sources, vec![project_agents]);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn turn_start_updates_runtime_workspace_roots_for_loaded_thread() -> Result<()> {
|
||||
let server = create_mock_responses_server_repeating_assistant("Done").await;
|
||||
|
||||
@@ -359,6 +359,83 @@ async fn thread_start_response_includes_loaded_instruction_sources() -> Result<(
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn thread_start_response_excludes_empty_project_instruction_source() -> Result<()> {
|
||||
let server = create_mock_responses_server_repeating_assistant("Done").await;
|
||||
let codex_home = TempDir::new()?;
|
||||
create_config_toml_without_approval_policy(codex_home.path(), &server.uri())?;
|
||||
let global_agents_path = codex_home.path().join("AGENTS.md");
|
||||
std::fs::write(&global_agents_path, "global instructions")?;
|
||||
let workspace = TempDir::new()?;
|
||||
let project_agents_path = workspace.path().join("AGENTS.md");
|
||||
std::fs::write(project_agents_path, "")?;
|
||||
|
||||
let mut mcp = TestAppServer::new(codex_home.path()).await?;
|
||||
timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??;
|
||||
|
||||
let request_id = mcp
|
||||
.send_thread_start_request(ThreadStartParams {
|
||||
cwd: Some(workspace.path().display().to_string()),
|
||||
..Default::default()
|
||||
})
|
||||
.await?;
|
||||
let response: JSONRPCResponse = timeout(
|
||||
DEFAULT_READ_TIMEOUT,
|
||||
mcp.read_stream_until_response_message(RequestId::Integer(request_id)),
|
||||
)
|
||||
.await??;
|
||||
let ThreadStartResponse {
|
||||
instruction_sources,
|
||||
..
|
||||
} = to_response::<ThreadStartResponse>(response)?;
|
||||
|
||||
let instruction_sources = instruction_sources
|
||||
.into_iter()
|
||||
.map(normalize_path_for_comparison)
|
||||
.collect::<Vec<_>>();
|
||||
let expected_instruction_sources = vec![normalize_path_for_comparison(std::fs::canonicalize(
|
||||
global_agents_path,
|
||||
)?)];
|
||||
|
||||
assert_eq!(instruction_sources, expected_instruction_sources);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn thread_start_without_selected_environment_excludes_instruction_sources() -> Result<()> {
|
||||
let server = create_mock_responses_server_repeating_assistant("Done").await;
|
||||
let codex_home = TempDir::new()?;
|
||||
create_config_toml_without_approval_policy(codex_home.path(), &server.uri())?;
|
||||
std::fs::write(codex_home.path().join("AGENTS.md"), "global instructions")?;
|
||||
let workspace = TempDir::new()?;
|
||||
std::fs::write(workspace.path().join("AGENTS.md"), "project instructions")?;
|
||||
|
||||
let mut mcp = TestAppServer::new(codex_home.path()).await?;
|
||||
timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??;
|
||||
|
||||
let request_id = mcp
|
||||
.send_thread_start_request(ThreadStartParams {
|
||||
cwd: Some(workspace.path().display().to_string()),
|
||||
environments: Some(Vec::new()),
|
||||
..Default::default()
|
||||
})
|
||||
.await?;
|
||||
let response: JSONRPCResponse = timeout(
|
||||
DEFAULT_READ_TIMEOUT,
|
||||
mcp.read_stream_until_response_message(RequestId::Integer(request_id)),
|
||||
)
|
||||
.await??;
|
||||
let ThreadStartResponse {
|
||||
instruction_sources,
|
||||
..
|
||||
} = to_response::<ThreadStartResponse>(response)?;
|
||||
|
||||
assert!(instruction_sources.is_empty());
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(windows)]
|
||||
fn normalize_path_for_comparison(path: impl AsRef<Path>) -> PathBuf {
|
||||
let path = path.as_ref();
|
||||
|
||||
Reference in New Issue
Block a user