2026-07-01 09:57:47 +00:00
|
|
|
/**
|
|
|
|
|
* @fileoverview Codex generated-artifact attachment registration.
|
|
|
|
|
*
|
2026-07-01 10:40:09 +00:00
|
|
|
* Codex image generation prints paths such as `Saved to: file://...`. These
|
2026-07-12 17:50:21 +02:00
|
|
|
* paths are registered directly when they fall within allowed locations (the
|
|
|
|
|
* session workspace or the well-known Codex generated-artifact directories
|
|
|
|
|
* anchored at the user's home). The trust decision is made on the
|
|
|
|
|
* realpath-RESOLVED path so a symlink staged at an allowed location cannot
|
|
|
|
|
* smuggle an arbitrary host file past workspace confinement.
|
2026-07-01 09:57:47 +00:00
|
|
|
*/
|
|
|
|
|
|
2026-07-12 17:50:21 +02:00
|
|
|
import { realpathSync } from 'node:fs';
|
|
|
|
|
import { homedir } from 'node:os';
|
|
|
|
|
import { join, normalize, sep } from 'node:path';
|
2026-07-01 09:57:47 +00:00
|
|
|
import { registerExternalAttachment, type AttachmentRegistrationResult } from './attachment-registry.js';
|
2026-09-14 17:06:42 +02:00
|
|
|
import type { SessionRemote } from './types/session.js';
|
2026-07-01 09:57:47 +00:00
|
|
|
|
|
|
|
|
export interface GeneratedArtifactRegistrationOptions {
|
|
|
|
|
sessionId: string;
|
|
|
|
|
filePath: string;
|
|
|
|
|
sessionWorkingDir: string;
|
2026-09-14 17:06:42 +02:00
|
|
|
/** Remote (SSH) case: the path lives on the remote host (see attachment-registry). */
|
|
|
|
|
remote?: SessionRemote;
|
2026-07-01 09:57:47 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export async function registerGeneratedArtifactAttachment(
|
|
|
|
|
options: GeneratedArtifactRegistrationOptions
|
|
|
|
|
): Promise<AttachmentRegistrationResult> {
|
2026-07-12 17:50:21 +02:00
|
|
|
// Decide trust on the symlink-resolved path. If it can't be resolved, fall
|
|
|
|
|
// back to the strict force-confined policy (registration will 404 a missing
|
|
|
|
|
// file anyway).
|
2026-09-14 17:06:42 +02:00
|
|
|
//
|
|
|
|
|
// A remote case keeps that strict policy unconditionally: the well-known Codex
|
|
|
|
|
// artifact directories are anchored at THIS host's home, which says nothing about
|
|
|
|
|
// a remote home, so only a file inside the remote workspace is trusted here.
|
|
|
|
|
const resolvedPath = options.remote ? undefined : tryRealpath(options.filePath);
|
|
|
|
|
const forceWorkspaceConfinement = !resolvedPath
|
|
|
|
|
? true
|
|
|
|
|
: !isAllowedGeneratedArtifactPath(resolvedPath, options.sessionWorkingDir);
|
2026-07-01 09:57:47 +00:00
|
|
|
return registerExternalAttachment(options.sessionId, options.filePath, {
|
|
|
|
|
sessionWorkingDir: options.sessionWorkingDir,
|
|
|
|
|
forceWorkspaceConfinement,
|
2026-09-14 17:06:42 +02:00
|
|
|
remote: options.remote,
|
2026-07-01 09:57:47 +00:00
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
2026-09-14 17:06:42 +02:00
|
|
|
/** `realpathSync` without the throw — undefined when the path does not resolve. */
|
|
|
|
|
function tryRealpath(path: string): string | undefined {
|
|
|
|
|
try {
|
|
|
|
|
return realpathSync(path);
|
|
|
|
|
} catch {
|
|
|
|
|
return undefined;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-12 17:50:21 +02:00
|
|
|
/** Well-known Codex generated-artifact directories, anchored at the user's home. */
|
|
|
|
|
function codexGeneratedDirs(): string[] {
|
|
|
|
|
const home = homedir();
|
|
|
|
|
return [
|
|
|
|
|
join(home, '.codex-personal', 'generated_images'),
|
|
|
|
|
join(home, '.codex', 'generated_images'),
|
|
|
|
|
join(home, '.codex-personal', 'generated_artifacts'),
|
|
|
|
|
join(home, '.codex', 'generated_artifacts'),
|
|
|
|
|
];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* True when `filePath` (absolute; callers should pass the realpath-resolved
|
|
|
|
|
* path) is inside the session workspace or one of the well-known Codex
|
|
|
|
|
* generated-artifact directories under the current user's home. The marker
|
|
|
|
|
* directories are prefix-anchored to `os.homedir()` — a `.codex/...` subtree
|
|
|
|
|
* elsewhere on the filesystem does NOT qualify.
|
|
|
|
|
*/
|
2026-07-01 09:57:47 +00:00
|
|
|
export function isAllowedGeneratedArtifactPath(filePath: string, workingDir: string): boolean {
|
2026-07-12 17:50:21 +02:00
|
|
|
const normalizedPath = normalize(filePath);
|
2026-07-01 09:57:47 +00:00
|
|
|
if (isPathInside(normalizedPath, workingDir)) return true;
|
2026-07-12 17:50:21 +02:00
|
|
|
return codexGeneratedDirs().some((dir) => isPathInside(normalizedPath, dir));
|
2026-07-01 09:57:47 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function isPathInside(filePath: string, rootPath: string): boolean {
|
2026-07-12 17:50:21 +02:00
|
|
|
const normalizedRoot = normalize(rootPath);
|
|
|
|
|
if (filePath === normalizedRoot) return true;
|
|
|
|
|
return filePath.startsWith(normalizedRoot.endsWith(sep) ? normalizedRoot : normalizedRoot + sep);
|
2026-07-01 09:57:47 +00:00
|
|
|
}
|