import { assert } from '../assertions/core.mjs'; import { createHash, fs, path } from '../dependencies.mjs'; import { mainAgentId, state } from '../runtime-state.mjs'; import { decodeUtf8Fatal, splitJsonlBufferLines } from './reporting.mjs'; export function collectApiKeys(value, keys = []) { if (!value || typeof value !== 'object') return keys; if (Array.isArray(value)) { for (const item of value) collectApiKeys(item, keys); return [...new Set(keys)]; } for (const [key, child] of Object.entries(value)) { if ( /^api_?key$/i.test(key) && typeof child === 'string' && child.length > 0 ) { keys.push(child); } else { collectApiKeys(child, keys); } } return [...new Set(keys)]; } export function parseAssignedJson(output, names) { const matches = []; for (const line of output.split(/\r?\n/u)) { for (const name of names) { if (line.startsWith(`${name}=`)) { matches.push({ name, payload: line.slice(name.length + 1) }); } } } assert(matches.length === 1, 'cli-assigned-json-output-invalid'); return JSON.parse(matches[0].payload); } export async function listFiles(root) { const files = []; let metadata; try { metadata = await fs.lstat(root); } catch (error) { if (error?.code === 'ENOENT') return files; throw error; } if (metadata.isSymbolicLink()) return files; if (metadata.isFile()) return [root]; const entries = await fs.readdir(root, { withFileTypes: true }); for (const entry of entries) { const file = path.join(root, entry.name); if (entry.isSymbolicLink()) continue; if (entry.isDirectory()) files.push(...(await listFiles(file))); else if (entry.isFile()) files.push(file); } return files; } export async function readJson(file) { return JSON.parse(await fs.readFile(file, 'utf8')); } export function validateCommandOutputSidecar(sidecar, audit, file) { const relative = relativeProjectPath(file); assert( sidecar?.schemaVersion === 'game-creator-command-output.v1' && sidecar.outputRef === relative && sidecar.identity?.agentId === mainAgentId && sidecar.identity?.taskId === audit.taskId && sidecar.identity?.sessionId === audit.sessionId && sidecar.identity?.runId === state.initialRunId && sidecar.identity?.actionId === audit.actionId && sidecar.identity?.actionFingerprint === audit.actionFingerprint && sidecar.outputSha256 === audit.outputSha256 && sidecar.totalLines === audit.totalLines && sidecar.captureTruncated === audit.captureTruncated && sidecar.exitCode === audit.exitCode && sidecar.timedOut === audit.timedOut && sidecar.sourceChanged === audit.sourceChanged && typeof sidecar.output === 'string' && createHash('sha256').update(sidecar.output).digest('hex') === sidecar.outputSha256 && (sidecar.output.length === 0 ? sidecar.totalLines === 0 : sidecar.output.split('\n').length === sidecar.totalLines), 'command-output-sidecar-identity-invalid', ); } export async function readJsonl(file) { const content = await fs.readFile(file); const records = []; for (const lineRecord of splitJsonlBufferLines(content)) { const line = decodeUtf8Fatal(lineRecord.bytes, 'jsonl-invalid-utf8'); if (line.trim().length > 0) records.push(JSON.parse(line)); } return records; } export async function readOptionalJsonl(file) { try { return await readJsonl(file); } catch (error) { if (error?.code === 'ENOENT') return []; throw error; } } export function resolveProjectRelative(value) { const candidate = path.isAbsolute(value) ? path.resolve(value) : path.resolve(state.projectRoot, value); assert( isPathInside(state.projectRoot, candidate), 'evidence-path-outside-project', ); return candidate; } export function relativeProjectPath(value) { const relative = path.relative(state.projectRoot, path.resolve(value)); assert( relative && !relative.startsWith('..') && !path.isAbsolute(relative), 'relative-evidence-path-invalid', ); return relative.split(path.sep).join('/'); } export function isPathInside(parent, child) { const relative = path.relative(path.resolve(parent), path.resolve(child)); return ( relative !== '' && !relative.startsWith('..') && !path.isAbsolute(relative) ); } export function isTerminalRuntime(runtime) { return ['completed', 'failed', 'cancelled', 'budget-exhausted'].includes( runtime.phase, ); } export function isLiveTask(task) { return ( ['pending', 'running', 'waiting-for-confirmation'].includes(task.status) || [ 'queued', 'running', 'executing', 'finalizing', 'waiting-for-confirmation', ].includes(task.phase) ); }