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
@@ -2573,7 +2573,11 @@ mod tests {
service_tier: None,
cwd,
runtime_workspace_roots: Vec::new(),
instruction_sources: vec![absolute_path("/tmp/AGENTS.md")],
instruction_sources: vec![
codex_utils_path_uri::LegacyAppPathString::from_abs_path(&absolute_path(
"/tmp/AGENTS.md",
)),
],
approval_policy: v2::AskForApproval::OnFailure,
approvals_reviewer: v2::ApprovalsReviewer::User,
sandbox: v2::SandboxPolicy::DangerFullAccess,
@@ -36,6 +36,7 @@ use codex_utils_absolute_path::AbsolutePathBuf;
use codex_utils_absolute_path::test_support::PathBufExt;
use codex_utils_absolute_path::test_support::test_path_buf;
use codex_utils_path_uri::LegacyAppPathString;
use codex_utils_path_uri::PathUri;
use pretty_assertions::assert_eq;
use serde_json::Value as JsonValue;
use serde_json::json;
@@ -3685,17 +3686,49 @@ fn thread_lifecycle_responses_default_missing_optional_fields() {
serde_json::from_value(response.clone()).expect("thread/start response");
let resume: ThreadResumeResponse =
serde_json::from_value(response.clone()).expect("thread/resume response");
let fork: ThreadForkResponse = serde_json::from_value(response).expect("thread/fork response");
let fork: ThreadForkResponse =
serde_json::from_value(response.clone()).expect("thread/fork response");
assert_eq!(start.instruction_sources, Vec::<AbsolutePathBuf>::new());
assert_eq!(start.instruction_sources, Vec::<LegacyAppPathString>::new());
assert_eq!(start.thread.parent_thread_id, None);
assert_eq!(start.thread.recency_at, None);
assert_eq!(resume.instruction_sources, Vec::<AbsolutePathBuf>::new());
assert_eq!(fork.instruction_sources, Vec::<AbsolutePathBuf>::new());
assert_eq!(
resume.instruction_sources,
Vec::<LegacyAppPathString>::new()
);
assert_eq!(fork.instruction_sources, Vec::<LegacyAppPathString>::new());
assert_eq!(start.active_permission_profile, None);
assert_eq!(resume.active_permission_profile, None);
assert_eq!(resume.initial_turns_page, None);
assert_eq!(fork.active_permission_profile, None);
let foreign_source: LegacyAppPathString =
serde_json::from_value(json!(r"C:\workspace\AGENTS.md")).expect("foreign source");
let mut response_with_foreign_source = response;
response_with_foreign_source["instructionSources"] = json!([foreign_source.as_str()]);
let start: ThreadStartResponse = serde_json::from_value(response_with_foreign_source.clone())
.expect("thread/start response with foreign source");
let resume: ThreadResumeResponse = serde_json::from_value(response_with_foreign_source.clone())
.expect("thread/resume response with foreign source");
let fork: ThreadForkResponse = serde_json::from_value(response_with_foreign_source)
.expect("thread/fork response with foreign source");
assert_eq!(start.instruction_sources, vec![foreign_source.clone()]);
assert_eq!(resume.instruction_sources, vec![foreign_source.clone()]);
assert_eq!(fork.instruction_sources, vec![foreign_source]);
let foreign_source_uri =
PathUri::parse("file:///C:/workspace/AGENTS.md").expect("foreign source URI");
assert_eq!(
start.instruction_source_path_uris(),
vec![foreign_source_uri.clone()]
);
assert_eq!(
resume.instruction_source_path_uris(),
vec![foreign_source_uri.clone()]
);
assert_eq!(
fork.instruction_source_path_uris(),
vec![foreign_source_uri]
);
}
#[test]
@@ -26,6 +26,8 @@ use codex_protocol::protocol::ThreadGoalStatus as CoreThreadGoalStatus;
use codex_protocol::protocol::TokenUsage as CoreTokenUsage;
use codex_protocol::protocol::TokenUsageInfo as CoreTokenUsageInfo;
use codex_utils_absolute_path::AbsolutePathBuf;
use codex_utils_path_uri::LegacyAppPathString;
use codex_utils_path_uri::PathUri;
use schemars::JsonSchema;
use serde::Deserialize;
use serde::Serialize;
@@ -161,9 +163,9 @@ pub struct ThreadStartResponse {
#[experimental("thread/start.runtimeWorkspaceRoots")]
#[serde(default)]
pub runtime_workspace_roots: Vec<AbsolutePathBuf>,
/// Instruction source files currently loaded for this thread.
/// Environment-native paths to instruction source files currently loaded for this thread.
#[serde(default)]
pub instruction_sources: Vec<AbsolutePathBuf>,
pub instruction_sources: Vec<LegacyAppPathString>,
#[experimental(nested)]
pub approval_policy: AskForApproval,
/// Reviewer currently used for approval requests on this thread.
@@ -179,6 +181,13 @@ pub struct ThreadStartResponse {
pub reasoning_effort: Option<ReasoningEffort>,
}
impl ThreadStartResponse {
/// Parses valid absolute instruction source paths and omits malformed legacy values.
pub fn instruction_source_path_uris(&self) -> Vec<PathUri> {
instruction_source_path_uris(&self.instruction_sources)
}
}
#[derive(
Serialize, Deserialize, Debug, Default, Clone, PartialEq, JsonSchema, TS, ExperimentalApi,
)]
@@ -375,9 +384,9 @@ pub struct ThreadResumeResponse {
#[experimental("thread/resume.runtimeWorkspaceRoots")]
#[serde(default)]
pub runtime_workspace_roots: Vec<AbsolutePathBuf>,
/// Instruction source files currently loaded for this thread.
/// Environment-native paths to instruction source files currently loaded for this thread.
#[serde(default)]
pub instruction_sources: Vec<AbsolutePathBuf>,
pub instruction_sources: Vec<LegacyAppPathString>,
#[experimental(nested)]
pub approval_policy: AskForApproval,
/// Reviewer currently used for approval requests on this thread.
@@ -397,6 +406,13 @@ pub struct ThreadResumeResponse {
pub initial_turns_page: Option<TurnsPage>,
}
impl ThreadResumeResponse {
/// Parses valid absolute instruction source paths and omits malformed legacy values.
pub fn instruction_source_path_uris(&self) -> Vec<PathUri> {
instruction_source_path_uris(&self.instruction_sources)
}
}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
#[serde(rename_all = "camelCase")]
#[ts(export_to = "v2/")]
@@ -523,9 +539,9 @@ pub struct ThreadForkResponse {
#[experimental("thread/fork.runtimeWorkspaceRoots")]
#[serde(default)]
pub runtime_workspace_roots: Vec<AbsolutePathBuf>,
/// Instruction source files currently loaded for this thread.
/// Environment-native paths to instruction source files currently loaded for this thread.
#[serde(default)]
pub instruction_sources: Vec<AbsolutePathBuf>,
pub instruction_sources: Vec<LegacyAppPathString>,
#[experimental(nested)]
pub approval_policy: AskForApproval,
/// Reviewer currently used for approval requests on this thread.
@@ -541,6 +557,30 @@ pub struct ThreadForkResponse {
pub reasoning_effort: Option<ReasoningEffort>,
}
impl ThreadForkResponse {
/// Parses valid absolute instruction source paths and omits malformed legacy values.
pub fn instruction_source_path_uris(&self) -> Vec<PathUri> {
instruction_source_path_uris(&self.instruction_sources)
}
}
fn instruction_source_path_uris(sources: &[LegacyAppPathString]) -> Vec<PathUri> {
// Instruction sources are advisory diagnostics. Warn and fail open so a malformed legacy
// path cannot fail thread start, resume, or fork.
sources
.iter()
.filter_map(|source| {
source.to_inferred_path_uri().or_else(|| {
tracing::warn!(
path = source.as_str(),
"ignoring invalid instruction source path from app-server"
);
None
})
})
.collect()
}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
#[serde(rename_all = "camelCase")]
#[ts(export_to = "v2/")]