mirror of
https://github.com/stablyai/orca.git
synced 2026-10-18 00:03:20 +00:00
fix(codex): align hook trust source paths
This commit is contained in:
@@ -0,0 +1,41 @@
|
||||
import { afterEach, describe, expect, it } from 'vitest'
|
||||
import { runCodexAppServerSession } from './codex-app-server-session'
|
||||
|
||||
const originalCodexHome = process.env.CODEX_HOME
|
||||
|
||||
afterEach(() => {
|
||||
if (originalCodexHome === undefined) {
|
||||
delete process.env.CODEX_HOME
|
||||
} else {
|
||||
process.env.CODEX_HOME = originalCodexHome
|
||||
}
|
||||
})
|
||||
|
||||
describe('runCodexAppServerSession environment', () => {
|
||||
it('removes inherited variables requested by a default-home invocation', async () => {
|
||||
process.env.CODEX_HOME = '/tmp/inherited-managed-home'
|
||||
const server = String.raw`
|
||||
const readline = require('node:readline')
|
||||
readline.createInterface({ input: process.stdin }).on('line', (line) => {
|
||||
const message = JSON.parse(line)
|
||||
if (typeof message.id !== 'number') return
|
||||
const result = message.method === 'env/get'
|
||||
? { codexHome: process.env.CODEX_HOME ?? null }
|
||||
: {}
|
||||
process.stdout.write(JSON.stringify({ id: message.id, result }) + '\n')
|
||||
})
|
||||
`
|
||||
|
||||
const result = await runCodexAppServerSession(
|
||||
{
|
||||
command: process.execPath,
|
||||
args: ['-e', server],
|
||||
envToDelete: ['CODEX_HOME'],
|
||||
timeoutMs: 5_000
|
||||
},
|
||||
(requestRpc) => requestRpc('env/get')
|
||||
)
|
||||
|
||||
expect(result).toEqual({ codexHome: null })
|
||||
})
|
||||
})
|
||||
@@ -6,6 +6,8 @@ export type CodexAppServerInvocation = {
|
||||
args: string[]
|
||||
/** Overlay applied on top of the inherited environment (for example, CODEX_HOME). */
|
||||
env?: Record<string, string>
|
||||
/** Variables that would make this child select a different runtime than its caller. */
|
||||
envToDelete?: readonly string[]
|
||||
/** Whole-session deadline; the Codex child is killed when it lapses. */
|
||||
timeoutMs: number
|
||||
}
|
||||
@@ -52,8 +54,14 @@ export async function runCodexAppServerSession<T>(
|
||||
run: (requestRpc: CodexAppServerRequestRpc) => Promise<T>,
|
||||
spawnImpl: typeof spawn = spawn
|
||||
): Promise<T> {
|
||||
const childEnv = { ...process.env, ...invocation.env }
|
||||
// Why: default-home RPCs must observe the same missing CODEX_HOME as the
|
||||
// pane whose hook keys they inspect; omission alone would inherit Orca's value.
|
||||
for (const key of invocation.envToDelete ?? []) {
|
||||
delete childEnv[key]
|
||||
}
|
||||
const child = spawnImpl(invocation.command, invocation.args, {
|
||||
env: { ...process.env, ...invocation.env },
|
||||
env: childEnv,
|
||||
stdio: ['pipe', 'pipe', 'pipe'],
|
||||
windowsHide: true
|
||||
}) as ChildProcessWithoutNullStreams
|
||||
|
||||
@@ -120,6 +120,19 @@ describe('grantManagedCodexHookTrust', () => {
|
||||
expect(getCodexTrustGrantDiagnostics()).toMatchObject({ granted: 1, fellBack: 0 })
|
||||
})
|
||||
|
||||
it('builds a default-home grant invocation without an inherited CODEX_HOME', () => {
|
||||
const entries = [managedEntry('stop')]
|
||||
const runner = vi.fn((_request: CodexHookTrustGrantRequest) => grantedSessionResult(entries))
|
||||
_internals.setGrantSessionRunnerSync(runner)
|
||||
|
||||
expect(
|
||||
grantManagedCodexHookTrust({ ...buildPlan(entries), useDefaultCodexHome: true })
|
||||
).toMatchObject({ lane: 'rpc' })
|
||||
const invocation = runner.mock.calls[0]![0]!.invocation
|
||||
expect(invocation.env?.CODEX_HOME).toBeUndefined()
|
||||
expect(invocation.envToDelete).toContain('CODEX_HOME')
|
||||
})
|
||||
|
||||
it('skips the RPC session while the ledger grant still holds, and re-grants on config drift', () => {
|
||||
const entries = [managedEntry('session_start')]
|
||||
const runner = vi.fn(() => grantedSessionResult(entries))
|
||||
|
||||
@@ -44,6 +44,8 @@ export type CodexManagedTrustGrantPlan = {
|
||||
/** Managed trust identities Orca just wrote (no trustedHash). */
|
||||
managedEntries: readonly CodexTrustEntry[]
|
||||
host: CodexTrustGrantHost
|
||||
/** Match a pane where CODEX_HOME is absent instead of an explicit managed home. */
|
||||
useDefaultCodexHome?: boolean
|
||||
}
|
||||
|
||||
export type CodexTrustGrantFallbackReason =
|
||||
@@ -226,7 +228,8 @@ export function grantManagedCodexHookTrust(
|
||||
resolvedHost.buildRequest({
|
||||
runtimeHomePath: plan.runtimeHomePath,
|
||||
managedCommand: plan.managedCommand,
|
||||
expectedTrustKeys: expected.map(({ normalizedKey }) => normalizedKey)
|
||||
expectedTrustKeys: expected.map(({ normalizedKey }) => normalizedKey),
|
||||
useDefaultCodexHome: plan.useDefaultCodexHome
|
||||
})
|
||||
)
|
||||
} catch (error) {
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import {
|
||||
computeTrustKey,
|
||||
computeTrustedHash,
|
||||
getCodexCanonicalTrustPath,
|
||||
getCodexExplicitHomeHookSourcePath,
|
||||
normalizeHookTrustKeyForLookup,
|
||||
normalizeCodexHookSourcePath,
|
||||
parseTrustKey,
|
||||
readHookTrustEntries,
|
||||
removeHookTrustEntries,
|
||||
@@ -58,22 +59,26 @@ export function removeCodexManagedHookTrustEntries(options: {
|
||||
command: string
|
||||
managedEventLabels: ReadonlySet<CodexEventLabel>
|
||||
timeoutSec: number
|
||||
/** Explicit native homes resolve their parent before hook discovery. */
|
||||
sourceUsesExplicitCodexHome?: boolean
|
||||
}): void {
|
||||
const existingEntries = readHookTrustEntries(options.tomlPath)
|
||||
const ledgerHome = readCodexTrustGrantLedgerHomeForReconciliation(options.runtimeHomePath)
|
||||
const canonicalSourcePath = getCodexCanonicalTrustPath(options.sourcePath)
|
||||
const expectedSourcePath = options.sourceUsesExplicitCodexHome
|
||||
? getCodexExplicitHomeHookSourcePath(options.sourcePath)
|
||||
: normalizeCodexHookSourcePath(options.sourcePath)
|
||||
const ownedKeys: string[] = []
|
||||
for (const [key, state] of existingEntries) {
|
||||
const parts = parseTrustKey(key)
|
||||
if (
|
||||
!parts ||
|
||||
getCodexCanonicalTrustPath(parts.sourcePath) !== canonicalSourcePath ||
|
||||
normalizeCodexHookSourcePath(parts.sourcePath) !== expectedSourcePath ||
|
||||
!options.managedEventLabels.has(parts.eventLabel)
|
||||
) {
|
||||
continue
|
||||
}
|
||||
const expectedEntry: CodexTrustEntry = {
|
||||
sourcePath: options.sourcePath,
|
||||
sourcePath: expectedSourcePath,
|
||||
eventLabel: parts.eventLabel,
|
||||
groupIndex: parts.groupIndex,
|
||||
handlerIndex: parts.handlerIndex,
|
||||
|
||||
@@ -116,9 +116,28 @@ describe('ensureRealHomeCodexHookState (install)', () => {
|
||||
const plan = grantMock.mock.calls[0]![0] as CodexManagedTrustGrantPlan
|
||||
expect(plan.runtimeHomePath).toBe(join(fakeHomeDir, '.codex'))
|
||||
expect(plan.host).toEqual({ kind: 'native' })
|
||||
expect(plan.useDefaultCodexHome).toBe(true)
|
||||
expect(plan.managedEntries.every((entry) => entry.groupIndex === 0)).toBe(true)
|
||||
})
|
||||
|
||||
it('keeps a symlinked default home logical in the keys sent to Codex', () => {
|
||||
grantSucceeds()
|
||||
const logicalHome = join(fakeHomeDir, '.codex')
|
||||
const targetHome = join(fakeHomeDir, 'dotfiles-codex')
|
||||
rmSync(logicalHome, { recursive: true })
|
||||
mkdirSync(targetHome)
|
||||
symlinkSync(targetHome, logicalHome)
|
||||
|
||||
expect(ensureRealHomeCodexHookState({ hooksEnabled: true, userDataPath: userDataDir })).toBe(
|
||||
'installed'
|
||||
)
|
||||
|
||||
const plan = grantMock.mock.calls[0]![0] as CodexManagedTrustGrantPlan
|
||||
expect(
|
||||
plan.managedEntries.map(computeTrustKey).every((key) => key.startsWith(logicalHome))
|
||||
).toBe(true)
|
||||
})
|
||||
|
||||
it('keeps the managed lane for unknown top-level fields Codex cannot load', () => {
|
||||
grantSucceeds()
|
||||
const userConfig = {
|
||||
|
||||
@@ -193,7 +193,8 @@ function installRealHomeCodexHook(userDataPath: string): RealHomeCodexHookLane {
|
||||
tomlPath: getRealHomeConfigTomlPath(),
|
||||
managedCommand: material.command,
|
||||
managedEntries,
|
||||
host: { kind: 'native' }
|
||||
host: { kind: 'native' },
|
||||
useDefaultCodexHome: true
|
||||
})
|
||||
if (grant.lane === 'rpc') {
|
||||
return 'installed'
|
||||
|
||||
@@ -23,6 +23,7 @@ type CodexTrustGrantRequestInput = {
|
||||
runtimeHomePath: string
|
||||
managedCommand: string
|
||||
expectedTrustKeys: string[]
|
||||
useDefaultCodexHome?: boolean
|
||||
}
|
||||
|
||||
export type ResolvedCodexTrustGrantHost = {
|
||||
@@ -54,11 +55,14 @@ export function resolveCodexTrustGrantHost(host: CodexTrustGrantHost): ResolvedC
|
||||
binaryStamp: command === 'codex' ? null : buildNativeCodexBinaryStamp(command),
|
||||
buildRequest: (input) => {
|
||||
const { spawnCmd, spawnArgs } = getSpawnArgsForWindows(command, ['app-server'])
|
||||
const useDefaultCodexHome = input.useDefaultCodexHome === true
|
||||
return {
|
||||
invocation: {
|
||||
command: spawnCmd,
|
||||
args: spawnArgs,
|
||||
env: { CODEX_HOME: input.runtimeHomePath },
|
||||
...(useDefaultCodexHome
|
||||
? { envToDelete: ['CODEX_HOME'] }
|
||||
: { env: { CODEX_HOME: input.runtimeHomePath } }),
|
||||
timeoutMs: NATIVE_GRANT_TIMEOUT_MS
|
||||
},
|
||||
hooksListCwd: input.runtimeHomePath,
|
||||
|
||||
@@ -136,6 +136,7 @@ describe('real-home user hook trust rebasing', () => {
|
||||
})
|
||||
|
||||
expect(requests).toHaveLength(2)
|
||||
expect(requests[0]?.invocation.envToDelete).toContain('CODEX_HOME')
|
||||
const repair = requests[1]
|
||||
expect(repair?.operation).toBe('repair-user-hook-trust')
|
||||
if (repair?.operation === 'repair-user-hook-trust') {
|
||||
|
||||
@@ -151,7 +151,8 @@ export function mutateRealHomeHooksPreservingUserTrust(args: {
|
||||
const baseRequest = resolveCodexTrustGrantHost({ kind: 'native' }).buildRequest({
|
||||
runtimeHomePath: args.runtimeHomePath,
|
||||
managedCommand: '',
|
||||
expectedTrustKeys: []
|
||||
expectedTrustKeys: [],
|
||||
useDefaultCodexHome: true
|
||||
})
|
||||
// Why: inspection happens before the write, so an unavailable RPC aborts
|
||||
// without shifting a user's positional trust key.
|
||||
|
||||
@@ -0,0 +1,80 @@
|
||||
import { resolve } from 'node:path'
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import {
|
||||
computeTrustKey,
|
||||
normalizeCodexHookSourcePath,
|
||||
normalizeHookTrustKeyForLookup,
|
||||
type CodexTrustEntry
|
||||
} from './config-toml-trust'
|
||||
|
||||
function entry(sourcePath: string): CodexTrustEntry {
|
||||
return {
|
||||
sourcePath,
|
||||
eventLabel: 'stop',
|
||||
groupIndex: 2,
|
||||
handlerIndex: 3,
|
||||
command: 'irrelevant'
|
||||
}
|
||||
}
|
||||
|
||||
describe('Codex hook trust source path normalization', () => {
|
||||
it.each([
|
||||
['C:/Users/Rod/.codex/./hooks.json', 'C:\\Users\\Rod\\.codex\\hooks.json'],
|
||||
['C:\\Users\\Rod\\tmp\\..\\.codex\\hooks.json', 'C:\\Users\\Rod\\.codex\\hooks.json'],
|
||||
['C:\\Users\\Rod\\.codex\\hooks.json\\', 'C:\\Users\\Rod\\.codex\\hooks.json'],
|
||||
['\\\\server\\share\\dir\\.\\hooks.json', '\\\\server\\share\\dir\\hooks.json'],
|
||||
['\\\\?\\C:\\Users\\Rod\\.codex\\hooks.json', 'C:\\Users\\Rod\\.codex\\hooks.json'],
|
||||
['\\\\?\\UNC\\server\\share\\dir\\hooks.json', '\\\\server\\share\\dir\\hooks.json'],
|
||||
['\\\\.\\C:\\Users\\Rod\\.codex\\hooks.json', 'C:\\Users\\Rod\\.codex\\hooks.json'],
|
||||
['\\\\.\\UNC\\server\\share\\dir\\hooks.json', '\\\\server\\share\\dir\\hooks.json']
|
||||
])('matches Windows PathBuf display semantics for %s', (sourcePath, expected) => {
|
||||
expect(normalizeCodexHookSourcePath(sourcePath)).toBe(expected)
|
||||
})
|
||||
|
||||
it('uses native semantics for the otherwise ambiguous forward-slash UNC form', () => {
|
||||
expect(normalizeCodexHookSourcePath('//server/share/dir/hooks.json')).toBe(
|
||||
process.platform === 'win32'
|
||||
? '\\\\server\\share\\dir\\hooks.json'
|
||||
: '/server/share/dir/hooks.json'
|
||||
)
|
||||
})
|
||||
|
||||
it('normalizes POSIX doubles, dot segments, repeated separators, and trailing slashes', () => {
|
||||
expect(normalizeCodexHookSourcePath('//tmp/a//b/../hooks.json/')).toBe('/tmp/a/hooks.json')
|
||||
})
|
||||
|
||||
it('resolves relative native paths to the absolute discovery shape', () => {
|
||||
expect(normalizeCodexHookSourcePath('relative/../hooks.json')).toBe(resolve('hooks.json'))
|
||||
})
|
||||
|
||||
it('preserves POSIX case, literal backslashes, spaces, and Unicode bytes', () => {
|
||||
const sourcePath = '/tmp/Üser Name/e\u0301\\literal/Hooks.JSON'
|
||||
expect(normalizeCodexHookSourcePath(sourcePath)).toBe(sourcePath)
|
||||
})
|
||||
|
||||
it('preserves Windows drive-letter and component casing in the lexical path', () => {
|
||||
expect(normalizeCodexHookSourcePath('d:/Üser Name/Codex/Hooks.JSON')).toBe(
|
||||
'd:\\Üser Name\\Codex\\Hooks.JSON'
|
||||
)
|
||||
})
|
||||
|
||||
it('builds the key byte-for-byte from the normalized source and positional suffix', () => {
|
||||
expect(computeTrustKey(entry('C:/Users/Rod/.codex/../.codex/hooks.json'))).toBe(
|
||||
'C:\\Users\\Rod\\.codex\\hooks.json:stop:2:3'
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe('Codex hook trust key lookup normalization', () => {
|
||||
it('folds Windows separator and case variants without changing the persisted key', () => {
|
||||
const native = 'C:\\Users\\Rod\\.codex\\hooks.json:stop:2:3'
|
||||
const slash = 'c:/users/rod/.codex/hooks.json:stop:2:3'
|
||||
expect(normalizeHookTrustKeyForLookup(native)).toBe(normalizeHookTrustKeyForLookup(slash))
|
||||
})
|
||||
|
||||
it('keeps POSIX paths case-sensitive', () => {
|
||||
const upper = '/home/Üser/.codex/hooks.json:stop:2:3'
|
||||
const lower = '/home/üser/.codex/hooks.json:stop:2:3'
|
||||
expect(normalizeHookTrustKeyForLookup(upper)).not.toBe(normalizeHookTrustKeyForLookup(lower))
|
||||
})
|
||||
})
|
||||
@@ -14,13 +14,14 @@ import {
|
||||
writeFileSync
|
||||
} from 'node:fs'
|
||||
import { tmpdir } from 'node:os'
|
||||
import { dirname, join } from 'node:path'
|
||||
import { join } from 'node:path'
|
||||
import { escapeRegex } from '../../shared/string-utils'
|
||||
import {
|
||||
computeTrustKey,
|
||||
computeTrustedHash,
|
||||
escapeTomlString,
|
||||
getCodexCanonicalTrustPath,
|
||||
getCodexExplicitHomeHookSourcePath,
|
||||
normalizeCodexHookSourcePath,
|
||||
normalizeCodexProjectPathForLookup,
|
||||
normalizeCodexProjectPathForRevocationLookup,
|
||||
parseTrustKey,
|
||||
@@ -277,7 +278,7 @@ describe('computeTrustKey', () => {
|
||||
).toBe('/Users/thebr/.codex/hooks.json:pre_tool_use:0:0')
|
||||
})
|
||||
|
||||
it('uses Codex canonicalized source paths when hooks.json exists', () => {
|
||||
it('lexically normalizes source paths without resolving default-home aliases', () => {
|
||||
const nestedDir = join(tmpDir, 'nested')
|
||||
mkdirSync(nestedDir)
|
||||
const hooksPath = join(nestedDir, '..', 'hooks.json')
|
||||
@@ -291,7 +292,7 @@ describe('computeTrustKey', () => {
|
||||
handlerIndex: 0,
|
||||
command: 'irrelevant'
|
||||
})
|
||||
).toBe(`${realpathSync.native(hooksPath)}:user_prompt_submit:0:0`)
|
||||
).toBe(`${join(tmpDir, 'hooks.json')}:user_prompt_submit:0:0`)
|
||||
})
|
||||
|
||||
it('preserves a hooks.json leaf symlink in the trust key', () => {
|
||||
@@ -308,7 +309,7 @@ describe('computeTrustKey', () => {
|
||||
handlerIndex: 0,
|
||||
command: 'irrelevant'
|
||||
})
|
||||
).toBe(`${join(realpathSync.native(dirname(hooksPath)), 'hooks.json')}:stop:0:0`)
|
||||
).toBe(`${hooksPath}:stop:0:0`)
|
||||
})
|
||||
|
||||
it.skipIf(process.platform === 'win32')(
|
||||
@@ -317,7 +318,7 @@ describe('computeTrustKey', () => {
|
||||
const hooksPath = `/${join(tmpDir, 'hooks.json')}`
|
||||
writeFileSync(hooksPath, '{"hooks":{}}\n', 'utf-8')
|
||||
|
||||
expect(getCodexCanonicalTrustPath(hooksPath)).toBe(realpathSync.native(hooksPath))
|
||||
expect(normalizeCodexHookSourcePath(hooksPath)).toBe(join(tmpDir, 'hooks.json'))
|
||||
}
|
||||
)
|
||||
|
||||
@@ -339,10 +340,24 @@ describe('computeTrustKey', () => {
|
||||
it('preserves literal backslashes in non-Windows-style fallback paths', () => {
|
||||
// Why: SSH/POSIX paths can legally contain `\` as a filename character;
|
||||
// only Windows-style separators should be normalized.
|
||||
expect(getCodexCanonicalTrustPath('/tmp/with\\literal/hooks.json')).toBe(
|
||||
expect(normalizeCodexHookSourcePath('/tmp/with\\literal/hooks.json')).toBe(
|
||||
'/tmp/with\\literal/hooks.json'
|
||||
)
|
||||
})
|
||||
|
||||
it('resolves an explicit home parent while preserving its hooks.json leaf symlink', () => {
|
||||
const logicalHome = join(tmpDir, 'logical-home')
|
||||
const targetHome = join(tmpDir, 'target-home')
|
||||
const targetHooks = join(tmpDir, 'target-hooks.json')
|
||||
mkdirSync(targetHome)
|
||||
writeFileSync(targetHooks, '{"hooks":{}}\n', 'utf-8')
|
||||
symlinkSync(targetHome, logicalHome)
|
||||
symlinkSync(targetHooks, join(targetHome, 'hooks.json'))
|
||||
|
||||
expect(getCodexExplicitHomeHookSourcePath(join(logicalHome, 'hooks.json'))).toBe(
|
||||
join(realpathSync.native(targetHome), 'hooks.json')
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe('upsertHookTrustEntries', () => {
|
||||
|
||||
@@ -9,7 +9,7 @@ import {
|
||||
unlinkSync,
|
||||
writeFileSync
|
||||
} from 'node:fs'
|
||||
import { basename, dirname, join } from 'node:path'
|
||||
import { basename, dirname, join, posix as pathPosix, win32 as pathWin32 } from 'node:path'
|
||||
import { createHash, randomUUID } from 'node:crypto'
|
||||
import { renameFileWithWindowsRetry } from '../codex-accounts/fs-utils'
|
||||
import { foldWslUncPathCaseInsensitiveParts } from '../../shared/wsl-paths'
|
||||
@@ -164,25 +164,64 @@ export function computeTrustedHash(entry: CodexTrustEntry): string {
|
||||
return `sha256:${createHash('sha256').update(serialized).digest('hex')}`
|
||||
}
|
||||
|
||||
// Why: key_source is already home-derived by discovery. Realpath here would
|
||||
// change default-home keys; explicit-home callers canonicalize the home first.
|
||||
export function computeTrustKey(entry: CodexTrustEntry): string {
|
||||
return `${getCodexCanonicalTrustPath(entry.sourcePath)}:${entry.eventLabel}:${entry.groupIndex}:${entry.handlerIndex}`
|
||||
return `${normalizeCodexHookSourcePath(entry.sourcePath)}:${entry.eventLabel}:${entry.groupIndex}:${entry.handlerIndex}`
|
||||
}
|
||||
|
||||
export function getCodexCanonicalTrustPath(sourcePath: string): string {
|
||||
if (process.platform !== 'win32' && isWindowsDriveOrBackslashUncPath(sourcePath)) {
|
||||
// Why: an existing `//` path is valid on POSIX, so let realpath distinguish
|
||||
// it from forward-slash UNC before falling back to Windows normalization.
|
||||
return normalizeWindowsPathForCodexLookup(sourcePath)
|
||||
/**
|
||||
* Returns the hook source path Codex derives after an explicit CODEX_HOME is
|
||||
* canonicalized. The source leaf remains logical because hook discovery only
|
||||
* normalizes the joined path lexically.
|
||||
*/
|
||||
export function getCodexExplicitHomeHookSourcePath(sourcePath: string): string {
|
||||
if (process.platform !== 'win32' && isUnambiguousWindowsPath(sourcePath)) {
|
||||
return normalizeCodexHookSourcePath(sourcePath)
|
||||
}
|
||||
try {
|
||||
// Why: Codex canonicalizes parent directories but preserves a hooks.json
|
||||
// leaf symlink in trust keys, so resolving the whole path misses its RPC listing.
|
||||
return join(realpathSync.native(dirname(sourcePath)), basename(sourcePath))
|
||||
// Why: explicit CODEX_HOME resolves the home directory before hooks.json
|
||||
// is appended, so a symlinked leaf must remain logical in the reported key.
|
||||
return normalizeCodexHookSourcePath(
|
||||
join(realpathSync.native(dirname(sourcePath)), basename(sourcePath))
|
||||
)
|
||||
} catch {
|
||||
return normalizeWindowsPathForCodexLookup(sourcePath)
|
||||
return normalizeCodexHookSourcePath(sourcePath)
|
||||
}
|
||||
}
|
||||
|
||||
/** Matches the platform-native lexical path displayed by hook discovery. */
|
||||
export function normalizeCodexHookSourcePath(sourcePath: string): string {
|
||||
if (isWindowsPathForTrustSource(sourcePath)) {
|
||||
const withoutDevicePrefix = stripWindowsDevicePrefix(sourcePath)
|
||||
const normalized = pathWin32.isAbsolute(withoutDevicePrefix)
|
||||
? pathWin32.normalize(withoutDevicePrefix)
|
||||
: pathWin32.resolve(withoutDevicePrefix)
|
||||
return trimNonRootTrailingSeparators(normalized, pathWin32.parse(normalized).root, /[\\/]/)
|
||||
}
|
||||
const normalized = pathPosix.isAbsolute(sourcePath)
|
||||
? pathPosix.normalize(sourcePath)
|
||||
: pathPosix.resolve(sourcePath)
|
||||
return trimNonRootTrailingSeparators(normalized, pathPosix.parse(normalized).root, /\//)
|
||||
}
|
||||
|
||||
function trimNonRootTrailingSeparators(path: string, root: string, separators: RegExp): string {
|
||||
let end = path.length
|
||||
while (end > root.length && separators.test(path[end - 1]!)) {
|
||||
end -= 1
|
||||
}
|
||||
return path.slice(0, end)
|
||||
}
|
||||
|
||||
function stripWindowsDevicePrefix(sourcePath: string): string {
|
||||
const unc = /^(?:\\\\\?|\\\\\.)\\UNC\\/i.exec(sourcePath)
|
||||
if (unc) {
|
||||
return `\\\\${sourcePath.slice(unc[0].length)}`
|
||||
}
|
||||
const drive = /^(?:\\\\\?|\\\\\.)\\(?=[A-Za-z]:[\\/])/i.exec(sourcePath)
|
||||
return drive ? sourcePath.slice(drive[0].length) : sourcePath
|
||||
}
|
||||
|
||||
function getCodexCanonicalProjectPath(projectPath: string): string {
|
||||
try {
|
||||
// Why: local project trust still needs Codex's realpath shape, but remote
|
||||
@@ -193,13 +232,6 @@ function getCodexCanonicalProjectPath(projectPath: string): string {
|
||||
}
|
||||
}
|
||||
|
||||
function normalizeWindowsPathForCodexLookup(sourcePath: string): string {
|
||||
if (!usesWindowsPathSeparators(sourcePath)) {
|
||||
return sourcePath
|
||||
}
|
||||
return sourcePath.replace(/\//g, '\\')
|
||||
}
|
||||
|
||||
function normalizeWindowsPathSeparators(sourcePath: string): string {
|
||||
if (!usesWindowsPathSeparators(sourcePath)) {
|
||||
return sourcePath
|
||||
@@ -208,13 +240,21 @@ function normalizeWindowsPathSeparators(sourcePath: string): string {
|
||||
}
|
||||
|
||||
function usesWindowsPathSeparators(sourcePath: string): boolean {
|
||||
return isWindowsDriveOrBackslashUncPath(sourcePath) || sourcePath.startsWith('//')
|
||||
return isUnambiguousWindowsPath(sourcePath) || sourcePath.startsWith('//')
|
||||
}
|
||||
|
||||
function isWindowsDriveOrBackslashUncPath(sourcePath: string): boolean {
|
||||
function isUnambiguousWindowsPath(sourcePath: string): boolean {
|
||||
return /^[A-Za-z]:[\\/]/.test(sourcePath) || sourcePath.startsWith('\\\\')
|
||||
}
|
||||
|
||||
function isWindowsPathForTrustSource(sourcePath: string): boolean {
|
||||
return (
|
||||
isUnambiguousWindowsPath(sourcePath) ||
|
||||
(process.platform === 'win32' &&
|
||||
(sourcePath.startsWith('//') || !pathPosix.isAbsolute(sourcePath)))
|
||||
)
|
||||
}
|
||||
|
||||
// Why: Codex and Orca can disagree on quote style, separators, and casing for
|
||||
// the same Windows project, including when the caller targets a remote host.
|
||||
export function normalizeCodexProjectPathForLookup(projectPath: string): string {
|
||||
@@ -336,7 +376,7 @@ export function upsertHookTrustEntriesInContent(
|
||||
const existing =
|
||||
existingContent.charCodeAt(0) === 0xfeff ? existingContent.slice(1) : existingContent
|
||||
let updated = entries.some((entry) =>
|
||||
usesWindowsPathSeparators(getCodexCanonicalTrustPath(entry.sourcePath))
|
||||
usesWindowsPathSeparators(normalizeCodexHookSourcePath(entry.sourcePath))
|
||||
)
|
||||
? ensureHooksStateParentTable(existing)
|
||||
: existing
|
||||
@@ -546,7 +586,13 @@ export function normalizeHookTrustKeyForLookup(key: string): string {
|
||||
const parsed = parseTrustKey(key)
|
||||
// Why: fold by path shape, not host platform — hook sources on WSL and SSH
|
||||
// Windows remotes need the same folding when Orca runs on macOS or Linux.
|
||||
const foldedPath = normalizeCodexProjectPathForLookup(parsed ? parsed.sourcePath : key)
|
||||
const foldedPath = normalizeCodexProjectPathForLookup(
|
||||
parsed
|
||||
? parsed.sourcePath.startsWith('//')
|
||||
? parsed.sourcePath
|
||||
: normalizeCodexHookSourcePath(parsed.sourcePath)
|
||||
: key
|
||||
)
|
||||
return parsed
|
||||
? `${foldedPath}:${parsed.eventLabel}:${parsed.groupIndex}:${parsed.handlerIndex}`
|
||||
: foldedPath
|
||||
|
||||
@@ -5,7 +5,6 @@ import {
|
||||
mkdirSync,
|
||||
mkdtempSync,
|
||||
readFileSync,
|
||||
realpathSync,
|
||||
rmSync,
|
||||
symlinkSync,
|
||||
writeFileSync
|
||||
@@ -17,7 +16,12 @@ import { spawn } from 'node:child_process'
|
||||
import { createServer } from 'node:http'
|
||||
import type { AddressInfo } from 'node:net'
|
||||
import { createManagedCommandMatcher, wrapPosixHookCommand } from '../agent-hooks/installer-utils'
|
||||
import { computeTrustedHash, upsertHookTrustEntriesInContent } from './config-toml-trust'
|
||||
import {
|
||||
computeTrustedHash,
|
||||
getCodexExplicitHomeHookSourcePath,
|
||||
normalizeCodexHookSourcePath,
|
||||
upsertHookTrustEntriesInContent
|
||||
} from './config-toml-trust'
|
||||
|
||||
const { getPathMock, homedirMock } = vi.hoisted(() => ({
|
||||
getPathMock: vi.fn<(name: string) => string>(),
|
||||
@@ -81,14 +85,14 @@ function escapeTomlBasicString(value: string): string {
|
||||
return value.replaceAll('\\', '\\\\').replaceAll('"', '\\"')
|
||||
}
|
||||
|
||||
function hookTrustHeader(key: string): string {
|
||||
const canonicalKey = canonicalizeHookTrustKeyForTest(key)
|
||||
function hookTrustHeader(key: string, useDefaultCodexHome = false): string {
|
||||
const canonicalKey = canonicalizeHookTrustKeyForTest(key, useDefaultCodexHome)
|
||||
return /^[A-Za-z]:[\\/]|^\\\\/.test(canonicalKey) && !canonicalKey.includes("'")
|
||||
? `[hooks.state.'${canonicalKey}']`
|
||||
: `[hooks.state."${escapeTomlBasicString(canonicalKey)}"]`
|
||||
}
|
||||
|
||||
function canonicalizeHookTrustKeyForTest(key: string): string {
|
||||
function canonicalizeHookTrustKeyForTest(key: string, useDefaultCodexHome: boolean): string {
|
||||
const lastColon = key.lastIndexOf(':')
|
||||
const secondLast = lastColon === -1 ? -1 : key.lastIndexOf(':', lastColon - 1)
|
||||
const thirdLast = secondLast === -1 ? -1 : key.lastIndexOf(':', secondLast - 1)
|
||||
@@ -96,15 +100,10 @@ function canonicalizeHookTrustKeyForTest(key: string): string {
|
||||
return key
|
||||
}
|
||||
const sourcePath = key.slice(0, thirdLast)
|
||||
try {
|
||||
// Why: mirrors getCodexCanonicalTrustPath so test expectations match the
|
||||
// native raw-backslash keys Codex writes after hook approval on Windows.
|
||||
return `${realpathSync.native(sourcePath)}${key.slice(thirdLast)}`
|
||||
} catch {
|
||||
return /^[A-Za-z]:[\\/]|^\\\\/.test(sourcePath)
|
||||
? `${sourcePath.replace(/\//g, '\\')}${key.slice(thirdLast)}`
|
||||
: key
|
||||
}
|
||||
const trustSourcePath = useDefaultCodexHome
|
||||
? normalizeCodexHookSourcePath(sourcePath)
|
||||
: getCodexExplicitHomeHookSourcePath(sourcePath)
|
||||
return `${trustSourcePath}${key.slice(thirdLast)}`
|
||||
}
|
||||
|
||||
function markHookTrustDisabled(toml: string, header: string): string {
|
||||
@@ -473,7 +472,7 @@ describe('CodexHookService', () => {
|
||||
const runtimeToml = readFileSync(join(managedCodexHome, 'config.toml'), 'utf-8')
|
||||
expect(runtimeToml).toContain(hookTrustHeader(`${managedHooksPath}:stop:1:0`))
|
||||
expect(runtimeToml).toContain(hookTrustHeader(`${managedHooksPath}:stop:0:0`))
|
||||
expect(runtimeToml).not.toContain(hookTrustHeader(`${systemHooksPath}:stop:0:0`))
|
||||
expect(runtimeToml).not.toContain(hookTrustHeader(`${systemHooksPath}:stop:0:0`, true))
|
||||
})
|
||||
|
||||
it('runs managed PostToolUse status before mirrored user hooks', () => {
|
||||
@@ -521,7 +520,7 @@ describe('CodexHookService', () => {
|
||||
const runtimeToml = readFileSync(join(managedCodexHome, 'config.toml'), 'utf-8')
|
||||
expect(runtimeToml).toContain(hookTrustHeader(`${managedHooksPath}:post_tool_use:0:0`))
|
||||
expect(runtimeToml).toContain(hookTrustHeader(`${managedHooksPath}:post_tool_use:1:0`))
|
||||
expect(runtimeToml).not.toContain(hookTrustHeader(`${systemHooksPath}:post_tool_use:0:0`))
|
||||
expect(runtimeToml).not.toContain(hookTrustHeader(`${systemHooksPath}:post_tool_use:0:0`, true))
|
||||
})
|
||||
|
||||
it('mirrors system user hook approvals when the system trust indices are stale', () => {
|
||||
@@ -573,8 +572,8 @@ describe('CodexHookService', () => {
|
||||
expect(runtimeToml).toContain(hookTrustHeader(`${managedHooksPath}:stop:0:0`))
|
||||
expect(runtimeToml).toContain(hookTrustHeader(`${managedHooksPath}:stop:1:0`))
|
||||
expect(runtimeToml).toContain(hookTrustHeader(`${managedHooksPath}:stop:2:0`))
|
||||
expect(runtimeToml).not.toContain(hookTrustHeader(`${systemHooksPath}:stop:0:0`))
|
||||
expect(runtimeToml).not.toContain(hookTrustHeader(`${systemHooksPath}:stop:1:0`))
|
||||
expect(runtimeToml).not.toContain(hookTrustHeader(`${systemHooksPath}:stop:0:0`, true))
|
||||
expect(runtimeToml).not.toContain(hookTrustHeader(`${systemHooksPath}:stop:1:0`, true))
|
||||
})
|
||||
|
||||
it('skips plugin-placeholder system hooks when mirroring into runtime CODEX_HOME', () => {
|
||||
@@ -679,7 +678,7 @@ describe('CodexHookService', () => {
|
||||
)}\n`,
|
||||
'utf-8'
|
||||
)
|
||||
const disabledPostCompactHeader = hookTrustHeader(`${systemHooksPath}:post_compact:0:0`)
|
||||
const disabledPostCompactHeader = hookTrustHeader(`${systemHooksPath}:post_compact:0:0`, true)
|
||||
const systemToml = upsertHookTrustEntriesInContent('model = "system-model"\n', [
|
||||
{
|
||||
sourcePath: systemHooksPath,
|
||||
@@ -719,8 +718,8 @@ describe('CodexHookService', () => {
|
||||
expect(runtimeToml).toContain(
|
||||
`${hookTrustHeader(`${managedHooksPath}:post_compact:0:0`)}\nenabled = false`
|
||||
)
|
||||
expect(runtimeToml).not.toContain(hookTrustHeader(`${systemHooksPath}:pre_compact:0:0`))
|
||||
expect(runtimeToml).not.toContain(hookTrustHeader(`${systemHooksPath}:post_compact:0:0`))
|
||||
expect(runtimeToml).not.toContain(hookTrustHeader(`${systemHooksPath}:pre_compact:0:0`, true))
|
||||
expect(runtimeToml).not.toContain(hookTrustHeader(`${systemHooksPath}:post_compact:0:0`, true))
|
||||
})
|
||||
|
||||
it('removes runtime user hook trust after system approval is revoked', () => {
|
||||
@@ -813,7 +812,7 @@ describe('CodexHookService', () => {
|
||||
})}\n`,
|
||||
'utf-8'
|
||||
)
|
||||
const disabledStopHeader = hookTrustHeader(`${systemHooksPath}:stop:0:0`)
|
||||
const disabledStopHeader = hookTrustHeader(`${systemHooksPath}:stop:0:0`, true)
|
||||
const systemToml = upsertHookTrustEntriesInContent('model = "system-model"\n', [
|
||||
{
|
||||
sourcePath: systemHooksPath,
|
||||
@@ -1336,7 +1335,7 @@ describe('CodexHookService', () => {
|
||||
`${managedHooksPath}:permission_request:0:0`
|
||||
)
|
||||
const legacyPermissionHeader = `[hooks.state."${escapeTomlBasicString(
|
||||
`${realpathSync.native(managedHooksPath).replace(/\\/g, '/')}:permission_request:0:0`
|
||||
`${getCodexExplicitHomeHookSourcePath(managedHooksPath).replace(/\\/g, '/')}:permission_request:0:0`
|
||||
)}"]`
|
||||
const installedToml = readFileSync(runtimeTomlPath, 'utf-8')
|
||||
expect(installedToml).toContain(canonicalPermissionHeader)
|
||||
|
||||
@@ -37,7 +37,8 @@ import {
|
||||
computeTrustKey,
|
||||
computeTrustedHash,
|
||||
escapeTomlString,
|
||||
getCodexCanonicalTrustPath,
|
||||
getCodexExplicitHomeHookSourcePath,
|
||||
normalizeCodexHookSourcePath,
|
||||
normalizeCodexProjectPathForLookup,
|
||||
normalizeHookTrustKeyForLookup,
|
||||
parseTrustKey,
|
||||
@@ -262,11 +263,11 @@ function removeStaleRuntimeHookTrustEntries(
|
||||
entry.trustedHash ?? computeTrustedHash(entry)
|
||||
])
|
||||
)
|
||||
const canonicalRuntimeHooksPath = getCodexCanonicalTrustPath(runtimeHooksPath)
|
||||
const canonicalRuntimeHooksPath = getCodexExplicitHomeHookSourcePath(runtimeHooksPath)
|
||||
const staleKeys: string[] = []
|
||||
for (const [key, state] of readHookTrustEntries(tomlPath)) {
|
||||
const parsed = parseTrustKey(key)
|
||||
if (!parsed || getCodexCanonicalTrustPath(parsed.sourcePath) !== canonicalRuntimeHooksPath) {
|
||||
if (!parsed || normalizeCodexHookSourcePath(parsed.sourcePath) !== canonicalRuntimeHooksPath) {
|
||||
continue
|
||||
}
|
||||
if (expectedHashes.get(normalizeHookTrustKeyForLookup(key)) === state.trustedHash) {
|
||||
@@ -434,13 +435,13 @@ function getTrustedSystemHookHashesByEvent(
|
||||
trustEntries: ReadonlyMap<string, CodexHookTrustState>
|
||||
): Map<CodexEventLabel, Map<string, boolean>> {
|
||||
const trustedHashesByEvent = new Map<CodexEventLabel, Map<string, boolean>>()
|
||||
const canonicalSystemConfigPath = getCodexCanonicalTrustPath(systemConfigPath)
|
||||
const canonicalSystemConfigPath = normalizeCodexHookSourcePath(systemConfigPath)
|
||||
for (const [key, state] of trustEntries) {
|
||||
const parsed = parseTrustKey(key)
|
||||
if (!parsed || !state.trustedHash) {
|
||||
continue
|
||||
}
|
||||
if (getCodexCanonicalTrustPath(parsed.sourcePath) !== canonicalSystemConfigPath) {
|
||||
if (normalizeCodexHookSourcePath(parsed.sourcePath) !== canonicalSystemConfigPath) {
|
||||
continue
|
||||
}
|
||||
let hashes = trustedHashesByEvent.get(parsed.eventLabel)
|
||||
@@ -469,6 +470,7 @@ function collectMirroredRuntimeUserHookTrustEntries(
|
||||
}
|
||||
|
||||
const entries: MirroredRuntimeUserHookTrustEntry[] = []
|
||||
const trustSourcePath = getCodexExplicitHomeHookSourcePath(runtimeConfigPath)
|
||||
for (const [eventName, definitions] of Object.entries(runtimeHooks)) {
|
||||
if (!Array.isArray(definitions)) {
|
||||
continue
|
||||
@@ -480,7 +482,7 @@ function collectMirroredRuntimeUserHookTrustEntries(
|
||||
return
|
||||
}
|
||||
const entry = createCodexHookTrustEntry(
|
||||
runtimeConfigPath,
|
||||
trustSourcePath,
|
||||
eventName,
|
||||
groupIndex,
|
||||
handlerIndex,
|
||||
@@ -733,7 +735,8 @@ function removeRuntimeManagedHookTrustEntries(configPath: string): void {
|
||||
sourcePath: configPath,
|
||||
command: getManagedCommand(getManagedScriptPath()),
|
||||
managedEventLabels: CODEX_MANAGED_EVENT_LABELS,
|
||||
timeoutSec: MANAGED_HOOK_TIMEOUT_SECONDS
|
||||
timeoutSec: MANAGED_HOOK_TIMEOUT_SECONDS,
|
||||
sourceUsesExplicitCodexHome: true
|
||||
})
|
||||
} catch (error) {
|
||||
// Best effort — stale trust entries are harmless once hooks.json no
|
||||
@@ -1194,6 +1197,7 @@ export class CodexHookService {
|
||||
const missing: string[] = []
|
||||
const trustMissing: string[] = []
|
||||
const disabled: string[] = []
|
||||
const trustSourcePath = getCodexExplicitHomeHookSourcePath(configPath)
|
||||
let presentCount = 0
|
||||
for (const eventName of CODEX_EVENTS) {
|
||||
const definitions = Array.isArray(config.hooks?.[eventName]) ? config.hooks![eventName]! : []
|
||||
@@ -1227,7 +1231,7 @@ export class CodexHookService {
|
||||
// Codex folds the handler timeout into its trust hash. Hash the same
|
||||
// timeout here or status would report every managed hook as stale-trust.
|
||||
const trustInput: CodexTrustEntry = {
|
||||
sourcePath: configPath,
|
||||
sourcePath: trustSourcePath,
|
||||
eventLabel: CODEX_EVENT_LABEL[eventName],
|
||||
groupIndex: foundGroupIndex,
|
||||
handlerIndex: foundHandlerIndex,
|
||||
@@ -1352,6 +1356,7 @@ export class CodexHookService {
|
||||
({ entry }) => entry
|
||||
)
|
||||
const managedTrustEntries: CodexTrustEntry[] = []
|
||||
const trustSourcePath = getCodexExplicitHomeHookSourcePath(configPath)
|
||||
for (const eventName of CODEX_EVENTS) {
|
||||
const current = Array.isArray(nextHooks[eventName]) ? nextHooks[eventName] : []
|
||||
const cleaned = removeManagedCommands(current, isManagedCommand)
|
||||
@@ -1365,7 +1370,7 @@ export class CodexHookService {
|
||||
// timeoutSec mirrors the hook's `timeout` so the trust hash matches the
|
||||
// entry actually written to hooks.json.
|
||||
managedTrustEntries.push({
|
||||
sourcePath: configPath,
|
||||
sourcePath: trustSourcePath,
|
||||
eventLabel: CODEX_EVENT_LABEL[eventName],
|
||||
groupIndex: 0,
|
||||
handlerIndex: 0,
|
||||
|
||||
@@ -1,11 +1,20 @@
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs'
|
||||
import {
|
||||
existsSync,
|
||||
mkdirSync,
|
||||
mkdtempSync,
|
||||
readFileSync,
|
||||
rmSync,
|
||||
symlinkSync,
|
||||
writeFileSync
|
||||
} from 'node:fs'
|
||||
import { tmpdir } from 'node:os'
|
||||
import type * as Os from 'node:os'
|
||||
import { join } from 'node:path'
|
||||
import {
|
||||
computeTrustKey,
|
||||
computeTrustedHash,
|
||||
getCodexExplicitHomeHookSourcePath,
|
||||
readHookTrustEntries,
|
||||
upsertHookTrustEntriesInContent,
|
||||
type CodexTrustEntry
|
||||
@@ -97,7 +106,7 @@ function runtimeUserStopEntry(): CodexTrustEntry {
|
||||
// install() prepends the managed status hook on Stop, so the mirrored user
|
||||
// hook lands at groupIndex 1.
|
||||
return {
|
||||
sourcePath: join(runtimeHomeDir(), 'hooks.json'),
|
||||
sourcePath: getCodexExplicitHomeHookSourcePath(join(runtimeHomeDir(), 'hooks.json')),
|
||||
eventLabel: 'stop',
|
||||
groupIndex: 1,
|
||||
handlerIndex: 0,
|
||||
@@ -121,6 +130,25 @@ function readSystemToml(): string {
|
||||
}
|
||||
|
||||
describe('codex hook trust write-back promotion', () => {
|
||||
it('mirrors default-home trust when the .codex directory is a symlink', () => {
|
||||
const targetHome = join(tmpHome, 'dotfiles-codex')
|
||||
mkdirSync(targetHome)
|
||||
symlinkSync(targetHome, systemCodexDir(), process.platform === 'win32' ? 'junction' : 'dir')
|
||||
writeSystemUserHook()
|
||||
const systemEntry = systemUserStopEntry()
|
||||
writeFileSync(
|
||||
join(systemCodexDir(), 'config.toml'),
|
||||
upsertHookTrustEntriesInContent('', [systemEntry])
|
||||
)
|
||||
|
||||
expect(new CodexHookService().install().state).toBe('installed')
|
||||
|
||||
const runtimeTrust = readHookTrustEntries(join(runtimeHomeDir(), 'config.toml'))
|
||||
expect(runtimeTrust.get(computeTrustKey(runtimeUserStopEntry()))?.trustedHash).toBe(
|
||||
computeTrustedHash(systemEntry)
|
||||
)
|
||||
})
|
||||
|
||||
it('keeps an in-Orca approval of a user hook across launches and promotes it to ~/.codex', () => {
|
||||
writeSystemUserHook()
|
||||
const service = new CodexHookService()
|
||||
|
||||
@@ -7,7 +7,8 @@ import {
|
||||
} from '../agent-hooks/installer-utils'
|
||||
import { getOrcaManagedCodexHomePath, getSystemCodexHomePath } from './codex-home-paths'
|
||||
import {
|
||||
getCodexCanonicalTrustPath,
|
||||
getCodexExplicitHomeHookSourcePath,
|
||||
normalizeCodexHookSourcePath,
|
||||
normalizeHookTrustKeyForLookup,
|
||||
parseTrustKey,
|
||||
readHookTrustEntries,
|
||||
@@ -81,11 +82,14 @@ export function snapshotCodexRuntimeHookTrustProvenance(): void {
|
||||
try {
|
||||
const runtimeHomePath = getOrcaManagedCodexHomePath()
|
||||
const runtimeHooksPath = join(runtimeHomePath, 'hooks.json')
|
||||
const canonicalRuntimeHooksPath = getCodexCanonicalTrustPath(runtimeHooksPath)
|
||||
const canonicalRuntimeHooksPath = getCodexExplicitHomeHookSourcePath(runtimeHooksPath)
|
||||
const entries: Record<string, HookTrustProvenanceEntry> = {}
|
||||
for (const [key, state] of readHookTrustEntries(join(runtimeHomePath, 'config.toml'))) {
|
||||
const parsed = parseTrustKey(key)
|
||||
if (!parsed || getCodexCanonicalTrustPath(parsed.sourcePath) !== canonicalRuntimeHooksPath) {
|
||||
if (
|
||||
!parsed ||
|
||||
normalizeCodexHookSourcePath(parsed.sourcePath) !== canonicalRuntimeHooksPath
|
||||
) {
|
||||
continue
|
||||
}
|
||||
entries[normalizeHookTrustKeyForLookup(key)] = {
|
||||
@@ -126,8 +130,8 @@ function promoteCodexRuntimeHookApprovalsToSystemUnsafe(): void {
|
||||
const systemHomePath = getSystemCodexHomePath()
|
||||
const runtimeHooksPath = join(runtimeHomePath, 'hooks.json')
|
||||
const systemHooksPath = join(systemHomePath, 'hooks.json')
|
||||
const canonicalRuntimeHooksPath = getCodexCanonicalTrustPath(runtimeHooksPath)
|
||||
if (canonicalRuntimeHooksPath === getCodexCanonicalTrustPath(systemHooksPath)) {
|
||||
const canonicalRuntimeHooksPath = getCodexExplicitHomeHookSourcePath(runtimeHooksPath)
|
||||
if (canonicalRuntimeHooksPath === normalizeCodexHookSourcePath(systemHooksPath)) {
|
||||
return
|
||||
}
|
||||
const runtimeTomlPath = join(runtimeHomePath, 'config.toml')
|
||||
@@ -164,7 +168,7 @@ function promoteCodexRuntimeHookApprovalsToSystemUnsafe(): void {
|
||||
continue
|
||||
}
|
||||
const parsed = parseTrustKey(key)
|
||||
if (!parsed || getCodexCanonicalTrustPath(parsed.sourcePath) !== canonicalRuntimeHooksPath) {
|
||||
if (!parsed || normalizeCodexHookSourcePath(parsed.sourcePath) !== canonicalRuntimeHooksPath) {
|
||||
continue
|
||||
}
|
||||
const previous = provenance.get(normalizeHookTrustKeyForLookup(key))
|
||||
@@ -191,7 +195,7 @@ function promoteCodexRuntimeHookApprovalsToSystemUnsafe(): void {
|
||||
continue
|
||||
}
|
||||
const runtimeEntry = createCodexHookTrustEntry(
|
||||
runtimeHooksPath,
|
||||
canonicalRuntimeHooksPath,
|
||||
eventName,
|
||||
parsed.groupIndex,
|
||||
parsed.handlerIndex,
|
||||
|
||||
Reference in New Issue
Block a user