import os from 'node:os'; import path from 'node:path'; import { isObject, parseStrictJsonObject, type JsonObject } from './strict-json.js'; export const AGENT_KEY = 'codex'; export const CONFIG_FILE = 'hooks.json'; export const GUARD_SCRIPT = 'guard.js'; export const AUDIT_SCRIPT = 'hook.js'; export const HOOK_TIMEOUT_SECONDS = 10; export const OWNED_DESCRIPTION = 'Elydora audit and freeze enforcement'; export const GUARD_STATUS = 'Checking Elydora agent state'; export const AUDIT_STATUS = 'Recording Elydora tool use'; export type CodexHooks = Record; export interface CodexDocument { readonly exists: boolean; readonly filePath: string; readonly root: JsonObject; readonly hooks: CodexHooks; readonly raw?: string; } export interface RenderedDocument { readonly document: CodexDocument; readonly changed: boolean; readonly next?: string; } export interface RuntimeContract { readonly agentId: string; readonly guardPath: string; readonly auditPath: string; } interface ParsedArgument { readonly value: string; readonly next: number; } function samePath(left: string, right: string): boolean { const normalizedLeft = path.resolve(left); const normalizedRight = path.resolve(right); return process.platform === 'win32' ? normalizedLeft.toLowerCase() === normalizedRight.toLowerCase() : normalizedLeft === normalizedRight; } function sameAgentId(left: string, right: string): boolean { return process.platform === 'win32' ? left.toLowerCase() === right.toLowerCase() : left === right; } function quotePosix(value: string): string { return `'${value.replaceAll("'", `'"'"'`)}'`; } function quotePowerShell(value: string): string { return `'${value.replaceAll("'", "''")}'`; } function windowsPowerShellPath(): string { const configuredRoot = process.platform === 'win32' ? process.env.SystemRoot : undefined; const systemRoot = configuredRoot && path.win32.isAbsolute(configuredRoot) && !/["%\r\n]/.test(configuredRoot) ? configuredRoot : 'C:\\Windows'; return path.win32.join( systemRoot, 'System32', 'WindowsPowerShell', 'v1.0', 'powershell.exe', ); } function windowsCommand(scriptPath: string): string { const source = `& ${quotePowerShell(process.execPath)} ${quotePowerShell(scriptPath)}; exit $LASTEXITCODE`; const encoded = Buffer.from(source, 'utf16le').toString('base64'); return `"${windowsPowerShellPath()}" -NoLogo -NoProfile -NonInteractive -EncodedCommand ${encoded}`; } export function buildHandler(scriptPath: string, statusMessage: string): JsonObject { return { type: 'command', command: `${quotePosix(process.execPath)} ${quotePosix(scriptPath)}`, commandWindows: windowsCommand(scriptPath), timeout: HOOK_TIMEOUT_SECONDS, statusMessage, }; } function readPosixArgument(command: string, start: number): ParsedArgument | undefined { if (command[start] !== "'") return undefined; const apostrophe = `'"'"'`; let value = ''; for (let index = start + 1; index < command.length;) { if (command.startsWith(apostrophe, index)) { value += "'"; index += apostrophe.length; continue; } if (command[index] === "'") return { value, next: index + 1 }; value += command[index]; index += 1; } return undefined; } function parsePosixCommand(command: unknown): readonly [string, string] | undefined { if (typeof command !== 'string') return undefined; const executable = readPosixArgument(command, 0); if (!executable || command[executable.next] !== ' ') return undefined; const script = readPosixArgument(command, executable.next + 1); if (!script || script.next !== command.length) return undefined; return [executable.value, script.value]; } function readPowerShellArgument(command: string, start: number): ParsedArgument | undefined { if (command[start] !== "'") return undefined; let value = ''; for (let index = start + 1; index < command.length; index += 1) { if (command[index] !== "'") { value += command[index]; continue; } if (command[index + 1] === "'") { value += "'"; index += 1; continue; } return { value, next: index + 1 }; } return undefined; } function parsePowerShellSource(source: string): readonly [string, string] | undefined { if (!source.startsWith('& ')) return undefined; const executable = readPowerShellArgument(source, 2); if (!executable || source[executable.next] !== ' ') return undefined; const script = readPowerShellArgument(source, executable.next + 1); if (!script || source.slice(script.next) !== '; exit $LASTEXITCODE') return undefined; return [executable.value, script.value]; } function parseWindowsCommand(command: unknown): readonly [string, string] | undefined { if (typeof command !== 'string') return undefined; const match = /^"([^"\r\n]+)" -NoLogo -NoProfile -NonInteractive -EncodedCommand ([A-Za-z0-9+/]+={0,2})$/.exec(command); if (!match || !path.win32.isAbsolute(match[1]) || path.win32.basename(match[1]).toLowerCase() !== 'powershell.exe') return undefined; const encoded = match[2]; if (!/^[A-Za-z0-9+/]+={0,2}$/.test(encoded)) return undefined; const buffer = Buffer.from(encoded, 'base64'); if (buffer.toString('base64') !== encoded || buffer.length % 2 !== 0) return undefined; return parsePowerShellSource(buffer.toString('utf16le')); } function parseLegacyWindowsCommand(command: unknown): readonly [string, string] | undefined { if (typeof command !== 'string') return undefined; const match = /^"([^"\r\n]+)" "([^"\r\n]+)"$/.exec(command); return match ? [match[1], match[2]] : undefined; } function isNodeExecutable(filePath: string): boolean { const basename = path.basename(filePath); return basename === 'node' || basename.toLowerCase() === 'node.exe'; } function exactHandlerKeys(handler: JsonObject): boolean { return Object.keys(handler).sort().join('|') === 'command|commandWindows|statusMessage|timeout|type'; } function managedScriptPath(handler: JsonObject, statusMessage: string): string | undefined { if (!exactHandlerKeys(handler) || handler.type !== 'command' || handler.timeout !== HOOK_TIMEOUT_SECONDS || handler.statusMessage !== statusMessage) return undefined; const posix = parsePosixCommand(handler.command); const windows = parseWindowsCommand(handler.commandWindows) ?? parseLegacyWindowsCommand(handler.commandWindows); if (!posix || !windows || !path.isAbsolute(posix[0]) || !path.isAbsolute(posix[1]) || !isNodeExecutable(posix[0]) || !isNodeExecutable(windows[0]) || !samePath(posix[0], windows[0]) || !samePath(posix[1], windows[1])) return undefined; return posix[1]; } function managedAgentId( handler: JsonObject, scriptName: string, statusMessage: string, ): string | undefined { const scriptPath = managedScriptPath(handler, statusMessage); if (!scriptPath || path.basename(scriptPath) !== scriptName) return undefined; const agentDirectory = path.dirname(scriptPath); if (!samePath(path.dirname(agentDirectory), path.join(os.homedir(), '.elydora'))) { return undefined; } const agentId = path.basename(agentDirectory); return agentId && agentId !== '.' && agentId !== '..' ? agentId : undefined; } function readHooks(value: unknown, label: string): CodexHooks { if (value === undefined) return {}; if (!isObject(value)) throw new Error(`${label} field "hooks" must be an object`); const hooks: CodexHooks = {}; for (const [event, candidate] of Object.entries(value)) { if (!Array.isArray(candidate)) { throw new Error(`${label} field "hooks.${event}" must be an array`); } hooks[event] = candidate.map((group, groupIndex) => { if (!isObject(group)) { throw new Error(`${label} matcher group hooks.${event}[${groupIndex}] must be an object`); } if (!Array.isArray(group.hooks)) { throw new Error( `${label} matcher group hooks.${event}[${groupIndex}] must contain a hooks array`, ); } for (const [handlerIndex, handler] of group.hooks.entries()) { if (!isObject(handler)) { throw new Error( `${label} handler hooks.${event}[${groupIndex}].hooks[${handlerIndex}] must be an object`, ); } } return group; }); } return hooks; } export function parseDocument(filePath: string, raw: string): CodexDocument { const label = `Codex user hooks at ${filePath}`; const root = parseStrictJsonObject(raw, label); return { exists: true, filePath, root, hooks: readHooks(root.hooks, label), raw, }; } export function createDocument(filePath: string): CodexDocument { return { exists: false, filePath, root: { description: OWNED_DESCRIPTION }, hooks: {}, }; } function removeFromGroups( groups: JsonObject[], scriptName: string, statusMessage: string, agentId?: string, ): JsonObject[] { const result: JsonObject[] = []; for (const group of groups) { const handlers = (group.hooks as JsonObject[]).filter((handler) => { const managedId = managedAgentId(handler, scriptName, statusMessage); return !managedId || (agentId !== undefined && !sameAgentId(managedId, agentId)); }); if (handlers.length > 0) result.push({ ...group, hooks: handlers }); else if (!exactMatcherGroup(group)) result.push({ ...group, hooks: [] }); } return result; } export function removeManagedHooks(hooks: CodexHooks, agentId?: string): CodexHooks { const next: CodexHooks = Object.fromEntries( Object.entries(hooks).map(([event, groups]) => [event, [...groups]]), ); for (const [event, scriptName, statusMessage] of [ ['PreToolUse', GUARD_SCRIPT, GUARD_STATUS], ['PostToolUse', AUDIT_SCRIPT, AUDIT_STATUS], ] as const) { const groups = removeFromGroups(next[event] ?? [], scriptName, statusMessage, agentId); if (groups.length > 0) next[event] = groups; else delete next[event]; } return next; } function entirelyManaged(document: CodexDocument): boolean { if (!document.exists || document.root.description !== OWNED_DESCRIPTION || !Object.keys(document.root).every((key) => key === 'description' || key === 'hooks')) { return false; } const events = Object.entries(document.hooks); if (events.length === 0) return false; let handlerCount = 0; for (const [event, groups] of events) { const contract = event === 'PreToolUse' ? [GUARD_SCRIPT, GUARD_STATUS] as const : event === 'PostToolUse' ? [AUDIT_SCRIPT, AUDIT_STATUS] as const : undefined; if (!contract || groups.length === 0) return false; for (const group of groups) { const handlers = group.hooks as JsonObject[]; if (!exactMatcherGroup(group) || handlers.length === 0 || handlers.some((handler) => !managedAgentId(handler, contract[0], contract[1]))) { return false; } handlerCount += handlers.length; } } return handlerCount > 0; } export function renderDocument( document: CodexDocument, hooks: CodexHooks, ): RenderedDocument { if (!document.exists && Object.keys(hooks).length === 0) { return { document, changed: false }; } if (Object.keys(hooks).length === 0 && entirelyManaged(document)) { return { document, changed: true }; } const root: JsonObject = { ...document.root }; if (Object.keys(hooks).length > 0) root.hooks = hooks; else delete root.hooks; const next = `${JSON.stringify(root, null, 2)}\n`; return { document, changed: next !== document.raw, next }; } function exactMatcherGroup(group: JsonObject): boolean { return Object.keys(group).sort().join('|') === 'hooks|matcher' && group.matcher === '*'; } function managedIds( groups: JsonObject[], scriptName: string, statusMessage: string, ): Map { const result = new Map(); for (const group of groups) { if (!exactMatcherGroup(group)) continue; for (const handler of group.hooks as JsonObject[]) { const agentId = managedAgentId(handler, scriptName, statusMessage); if (!agentId) continue; const key = process.platform === 'win32' ? agentId.toLowerCase() : agentId; result.set(key, agentId); } } return result; } export function runtimeContracts(hooks: CodexHooks): RuntimeContract[] { const guards = managedIds(hooks.PreToolUse ?? [], GUARD_SCRIPT, GUARD_STATUS); const audits = managedIds(hooks.PostToolUse ?? [], AUDIT_SCRIPT, AUDIT_STATUS); const root = path.join(os.homedir(), '.elydora'); return [...guards] .filter(([key]) => audits.has(key)) .map(([, agentId]) => ({ agentId, guardPath: path.join(root, agentId, GUARD_SCRIPT), auditPath: path.join(root, agentId, AUDIT_SCRIPT), })); }