fix(codex): share the user's global AGENTS.md into the managed Codex runtime home (#7927)

* fix(codex): share global AGENTS.md into managed runtime homes

Orca launches Codex with a managed CODEX_HOME, so host and WSL sessions otherwise lose the user's global instructions. Share AGENTS.md through the existing ownership-aware link-or-copy path, with a narrow WSL launch sync that avoids copying unrelated resource directories.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(codex): copy global AGENTS.md into WSL runtime homes

Symlinking across the \wsl.localhost 9P boundary stores a Windows UNC
target the distro cannot resolve, so the global AGENTS.md silently would
not load inside WSL when the host symlink succeeded. Copy it like the
config mirror already does across the same boundary.

Co-authored-by: Orca <help@stably.ai>

* Fix WSL global-instruction sync to dereference symlinks and skip redunda

- Copy AGENTS.md contents (not the symlink) into WSL runtime homes, since
  a host-side symlink is unusable inside the distro
- Skip rewriting the fallback copy when source contents are unchanged,
  avoiding needless writes across the UNC boundary on every Codex launch
- Handle non-regular-file sources and malformed/undeletable marker
  directories without blocking sync or stranding stale instructions

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Co-authored-by: Jinjing <6427696+AmethystLiang@users.noreply.github.com>
Co-authored-by: Orca <help@stably.ai>
This commit is contained in:
Robot.
2026-07-12 01:30:22 -07:00
committed by GitHub
co-authored by Claude Fable 5 Jinjing Orca
parent c2aa9c2ead
commit 38692c3fd5
4 changed files with 355 additions and 20 deletions
@@ -495,6 +495,9 @@ describe('CodexRuntimeHomeService', () => {
getDefaultWslDistro: () => 'Ubuntu',
getWslHome: () => wslHome
}))
const wslSystemHomePath = join(wslHome, '.codex')
mkdirSync(wslSystemHomePath, { recursive: true })
writeFileSync(join(wslSystemHomePath, 'AGENTS.md'), '# WSL instructions\n', 'utf-8')
const store = createStore(
createSettings({
activeCodexManagedAccountId: null,
@@ -520,9 +523,12 @@ describe('CodexRuntimeHomeService', () => {
expect(startWslCodexSessionBridgeInBackground).toHaveBeenCalledTimes(1)
expect(startWslCodexSessionBridgeInBackground).toHaveBeenCalledWith({
distro: 'Ubuntu',
systemCodexHomePath: join(wslHome, '.codex'),
systemCodexHomePath: wslSystemHomePath,
managedCodexHomePath: wslRuntimeHomePath
})
const runtimeAgentsPath = join(wslRuntimeHomePath, 'AGENTS.md')
expect(readFileSync(runtimeAgentsPath, 'utf-8')).toBe('# WSL instructions\n')
expect(lstatSync(runtimeAgentsPath).isSymbolicLink()).toBe(false)
} finally {
vi.doUnmock('../codex/wsl-codex-session-bridge')
vi.doUnmock('../wsl')
@@ -38,6 +38,7 @@ import { writeFileAtomically } from './fs-utils'
import {
getOrcaManagedCodexHomePath,
getSystemCodexHomePath,
syncCodexGlobalInstructionsIntoManagedHome,
syncSystemCodexResourcesIntoManagedHome
} from '../codex/codex-home-paths'
import { startSystemCodexSessionBridgeInBackground } from '../codex/codex-session-bridge'
@@ -141,7 +142,7 @@ export class CodexRuntimeHomeService {
if (target?.runtime === 'wsl') {
const wslTarget = this.resolveWslDefaultTarget(target)
const syncedRuntimeHomePath = this.syncWslRuntimeForCurrentSelection(wslTarget)
this.syncWslConfigSettingsForLaunch(wslTarget, syncedRuntimeHomePath)
this.syncWslConfigAndGlobalInstructionsForLaunch(wslTarget, syncedRuntimeHomePath)
const runtimeHomePath = syncedRuntimeHomePath ?? this.getWslSystemCodexHomePath(wslTarget)
this.startWslSessionBridgeForLaunch(wslTarget, runtimeHomePath)
return runtimeHomePath
@@ -223,10 +224,7 @@ export class CodexRuntimeHomeService {
return home ? this.joinWslPath(home, '.codex') : null
}
// Why: WSL needs the same promote-then-mirror transaction as host. It keeps
// in-Orca changes while reconciling a newer external ~/.codex edit before
// advancing the per-distro baseline. Only runs on a materialized runtime.
private syncWslConfigSettingsForLaunch(
private syncWslConfigAndGlobalInstructionsForLaunch(
target: CodexAccountSelectionTarget,
runtimeHomePath: string | null
): void {
@@ -242,6 +240,12 @@ export class CodexRuntimeHomeService {
if (!systemHomePath || systemHomePath === runtimeHomePath) {
return
}
// Why: WSL uses a distro-local CODEX_HOME, so host resource mirroring
// cannot provide the distro user's global instructions.
syncCodexGlobalInstructionsIntoManagedHome({
systemHomePath,
managedHomePath: runtimeHomePath
})
syncSystemConfigIntoManagedCodexHome({ runtimeHomePath, systemHomePath })
}
+209 -2
View File
@@ -21,13 +21,28 @@ const { getPathMock, homedirMock } = vi.hoisted(() => ({
}))
const { fsMockState } = vi.hoisted(() => ({
fsMockState: { failSymlink: false }
fsMockState: {
copyCount: 0,
failSymlink: false,
trackedReadCount: 0,
trackedReadPath: null as string | null
}
}))
vi.mock('node:fs', async () => {
const actual = await vi.importActual<typeof NodeFs>('node:fs')
return {
...actual,
cpSync: (...args: Parameters<typeof actual.cpSync>) => {
fsMockState.copyCount += 1
return actual.cpSync(...args)
},
readFileSync: (...args: Parameters<typeof actual.readFileSync>) => {
if (args[0] === fsMockState.trackedReadPath) {
fsMockState.trackedReadCount += 1
}
return actual.readFileSync(...args)
},
symlinkSync: (...args: Parameters<typeof actual.symlinkSync>) => {
if (fsMockState.failSymlink) {
throw new Error('symlink disabled for test')
@@ -51,7 +66,10 @@ vi.mock('node:os', async () => {
}
})
import { syncSystemCodexResourcesIntoManagedHome } from './codex-home-paths'
import {
syncCodexGlobalInstructionsIntoManagedHome,
syncSystemCodexResourcesIntoManagedHome
} from './codex-home-paths'
let fakeHomeDir: string
let userDataDir: string
@@ -88,7 +106,10 @@ function mockElectronAppPaths(): void {
beforeEach(() => {
mockElectronAppPaths()
fsMockState.copyCount = 0
fsMockState.failSymlink = false
fsMockState.trackedReadCount = 0
fsMockState.trackedReadPath = null
fakeHomeDir = mkdtempSync(join(tmpdir(), 'orca-codex-resource-home-'))
userDataDir = mkdtempSync(join(tmpdir(), 'orca-codex-resource-user-data-'))
previousUserDataPath = process.env.ORCA_USER_DATA_PATH
@@ -229,4 +250,190 @@ describe('syncSystemCodexResourcesIntoManagedHome', () => {
expect(lstatSync(runtimeHooksPath).isSymbolicLink()).toBe(true)
expectSymbolicLinkTargetIfLinked(runtimeHooksPath, systemHooksPath)
})
it('mirrors the global AGENTS.md into the managed runtime home so user instructions survive', () => {
const systemAgentsPath = join(getSystemCodexHomePath(), 'AGENTS.md')
const runtimeAgentsPath = join(getRuntimeCodexHomePath(), 'AGENTS.md')
writeFileSync(systemAgentsPath, '# Global instructions\n')
syncSystemCodexResourcesIntoManagedHome()
expect(readFileSync(runtimeAgentsPath, 'utf-8')).toBe('# Global instructions\n')
expectSymbolicLinkTargetIfLinked(runtimeAgentsPath, systemAgentsPath)
})
it('skips unchanged global-instruction fallback copies when symlinks fail', () => {
fsMockState.failSymlink = true
const systemAgentsPath = join(getSystemCodexHomePath(), 'AGENTS.md')
writeFileSync(systemAgentsPath, 'first\n')
syncSystemCodexResourcesIntoManagedHome()
expect(fsMockState.copyCount).toBe(1)
syncSystemCodexResourcesIntoManagedHome()
expect(fsMockState.copyCount).toBe(1)
writeFileSync(systemAgentsPath, 'second\n')
syncSystemCodexResourcesIntoManagedHome()
expect(fsMockState.copyCount).toBe(2)
expect(readFileSync(join(getRuntimeCodexHomePath(), 'AGENTS.md'), 'utf-8')).toBe('second\n')
})
it('mirrors only global instructions when explicit Codex homes are provided', () => {
const systemHomePath = getSystemCodexHomePath()
const managedHomePath = join(userDataDir, 'wsl-runtime-home')
mkdirSync(join(systemHomePath, 'skills'), { recursive: true })
writeFileSync(join(systemHomePath, 'skills', 'system.md'), 'skill\n')
writeFileSync(join(systemHomePath, 'AGENTS.md'), '# WSL instructions\n')
syncCodexGlobalInstructionsIntoManagedHome({ systemHomePath, managedHomePath })
const runtimeAgentsPath = join(managedHomePath, 'AGENTS.md')
expect(readFileSync(runtimeAgentsPath, 'utf-8')).toBe('# WSL instructions\n')
// Why: WSL homes are \\wsl.localhost UNC paths, so a host-side symlink would
// store a target the distro cannot resolve; global instructions must be a
// real copy even when symlinks are available.
expect(lstatSync(runtimeAgentsPath).isSymbolicLink()).toBe(false)
expect(existsSync(join(managedHomePath, 'skills'))).toBe(false)
})
// Why: creating file symlinks on Windows requires developer mode; the
// runtime-home integration test still enforces real-copy behavior there.
it.skipIf(process.platform === 'win32')(
'materializes symlinked global instructions as a real file for WSL',
() => {
const systemHomePath = getSystemCodexHomePath()
const managedHomePath = join(userDataDir, 'wsl-runtime-home')
const instructionSourcePath = join(userDataDir, 'global-instructions.md')
writeFileSync(instructionSourcePath, 'linked instructions\n')
symlinkSync(instructionSourcePath, join(systemHomePath, 'AGENTS.md'))
syncCodexGlobalInstructionsIntoManagedHome({ systemHomePath, managedHomePath })
const runtimeAgentsPath = join(managedHomePath, 'AGENTS.md')
expect(lstatSync(runtimeAgentsPath).isSymbolicLink()).toBe(false)
expect(readFileSync(runtimeAgentsPath, 'utf-8')).toBe('linked instructions\n')
}
)
it.skipIf(process.platform === 'win32')(
'replaces an existing system-instruction link despite a malformed marker directory',
() => {
const systemHomePath = getSystemCodexHomePath()
const managedHomePath = join(userDataDir, 'wsl-runtime-home')
const systemAgentsPath = join(systemHomePath, 'AGENTS.md')
const runtimeAgentsPath = join(managedHomePath, 'AGENTS.md')
mkdirSync(managedHomePath, { recursive: true })
writeFileSync(systemAgentsPath, 'system\n')
symlinkSync(systemAgentsPath, runtimeAgentsPath)
mkdirSync(join(managedHomePath, '.orca-resource-copies', 'AGENTS.md.json'), {
recursive: true
})
syncCodexGlobalInstructionsIntoManagedHome({ systemHomePath, managedHomePath })
expect(lstatSync(runtimeAgentsPath).isSymbolicLink()).toBe(false)
expect(readFileSync(runtimeAgentsPath, 'utf-8')).toBe('system\n')
}
)
it('removes an unowned copy when recording copy ownership fails', () => {
const systemHomePath = getSystemCodexHomePath()
const managedHomePath = join(userDataDir, 'wsl-runtime-home')
writeFileSync(join(systemHomePath, 'AGENTS.md'), 'system\n')
mkdirSync(managedHomePath, { recursive: true })
writeFileSync(join(managedHomePath, '.orca-resource-copies'), 'blocks marker directory\n')
syncCodexGlobalInstructionsIntoManagedHome({ systemHomePath, managedHomePath })
expect(existsSync(join(managedHomePath, 'AGENTS.md'))).toBe(false)
})
it('skips unchanged copies, then refreshes and removes owned global instructions', () => {
const systemHomePath = getSystemCodexHomePath()
const managedHomePath = join(userDataDir, 'wsl-runtime-home')
const systemAgentsPath = join(systemHomePath, 'AGENTS.md')
const runtimeAgentsPath = join(managedHomePath, 'AGENTS.md')
writeFileSync(systemAgentsPath, 'first\n')
syncCodexGlobalInstructionsIntoManagedHome({ systemHomePath, managedHomePath })
expect(fsMockState.copyCount).toBe(1)
syncCodexGlobalInstructionsIntoManagedHome({ systemHomePath, managedHomePath })
expect(fsMockState.copyCount).toBe(1)
writeFileSync(systemAgentsPath, 'second\n')
syncCodexGlobalInstructionsIntoManagedHome({ systemHomePath, managedHomePath })
expect(fsMockState.copyCount).toBe(2)
expect(lstatSync(runtimeAgentsPath).isSymbolicLink()).toBe(false)
expect(readFileSync(runtimeAgentsPath, 'utf-8')).toBe('second\n')
rmSync(systemAgentsPath)
syncCodexGlobalInstructionsIntoManagedHome({ systemHomePath, managedHomePath })
expect(existsSync(runtimeAgentsPath)).toBe(false)
})
it('replaces an owned non-file instruction entry without reading it', () => {
const systemHomePath = getSystemCodexHomePath()
const managedHomePath = join(userDataDir, 'wsl-runtime-home')
const runtimeAgentsPath = join(managedHomePath, 'AGENTS.md')
writeFileSync(join(systemHomePath, 'AGENTS.md'), 'system\n')
syncCodexGlobalInstructionsIntoManagedHome({ systemHomePath, managedHomePath })
rmSync(runtimeAgentsPath)
mkdirSync(runtimeAgentsPath)
fsMockState.trackedReadPath = runtimeAgentsPath
syncCodexGlobalInstructionsIntoManagedHome({ systemHomePath, managedHomePath })
expect(fsMockState.trackedReadCount).toBe(0)
expect(lstatSync(runtimeAgentsPath).isFile()).toBe(true)
expect(readFileSync(runtimeAgentsPath, 'utf-8')).toBe('system\n')
})
it('removes owned instructions instead of mirroring a non-file source', () => {
const systemHomePath = getSystemCodexHomePath()
const managedHomePath = join(userDataDir, 'wsl-runtime-home')
const systemAgentsPath = join(systemHomePath, 'AGENTS.md')
const runtimeAgentsPath = join(managedHomePath, 'AGENTS.md')
writeFileSync(systemAgentsPath, 'system\n')
syncCodexGlobalInstructionsIntoManagedHome({ systemHomePath, managedHomePath })
rmSync(systemAgentsPath)
mkdirSync(systemAgentsPath)
const warn = vi.spyOn(console, 'warn').mockImplementation(() => {})
try {
syncCodexGlobalInstructionsIntoManagedHome({ systemHomePath, managedHomePath })
} finally {
warn.mockRestore()
}
expect(existsSync(runtimeAgentsPath)).toBe(false)
})
it('preserves runtime-owned global instructions in an explicit managed home', () => {
const systemHomePath = getSystemCodexHomePath()
const managedHomePath = join(userDataDir, 'wsl-runtime-home')
mkdirSync(managedHomePath, { recursive: true })
writeFileSync(join(systemHomePath, 'AGENTS.md'), 'system\n')
writeFileSync(join(managedHomePath, 'AGENTS.md'), 'runtime\n')
syncCodexGlobalInstructionsIntoManagedHome({ systemHomePath, managedHomePath })
expect(readFileSync(join(managedHomePath, 'AGENTS.md'), 'utf-8')).toBe('runtime\n')
})
it.skipIf(process.platform === 'win32')(
'preserves an unowned dangling runtime instruction symlink',
() => {
const systemHomePath = getSystemCodexHomePath()
const managedHomePath = join(userDataDir, 'wsl-runtime-home')
const runtimeAgentsPath = join(managedHomePath, 'AGENTS.md')
const missingTargetPath = join(userDataDir, 'missing-runtime-instructions.md')
mkdirSync(managedHomePath, { recursive: true })
writeFileSync(join(systemHomePath, 'AGENTS.md'), 'system\n')
symlinkSync(missingTargetPath, runtimeAgentsPath)
syncCodexGlobalInstructionsIntoManagedHome({ systemHomePath, managedHomePath })
expect(lstatSync(runtimeAgentsPath).isSymbolicLink()).toBe(true)
expect(readlinkSync(runtimeAgentsPath)).toBe(missingTargetPath)
}
)
})
+130 -12
View File
@@ -7,6 +7,7 @@ import {
readlinkSync,
rmdirSync,
rmSync,
statSync,
symlinkSync,
unlinkSync,
writeFileSync
@@ -14,6 +15,8 @@ import {
import { homedir } from 'node:os'
import { dirname, join } from 'node:path'
const CODEX_GLOBAL_INSTRUCTIONS_ENTRY = 'AGENTS.md'
const CODEX_SYSTEM_RESOURCE_ENTRIES = [
'skills',
'hooks',
@@ -21,7 +24,8 @@ const CODEX_SYSTEM_RESOURCE_ENTRIES = [
'plugin-state',
'profile-v2',
'themes',
'prompts'
'prompts',
CODEX_GLOBAL_INSTRUCTIONS_ENTRY
] as const
export function getSystemCodexHomePath(): string {
@@ -57,10 +61,28 @@ export function syncSystemCodexResourcesIntoManagedHome(): void {
}
}
export function syncCodexGlobalInstructionsIntoManagedHome({
systemHomePath,
managedHomePath
}: {
systemHomePath: string
managedHomePath: string
}): void {
mkdirSync(managedHomePath, { recursive: true })
// Why: this only runs for WSL runtime homes, whose system + managed homes are
// both \\wsl.localhost UNC paths. A host-side symlink there stores a Windows
// UNC target the distro cannot resolve, so copy the file like the config
// mirror does across the same boundary.
linkSystemCodexResource(systemHomePath, managedHomePath, CODEX_GLOBAL_INSTRUCTIONS_ENTRY, {
preferCopy: true
})
}
function linkSystemCodexResource(
systemHomePath: string,
managedHomePath: string,
entryName: string
entryName: string,
{ preferCopy = false }: { preferCopy?: boolean } = {}
): void {
const sourcePath = join(systemHomePath, entryName)
const targetPath = join(managedHomePath, entryName)
@@ -68,10 +90,20 @@ function linkSystemCodexResource(
removeCopiedResourceIfOwned(targetPath, managedHomePath, entryName, sourcePath)
return
}
if (
entryName === CODEX_GLOBAL_INSTRUCTIONS_ENTRY &&
!systemResourceIsRegularFile(sourcePath)
) {
removeCopiedResourceIfOwned(targetPath, managedHomePath, entryName, sourcePath)
console.warn('[codex-home] Ignoring non-file system Codex resource:', entryName)
return
}
if (targetAlreadyPointsToSource(targetPath, sourcePath)) {
clearCopiedResourceMarker(managedHomePath, entryName)
return
if (!preferCopy || !removeSymlinkEntry(targetPath)) {
return
}
}
const shouldRefreshFallbackCopy = targetIsOwnedFallbackCopy(
targetPath,
@@ -79,13 +111,26 @@ function linkSystemCodexResource(
entryName,
sourcePath
)
if (existsSync(targetPath) && !shouldRefreshFallbackCopy) {
if (pathEntryExists(targetPath) && !shouldRefreshFallbackCopy) {
return
}
if (shouldRefreshFallbackCopy) {
// Why: WSL launch preparation runs before every Codex start. Avoid
// rewriting an unchanged file across the UNC boundary on every launch.
if (
entryName === CODEX_GLOBAL_INSTRUCTIONS_ENTRY &&
copiedFileContentsMatch(sourcePath, targetPath)
) {
return
}
rmSync(targetPath, { recursive: true, force: true })
}
if (preferCopy) {
copySystemCodexResourceAsOwnedFallback(sourcePath, targetPath, managedHomePath, entryName)
return
}
try {
const sourceStat = lstatSync(sourcePath)
symlinkSync(
@@ -95,16 +140,84 @@ function linkSystemCodexResource(
)
clearCopiedResourceMarker(managedHomePath, entryName)
} catch (error) {
// Why: Windows can reject file symlinks outside developer mode. Copy is
// a fallback for launch-time resources; mark ownership so later syncs can
// refresh the copy without touching user-created runtime resources.
copySystemCodexResourceAsOwnedFallback(
sourcePath,
targetPath,
managedHomePath,
entryName,
error
)
}
}
function copySystemCodexResourceAsOwnedFallback(
sourcePath: string,
targetPath: string,
managedHomePath: string,
entryName: string,
symlinkError?: unknown
): void {
try {
rmSync(targetPath, { recursive: true, force: true })
cpSync(sourcePath, targetPath, {
recursive: true,
force: false,
errorOnExist: true,
// Why: dotfile managers commonly symlink AGENTS.md. WSL needs the file
// contents because a copied host-side link is not usable in the distro.
dereference: entryName === CODEX_GLOBAL_INSTRUCTIONS_ENTRY
})
markCopiedResource(managedHomePath, entryName, sourcePath)
} catch (copyError) {
// Why: an unmarked copy cannot be refreshed or safely removed later.
// Roll it back instead of stranding stale instructions in the runtime home.
try {
rmSync(targetPath, { recursive: true, force: true })
// Why: Windows can reject file symlinks outside developer mode. Copy is
// a fallback for launch-time resources; mark ownership so later syncs can
// refresh the copy without touching user-created runtime resources.
cpSync(sourcePath, targetPath, { recursive: true, force: false, errorOnExist: true })
markCopiedResource(managedHomePath, entryName, sourcePath)
} catch {
console.warn('[codex-home] Failed to link system Codex resource:', entryName, error)
} catch (cleanupError) {
console.warn(
'[codex-home] Failed to remove incomplete resource copy:',
entryName,
cleanupError
)
}
console.warn(
'[codex-home] Failed to mirror system Codex resource:',
entryName,
symlinkError ?? copyError
)
}
}
function systemResourceIsRegularFile(sourcePath: string): boolean {
try {
return statSync(sourcePath).isFile()
} catch {
return false
}
}
function pathEntryExists(entryPath: string): boolean {
try {
lstatSync(entryPath)
return true
} catch {
return false
}
}
function copiedFileContentsMatch(sourcePath: string, targetPath: string): boolean {
try {
// Why: reading a FIFO or device synchronously can block Codex launch.
// Follow source symlinks, but only compare two regular files.
if (!statSync(sourcePath).isFile() || !lstatSync(targetPath).isFile()) {
return false
}
return readFileSync(sourcePath).equals(readFileSync(targetPath))
} catch {
return false
}
}
@@ -159,7 +272,12 @@ function readCopiedResourceSourcePath(managedHomePath: string, entryName: string
}
function clearCopiedResourceMarker(managedHomePath: string, entryName: string): void {
rmSync(getResourceCopyMarkerPath(managedHomePath, entryName), { force: true })
// Why: a malformed marker directory must not block Codex launch or prevent
// an owned resource from being repaired.
rmSync(getResourceCopyMarkerPath(managedHomePath, entryName), {
recursive: true,
force: true
})
}
function targetIsOwnedFallbackCopy(