mirror of
https://github.com/stablyai/orca.git
synced 2026-10-18 08:03:22 +00:00
feat(codex): give each managed account a self-contained CODEX_HOME; retire shared mirror (PR-E)
With the real-home flag ON, a host managed account now launches directly against its own codex-accounts/<id>/home instead of the shared runtime mirror + auth.json hot-swap: - codex-home-paths: syncSystemCodexResourcesIntoManagedHome links system resources into any managed home (ownership-marker discipline; never symlinks into / mutates ~/.codex). - runtime-home-service: prepareForCodexLaunch / prepareForRateLimitFetch / syncForCurrentSelection route the per-account home directly and skip the shared-home hot-swap + token read-back; each home keeps its own auth in place (fixes GAP-5 concurrent auth race). Session discovery scans every per-account home. - hook-service / hook-trust-promotion: install/getStatus/refresh accept a runtimeHomePath so hooks + RPC-granted trust land in the per-account home. - service: config mirror into a self-contained home uses the trust- preserving merge so granted hook/project trust survives account switches. - codex-session-root-dedup: rank codex-accounts/<id>/home as canonical managed alongside the shared runtime home. Flag-OFF and the system-default real-home (null) lane are unchanged; the nested-Orca CODEX_HOME===ORCA_CODEX_HOME daemon strip (#5370) is preserved. Sandboxed tests only; ~/.codex is never mutated.
This commit is contained in:
@@ -167,6 +167,41 @@ describe('dedupeCodexRolloutFileAliases', () => {
|
||||
|
||||
expect(dedupeCodexRolloutFileAliases([host, wsl], accessors)).toEqual([host, wsl])
|
||||
})
|
||||
|
||||
it('prefers a per-account self-contained home over other non-default homes', () => {
|
||||
const perAccount = {
|
||||
agent: 'codex',
|
||||
path: '/Users/ada/Library/Application Support/orca/codex-accounts/019f0000-aaaa-bbbb/home/sessions/2026/07/01/rollout-2026-07-01T10-00-00-019f0000-1111-7222-8333-444444444444.jsonl',
|
||||
codexHome:
|
||||
'/Users/ada/Library/Application Support/orca/codex-accounts/019f0000-aaaa-bbbb/home',
|
||||
hardlinkIdentity: '3:71'
|
||||
}
|
||||
const custom = {
|
||||
agent: 'codex',
|
||||
path: '/Users/ada/custom-codex/sessions/2026/07/01/rollout-2026-07-01T10-00-00-019f0000-1111-7222-8333-444444444444.jsonl',
|
||||
codexHome: '/Users/ada/custom-codex',
|
||||
hardlinkIdentity: '3:71'
|
||||
}
|
||||
expect(dedupeCodexRolloutFileAliases([custom, perAccount], accessors)).toEqual([perAccount])
|
||||
expect(dedupeCodexRolloutFileAliases([perAccount, custom], accessors)).toEqual([perAccount])
|
||||
})
|
||||
|
||||
it('keeps the real home over a per-account home when they alias', () => {
|
||||
const perAccount = {
|
||||
agent: 'codex',
|
||||
path: '/Users/ada/Library/Application Support/orca/codex-accounts/019f0000-aaaa-bbbb/home/sessions/2026/07/01/rollout-2026-07-01T10-00-00-019f0000-1111-7222-8333-444444444444.jsonl',
|
||||
codexHome:
|
||||
'/Users/ada/Library/Application Support/orca/codex-accounts/019f0000-aaaa-bbbb/home',
|
||||
hardlinkIdentity: '1:42'
|
||||
}
|
||||
const real = {
|
||||
agent: 'codex',
|
||||
path: REAL_HOME_ROLLOUT,
|
||||
codexHome: null,
|
||||
hardlinkIdentity: '1:42'
|
||||
}
|
||||
expect(dedupeCodexRolloutFileAliases([perAccount, real], accessors)).toEqual([real])
|
||||
})
|
||||
})
|
||||
|
||||
describe('dedupeCodexSessionsBySessionId', () => {
|
||||
|
||||
@@ -53,16 +53,20 @@ export function codexRolloutHardlinkIdentity(file: {
|
||||
*
|
||||
* Host real home (null) is canonical: after the real-home flip the managed
|
||||
* home's auth.json is no longer refreshed, so resume must not stamp it. The
|
||||
* Orca managed runtime home still beats other homes (WSL/remote real homes,
|
||||
* custom CODEX_HOMEs) because those lanes have not flipped — their launches
|
||||
* keep managed auth, so resume keeps the managed stamp as today.
|
||||
* Orca managed runtime home and each per-account self-contained home
|
||||
* (codex-accounts/<id>/home) beat other homes (WSL/remote real homes, custom
|
||||
* CODEX_HOMEs) because those are the roots codex actually refreshes for their
|
||||
* lane — the shared runtime home for the managed mirror, and the per-account
|
||||
* home once a managed account launches directly against it.
|
||||
*/
|
||||
function codexSessionRootRank(codexHome: string | null): number {
|
||||
if (codexHome === null) {
|
||||
return 0
|
||||
}
|
||||
const segments = codexHome.split(/[\\/]/).filter(Boolean)
|
||||
return segments.at(-2) === 'codex-runtime-home' && segments.at(-1) === 'home' ? 1 : 2
|
||||
const isSharedRuntimeHome = segments.at(-2) === 'codex-runtime-home' && segments.at(-1) === 'home'
|
||||
const isPerAccountManagedHome = segments.at(-3) === 'codex-accounts' && segments.at(-1) === 'home'
|
||||
return isSharedRuntimeHome || isPerAccountManagedHome ? 1 : 2
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -1069,7 +1069,9 @@ describe('CodexRuntimeHomeService', () => {
|
||||
}
|
||||
})
|
||||
|
||||
it('keeps the managed home for a host MANAGED account even when the flag is ON', async () => {
|
||||
it('routes a host MANAGED account to its own self-contained home when the flag is ON', async () => {
|
||||
writeFileSync(getSystemCodexAuthPath(), '{"account":"system"}\n', 'utf-8')
|
||||
const runtimeAuthPath = getRuntimeCodexAuthPath()
|
||||
const managedHomePath = createManagedAuth(
|
||||
testState.userDataDir,
|
||||
'account-1',
|
||||
@@ -1098,8 +1100,213 @@ describe('CodexRuntimeHomeService', () => {
|
||||
const { CodexRuntimeHomeService } = await import('./runtime-home-service')
|
||||
const service = new CodexRuntimeHomeService(store as never)
|
||||
|
||||
// Flag ON + host managed account = the account's own home is CODEX_HOME.
|
||||
expect(service.isHostSystemDefaultRealHome()).toBe(false)
|
||||
expect(service.prepareForCodexLaunch()).toBe(managedHomePath)
|
||||
// The per-account home keeps its own auth in place; the shared mirror's
|
||||
// auth.json is never hot-swapped, so two accounts cannot race one file.
|
||||
expect(readFileSync(join(managedHomePath, 'auth.json'), 'utf-8')).toBe(
|
||||
'{"account":"managed"}\n'
|
||||
)
|
||||
expect(existsSync(runtimeAuthPath)).toBe(false)
|
||||
// Session discovery includes the per-account home so its rollouts surface.
|
||||
expect(service.getHostCodexHomePathsForSessionDiscovery()).toContain(managedHomePath)
|
||||
})
|
||||
|
||||
it('gives two managed accounts distinct homes without racing one auth.json (flag ON)', async () => {
|
||||
writeFileSync(getSystemCodexAuthPath(), '{"account":"system"}\n', 'utf-8')
|
||||
const account1Auth = createCodexAuthJson('one@example.com', 'acct-1', 'one')
|
||||
const account2Auth = createCodexAuthJson('two@example.com', 'acct-2', 'two')
|
||||
const home1 = createManagedAuth(testState.userDataDir, 'account-1', account1Auth)
|
||||
const home2 = createManagedAuth(testState.userDataDir, 'account-2', account2Auth)
|
||||
const settings = createSettings({
|
||||
codexSystemDefaultRealHomeEnabled: true,
|
||||
codexManagedAccounts: [
|
||||
{
|
||||
id: 'account-1',
|
||||
email: 'one@example.com',
|
||||
managedHomePath: home1,
|
||||
providerAccountId: 'acct-1',
|
||||
workspaceLabel: null,
|
||||
workspaceAccountId: 'acct-1',
|
||||
createdAt: 1,
|
||||
updatedAt: 1,
|
||||
lastAuthenticatedAt: 1
|
||||
},
|
||||
{
|
||||
id: 'account-2',
|
||||
email: 'two@example.com',
|
||||
managedHomePath: home2,
|
||||
providerAccountId: 'acct-2',
|
||||
workspaceLabel: null,
|
||||
workspaceAccountId: 'acct-2',
|
||||
createdAt: 2,
|
||||
updatedAt: 2,
|
||||
lastAuthenticatedAt: 2
|
||||
}
|
||||
],
|
||||
activeCodexManagedAccountId: 'account-1',
|
||||
activeCodexManagedAccountIdsByRuntime: { host: 'account-1', wsl: {} }
|
||||
})
|
||||
const store = createStore(settings)
|
||||
const { CodexRuntimeHomeService } = await import('./runtime-home-service')
|
||||
const service = new CodexRuntimeHomeService(store as never)
|
||||
|
||||
// A pane for account-1 launches, then the user switches and a second pane
|
||||
// for account-2 launches concurrently — each gets its OWN CODEX_HOME.
|
||||
expect(service.prepareForCodexLaunch()).toBe(home1)
|
||||
settings.activeCodexManagedAccountId = 'account-2'
|
||||
settings.activeCodexManagedAccountIdsByRuntime = { host: 'account-2', wsl: {} }
|
||||
expect(service.prepareForCodexLaunch()).toBe(home2)
|
||||
|
||||
// Nothing is hot-swapped, so the still-running account-1 pane keeps seeing
|
||||
// account-1's credentials — the single-auth.json race (GAP-5) is gone.
|
||||
expect(readFileSync(join(home1, 'auth.json'), 'utf-8')).toBe(account1Auth)
|
||||
expect(readFileSync(join(home2, 'auth.json'), 'utf-8')).toBe(account2Auth)
|
||||
expect(existsSync(getRuntimeCodexAuthPath())).toBe(false)
|
||||
})
|
||||
|
||||
it('materializes resources and config into the per-account home on launch (flag ON)', async () => {
|
||||
writeFileSync(getSystemCodexAuthPath(), '{"account":"system"}\n', 'utf-8')
|
||||
mkdirSync(join(getSystemCodexHomePath(), 'skills', 'review'), { recursive: true })
|
||||
writeFileSync(
|
||||
join(getSystemCodexHomePath(), 'skills', 'review', 'SKILL.md'),
|
||||
'skill\n',
|
||||
'utf-8'
|
||||
)
|
||||
writeFileSync(
|
||||
join(getSystemCodexHomePath(), 'config.toml'),
|
||||
'approval_policy = "never"\n',
|
||||
'utf-8'
|
||||
)
|
||||
const home1 = createManagedAuth(
|
||||
testState.userDataDir,
|
||||
'account-1',
|
||||
createCodexAuthJson('one@example.com', 'acct-1', 'one')
|
||||
)
|
||||
const store = createStore(
|
||||
createSettings({
|
||||
codexSystemDefaultRealHomeEnabled: true,
|
||||
codexManagedAccounts: [
|
||||
{
|
||||
id: 'account-1',
|
||||
email: 'one@example.com',
|
||||
managedHomePath: home1,
|
||||
providerAccountId: 'acct-1',
|
||||
workspaceLabel: null,
|
||||
workspaceAccountId: 'acct-1',
|
||||
createdAt: 1,
|
||||
updatedAt: 1,
|
||||
lastAuthenticatedAt: 1
|
||||
}
|
||||
],
|
||||
activeCodexManagedAccountId: 'account-1',
|
||||
activeCodexManagedAccountIdsByRuntime: { host: 'account-1', wsl: {} }
|
||||
})
|
||||
)
|
||||
const { CodexRuntimeHomeService } = await import('./runtime-home-service')
|
||||
const service = new CodexRuntimeHomeService(store as never)
|
||||
|
||||
expect(service.prepareForCodexLaunch()).toBe(home1)
|
||||
// Resources link/copy into THIS home; config mirrors into it.
|
||||
expectResourceLinkedOrCopied(join(home1, 'skills'), join(getSystemCodexHomePath(), 'skills'))
|
||||
expect(readFileSync(join(home1, 'skills', 'review', 'SKILL.md'), 'utf-8')).toBe('skill\n')
|
||||
expect(readFileSync(join(home1, 'config.toml'), 'utf-8')).toContain('approval_policy = "never"')
|
||||
// ~/.codex is never mutated: no auth churn, no per-account dir written back.
|
||||
expect(readFileSync(getSystemCodexAuthPath(), 'utf-8')).toBe('{"account":"system"}\n')
|
||||
})
|
||||
|
||||
it('points the rate-limit fetch at the per-account home when the flag is ON', async () => {
|
||||
const home1 = createManagedAuth(testState.userDataDir, 'account-1', '{"account":"managed"}\n')
|
||||
const store = createStore(
|
||||
createSettings({
|
||||
codexSystemDefaultRealHomeEnabled: true,
|
||||
codexManagedAccounts: [
|
||||
{
|
||||
id: 'account-1',
|
||||
email: 'one@example.com',
|
||||
managedHomePath: home1,
|
||||
providerAccountId: null,
|
||||
workspaceLabel: null,
|
||||
workspaceAccountId: null,
|
||||
createdAt: 1,
|
||||
updatedAt: 1,
|
||||
lastAuthenticatedAt: 1
|
||||
}
|
||||
],
|
||||
activeCodexManagedAccountId: 'account-1',
|
||||
activeCodexManagedAccountIdsByRuntime: { host: 'account-1', wsl: {} }
|
||||
})
|
||||
)
|
||||
const { CodexRuntimeHomeService } = await import('./runtime-home-service')
|
||||
const service = new CodexRuntimeHomeService(store as never)
|
||||
|
||||
expect(service.prepareForRateLimitFetch()).toBe(home1)
|
||||
})
|
||||
|
||||
it('drops a managed selection whose auth.json vanished and resolves the real home (flag ON)', async () => {
|
||||
writeFileSync(getSystemCodexAuthPath(), '{"account":"system"}\n', 'utf-8')
|
||||
// A managed home that has lost its auth.json (only the marker remains).
|
||||
const brokenHome = join(testState.userDataDir, 'codex-accounts', 'account-1', 'home')
|
||||
mkdirSync(brokenHome, { recursive: true })
|
||||
writeFileSync(join(brokenHome, '.orca-managed-home'), 'account-1\n', 'utf-8')
|
||||
const settings = createSettings({
|
||||
codexSystemDefaultRealHomeEnabled: true,
|
||||
codexManagedAccounts: [
|
||||
{
|
||||
id: 'account-1',
|
||||
email: 'one@example.com',
|
||||
managedHomePath: brokenHome,
|
||||
providerAccountId: null,
|
||||
workspaceLabel: null,
|
||||
workspaceAccountId: null,
|
||||
createdAt: 1,
|
||||
updatedAt: 1,
|
||||
lastAuthenticatedAt: 1
|
||||
}
|
||||
],
|
||||
activeCodexManagedAccountId: 'account-1',
|
||||
activeCodexManagedAccountIdsByRuntime: { host: 'account-1', wsl: {} }
|
||||
})
|
||||
const store = createStore(settings)
|
||||
const { CodexRuntimeHomeService } = await import('./runtime-home-service')
|
||||
const service = new CodexRuntimeHomeService(store as never)
|
||||
|
||||
// The broken account is dropped and the launch resolves to the real home.
|
||||
expect(service.prepareForCodexLaunch()).toBeNull()
|
||||
expect(store.getSettings().activeCodexManagedAccountId).toBeNull()
|
||||
})
|
||||
|
||||
it('keeps the shared runtime home + auth hot-swap for managed accounts when the flag is OFF', async () => {
|
||||
const runtimeAuthPath = getRuntimeCodexAuthPath()
|
||||
const account1Auth = createCodexAuthJson('one@example.com', 'acct-1', 'one')
|
||||
const home1 = createManagedAuth(testState.userDataDir, 'account-1', account1Auth)
|
||||
const store = createStore(
|
||||
createSettings({
|
||||
codexSystemDefaultRealHomeEnabled: false,
|
||||
codexManagedAccounts: [
|
||||
{
|
||||
id: 'account-1',
|
||||
email: 'one@example.com',
|
||||
managedHomePath: home1,
|
||||
providerAccountId: 'acct-1',
|
||||
workspaceLabel: null,
|
||||
workspaceAccountId: 'acct-1',
|
||||
createdAt: 1,
|
||||
updatedAt: 1,
|
||||
lastAuthenticatedAt: 1
|
||||
}
|
||||
],
|
||||
activeCodexManagedAccountId: 'account-1',
|
||||
activeCodexManagedAccountIdsByRuntime: { host: 'account-1', wsl: {} }
|
||||
})
|
||||
)
|
||||
const { CodexRuntimeHomeService } = await import('./runtime-home-service')
|
||||
const service = new CodexRuntimeHomeService(store as never)
|
||||
|
||||
// Flag OFF is byte-identical to today: shared mirror home + hot-swapped auth.
|
||||
expect(service.prepareForCodexLaunch()).toBe(getRuntimeCodexHomePath())
|
||||
expect(readFileSync(runtimeAuthPath, 'utf-8')).toBe(account1Auth)
|
||||
})
|
||||
|
||||
it('uses the same host CODEX_HOME after switching managed Codex accounts', async () => {
|
||||
|
||||
@@ -169,6 +169,15 @@ export class CodexRuntimeHomeService {
|
||||
this.startWslSessionBridgeForLaunch(wslTarget, runtimeHomePath)
|
||||
return runtimeHomePath
|
||||
}
|
||||
const selfContainedAccount = this.getSelfContainedManagedHostAccount()
|
||||
if (selfContainedAccount) {
|
||||
const perAccountHome = this.prepareSelfContainedManagedHomeForLaunch(selfContainedAccount)
|
||||
if (perAccountHome) {
|
||||
return perAccountHome
|
||||
}
|
||||
// Why: the account's home lost its auth.json, so the selection was just
|
||||
// dropped. Fall through and resolve this launch as the system default.
|
||||
}
|
||||
if (this.isHostSystemDefaultRealHome(launchEnv)) {
|
||||
// Why (flag ON, system default): run Codex on the user's own ~/.codex.
|
||||
// Returning null tells the PTY/env layer to inject no managed CODEX_HOME;
|
||||
@@ -189,6 +198,80 @@ export class CodexRuntimeHomeService {
|
||||
return this.getRuntimeHomePath()
|
||||
}
|
||||
|
||||
// Why: with the real-home flag ON, a managed HOST account runs against its own
|
||||
// self-contained CODEX_HOME (codex-accounts/<id>/home) instead of the shared
|
||||
// runtime mirror. Its auth.json lives there and codex refreshes it in place,
|
||||
// so two accounts never race one auth.json (GAP-5) and the mirror can be
|
||||
// deleted once no lane still injects it (GAP-1). WSL accounts keep their
|
||||
// per-distro lane; the flag-OFF opt-out keeps the shared-home hot-swap.
|
||||
private getSelfContainedManagedHostAccount(): CodexManagedAccount | null {
|
||||
const settings = this.store.getSettings()
|
||||
if (!isCodexSystemDefaultRealHomeEnabled(settings)) {
|
||||
return null
|
||||
}
|
||||
const account = this.getActiveAccount(
|
||||
settings.codexManagedAccounts,
|
||||
normalizeCodexRuntimeSelection(settings).host
|
||||
)
|
||||
if (!account || this.getWslManagedHomePath(account)) {
|
||||
return null
|
||||
}
|
||||
return account
|
||||
}
|
||||
|
||||
private getSelfContainedManagedHostAccountHomes(): string[] {
|
||||
const settings = this.store.getSettings()
|
||||
if (!isCodexSystemDefaultRealHomeEnabled(settings)) {
|
||||
return []
|
||||
}
|
||||
return settings.codexManagedAccounts
|
||||
.filter((account) => !this.getWslManagedHomePath(account))
|
||||
.map((account) => account.managedHomePath)
|
||||
}
|
||||
|
||||
private prepareSelfContainedManagedHomeForLaunch(account: CodexManagedAccount): string | null {
|
||||
const perAccountHome = account.managedHomePath
|
||||
if (!existsSync(join(perAccountHome, 'auth.json'))) {
|
||||
// Why: drop the selection so this and future launches resolve to the
|
||||
// system default rather than a home codex cannot authenticate against.
|
||||
this.syncSelfContainedManagedSelection(account)
|
||||
return null
|
||||
}
|
||||
// Why: link the user's real ~/.codex resources and mirror config into THIS
|
||||
// home (never symlinking into or mutating ~/.codex), so the per-account home
|
||||
// is a complete CODEX_HOME. Hooks/trust are installed by the launch caller.
|
||||
this.lastSyncedAccountId = account.id
|
||||
syncSystemCodexResourcesIntoManagedHome(perAccountHome)
|
||||
syncSystemConfigIntoManagedCodexHome({
|
||||
runtimeHomePath: perAccountHome,
|
||||
systemHomePath: getSystemCodexHomePath()
|
||||
})
|
||||
return perAccountHome
|
||||
}
|
||||
|
||||
// Why: the per-account home is both the launch CODEX_HOME and the credential
|
||||
// store, so codex reads/refreshes auth.json in place — there is no shared-home
|
||||
// hot-swap or token read-back to reconcile. Only validate the credential
|
||||
// survives; a vanished auth.json drops the selection to the system default.
|
||||
private syncSelfContainedManagedSelection(account: CodexManagedAccount): void {
|
||||
if (existsSync(join(account.managedHomePath, 'auth.json'))) {
|
||||
this.lastSyncedAccountId = account.id
|
||||
return
|
||||
}
|
||||
console.warn(
|
||||
'[codex-runtime-home] Active managed account is missing auth.json, clearing selection'
|
||||
)
|
||||
const settings = this.store.getSettings()
|
||||
this.store.updateSettings({
|
||||
activeCodexManagedAccountId: null,
|
||||
activeCodexManagedAccountIdsByRuntime: {
|
||||
...normalizeCodexRuntimeSelection(settings),
|
||||
host: null
|
||||
}
|
||||
})
|
||||
this.lastSyncedAccountId = null
|
||||
}
|
||||
|
||||
private invalidateBackfillAfterManagedSystemDefaultLaunch(launchEnv?: NodeJS.ProcessEnv): void {
|
||||
const settings = this.store.getSettings()
|
||||
if (normalizeCodexRuntimeSelection(settings).host !== null) {
|
||||
@@ -238,6 +321,12 @@ export class CodexRuntimeHomeService {
|
||||
// explicitly include the real lane so its sessions remain discoverable.
|
||||
homes.push(getSystemCodexHomePath())
|
||||
}
|
||||
// Why: flag ON routes each managed host account to its own self-contained
|
||||
// home, so its rollouts live there rather than in the shared mirror. Scan
|
||||
// every such home so account-scoped sessions still surface in the AI Vault.
|
||||
for (const perAccountHome of this.getSelfContainedManagedHostAccountHomes()) {
|
||||
homes.push(perAccountHome)
|
||||
}
|
||||
return homes.filter((home, index) => homes.indexOf(home) === index)
|
||||
}
|
||||
|
||||
@@ -344,6 +433,16 @@ export class CodexRuntimeHomeService {
|
||||
const syncedRuntimeHomePath = this.getPreparedWslRateLimitHomePath(wslTarget)
|
||||
return syncedRuntimeHomePath ?? this.getWslSystemCodexHomePath(wslTarget)
|
||||
}
|
||||
const selfContainedAccount = this.getSelfContainedManagedHostAccount()
|
||||
if (
|
||||
selfContainedAccount &&
|
||||
existsSync(join(selfContainedAccount.managedHomePath, 'auth.json'))
|
||||
) {
|
||||
// Why: the quota fetch reads the account's own auth.json in place; no
|
||||
// shared-home hot-swap, and no per-poll resource relink (that is launch
|
||||
// prep). Config was mirrored on add/select and refreshed at launch.
|
||||
return selfContainedAccount.managedHomePath
|
||||
}
|
||||
if (this.isHostSystemDefaultRealHome()) {
|
||||
// Why: null lets the fetcher fall back to the main process's inherited
|
||||
// CODEX_HOME before ~/.codex. Nested Orca launches can inherit the
|
||||
@@ -363,6 +462,14 @@ export class CodexRuntimeHomeService {
|
||||
return
|
||||
}
|
||||
|
||||
const selfContainedAccount = this.getSelfContainedManagedHostAccount()
|
||||
if (selfContainedAccount) {
|
||||
// Why: self-contained managed homes hold their own auth, so the shared
|
||||
// runtime home's snapshot/hot-swap/read-back machinery below must not run.
|
||||
this.syncSelfContainedManagedSelection(selfContainedAccount)
|
||||
return
|
||||
}
|
||||
|
||||
const settings = this.store.getSettings()
|
||||
const runtimeAuthExistedBeforeSync = existsSync(this.getRuntimeAuthPath())
|
||||
if (this.lastSyncedAccountId === null) {
|
||||
|
||||
@@ -48,6 +48,10 @@ function createSettings(overrides: Partial<GlobalSettings> = {}): GlobalSettings
|
||||
const agentStatusHooksEnabled = overrides.agentStatusHooksEnabled ?? true
|
||||
const tabAutoGenerateTitle = overrides.tabAutoGenerateTitle ?? false
|
||||
return {
|
||||
// Config-sync/hot-swap tests assert the shared-mirror path; the RC defaults
|
||||
// the real-home flag ON (per-account self-contained homes), so opt these
|
||||
// managed cases out unless a test overrides it.
|
||||
codexSystemDefaultRealHomeEnabled: overrides.codexSystemDefaultRealHomeEnabled ?? false,
|
||||
workspaceDir: testState.fakeHomeDir,
|
||||
nestWorkspaces: false,
|
||||
refreshLocalBaseRefOnWorktreeCreate: false,
|
||||
|
||||
@@ -19,6 +19,9 @@ import { rewriteRelativePathConfigValues } from '../codex/codex-config-path-refe
|
||||
import { stripCodexManagedHookTrustEntriesFromConfig } from '../codex/codex-managed-trust-reconciliation'
|
||||
import { isCodexSystemDefaultRealHomeEnabled } from '../codex/codex-real-home-flag'
|
||||
import { getCodexManagedHookInstallMaterial } from '../codex/hook-service'
|
||||
import { syncSystemConfigIntoManagedCodexHome } from '../codex/codex-config-mirror'
|
||||
import { getSystemCodexHomePath } from '../codex/codex-home-paths'
|
||||
import { isCodexSystemDefaultRealHomeEnabled } from '../codex/codex-real-home-flag'
|
||||
import { MANAGED_HOOK_TIMEOUT_SECONDS } from '../agent-hooks/installer-utils'
|
||||
import { resolveCodexCommand } from '../codex-cli/command'
|
||||
import type { Store } from '../persistence'
|
||||
@@ -458,6 +461,15 @@ export class CodexAccountService {
|
||||
}
|
||||
}
|
||||
|
||||
private isSelfContainedHostManagedHome(managedHomePath: string): boolean {
|
||||
// Why: flag ON makes each host account home its own launch CODEX_HOME. WSL
|
||||
// homes keep their distro-local seed lane; the flag-OFF opt-out is unchanged.
|
||||
return (
|
||||
isCodexSystemDefaultRealHomeEnabled(this.store.getSettings()) &&
|
||||
!parseWslUncPath(managedHomePath)
|
||||
)
|
||||
}
|
||||
|
||||
private syncCanonicalConfigIntoManagedHome(
|
||||
managedHomePath: string,
|
||||
canonicalConfig = this.readCanonicalConfigForManagedHome(managedHomePath)
|
||||
@@ -467,6 +479,17 @@ export class CodexAccountService {
|
||||
}
|
||||
|
||||
const trustedManagedHomePath = this.assertManagedHomePath(managedHomePath)
|
||||
if (this.isSelfContainedHostManagedHome(trustedManagedHomePath)) {
|
||||
// Why: this home is codex's live CODEX_HOME, so mirror config with the
|
||||
// trust-preserving merge — the plain overwrite below would wipe the
|
||||
// hook/project trust codex granted in this home, forcing a re-approval and
|
||||
// an app-server re-grant on every account switch.
|
||||
syncSystemConfigIntoManagedCodexHome({
|
||||
runtimeHomePath: trustedManagedHomePath,
|
||||
systemHomePath: getSystemCodexHomePath()
|
||||
})
|
||||
return
|
||||
}
|
||||
// Why: Orca account switching is meant to swap Codex credentials and quota
|
||||
// identity, not silently fork the user's sandbox/config defaults. Syncing
|
||||
// one canonical config into every managed home keeps auth isolated per
|
||||
|
||||
@@ -182,6 +182,23 @@ describe('syncSystemCodexResourcesIntoManagedHome', () => {
|
||||
expect(existsSync(join(getRuntimeCodexHomePath(), 'history.jsonl'))).toBe(false)
|
||||
})
|
||||
|
||||
it('mirrors resources into an explicit per-account home without mutating ~/.codex', () => {
|
||||
mkdirSync(join(getSystemCodexHomePath(), 'skills', 'review'), { recursive: true })
|
||||
writeFileSync(join(getSystemCodexHomePath(), 'skills', 'review', 'SKILL.md'), 'skill\n')
|
||||
const perAccountHome = join(userDataDir, 'codex-accounts', 'account-1', 'home')
|
||||
mkdirSync(perAccountHome, { recursive: true })
|
||||
|
||||
syncSystemCodexResourcesIntoManagedHome(perAccountHome)
|
||||
|
||||
const perAccountSkillsPath = join(perAccountHome, 'skills')
|
||||
expect(readFileSync(join(perAccountSkillsPath, 'review', 'SKILL.md'), 'utf-8')).toBe('skill\n')
|
||||
expectSymbolicLinkTargetIfLinked(perAccountSkillsPath, join(getSystemCodexHomePath(), 'skills'))
|
||||
// The shared runtime mirror is untouched by an explicit per-account sync.
|
||||
expect(existsSync(join(getRuntimeCodexHomePath(), 'skills'))).toBe(false)
|
||||
// ~/.codex gains nothing — resources are only read from it, never written back.
|
||||
expect(existsSync(join(getSystemCodexHomePath(), 'codex-accounts'))).toBe(false)
|
||||
})
|
||||
|
||||
it('does not replace an existing runtime-owned resource entry', () => {
|
||||
mkdirSync(join(getSystemCodexHomePath(), 'skills'), { recursive: true })
|
||||
mkdirSync(join(getRuntimeCodexHomePath(), 'skills'), { recursive: true })
|
||||
|
||||
@@ -57,11 +57,15 @@ function getOrcaUserDataPath(): string {
|
||||
return join(process.env.XDG_CONFIG_HOME || join(homedir(), '.config'), 'orca')
|
||||
}
|
||||
|
||||
export function syncSystemCodexResourcesIntoManagedHome(): void {
|
||||
// Why: each managed home (the shared runtime mirror, or a per-account
|
||||
// self-contained CODEX_HOME that the caller has already created) links the same
|
||||
// system resources with its own ownership markers, so a per-account launch home
|
||||
// is complete without ever symlinking into or mutating the user's real ~/.codex.
|
||||
export function syncSystemCodexResourcesIntoManagedHome(managedHomePath?: string): void {
|
||||
const targetHome = managedHomePath ?? getOrcaManagedCodexHomePath()
|
||||
const systemHomePath = getSystemCodexHomePath()
|
||||
const managedHomePath = getOrcaManagedCodexHomePath()
|
||||
for (const entryName of CODEX_SYSTEM_RESOURCE_ENTRIES) {
|
||||
linkSystemCodexResource(systemHomePath, managedHomePath, entryName)
|
||||
linkSystemCodexResource(systemHomePath, targetHome, entryName)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -157,6 +157,33 @@ describe('CodexHookService', () => {
|
||||
expect(trustConfig).toContain(':permission_request:0:0')
|
||||
})
|
||||
|
||||
it('installs managed hooks + trust into a per-account self-contained home, not the shared mirror', () => {
|
||||
const systemCodexHome = join(tmpHome, '.codex')
|
||||
mkdirSync(systemCodexHome, { recursive: true })
|
||||
writeFileSync(join(systemCodexHome, 'config.toml'), 'approval_policy = "on-request"\n', 'utf-8')
|
||||
|
||||
const perAccountHome = join(userDataDir, 'codex-accounts', 'account-1', 'home')
|
||||
mkdirSync(perAccountHome, { recursive: true })
|
||||
writeFileSync(join(perAccountHome, '.orca-managed-home'), 'account-1\n', 'utf-8')
|
||||
|
||||
const status = new CodexHookService().install(perAccountHome)
|
||||
expect(status.state).toBe('installed')
|
||||
|
||||
// Hooks + trust land in THIS account's home.
|
||||
const hooksConfig = JSON.parse(readFileSync(join(perAccountHome, 'hooks.json'), 'utf-8')) as {
|
||||
hooks: Record<string, unknown>
|
||||
}
|
||||
expect(Object.keys(hooksConfig.hooks).sort()).toEqual(localManagedCodexEvents())
|
||||
const trustConfig = readFileSync(join(perAccountHome, 'config.toml'), 'utf-8')
|
||||
expect(trustConfig).toContain('approval_policy = "on-request"')
|
||||
expect(trustConfig).toContain(':permission_request:0:0')
|
||||
|
||||
// The shared runtime mirror is never touched by a per-account install.
|
||||
expect(existsSync(join(userDataDir, 'codex-runtime-home', 'home', 'hooks.json'))).toBe(false)
|
||||
// ~/.codex is only read for canonical config, never mutated with hooks.
|
||||
expect(existsSync(join(systemCodexHome, 'hooks.json'))).toBe(false)
|
||||
})
|
||||
|
||||
it('drops plugin manager metadata from runtime hooks.json during install', () => {
|
||||
const managedCodexHome = join(userDataDir, 'codex-runtime-home', 'home')
|
||||
mkdirSync(managedCodexHome, { recursive: true })
|
||||
|
||||
@@ -95,8 +95,8 @@ const CODEX_EVENTS = [
|
||||
'Stop'
|
||||
] as const
|
||||
|
||||
function getConfigPath(): string {
|
||||
return join(getOrcaManagedCodexHomePath(), 'hooks.json')
|
||||
function getConfigPath(runtimeHomePath: string = getOrcaManagedCodexHomePath()): string {
|
||||
return join(runtimeHomePath, 'hooks.json')
|
||||
}
|
||||
|
||||
function writeCodexHooksJson(configPath: string, hooks: Record<string, HookDefinition[]>): void {
|
||||
@@ -105,8 +105,8 @@ function writeCodexHooksJson(configPath: string, hooks: Record<string, HookDefin
|
||||
writeHooksJson(configPath, { hooks })
|
||||
}
|
||||
|
||||
function getCodexConfigTomlPath(): string {
|
||||
return join(getOrcaManagedCodexHomePath(), 'config.toml')
|
||||
function getCodexConfigTomlPath(runtimeHomePath: string = getOrcaManagedCodexHomePath()): string {
|
||||
return join(runtimeHomePath, 'config.toml')
|
||||
}
|
||||
|
||||
// Why: the managed-event subset of the shared PascalCase→label map; the
|
||||
@@ -297,14 +297,14 @@ function removeCodexPluginEnvironmentCommands(definitions: HookDefinition[]): Ho
|
||||
|
||||
function getRuntimeHooksWithSystemUserHooks(
|
||||
runtimeHooks: Record<string, HookDefinition[]> | undefined,
|
||||
isManagedCommand: (command: string | undefined) => boolean
|
||||
isManagedCommand: (command: string | undefined) => boolean,
|
||||
runtimeConfigPath: string = getConfigPath()
|
||||
): {
|
||||
hooks: Record<string, HookDefinition[]>
|
||||
trustEntries: MirroredRuntimeUserHookTrustEntry[]
|
||||
} {
|
||||
const systemConfigPath = getSystemConfigPath()
|
||||
const runtimeConfigPath = getConfigPath()
|
||||
if (systemConfigPath === getConfigPath()) {
|
||||
if (systemConfigPath === runtimeConfigPath) {
|
||||
return { hooks: { ...runtimeHooks }, trustEntries: [] }
|
||||
}
|
||||
|
||||
@@ -1140,14 +1140,15 @@ export class CodexHookService {
|
||||
return wslPlan ? refreshWslRuntimeUserHooks(wslPlan) : null
|
||||
}
|
||||
|
||||
getStatus(): AgentHookInstallStatus {
|
||||
return this.getStatusAfterInstall(null)
|
||||
getStatus(runtimeHomePath: string = getOrcaManagedCodexHomePath()): AgentHookInstallStatus {
|
||||
return this.getStatusAfterInstall(null, runtimeHomePath)
|
||||
}
|
||||
|
||||
private getStatusAfterInstall(
|
||||
recentGrantEntries: readonly CodexTrustEntry[] | null
|
||||
recentGrantEntries: readonly CodexTrustEntry[] | null,
|
||||
runtimeHomePath: string = getOrcaManagedCodexHomePath()
|
||||
): AgentHookInstallStatus {
|
||||
const configPath = getConfigPath()
|
||||
const configPath = getConfigPath(runtimeHomePath)
|
||||
const scriptPath = getManagedScriptPath()
|
||||
const config = readHooksJson(configPath)
|
||||
if (!config) {
|
||||
@@ -1164,7 +1165,7 @@ export class CodexHookService {
|
||||
// trust entries are missing/stale. Codex 0.129+ silently drops untrusted
|
||||
// hooks, so a green status without trust verification is misleading.
|
||||
const command = getManagedCommand(scriptPath)
|
||||
const tomlPath = getCodexConfigTomlPath()
|
||||
const tomlPath = getCodexConfigTomlPath(runtimeHomePath)
|
||||
// Why: an unreadable config.toml (EACCES/EIO) is distinct from "file
|
||||
// absent" (which returns an empty Map without throwing). Hooks.json may
|
||||
// still be fine, so report partial with a specific reason rather than
|
||||
@@ -1184,7 +1185,7 @@ export class CodexHookService {
|
||||
// hashes or wrote fallback hashes. Re-resolving PATH here doubles sync launch work.
|
||||
const ledgerHome =
|
||||
recentGrantEntries === null
|
||||
? readCurrentCodexTrustGrantLedgerHome(getOrcaManagedCodexHomePath(), { kind: 'native' })
|
||||
? readCurrentCodexTrustGrantLedgerHome(runtimeHomePath, { kind: 'native' })
|
||||
: null
|
||||
const recentGrantHashes = new Map<string, { signature: string; trustedHash: string }>()
|
||||
for (const entry of recentGrantEntries ?? []) {
|
||||
@@ -1295,14 +1296,17 @@ export class CodexHookService {
|
||||
return { agent: 'codex', state, configPath, managedHooksPresent, detail }
|
||||
}
|
||||
|
||||
install(): AgentHookInstallStatus {
|
||||
const configPath = getConfigPath()
|
||||
// Why: runtimeHomePath defaults to the shared managed mirror, but a managed
|
||||
// account launching against its own self-contained CODEX_HOME passes that
|
||||
// per-account home so hooks.json/config.toml/trust land where codex reads.
|
||||
install(runtimeHomePath: string = getOrcaManagedCodexHomePath()): AgentHookInstallStatus {
|
||||
const configPath = getConfigPath(runtimeHomePath)
|
||||
const scriptPath = getManagedScriptPath()
|
||||
// Why: must run before this install rewrites hooks.json/config.toml —
|
||||
// approvals the user made inside Orca-launched Codex are keyed to the
|
||||
// previous launch's runtime layout, and stale-trust cleanup below would
|
||||
// delete them once the system config stops backing them.
|
||||
promoteCodexRuntimeHookApprovalsToSystem()
|
||||
promoteCodexRuntimeHookApprovalsToSystem(runtimeHomePath)
|
||||
const config = readHooksJson(configPath)
|
||||
if (!config) {
|
||||
return {
|
||||
@@ -1320,7 +1324,7 @@ export class CodexHookService {
|
||||
// accumulate duplicate hook entries pointing at defunct scripts.
|
||||
const isManagedCommand = createManagedCommandMatcher(getCodexManagedScriptFileName())
|
||||
const command = getManagedCommand(scriptPath)
|
||||
const hookPlan = getRuntimeHooksWithSystemUserHooks(config.hooks, isManagedCommand)
|
||||
const hookPlan = getRuntimeHooksWithSystemUserHooks(config.hooks, isManagedCommand, configPath)
|
||||
const nextHooks = hookPlan.hooks
|
||||
const managedEvents = new Set<string>(CODEX_EVENTS)
|
||||
|
||||
@@ -1390,8 +1394,11 @@ export class CodexHookService {
|
||||
// pointing at a hook that doesn't exist. Surface failures — without this,
|
||||
// getStatus would report green for a hook Codex won't actually fire.
|
||||
try {
|
||||
const tomlPath = getCodexConfigTomlPath()
|
||||
syncSystemConfigIntoManagedCodexHome()
|
||||
const tomlPath = getCodexConfigTomlPath(runtimeHomePath)
|
||||
syncSystemConfigIntoManagedCodexHome({
|
||||
runtimeHomePath,
|
||||
systemHomePath: getSystemCodexHomePath()
|
||||
})
|
||||
// Why: Codex is the only authority on its trust-hash algorithm, so the
|
||||
// managed entries are granted through codex app-server RPCs (verified by
|
||||
// re-list) whenever the installed CLI supports them; the granted entries
|
||||
@@ -1399,7 +1406,7 @@ export class CodexHookService {
|
||||
// delete what Codex just wrote. Mirrored user trust keeps its existing
|
||||
// verbatim-carry lane either way.
|
||||
const grant = grantManagedCodexHookTrust({
|
||||
runtimeHomePath: getOrcaManagedCodexHomePath(),
|
||||
runtimeHomePath,
|
||||
tomlPath,
|
||||
managedCommand: command,
|
||||
managedEntries: managedTrustEntries,
|
||||
@@ -1431,14 +1438,14 @@ export class CodexHookService {
|
||||
detail: `Hooks installed but trust entries could not be written: ${error instanceof Error ? error.message : String(error)}. Run /hooks in Codex to approve.`
|
||||
}
|
||||
}
|
||||
snapshotCodexRuntimeHookTrustProvenance()
|
||||
snapshotCodexRuntimeHookTrustProvenance(runtimeHomePath)
|
||||
try {
|
||||
cleanupLegacySystemManagedHooks()
|
||||
cleanupLegacyCodexProfileHooks()
|
||||
} catch (error) {
|
||||
console.warn('[codex-hook-service] failed to clean legacy Codex hooks', error)
|
||||
}
|
||||
return this.getStatusAfterInstall(recentGrantEntries)
|
||||
return this.getStatusAfterInstall(recentGrantEntries, runtimeHomePath)
|
||||
}
|
||||
|
||||
async installRemote(
|
||||
@@ -1563,11 +1570,13 @@ export class CodexHookService {
|
||||
}
|
||||
}
|
||||
|
||||
refreshRuntimeUserHooks(): AgentHookInstallStatus {
|
||||
const configPath = getConfigPath()
|
||||
refreshRuntimeUserHooks(
|
||||
runtimeHomePath: string = getOrcaManagedCodexHomePath()
|
||||
): AgentHookInstallStatus {
|
||||
const configPath = getConfigPath(runtimeHomePath)
|
||||
// Why: same as install() — capture in-Orca approvals before this refresh
|
||||
// rewrites the runtime files they are keyed against.
|
||||
promoteCodexRuntimeHookApprovalsToSystem()
|
||||
promoteCodexRuntimeHookApprovalsToSystem(runtimeHomePath)
|
||||
const config = readHooksJson(configPath)
|
||||
if (!config) {
|
||||
// Why: disabled launch prep used to call remove(); preserve its legacy
|
||||
@@ -1583,14 +1592,17 @@ export class CodexHookService {
|
||||
}
|
||||
|
||||
const isManagedCommand = createManagedCommandMatcher(getCodexManagedScriptFileName())
|
||||
const hookPlan = getRuntimeHooksWithSystemUserHooks(config.hooks, isManagedCommand)
|
||||
const hookPlan = getRuntimeHooksWithSystemUserHooks(config.hooks, isManagedCommand, configPath)
|
||||
config.hooks = hookPlan.hooks
|
||||
writeCodexHooksJson(configPath, hookPlan.hooks)
|
||||
|
||||
try {
|
||||
const tomlPath = getCodexConfigTomlPath()
|
||||
const tomlPath = getCodexConfigTomlPath(runtimeHomePath)
|
||||
const trustEntries = hookPlan.trustEntries.map(({ entry }) => entry)
|
||||
syncSystemConfigIntoManagedCodexHome()
|
||||
syncSystemConfigIntoManagedCodexHome({
|
||||
runtimeHomePath,
|
||||
systemHomePath: getSystemCodexHomePath()
|
||||
})
|
||||
// Why: this path is used when Orca status hooks are disabled. The
|
||||
// runtime CODEX_HOME should keep user hooks, but not Orca-managed trust.
|
||||
// Write current mirrored user trust first so stale cleanup compares
|
||||
@@ -1607,10 +1619,10 @@ export class CodexHookService {
|
||||
detail: `User hooks refreshed but trust entries could not be written: ${error instanceof Error ? error.message : String(error)}. Run /hooks in Codex to approve.`
|
||||
}
|
||||
}
|
||||
snapshotCodexRuntimeHookTrustProvenance()
|
||||
snapshotCodexRuntimeHookTrustProvenance(runtimeHomePath)
|
||||
|
||||
cleanupLegacyManagedHookRepresentations()
|
||||
return this.getStatus()
|
||||
return this.getStatus(runtimeHomePath)
|
||||
}
|
||||
|
||||
remove(): AgentHookInstallStatus {
|
||||
|
||||
@@ -78,9 +78,10 @@ function readHookTrustProvenance(
|
||||
* install/refresh, so the next launch can tell "entry Orca wrote" apart from
|
||||
* "entry Codex wrote after a user approval". Call after all trust writes.
|
||||
*/
|
||||
export function snapshotCodexRuntimeHookTrustProvenance(): void {
|
||||
export function snapshotCodexRuntimeHookTrustProvenance(
|
||||
runtimeHomePath: string = getOrcaManagedCodexHomePath()
|
||||
): void {
|
||||
try {
|
||||
const runtimeHomePath = getOrcaManagedCodexHomePath()
|
||||
const runtimeHooksPath = join(runtimeHomePath, 'hooks.json')
|
||||
const canonicalRuntimeHooksPath = getCodexExplicitHomeHookSourcePath(runtimeHooksPath)
|
||||
const entries: Record<string, HookTrustProvenanceEntry> = {}
|
||||
@@ -115,9 +116,11 @@ export function snapshotCodexRuntimeHookTrustProvenance(): void {
|
||||
* the user's own hooks.json. Runs before the config mirror so the promoted
|
||||
* trust is mirrored back on the same launch.
|
||||
*/
|
||||
export function promoteCodexRuntimeHookApprovalsToSystem(): void {
|
||||
export function promoteCodexRuntimeHookApprovalsToSystem(
|
||||
runtimeHomePath: string = getOrcaManagedCodexHomePath()
|
||||
): void {
|
||||
try {
|
||||
promoteCodexRuntimeHookApprovalsToSystemUnsafe()
|
||||
promoteCodexRuntimeHookApprovalsToSystemUnsafe(runtimeHomePath)
|
||||
} catch (error) {
|
||||
// Why: promotion is best-effort launch prep; a malformed runtime file
|
||||
// must not block hook install or the Codex launch itself.
|
||||
@@ -125,8 +128,7 @@ export function promoteCodexRuntimeHookApprovalsToSystem(): void {
|
||||
}
|
||||
}
|
||||
|
||||
function promoteCodexRuntimeHookApprovalsToSystemUnsafe(): void {
|
||||
const runtimeHomePath = getOrcaManagedCodexHomePath()
|
||||
function promoteCodexRuntimeHookApprovalsToSystemUnsafe(runtimeHomePath: string): void {
|
||||
const systemHomePath = getSystemCodexHomePath()
|
||||
const runtimeHooksPath = join(runtimeHomePath, 'hooks.json')
|
||||
const systemHooksPath = join(systemHomePath, 'hooks.json')
|
||||
|
||||
@@ -1950,6 +1950,43 @@ describe('createPtySubprocess', () => {
|
||||
expect(env.ORCA_CODEX_HOME).toBeUndefined()
|
||||
})
|
||||
|
||||
it('strips an inherited per-account self-contained CODEX_HOME overlay in a nested Orca (#5370)', () => {
|
||||
const proc = mockPtyProcess()
|
||||
spawnMock.mockReturnValue(proc)
|
||||
const previousCodexHome = process.env.CODEX_HOME
|
||||
const previousOrcaCodexHome = process.env.ORCA_CODEX_HOME
|
||||
// A per-account home is injected as CODEX_HOME === ORCA_CODEX_HOME, so the
|
||||
// nested-Orca strip must clear it exactly as it does the shared mirror.
|
||||
const perAccountHome = '/daemon/managed/codex-accounts/019f0000-aaaa/home'
|
||||
process.env.CODEX_HOME = perAccountHome
|
||||
process.env.ORCA_CODEX_HOME = perAccountHome
|
||||
|
||||
try {
|
||||
createPtySubprocess({
|
||||
sessionId: 'test',
|
||||
cols: 80,
|
||||
rows: 24,
|
||||
env: { SHELL: '/bin/bash' },
|
||||
envToDelete: ['ORCA_CODEX_HOME']
|
||||
})
|
||||
} finally {
|
||||
if (previousCodexHome === undefined) {
|
||||
delete process.env.CODEX_HOME
|
||||
} else {
|
||||
process.env.CODEX_HOME = previousCodexHome
|
||||
}
|
||||
if (previousOrcaCodexHome === undefined) {
|
||||
delete process.env.ORCA_CODEX_HOME
|
||||
} else {
|
||||
process.env.ORCA_CODEX_HOME = previousOrcaCodexHome
|
||||
}
|
||||
}
|
||||
|
||||
const env = spawnMock.mock.calls.at(-1)![2].env
|
||||
expect(env.CODEX_HOME).toBeUndefined()
|
||||
expect(env.ORCA_CODEX_HOME).toBeUndefined()
|
||||
})
|
||||
|
||||
it('preserves a daemon-owned custom Codex home while deleting a stale private marker', () => {
|
||||
const proc = mockPtyProcess()
|
||||
spawnMock.mockReturnValue(proc)
|
||||
|
||||
+4
-2
@@ -792,9 +792,11 @@ function prepareCodexRuntimeHomeForLaunch(
|
||||
// the persisted off switch so those launches cannot reinstall removed hooks.
|
||||
const status = hooksEnabled
|
||||
? (codexHookService.installForRuntimeHome(runtimeHomePath, hookTarget) ??
|
||||
codexHookService.install())
|
||||
// Why: a managed account's launch home is its own self-contained
|
||||
// CODEX_HOME, so hooks/trust must install there, not the shared mirror.
|
||||
codexHookService.install(runtimeHomePath ?? undefined))
|
||||
: (codexHookService.refreshRuntimeUserHooksForRuntimeHome(runtimeHomePath, hookTarget) ??
|
||||
codexHookService.refreshRuntimeUserHooks())
|
||||
codexHookService.refreshRuntimeUserHooks(runtimeHomePath ?? undefined))
|
||||
if (status.state === 'error') {
|
||||
console.warn(
|
||||
`[codex-hook-service] failed to ${
|
||||
|
||||
Reference in New Issue
Block a user