feat: Phase 1 完整整合 — E2B Sandbox + AuditLog + Reaper + Replaybook 對帳修復

This commit is contained in:
OG T
2026-06-07 12:43:21 +08:00
parent 9e79e58f87
commit a270ad7813
33 changed files with 16583 additions and 76 deletions

View File

@@ -1,7 +1,9 @@
export const dynamic = 'force-dynamic';
import { NextResponse } from "next/server";
import { prisma } from "@/lib/prisma";
import { logAuditEvent } from "@/lib/audit";
import { TaskStatus } from "@agent-bounty/contracts";
import { releasePayment } from "@/lib/payment";
// Optional: restrict to cron secret
// export const maxDuration = 60; // Next.js edge/serverless config
@@ -49,6 +51,8 @@ export async function GET(req: Request) {
afterState: { status: TaskStatus.OPEN },
reason: `Claim ${claim.id} expired at ${claim.expires_at.toISOString()}`,
});
await releasePayment(tx, claim.task_id, `${claim.id}-release`);
});
rolledBackIds.push(claim.task_id);
}

View File

@@ -1,3 +1,4 @@
export const dynamic = 'force-dynamic';
import { NextRequest, NextResponse } from "next/server";
import {
ListOpenTasksRequestSchema,
@@ -10,7 +11,9 @@ import { prisma } from "@/lib/prisma";
import { runSubmissionInSandbox } from "@/lib/sandbox";
import { logAuditEvent } from "@/lib/audit";
import { redis } from "@/lib/redis";
import { authHold, capturePayment } from "@/lib/payment";
import crypto from "crypto";
import { z } from "zod";
export async function POST(request: NextRequest, props: { params: Promise<{ tool: string }> }) {
const params = await props.params;
@@ -87,6 +90,15 @@ export async function POST(request: NextRequest, props: { params: Promise<{ tool
}
});
await authHold(
tx,
task.id,
task.reward_amount,
task.reward_currency,
parsed.developer_wallet,
`${newClaim.id}-auth-hold`
);
await logAuditEvent(tx, {
actorType: "AGENT",
actorId: parsed.developer_wallet,
@@ -166,51 +178,56 @@ export async function POST(request: NextRequest, props: { params: Promise<{ tool
// Fire and forget
runSubmissionInSandbox(
submission.id,
parsed.deliverables as Record<string, string>,
parsed.deliverables as unknown as Record<string, string>,
criteria.test_file_content
).then(async (result) => {
// Update submission
await prisma.submission.update({
where: { id: submission.id },
data: { status: "JUDGED" }
});
await prisma.$transaction(async (tx) => {
// Update submission
await tx.submission.update({
where: { id: submission.id },
data: { status: "JUDGED" }
});
// Create JudgeResult
await prisma.judgeResult.create({
data: {
submission_id: submission.id,
overall_result: result.overall_result,
tests: result.tests,
artifacts: result.artifacts,
error_classification: result.error_classification,
resource_usage: result.resource_usage
// Create JudgeResult
await tx.judgeResult.create({
data: {
submission_id: submission.id,
overall_result: result.overall_result,
tests: result.tests,
artifacts: result.artifacts,
error_classification: result.error_classification,
resource_usage: result.resource_usage
}
});
// Update Task & Claim Status
const newTaskStatus = result.overall_result === JudgeOverallResult.PASS
? TaskStatus.COMPLETED
: TaskStatus.FAILED_RETRYABLE;
await tx.task.update({
where: { id: submission.task_id },
data: { status: newTaskStatus }
});
await tx.claim.update({
where: { id: submission.claim_id },
data: { status: newTaskStatus }
});
if (newTaskStatus === TaskStatus.COMPLETED) {
await capturePayment(tx, submission.task_id, `${submission.claim_id}-capture`);
}
});
// Update Task & Claim Status
const newTaskStatus = result.overall_result === JudgeOverallResult.PASS
? TaskStatus.COMPLETED
: TaskStatus.FAILED_RETRYABLE;
await prisma.task.update({
where: { id: submission.task_id },
data: { status: newTaskStatus }
});
await prisma.claim.update({
where: { id: submission.claim_id },
data: { status: newTaskStatus }
});
// @ts-ignore prisma transaction client vs prisma client
await logAuditEvent(prisma, {
actorType: "SYSTEM",
action: "JUDGE_COMPLETE",
entityType: "TASK",
entityId: submission.task_id,
beforeState: { status: TaskStatus.VERIFYING },
afterState: { status: newTaskStatus },
metadata: { overall_result: result.overall_result, error_classification: result.error_classification }
await logAuditEvent(tx, {
actorType: "SYSTEM",
action: "JUDGE_COMPLETE",
entityType: "TASK",
entityId: submission.task_id,
beforeState: { status: TaskStatus.VERIFYING },
afterState: { status: newTaskStatus },
metadata: { overall_result: result.overall_result, error_classification: result.error_classification }
});
});
}).catch(console.error);
}
@@ -225,13 +242,35 @@ export async function POST(request: NextRequest, props: { params: Promise<{ tool
}
case "check_payout_status": {
// Mocked for now until Settlement phase is implemented
const parsed = z.object({ task_id: z.string().uuid() }).parse(body);
const task = await prisma.task.findUnique({ where: { id: parsed.task_id } });
if (!task) {
throw new Error("Task not found");
}
const ledger = await prisma.ledgerEntry.findFirst({
where: { task_id: parsed.task_id },
orderBy: { created_at: 'desc' }
});
if (!ledger) {
return NextResponse.json({
task_id: task.id,
phase: task.status === "COMPLETED" ? "PAYOUT_READY" : "NO_LEDGER",
amount: task.reward_amount,
currency: task.reward_currency,
updated_at: task.updated_at.toISOString(),
});
}
return NextResponse.json({
task_id: body.task_id,
phase: "PAYOUT_READY",
amount: 100,
currency: "USD",
updated_at: new Date().toISOString(),
task_id: task.id,
phase: ledger.phase,
amount: task.reward_amount,
currency: task.reward_currency,
updated_at: ledger.updated_at.toISOString(),
ledger_entry: ledger
});
}
@@ -240,6 +279,16 @@ export async function POST(request: NextRequest, props: { params: Promise<{ tool
}
} catch (error: any) {
console.error(`[API Gateway] Error handling ${tool}:`, error);
return NextResponse.json({ error: error.message || String(error) }, { status: 400 });
if (error.name === "ZodError") {
return NextResponse.json({ error_type: "InvalidParams", message: error.errors }, { status: 400 });
}
const msg = error.message || String(error);
if (msg.includes("not OPEN") || msg.includes("not EXECUTING") || msg.includes("Invalid claim token") || msg.includes("not found")) {
return NextResponse.json({ error_type: "StateConflict", message: msg }, { status: 409 });
}
return NextResponse.json({ error_type: "InternalError", message: msg }, { status: 500 });
}
}