mirror of
https://github.com/Ark0N/Codeman.git
synced 2026-10-08 16:39:42 +02:00
fix: COD-152 preserve generated artifact filenames
This commit is contained in:
committed by
Aamer Akhter
parent
f8aa93969b
commit
978ca57343
@@ -1,28 +1,13 @@
|
|||||||
/**
|
/**
|
||||||
* @fileoverview Codex generated-artifact attachment registration.
|
* @fileoverview Codex generated-artifact attachment registration.
|
||||||
*
|
*
|
||||||
* Codex image generation prints paths such as `Saved to: file://...`. For local
|
* Codex image generation prints paths such as `Saved to: file://...`. These
|
||||||
* sessions these paths can be registered directly when they are safe. For remote
|
* paths are registered directly when they fall within allowed locations (workspace
|
||||||
* SSH sessions the path exists on the remote host, so Codeman first copies the
|
* or well-known Codex generated-image directories).
|
||||||
* bytes into its instance data directory and then serves that cached copy through
|
|
||||||
* the existing attachment registry.
|
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { createHash } from 'node:crypto';
|
import { posix as posixPath } from 'node:path';
|
||||||
import { execFile } from 'node:child_process';
|
|
||||||
import { mkdir } from 'node:fs/promises';
|
|
||||||
import { basename, join, posix as posixPath } from 'node:path';
|
|
||||||
import { promisify } from 'node:util';
|
|
||||||
import fs from 'node:fs/promises';
|
|
||||||
import { dataPath } from './config/instance.js';
|
|
||||||
import { registerExternalAttachment, type AttachmentRegistrationResult } from './attachment-registry.js';
|
import { registerExternalAttachment, type AttachmentRegistrationResult } from './attachment-registry.js';
|
||||||
import { buildSshConnectionArgv, remoteSshTarget, shellescape } from './remote-hosts.js';
|
|
||||||
import type { SessionRemote } from './types/session.js';
|
|
||||||
|
|
||||||
const execFileAsync = promisify(execFile);
|
|
||||||
|
|
||||||
const MAX_GENERATED_ARTIFACT_BYTES = 50 * 1024 * 1024;
|
|
||||||
const REMOTE_FETCH_TIMEOUT_MS = 30_000;
|
|
||||||
|
|
||||||
const CODEX_GENERATED_DIR_MARKERS = [
|
const CODEX_GENERATED_DIR_MARKERS = [
|
||||||
'/.codex-personal/generated_images/',
|
'/.codex-personal/generated_images/',
|
||||||
@@ -35,20 +20,11 @@ export interface GeneratedArtifactRegistrationOptions {
|
|||||||
sessionId: string;
|
sessionId: string;
|
||||||
filePath: string;
|
filePath: string;
|
||||||
sessionWorkingDir: string;
|
sessionWorkingDir: string;
|
||||||
remote?: SessionRemote;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function registerGeneratedArtifactAttachment(
|
export async function registerGeneratedArtifactAttachment(
|
||||||
options: GeneratedArtifactRegistrationOptions
|
options: GeneratedArtifactRegistrationOptions
|
||||||
): Promise<AttachmentRegistrationResult> {
|
): Promise<AttachmentRegistrationResult> {
|
||||||
if (options.remote) {
|
|
||||||
if (!isAllowedGeneratedArtifactPath(options.filePath, options.remote.remotePath)) {
|
|
||||||
throw new Error('Generated artifact path is outside allowed remote locations');
|
|
||||||
}
|
|
||||||
const localPath = await materializeRemoteGeneratedArtifact(options.sessionId, options.remote, options.filePath);
|
|
||||||
return registerExternalAttachment(options.sessionId, localPath, { sessionWorkingDir: options.sessionWorkingDir });
|
|
||||||
}
|
|
||||||
|
|
||||||
const forceWorkspaceConfinement = !isAllowedGeneratedArtifactPath(options.filePath, options.sessionWorkingDir);
|
const forceWorkspaceConfinement = !isAllowedGeneratedArtifactPath(options.filePath, options.sessionWorkingDir);
|
||||||
return registerExternalAttachment(options.sessionId, options.filePath, {
|
return registerExternalAttachment(options.sessionId, options.filePath, {
|
||||||
sessionWorkingDir: options.sessionWorkingDir,
|
sessionWorkingDir: options.sessionWorkingDir,
|
||||||
@@ -71,42 +47,3 @@ function isPathInside(filePath: string, rootPath: string): boolean {
|
|||||||
function ensureTrailingSlash(value: string): string {
|
function ensureTrailingSlash(value: string): string {
|
||||||
return value.endsWith('/') ? value : `${value}/`;
|
return value.endsWith('/') ? value : `${value}/`;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function materializeRemoteGeneratedArtifact(
|
|
||||||
sessionId: string,
|
|
||||||
remote: SessionRemote,
|
|
||||||
remotePath: string
|
|
||||||
): Promise<string> {
|
|
||||||
const fileName = sanitizeCacheFileName(basename(remotePath));
|
|
||||||
const digest = createHash('sha256')
|
|
||||||
.update(`${remote.username}@${remote.host}:${remote.port ?? 22}:${remotePath}`)
|
|
||||||
.digest('hex')
|
|
||||||
.slice(0, 16);
|
|
||||||
const cacheDir = dataPath('generated-artifacts', sessionId);
|
|
||||||
await mkdir(cacheDir, { recursive: true });
|
|
||||||
const localPath = join(cacheDir, `${digest}-${fileName}`);
|
|
||||||
|
|
||||||
const args = buildRemoteGeneratedArtifactFetchArgs(remote, remotePath);
|
|
||||||
const { stdout } = (await execFileAsync('ssh', args, {
|
|
||||||
encoding: 'buffer',
|
|
||||||
timeout: REMOTE_FETCH_TIMEOUT_MS,
|
|
||||||
maxBuffer: MAX_GENERATED_ARTIFACT_BYTES,
|
|
||||||
})) as { stdout: Buffer };
|
|
||||||
|
|
||||||
await fs.writeFile(localPath, stdout);
|
|
||||||
return localPath;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function buildRemoteGeneratedArtifactFetchArgs(remote: SessionRemote, remotePath: string): string[] {
|
|
||||||
return [
|
|
||||||
...buildSshConnectionArgv(remote),
|
|
||||||
'-o',
|
|
||||||
'ConnectTimeout=10',
|
|
||||||
remoteSshTarget(remote),
|
|
||||||
`cat -- ${shellescape(remotePath)}`,
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
function sanitizeCacheFileName(fileName: string): string {
|
|
||||||
return fileName.replace(/[^A-Za-z0-9._-]/g, '_') || 'artifact';
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1359,7 +1359,6 @@ export class WebServer extends EventEmitter {
|
|||||||
sessionId,
|
sessionId,
|
||||||
filePath,
|
filePath,
|
||||||
sessionWorkingDir: session.workingDir,
|
sessionWorkingDir: session.workingDir,
|
||||||
remote: session.remote,
|
|
||||||
})
|
})
|
||||||
: await registerExternalAttachment(sessionId, filePath, {
|
: await registerExternalAttachment(sessionId, filePath, {
|
||||||
sessionWorkingDir: session.workingDir,
|
sessionWorkingDir: session.workingDir,
|
||||||
|
|||||||
@@ -1,9 +1,5 @@
|
|||||||
import { describe, expect, it } from 'vitest';
|
import { describe, expect, it } from 'vitest';
|
||||||
import {
|
import { isAllowedGeneratedArtifactPath } from '../src/generated-artifact-attachments.js';
|
||||||
buildRemoteGeneratedArtifactFetchArgs,
|
|
||||||
isAllowedGeneratedArtifactPath,
|
|
||||||
} from '../src/generated-artifact-attachments.js';
|
|
||||||
import type { SessionRemote } from '../src/types/session.js';
|
|
||||||
|
|
||||||
describe('generated artifact attachments', () => {
|
describe('generated artifact attachments', () => {
|
||||||
it('allows workspace artifacts and known Codex generated image directories', () => {
|
it('allows workspace artifacts and known Codex generated image directories', () => {
|
||||||
@@ -16,40 +12,4 @@ describe('generated artifact attachments', () => {
|
|||||||
isAllowedGeneratedArtifactPath('/Users/aamer/.codex-personal/generated_images/../../.ssh/id_rsa.png', '/repo')
|
isAllowedGeneratedArtifactPath('/Users/aamer/.codex-personal/generated_images/../../.ssh/id_rsa.png', '/repo')
|
||||||
).toBe(false);
|
).toBe(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('builds remote fetch argv from the session SSH configuration', () => {
|
|
||||||
const remote: SessionRemote = {
|
|
||||||
hostId: 'mac-mini',
|
|
||||||
label: 'mac-mini',
|
|
||||||
host: '192.168.1.20',
|
|
||||||
username: 'aamer',
|
|
||||||
port: 2222,
|
|
||||||
remotePath: '/Users/aamer/projects/app',
|
|
||||||
identityFile: '~/.ssh/remote_ed25519',
|
|
||||||
socksProxy: '127.0.0.1:1080',
|
|
||||||
jumpHost: 'jump.example.com',
|
|
||||||
extraSshOptions: ['StrictHostKeyChecking=no'],
|
|
||||||
};
|
|
||||||
|
|
||||||
expect(
|
|
||||||
buildRemoteGeneratedArtifactFetchArgs(remote, "/Users/aamer/.codex-personal/generated_images/a'b.png")
|
|
||||||
).toEqual([
|
|
||||||
'-o',
|
|
||||||
'BatchMode=yes',
|
|
||||||
'-p',
|
|
||||||
'2222',
|
|
||||||
'-i',
|
|
||||||
expect.stringMatching(/remote_ed25519$/),
|
|
||||||
'-J',
|
|
||||||
'jump.example.com',
|
|
||||||
'-o',
|
|
||||||
'ProxyCommand=nc -X 5 -x 127.0.0.1:1080 %h %p',
|
|
||||||
'-o',
|
|
||||||
'StrictHostKeyChecking=no',
|
|
||||||
'-o',
|
|
||||||
'ConnectTimeout=10',
|
|
||||||
'aamer@192.168.1.20',
|
|
||||||
"cat -- '/Users/aamer/.codex-personal/generated_images/a'\\''b.png'",
|
|
||||||
]);
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user