Add native Codex hooks integration (#2319)

* Add native Codex hooks integration

* Address Codex review feedback

* Use durable Codex marketplace root

* Address Codex file context review feedback

* Harden Codex installer review paths

* Report Codex legacy cleanup failures

* fix: keep MCP manifests in marketplace sync

* fix: bundle zod in MCP server

* fix: warn on Codex legacy cleanup failure

* Fix hook observation readiness timeouts

* Address Codex hook review notes

* Tighten Codex MCP file context matching

* Resolve final Codex review nits

* Add Codex marketplace version guidance

* Reset worker failure counter on API fallback

* Fix Codex cat flag file extraction
This commit is contained in:
Alex Newman
2026-05-06 01:55:27 -07:00
committed by GitHub
parent a5bb6b346a
commit 56db06811e
33 changed files with 1628 additions and 504 deletions
@@ -0,0 +1,63 @@
import { afterEach, beforeEach, describe, expect, it } from 'bun:test';
import { mkdtempSync, rmSync, writeFileSync } from 'fs';
import { tmpdir } from 'os';
import { join } from 'path';
import { extractFilePaths } from '../../../src/cli/adapters/codex-file-context.js';
let tmpDir: string;
beforeEach(() => {
tmpDir = mkdtempSync(join(tmpdir(), 'codex-file-context-'));
writeFileSync(join(tmpDir, 'README.md'), 'readme');
writeFileSync(join(tmpDir, 'src.ts'), 'source');
writeFileSync(join(tmpDir, 'notes.txt'), 'notes');
});
afterEach(() => {
rmSync(tmpDir, { recursive: true, force: true });
});
describe('extractFilePaths', () => {
it('extracts existing files from Codex Bash read commands', () => {
const paths = extractFilePaths('Bash', {
command: 'cat README.md && head -n 20 src.ts && cat missing.md',
}, tmpDir);
expect(paths).toEqual(['README.md', 'src.ts']);
});
it('does not consume cat boolean flags as file arguments', () => {
const paths = extractFilePaths('Bash', {
command: 'cat -n README.md',
}, tmpDir);
expect(paths).toEqual(['README.md']);
});
it('ignores non-read Bash commands', () => {
const paths = extractFilePaths('Bash', {
command: 'rm README.md; echo src.ts',
}, tmpDir);
expect(paths).toEqual([]);
});
it('extracts MCP read tool path arrays', () => {
const paths = extractFilePaths('mcp__local_filesystem__read_file', {
paths: ['README.md', 'notes.txt', 'missing.txt'],
}, tmpDir);
expect(paths).toEqual(['README.md', 'notes.txt']);
});
it('extracts MCP exact read/view/cat tool names', () => {
expect(extractFilePaths('mcp__fs__read', { path: 'README.md' }, tmpDir)).toEqual(['README.md']);
expect(extractFilePaths('mcp__fs__view_files', { paths: ['README.md'] }, tmpDir)).toEqual(['README.md']);
});
it('ignores MCP tool names that only contain read verbs as a prefix', () => {
expect(extractFilePaths('mcp__fs__read_write', { path: 'README.md' }, tmpDir)).toEqual([]);
expect(extractFilePaths('mcp__server__readonly', { path: 'README.md' }, tmpDir)).toEqual([]);
});
});
@@ -18,8 +18,12 @@ mock.module('../../../src/shared/hook-settings.js', () => ({
}));
let mockExtractedMessage: string = '';
let extractCallCount = 0;
mock.module('../../../src/shared/transcript-parser.js', () => ({
extractLastMessage: () => mockExtractedMessage,
extractLastMessage: () => {
extractCallCount += 1;
return mockExtractedMessage;
},
}));
const workerCallLog: Array<{ path: string; method: string; body: any }> = [];
@@ -44,6 +48,7 @@ let loggerSpies: ReturnType<typeof spyOn>[] = [];
beforeEach(() => {
workerCallLog.length = 0;
mockExtractedMessage = '';
extractCallCount = 0;
loggerSpies = [
spyOn(logger, 'info').mockImplementation(() => {}),
spyOn(logger, 'debug').mockImplementation(() => {}),
@@ -72,6 +77,39 @@ function postedBody(): any {
}
describe('summarizeHandler — privacy tag stripping', () => {
it('uses Codex lastAssistantMessage directly without reading a transcript', async () => {
const { summarizeHandler } = await import('../../../src/cli/handlers/summarize.js');
const result = await summarizeHandler.execute({
sessionId: 'sess-codex',
cwd: '/tmp',
platform: 'codex',
lastAssistantMessage: 'Codex answer <private>SECRET</private>',
});
expect(result.continue).toBe(true);
expect(extractCallCount).toBe(0);
const body = postedBody();
expect(body.last_assistant_message).toBe('Codex answer');
expect(body.platformSource).toBe('codex');
});
it('short-circuits Codex stop hook re-entry', async () => {
const { summarizeHandler } = await import('../../../src/cli/handlers/summarize.js');
const result = await summarizeHandler.execute({
sessionId: 'sess-codex',
cwd: '/tmp',
platform: 'codex',
stopHookActive: true,
lastAssistantMessage: 'ignored',
});
expect(result.continue).toBe(true);
expect(result.suppressOutput).toBe(true);
expect(result.exitCode).toBe(0);
expect(extractCallCount).toBe(0);
expect(workerCallLog).toHaveLength(0);
});
it('strips <private> tags and their content from last_assistant_message', async () => {
mockExtractedMessage = 'Hello <private>SECRET-VALUE-42</private> world';