mirror of
https://github.com/JimLiu/baoyu-skills.git
synced 2026-07-09 20:51:22 +00:00
51b31aa050
Stop Codex from satisfying a request by copying an unrelated pre-existing image from generated_images instead of generating a new one. Verification now requires a PNG in this thread's generated_images dir (or an image_gen stream item, kept as a forward-compatible signal), and the spawned-agent instruction forbids reading or reusing history images before image_gen is called. Removes the findCpToTarget fallback that allowed the shortcut. Validated against a real codex exec run: image_gen leaves no stream item, so the filesystem check is the load-bearing signal; locked in via a condensed real-stream regression fixture. Closes #185
57 lines
2.3 KiB
TypeScript
57 lines
2.3 KiB
TypeScript
import { stat, readdir } from "node:fs/promises";
|
|
import { homedir } from "node:os";
|
|
import path from "node:path";
|
|
import { GenError } from "./types.ts";
|
|
import type { ToolCall } from "./types.ts";
|
|
import { hasImageGenInvocation } from "./parser.ts";
|
|
|
|
const PNG_MAGIC = Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]);
|
|
|
|
export function codexHome(): string {
|
|
return process.env.CODEX_HOME ?? path.join(homedir(), ".codex");
|
|
}
|
|
|
|
export async function verifyImageGenWasInvoked(threadId: string | null): Promise<{ ok: boolean; reason?: string }> {
|
|
if (!threadId) return { ok: false, reason: "no thread id" };
|
|
const dir = path.join(codexHome(), "generated_images", threadId);
|
|
try {
|
|
const entries = await readdir(dir);
|
|
const pngs = entries.filter((e) => e.toLowerCase().endsWith(".png"));
|
|
if (pngs.length === 0) return { ok: false, reason: `no PNG in ${dir}` };
|
|
return { ok: true };
|
|
} catch (e: any) {
|
|
return { ok: false, reason: `cannot read ${dir}: ${e?.code ?? e?.message}` };
|
|
}
|
|
}
|
|
|
|
// Real evidence that image_gen ran in THIS thread. Codex's image_gen tool does
|
|
// not surface as a stream item, so a successful run shows only reasoning/shell/
|
|
// agent_message — `dirHasImage` (a PNG in this thread's generated_images dir) is
|
|
// what proves it. The stream check is kept as a forward-compatible signal in
|
|
// case a future Codex version emits the item. The #185 shortcut (copying an
|
|
// unrelated history image, which lives under a different thread id) yields
|
|
// neither, so it is correctly rejected.
|
|
export function hasImageGenEvidence(toolCalls: ToolCall[], dirHasImage: boolean): boolean {
|
|
return dirHasImage || hasImageGenInvocation(toolCalls);
|
|
}
|
|
|
|
export async function verifyOutput(outputPath: string): Promise<{ bytes: number }> {
|
|
let s;
|
|
try {
|
|
s = await stat(outputPath);
|
|
} catch {
|
|
throw new GenError("output_missing", `Output file not created: ${outputPath}`);
|
|
}
|
|
if (s.size < 1000) {
|
|
throw new GenError("invalid_png", `Output file too small (${s.size} bytes)`);
|
|
}
|
|
const file = Bun.file(outputPath);
|
|
const head = new Uint8Array(await file.slice(0, 8).arrayBuffer());
|
|
for (let i = 0; i < PNG_MAGIC.length; i++) {
|
|
if (head[i] !== PNG_MAGIC[i]) {
|
|
throw new GenError("invalid_png", `Output is not a valid PNG (magic mismatch)`);
|
|
}
|
|
}
|
|
return { bytes: s.size };
|
|
}
|