Files
Codeman/src/attachment-magic.ts
T
Codeman maintainer 13c877f938 fix(review): harden Codex generated-artifact attachment pipeline (PR #150)
- Pass the attachment request `source` through the server deps lambda and make
  it a required param on SessionListenerDeps.registerAttachment + the wiring
  event type (the 2-arg lambda silently dropped `source`, force-confining every
  codex-generated artifact — the feature never worked outside the workspace);
  new test/session-listener-wiring.test.ts asserts the pass-through
- Gate the Codex `Saved to: file://` scanner on mode === 'codex' via a
  codexArtifacts option threaded from the session call site; magic links stay
  mode-agnostic; tests assert claude/shell sessions never emit codex-generated
  requests
- Decide the generated-artifact trust policy on the realpath-RESOLVED path
  (unresolvable → force-confined) and anchor the ~/.codex marker dirs to
  os.homedir() prefixes with startsWith instead of substring matching; symlink
  escape + unanchored-marker regression tests added
- Run the Codex scanner on stripAnsi'd data so trailing SGR sequences don't
  ride into the captured URL; styled 'Saved to:' test added
- Extend generateFirstPageThumbnail with jpg/jpeg/gif/webp passthrough and
  per-extension content types (mirrors the png passthrough) so the PR's new
  image formats render real thumbnails instead of 204 letter-tiles

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-12 17:50:21 +02:00

102 lines
3.4 KiB
TypeScript

/**
* @fileoverview Parses terminal magic links that request attachment cards.
*/
import { isAbsolute } from 'node:path';
import { fileURLToPath } from 'node:url';
import { isSupportedAttachmentExtension } from './attachment-registry.js';
import { stripAnsi } from './utils/index.js';
const MAGIC_LINK_RE = /codeman:\/\/attach\?([^\s<>"']+)/g;
const CODEX_SAVED_FILE_RE = /\bSaved to:\s*(file:\/\/[^\s<>"']+)/gi;
export interface TerminalAttachmentRequest {
path: string;
source: 'external' | 'codex-generated';
}
export interface ParseTerminalAttachmentOptions {
/**
* Enable the Codex `Saved to: file://...` scanner. Only codex-mode sessions
* may set this — the relaxed codex-generated trust policy must never be
* reachable from other modes' (prompt-injectable) terminal output.
*/
codexArtifacts?: boolean;
}
export function parseAttachmentMagicLinks(data: string): string[] {
return parseMagicAttachmentRequests(data).map((request) => request.path);
}
export function parseTerminalAttachmentRequests(
data: string,
options: ParseTerminalAttachmentOptions = {}
): TerminalAttachmentRequest[] {
const results: TerminalAttachmentRequest[] = [];
const seen = new Set<string>();
const requests = options.codexArtifacts
? [...parseMagicAttachmentRequests(data), ...parseCodexGeneratedArtifactRequests(data)]
: parseMagicAttachmentRequests(data);
for (const request of requests) {
const key = `${request.source}:${request.path}`;
if (seen.has(key)) continue;
seen.add(key);
results.push(request);
}
return results;
}
function parseMagicAttachmentRequests(data: string): TerminalAttachmentRequest[] {
const results: string[] = [];
const seen = new Set<string>();
for (const match of data.matchAll(MAGIC_LINK_RE)) {
const query = trimTrailingPunctuation(match[1] || '');
try {
const params = new URLSearchParams(query);
const filePath = params.get('path');
if (!filePath || !isAbsolute(filePath)) continue;
const extension = filePath.split('.').pop()?.toLowerCase() || '';
if (!isSupportedAttachmentExtension(extension)) continue;
if (seen.has(filePath)) continue;
seen.add(filePath);
results.push(filePath);
} catch {
// Ignore malformed terminal text. Magic links are advisory.
}
}
return results.map((path) => ({ path, source: 'external' }));
}
function parseCodexGeneratedArtifactRequests(data: string): TerminalAttachmentRequest[] {
const results: TerminalAttachmentRequest[] = [];
const seen = new Set<string>();
// Codex styles its TUI output — strip ANSI first so a trailing SGR reset
// (e.g. `...mockup.png\x1b[0m`) doesn't ride into the captured URL and break
// the extension allowlist check.
for (const match of stripAnsi(data).matchAll(CODEX_SAVED_FILE_RE)) {
const rawUrl = trimTrailingPunctuation(match[1] || '');
try {
const filePath = fileURLToPath(rawUrl);
if (!isAbsolute(filePath)) continue;
const extension = filePath.split('.').pop()?.toLowerCase() || '';
if (!isSupportedAttachmentExtension(extension)) continue;
if (seen.has(filePath)) continue;
seen.add(filePath);
results.push({ path: filePath, source: 'codex-generated' });
} catch {
// Ignore malformed terminal text. Generated-artifact links are advisory.
}
}
return results;
}
function trimTrailingPunctuation(value: string): string {
return value.replace(/[),.;:]+$/g, '');
}