Merge PR #150 from aakhter/cod-166-codex-generated-artifact-attachments: Codex generated artifacts as attachment cards

Includes review fixes: source arg threaded through the deps lambda (was silently dropped), codex-mode gating, realpath-first trust decisions with homedir-anchored markers, image thumbnail passthrough, ANSI-stripped scanning.

# Conflicts:
#	src/session.ts
This commit is contained in:
Codeman maintainer
2026-07-12 20:08:32 +02:00
11 changed files with 402 additions and 27 deletions
+77 -1
View File
@@ -1,6 +1,7 @@
import { describe, expect, it } from 'vitest';
import { Session } from '../src/session.js';
import { parseAttachmentMagicLinks } from '../src/attachment-magic.js';
import { parseAttachmentMagicLinks, parseTerminalAttachmentRequests } from '../src/attachment-magic.js';
import { isSupportedAttachmentExtension } from '../src/attachment-registry.js';
describe('attachment magic links', () => {
it('extracts absolute paths from codeman attach magic URLs', () => {
@@ -54,4 +55,79 @@ describe('attachment magic links', () => {
expect(requested).toEqual(['/tmp/deck.pptx']);
});
it('extracts Codex generated image file URLs from saved-to terminal output', () => {
const requests = parseTerminalAttachmentRequests(
'Saved to: file:///Users/aamer/.codex-personal/generated_images/mockup%20one.png',
{ codexArtifacts: true }
);
expect(requests).toEqual([
{
path: '/Users/aamer/.codex-personal/generated_images/mockup one.png',
source: 'codex-generated',
},
]);
});
it('ignores Codex saved-to output unless the codex scanner is enabled', () => {
const requests = parseTerminalAttachmentRequests(
'Saved to: file:///Users/aamer/.codex-personal/generated_images/mockup.png'
);
expect(requests).toEqual([]);
});
it('strips ANSI styling around Codex saved-to lines before capturing the URL', () => {
const requests = parseTerminalAttachmentRequests(
'\x1b[1mSaved to:\x1b[0m file:///Users/aamer/.codex/generated_images/mockup.png\x1b[0m\r\n',
{ codexArtifacts: true }
);
expect(requests).toEqual([
{
path: '/Users/aamer/.codex/generated_images/mockup.png',
source: 'codex-generated',
},
]);
});
it('emits generated artifact requests from Codex saved-to output', () => {
const session = new Session({ id: 'session-generated-artifact-test', workingDir: '/tmp', mode: 'codex' });
const requested: Array<{ path: string; source?: string }> = [];
session.on('attachmentRequested', (event: { path: string; source?: string }) => requested.push(event));
(session as unknown as { _handleTerminalOutput(data: string): void })._handleTerminalOutput(
'Saved to: file:///Users/aamer/.codex-personal/generated_images/output.png'
);
expect(requested).toEqual([
{
sessionId: 'session-generated-artifact-test',
path: '/Users/aamer/.codex-personal/generated_images/output.png',
source: 'codex-generated',
timestamp: expect.any(Number),
},
]);
});
it('does not emit codex-generated requests from non-codex session modes', () => {
for (const mode of ['claude', 'shell'] as const) {
const session = new Session({ id: `session-generated-artifact-${mode}`, workingDir: '/tmp', mode });
const requested: Array<{ path: string }> = [];
session.on('attachmentRequested', (event: { path: string }) => requested.push(event));
(session as unknown as { _handleTerminalOutput(data: string): void })._handleTerminalOutput(
'Saved to: file:///Users/aamer/.codex-personal/generated_images/output.png'
);
expect(requested).toEqual([]);
}
});
it('supports generated image attachment extensions beyond png', () => {
expect(isSupportedAttachmentExtension('jpg')).toBe(true);
expect(isSupportedAttachmentExtension('jpeg')).toBe(true);
expect(isSupportedAttachmentExtension('webp')).toBe(true);
});
});
+16
View File
@@ -71,6 +71,22 @@ describe('document-thumbnailer', () => {
);
});
it('passes through generated image formats with per-extension content types', async () => {
const expectations: Array<[string, string]> = [
['jpg', 'image/jpeg'],
['jpeg', 'image/jpeg'],
['gif', 'image/gif'],
['webp', 'image/webp'],
['png', 'image/png'],
];
for (const [ext, contentType] of expectations) {
const result = await generateFirstPageThumbnail(`/tmp/mockup.${ext}`, ext);
expect(result).toEqual({ content: Buffer.from('large thumbnail'), contentType });
}
expect(mockedExecFile).not.toHaveBeenCalled();
});
it('renders Office thumbnails from the cached converted PDF after conversion cleanup', async () => {
mockedMkdtemp.mockImplementation(async (prefix) =>
String(prefix).includes('codeman-document-preview-cache')
@@ -0,0 +1,78 @@
import { afterEach, describe, expect, it } from 'vitest';
import fs from 'node:fs/promises';
import { homedir, tmpdir } from 'node:os';
import { join } from 'node:path';
import {
isAllowedGeneratedArtifactPath,
registerGeneratedArtifactAttachment,
} from '../src/generated-artifact-attachments.js';
import { attachmentRegistry } from '../src/attachment-registry.js';
describe('generated artifact attachments', () => {
it('allows workspace artifacts and home-anchored Codex generated image directories', () => {
const home = homedir();
expect(isAllowedGeneratedArtifactPath('/repo/out/mockup.png', '/repo')).toBe(true);
expect(
isAllowedGeneratedArtifactPath(join(home, '.codex-personal', 'generated_images', 'mockup.png'), '/repo')
).toBe(true);
expect(isAllowedGeneratedArtifactPath(join(home, '.codex', 'generated_artifacts', 'report.pdf'), '/repo')).toBe(
true
);
expect(isAllowedGeneratedArtifactPath('/etc/secret.png', '/repo')).toBe(false);
expect(
isAllowedGeneratedArtifactPath(
join(home, '.codex-personal', 'generated_images', '..', '..', '.ssh', 'id_rsa.png'),
'/repo'
)
).toBe(false);
});
it('rejects .codex marker directories that are not anchored at the user home', () => {
expect(isAllowedGeneratedArtifactPath('/var/tmp/staging/.codex/generated_images/leak.png', '/repo')).toBe(false);
expect(isAllowedGeneratedArtifactPath('/var/tmp/.codex-personal/generated_artifacts/leak.md', '/repo')).toBe(false);
});
describe('symlink resolution', () => {
let workspaceDir: string | undefined;
let outsideDir: string | undefined;
const sessionId = 'generated-artifact-symlink-test';
afterEach(async () => {
attachmentRegistry.clearSession(sessionId);
for (const dir of [workspaceDir, outsideDir]) {
if (dir) await fs.rm(dir, { recursive: true, force: true });
}
workspaceDir = undefined;
outsideDir = undefined;
});
it('confines on the resolved path: a workspace symlink to an outside file is rejected', async () => {
// realpath so a symlinked tmpdir (e.g. macOS /var -> /private/var) can't skew containment checks
workspaceDir = await fs.realpath(await fs.mkdtemp(join(tmpdir(), 'codeman-genart-ws-')));
outsideDir = await fs.realpath(await fs.mkdtemp(join(tmpdir(), 'codeman-genart-out-')));
const outsideFile = join(outsideDir, 'private-notes.md');
await fs.writeFile(outsideFile, 'secret');
const linkPath = join(workspaceDir, 'x.md');
await fs.symlink(outsideFile, linkPath);
await expect(
registerGeneratedArtifactAttachment({ sessionId, filePath: linkPath, sessionWorkingDir: workspaceDir })
).rejects.toMatchObject({ statusCode: 403 });
});
it('registers a real workspace file', async () => {
workspaceDir = await fs.realpath(await fs.mkdtemp(join(tmpdir(), 'codeman-genart-ws-')));
const filePath = join(workspaceDir, 'mockup.png');
await fs.writeFile(filePath, 'png-bytes');
const event = await registerGeneratedArtifactAttachment({
sessionId,
filePath,
sessionWorkingDir: workspaceDir,
});
expect(event.fileName).toBe('mockup.png');
expect(event.attachmentType).toBe('image');
});
});
});
+23
View File
@@ -0,0 +1,23 @@
import { describe, expect, it, vi } from 'vitest';
import { Session } from '../src/session.js';
import { createSessionListeners } from '../src/web/session-listener-wiring.js';
describe('session listener wiring', () => {
it('forwards the attachment request source through registerAttachment', async () => {
const session = new Session({ id: 'wiring-attach-source-test', workingDir: '/tmp', mode: 'codex' });
const registerAttachment = vi.fn(async () => undefined);
const deps = { registerAttachment } as unknown as Parameters<typeof createSessionListeners>[1];
const refs = createSessionListeners(session, deps);
refs.attachmentRequested({ path: '/tmp/mockup.png', source: 'codex-generated' });
refs.attachmentRequested({ path: '/tmp/report.pdf', source: 'external' });
expect(registerAttachment).toHaveBeenNthCalledWith(
1,
'wiring-attach-source-test',
'/tmp/mockup.png',
'codex-generated'
);
expect(registerAttachment).toHaveBeenNthCalledWith(2, 'wiring-attach-source-test', '/tmp/report.pdf', 'external');
});
});