fix(codex): don't revoke a just-granted WSL home on a false 'missing' probe

The async wsl.exe canonical-path settlement could report the runtime home
'missing' immediately after a verified RPC grant (a false negative — codex
had just written and re-listed trust there), which drove the reconciliation
'remove' branch to delete all six granted [hooks.state] tables, leaving a bare
[hooks.state] the launching pane read as 'hooks need review'. A 'missing'
settlement now revokes only when no successful install ran this generation; a
genuinely moved home still resolves to a different path and reinstalls.
This commit is contained in:
Brennan Benson
2026-07-15 17:24:30 -07:00
parent 87db673fcd
commit caf6cac3df
2 changed files with 44 additions and 10 deletions
@@ -209,7 +209,8 @@ describe('Codex WSL runtime hook install', () => {
settlement: { status: 'unavailable' },
isCurrentGeneration: true,
installedTrustConfigPath: '/mnt/d/home/hooks.json',
resolvedTrustConfigPath: null
resolvedTrustConfigPath: null,
installSucceeded: false
})
).toBe('none')
@@ -218,7 +219,8 @@ describe('Codex WSL runtime hook install', () => {
settlement: { status: 'missing' },
isCurrentGeneration: false,
installedTrustConfigPath: '/mnt/d/home/hooks.json',
resolvedTrustConfigPath: null
resolvedTrustConfigPath: null,
installSucceeded: false
})
).toBe('none')
@@ -227,25 +229,42 @@ describe('Codex WSL runtime hook install', () => {
settlement: { status: 'missing' },
isCurrentGeneration: true,
installedTrustConfigPath: '/mnt/d/home/hooks.json',
resolvedTrustConfigPath: null
resolvedTrustConfigPath: null,
installSucceeded: false
})
).toBe('remove')
// Why: a `missing` probe right after a verified grant is a false negative;
// revoking would delete the fresh trust the launching pane reads (#8847).
expect(
_internals.getWslHookReconciliationAction({
settlement: { status: 'resolved', canonicalPath: '/windows/d/home' },
settlement: { status: 'missing' },
isCurrentGeneration: true,
installedTrustConfigPath: '/windows/d/home/hooks.json',
resolvedTrustConfigPath: '/windows/d/home/hooks.json'
installedTrustConfigPath: '/mnt/d/home/hooks.json',
resolvedTrustConfigPath: null,
installSucceeded: true
})
).toBe('none')
expect(
_internals.getWslHookReconciliationAction({
settlement: { status: 'resolved', canonicalPath: '/windows/d/home' },
isCurrentGeneration: true,
installedTrustConfigPath: '/windows/d/home/hooks.json',
resolvedTrustConfigPath: '/windows/d/home/hooks.json',
installSucceeded: true
})
).toBe('none')
// Why: a genuinely moved home resolves to a different path and still
// reinstalls, even though the original install succeeded.
expect(
_internals.getWslHookReconciliationAction({
settlement: { status: 'resolved', canonicalPath: '/windows/d/home' },
isCurrentGeneration: true,
installedTrustConfigPath: '/mnt/d/home/hooks.json',
resolvedTrustConfigPath: '/windows/d/home/hooks.json'
resolvedTrustConfigPath: '/windows/d/home/hooks.json',
installSucceeded: true
})
).toBe('reinstall')
})
+18 -3
View File
@@ -930,12 +930,19 @@ function getWslHookReconciliationAction(args: {
isCurrentGeneration: boolean
installedTrustConfigPath: string | null
resolvedTrustConfigPath: string | null
/** Whether the synchronous install for this generation wrote trust. */
installSucceeded: boolean
}): 'none' | 'remove' | 'reinstall' {
if (!args.isCurrentGeneration) {
return 'none'
}
if (args.settlement.status === 'missing') {
return 'remove'
// Why: a `missing` directory probe right after a verified install/grant is
// a false negative — the RPC (or fallback) just wrote and read trust in
// that home, so it exists. Revoking here would delete the fresh grant the
// launching pane needs, resurfacing "hooks need review". A genuinely moved
// home resolves to a different path and takes the `reinstall` branch below.
return args.installSucceeded ? 'none' : 'remove'
}
if (
args.settlement.status !== 'resolved' ||
@@ -972,6 +979,10 @@ export class CodexHookService {
): AgentHookInstallStatus | null {
const generation = this.supersedeWslReconciliation(runtimeHomePath)
let installedTrustConfigPath: string | null = null
// Why: JS is single-threaded, so the synchronous install below finishes
// before any async `wsl.exe` settlement callback runs — this flag is
// always set by the time the callback reads it.
let installSucceeded = false
const onCanonicalPathSettled = (settlement: WslCanonicalPathSettlement): void => {
if (!runtimeHomePath) {
return
@@ -989,7 +1000,8 @@ export class CodexHookService {
settlement,
isCurrentGeneration: this.wslReconciliationGeneration.get(key) === generation,
installedTrustConfigPath,
resolvedTrustConfigPath: resolvedPlan?.trustConfigPath ?? null
resolvedTrustConfigPath: resolvedPlan?.trustConfigPath ?? null,
installSucceeded
})
if (action === 'none') {
return
@@ -1014,6 +1026,7 @@ export class CodexHookService {
return
}
installedTrustConfigPath = resolvedPlan.trustConfigPath
installSucceeded = status.state === 'installed'
}
const wslPlan = createCodexWslRuntimeHookInstallPlan(
runtimeHomePath,
@@ -1022,7 +1035,9 @@ export class CodexHookService {
onCanonicalPathSettled
)
installedTrustConfigPath = wslPlan?.trustConfigPath ?? null
return wslPlan ? installManagedHooksIntoWslRuntime(wslPlan) : null
const status = wslPlan ? installManagedHooksIntoWslRuntime(wslPlan) : null
installSucceeded = status?.state === 'installed'
return status
}
refreshRuntimeUserHooksForRuntimeHome(