feat: Phase 1 完整整合 — E2B Sandbox + AuditLog + Reaper + Replaybook 對帳修復
This commit is contained in:
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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 });
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user