Files
EJClaw/runners/agent-runner/src/verification.ts

125 lines
3.4 KiB
TypeScript

import { createHash } from 'crypto';
import fs from 'fs';
import path from 'path';
export const VERIFICATION_PROFILES = [
'test',
'typecheck',
'build',
] as const;
export type VerificationProfile = (typeof VERIFICATION_PROFILES)[number];
export interface VerificationResponse {
requestId: string;
ok: boolean;
profile: VerificationProfile;
command: string;
stdout: string;
stderr: string;
exitCode: number;
snapshotId: string;
runtimeVersion: string;
workdir?: string;
error?: string;
}
const SNAPSHOT_EXCLUDE_NAMES = new Set(['.git', 'node_modules', '.env']);
export function isVerificationProfile(
value: unknown,
): value is VerificationProfile {
return (
typeof value === 'string' &&
VERIFICATION_PROFILES.includes(value as VerificationProfile)
);
}
function updateSnapshotHash(
hash: ReturnType<typeof createHash>,
repoDir: string,
currentPath: string,
): void {
const relPath = path.relative(repoDir, currentPath) || '.';
const stat = fs.lstatSync(currentPath);
if (stat.isDirectory()) {
if (relPath !== '.') {
hash.update(`dir\0${relPath}\0`);
}
for (const entry of fs.readdirSync(currentPath).sort()) {
if (SNAPSHOT_EXCLUDE_NAMES.has(entry)) continue;
updateSnapshotHash(hash, repoDir, path.join(currentPath, entry));
}
return;
}
if (stat.isSymbolicLink()) {
hash.update(`symlink\0${relPath}\0${fs.readlinkSync(currentPath)}\0`);
return;
}
if (stat.isFile()) {
hash.update(`file\0${relPath}\0`);
hash.update(fs.readFileSync(currentPath));
hash.update('\0');
}
}
export function computeVerificationSnapshotId(repoDir: string): string {
const hash = createHash('sha256');
updateSnapshotHash(hash, repoDir, repoDir);
return `fs:${hash.digest('hex').slice(0, 24)}`;
}
export function resolveVerificationResponsesDir(hostIpcDir: string): string {
return path.join(hostIpcDir, 'verification-responses');
}
export async function waitForVerificationResponse(
responseDir: string,
requestId: string,
options?: {
timeoutMs?: number;
pollMs?: number;
},
): Promise<VerificationResponse> {
const timeoutMs = options?.timeoutMs ?? 20 * 60 * 1000;
const pollMs = options?.pollMs ?? 100;
const responsePath = path.join(responseDir, `${requestId}.json`);
const deadline = Date.now() + timeoutMs;
while (Date.now() <= deadline) {
if (fs.existsSync(responsePath)) {
const response = JSON.parse(
fs.readFileSync(responsePath, 'utf-8'),
) as VerificationResponse;
fs.unlinkSync(responsePath);
return response;
}
await new Promise((resolve) => setTimeout(resolve, pollMs));
}
throw new Error(
`Timed out waiting for verification response: ${requestId}`,
);
}
export function formatVerificationResponse(
response: VerificationResponse,
): string {
const parts = [
`Verification profile: ${response.profile}`,
`Snapshot: ${response.snapshotId}`,
`Runtime: ${response.runtimeVersion}`,
`Exit code: ${response.exitCode}`,
response.workdir ? `Workdir: ${response.workdir}` : null,
response.command ? `$ ${response.command}` : null,
response.stdout ? response.stdout.trimEnd() : null,
response.stderr ? `[stderr]\n${response.stderr.trimEnd()}` : null,
response.error ? `[error] ${response.error}` : null,
].filter((value): value is string => Boolean(value));
return parts.join('\n');
}