mirror of
https://github.com/Ark0N/Codeman.git
synced 2026-10-02 13:39:41 +02:00
- 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>
71 lines
2.8 KiB
TypeScript
71 lines
2.8 KiB
TypeScript
/**
|
|
* @fileoverview Codex generated-artifact attachment registration.
|
|
*
|
|
* Codex image generation prints paths such as `Saved to: file://...`. These
|
|
* 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.
|
|
*/
|
|
|
|
import { realpathSync } from 'node:fs';
|
|
import { homedir } from 'node:os';
|
|
import { join, normalize, sep } from 'node:path';
|
|
import { registerExternalAttachment, type AttachmentRegistrationResult } from './attachment-registry.js';
|
|
|
|
export interface GeneratedArtifactRegistrationOptions {
|
|
sessionId: string;
|
|
filePath: string;
|
|
sessionWorkingDir: string;
|
|
}
|
|
|
|
export async function registerGeneratedArtifactAttachment(
|
|
options: GeneratedArtifactRegistrationOptions
|
|
): Promise<AttachmentRegistrationResult> {
|
|
// 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).
|
|
let forceWorkspaceConfinement = true;
|
|
try {
|
|
const resolvedPath = realpathSync(options.filePath);
|
|
forceWorkspaceConfinement = !isAllowedGeneratedArtifactPath(resolvedPath, options.sessionWorkingDir);
|
|
} catch {
|
|
// Keep force confinement.
|
|
}
|
|
return registerExternalAttachment(options.sessionId, options.filePath, {
|
|
sessionWorkingDir: options.sessionWorkingDir,
|
|
forceWorkspaceConfinement,
|
|
});
|
|
}
|
|
|
|
/** 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.
|
|
*/
|
|
export function isAllowedGeneratedArtifactPath(filePath: string, workingDir: string): boolean {
|
|
const normalizedPath = normalize(filePath);
|
|
if (isPathInside(normalizedPath, workingDir)) return true;
|
|
return codexGeneratedDirs().some((dir) => isPathInside(normalizedPath, dir));
|
|
}
|
|
|
|
function isPathInside(filePath: string, rootPath: string): boolean {
|
|
const normalizedRoot = normalize(rootPath);
|
|
if (filePath === normalizedRoot) return true;
|
|
return filePath.startsWith(normalizedRoot.endsWith(sep) ? normalizedRoot : normalizedRoot + sep);
|
|
}
|