[codex] Preserve logical paths during AGENTS.md discovery (#26465)

## Intent

Follow up on #26205 by avoiding unnecessary filesystem canonicalization
during `AGENTS.md` discovery. The configured working directory is
already absolute, and canonicalization incorrectly switches symlinked
workspaces from their logical parent hierarchy to the target's
hierarchy.

## User-facing behavior

For a symlinked working directory such as:

```text
test-root/
|-- logical-repo/
|   |-- AGENTS.md              ("logical parent doc")
|   `-- workspace ------------> physical-repo/workspace/
`-- physical-repo/
    |-- AGENTS.md              ("physical parent doc")
    `-- workspace/
        `-- AGENTS.md          ("workspace doc")
```

Before this change, Codex canonicalized `logical-repo/workspace` to
`physical-repo/workspace` before discovery. It therefore loaded
`physical-repo/AGENTS.md` and `physical-repo/workspace/AGENTS.md`,
ignoring the instructions from the repository through which the user
entered the workspace.

After this change, ancestor discovery walks the configured logical path,
so Codex loads `logical-repo/AGENTS.md`. Opening
`logical-repo/workspace/AGENTS.md` still follows the symlink through the
host filesystem, so the workspace document is also loaded.
`physical-repo/AGENTS.md` is not loaded.

## Implementation

Use the logical absolute working directory when discovering project
instructions and reporting instruction sources. Filesystem reads still
follow the working-directory symlink, so an `AGENTS.md` in the target
workspace continues to load while ancestor discovery uses the symlink's
parents.

## Validation

Added integration coverage proving that discovery loads the logical
parent's instructions and the target workspace's instructions, but not
the target parent's instructions.
This commit is contained in:
Adam Perry @ OpenAI
2026-06-04 15:08:52 -07:00
committed by GitHub
Unverified
parent 37c8aefa14
commit 59ca34206b
6 changed files with 130 additions and 43 deletions
@@ -284,7 +284,7 @@ async fn thread_resume_running_thread_uses_cached_instruction_sources() -> Resul
instruction_sources,
..
} = to_response::<ThreadStartResponse>(start_resp)?;
let project_agents = AbsolutePathBuf::try_from(std::fs::canonicalize(project_agents)?)?;
let project_agents = AbsolutePathBuf::try_from(project_agents)?;
assert_eq!(instruction_sources, vec![project_agents.clone()]);
let turn_id = mcp
@@ -348,7 +348,7 @@ async fn thread_start_response_includes_loaded_instruction_sources() -> Result<(
.collect::<Vec<_>>();
let expected_instruction_sources = vec![
std::fs::canonicalize(global_agents_path)?,
std::fs::canonicalize(project_agents_path)?,
project_agents_path,
]
.into_iter()
.map(normalize_path_for_comparison)