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

@@ -15,7 +15,8 @@
"ioredis": "^5.11.1",
"next": "16.2.7",
"react": "19.2.4",
"react-dom": "19.2.4"
"react-dom": "19.2.4",
"zod": "^3.23.0"
},
"devDependencies": {
"@tailwindcss/postcss": "^4",

View File

@@ -0,0 +1 @@
export * from "./index"

View File

@@ -0,0 +1,5 @@
/* !!! This is code generated by Prisma. Do not edit directly. !!!
/* eslint-disable */
// biome-ignore-all lint: generated file
module.exports = { ...require('.') }

View File

@@ -0,0 +1 @@
export * from "./index"

View File

@@ -0,0 +1,5 @@
/* !!! This is code generated by Prisma. Do not edit directly. !!!
/* eslint-disable */
// biome-ignore-all lint: generated file
module.exports = { ...require('#main-entry-point') }

View File

@@ -0,0 +1 @@
export * from "./default"

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,274 @@
/* !!! This is code generated by Prisma. Do not edit directly. !!!
/* eslint-disable */
// biome-ignore-all lint: generated file
Object.defineProperty(exports, "__esModule", { value: true });
const {
Decimal,
DbNull,
JsonNull,
AnyNull,
NullTypes,
makeStrictEnum,
Public,
getRuntime,
skip
} = require('./runtime/index-browser.js')
const Prisma = {}
exports.Prisma = Prisma
exports.$Enums = {}
/**
* Prisma Client JS version: 7.8.0
* Query Engine version: 3c6e192761c0362d496ed980de936e2f3cebcd3a
*/
Prisma.prismaVersion = {
client: "7.8.0",
engine: "3c6e192761c0362d496ed980de936e2f3cebcd3a"
}
Prisma.PrismaClientKnownRequestError = () => {
const runtimeName = getRuntime().prettyName;
throw new Error(`PrismaClientKnownRequestError is unable to run in this browser environment, or has been bundled for the browser (running in ${runtimeName}).
In case this error is unexpected for you, please report it in https://pris.ly/prisma-prisma-bug-report`,
)};
Prisma.PrismaClientUnknownRequestError = () => {
const runtimeName = getRuntime().prettyName;
throw new Error(`PrismaClientUnknownRequestError is unable to run in this browser environment, or has been bundled for the browser (running in ${runtimeName}).
In case this error is unexpected for you, please report it in https://pris.ly/prisma-prisma-bug-report`,
)}
Prisma.PrismaClientRustPanicError = () => {
const runtimeName = getRuntime().prettyName;
throw new Error(`PrismaClientRustPanicError is unable to run in this browser environment, or has been bundled for the browser (running in ${runtimeName}).
In case this error is unexpected for you, please report it in https://pris.ly/prisma-prisma-bug-report`,
)}
Prisma.PrismaClientInitializationError = () => {
const runtimeName = getRuntime().prettyName;
throw new Error(`PrismaClientInitializationError is unable to run in this browser environment, or has been bundled for the browser (running in ${runtimeName}).
In case this error is unexpected for you, please report it in https://pris.ly/prisma-prisma-bug-report`,
)}
Prisma.PrismaClientValidationError = () => {
const runtimeName = getRuntime().prettyName;
throw new Error(`PrismaClientValidationError is unable to run in this browser environment, or has been bundled for the browser (running in ${runtimeName}).
In case this error is unexpected for you, please report it in https://pris.ly/prisma-prisma-bug-report`,
)}
Prisma.Decimal = Decimal
/**
* Re-export of sql-template-tag
*/
Prisma.sql = () => {
const runtimeName = getRuntime().prettyName;
throw new Error(`sqltag is unable to run in this browser environment, or has been bundled for the browser (running in ${runtimeName}).
In case this error is unexpected for you, please report it in https://pris.ly/prisma-prisma-bug-report`,
)}
Prisma.empty = () => {
const runtimeName = getRuntime().prettyName;
throw new Error(`empty is unable to run in this browser environment, or has been bundled for the browser (running in ${runtimeName}).
In case this error is unexpected for you, please report it in https://pris.ly/prisma-prisma-bug-report`,
)}
Prisma.join = () => {
const runtimeName = getRuntime().prettyName;
throw new Error(`join is unable to run in this browser environment, or has been bundled for the browser (running in ${runtimeName}).
In case this error is unexpected for you, please report it in https://pris.ly/prisma-prisma-bug-report`,
)}
Prisma.raw = () => {
const runtimeName = getRuntime().prettyName;
throw new Error(`raw is unable to run in this browser environment, or has been bundled for the browser (running in ${runtimeName}).
In case this error is unexpected for you, please report it in https://pris.ly/prisma-prisma-bug-report`,
)}
Prisma.validator = Public.validator
/**
* Extensions
*/
Prisma.getExtensionContext = () => {
const runtimeName = getRuntime().prettyName;
throw new Error(`Extensions.getExtensionContext is unable to run in this browser environment, or has been bundled for the browser (running in ${runtimeName}).
In case this error is unexpected for you, please report it in https://pris.ly/prisma-prisma-bug-report`,
)}
Prisma.defineExtension = () => {
const runtimeName = getRuntime().prettyName;
throw new Error(`Extensions.defineExtension is unable to run in this browser environment, or has been bundled for the browser (running in ${runtimeName}).
In case this error is unexpected for you, please report it in https://pris.ly/prisma-prisma-bug-report`,
)}
/**
* Shorthand utilities for JSON filtering
*/
Prisma.DbNull = DbNull
Prisma.JsonNull = JsonNull
Prisma.AnyNull = AnyNull
Prisma.NullTypes = NullTypes
/**
* Enums
*/
exports.Prisma.TransactionIsolationLevel = makeStrictEnum({
ReadUncommitted: 'ReadUncommitted',
ReadCommitted: 'ReadCommitted',
RepeatableRead: 'RepeatableRead',
Serializable: 'Serializable'
});
exports.Prisma.TaskScalarFieldEnum = {
id: 'id',
title: 'title',
description: 'description',
status: 'status',
difficulty: 'difficulty',
scope_clarity_score: 'scope_clarity_score',
error_classification: 'error_classification',
reward_amount: 'reward_amount',
reward_currency: 'reward_currency',
acceptance_criteria: 'acceptance_criteria',
required_stack: 'required_stack',
retry_count: 'retry_count',
stripe_payment_intent_id: 'stripe_payment_intent_id',
expires_at: 'expires_at',
created_at: 'created_at',
updated_at: 'updated_at'
};
exports.Prisma.ClaimScalarFieldEnum = {
id: 'id',
task_id: 'task_id',
developer_wallet: 'developer_wallet',
status: 'status',
claim_token: 'claim_token',
held_amount: 'held_amount',
held_currency: 'held_currency',
expires_at: 'expires_at',
created_at: 'created_at',
updated_at: 'updated_at'
};
exports.Prisma.SubmissionScalarFieldEnum = {
id: 'id',
task_id: 'task_id',
claim_id: 'claim_id',
status: 'status',
deliverables: 'deliverables',
estimated_judge_complete_at: 'estimated_judge_complete_at',
created_at: 'created_at',
updated_at: 'updated_at'
};
exports.Prisma.JudgeResultScalarFieldEnum = {
id: 'id',
submission_id: 'submission_id',
overall_result: 'overall_result',
tests: 'tests',
artifacts: 'artifacts',
error_classification: 'error_classification',
error_signature: 'error_signature',
retryable: 'retryable',
resource_usage: 'resource_usage',
judge_completed_at: 'judge_completed_at'
};
exports.Prisma.AuditEventScalarFieldEnum = {
id: 'id',
actorType: 'actorType',
actorId: 'actorId',
action: 'action',
entityType: 'entityType',
entityId: 'entityId',
beforeState: 'beforeState',
afterState: 'afterState',
reason: 'reason',
metadata: 'metadata',
createdAt: 'createdAt'
};
exports.Prisma.LedgerEntryScalarFieldEnum = {
id: 'id',
task_id: 'task_id',
phase: 'phase',
idempotency_key: 'idempotency_key',
stripe_object_id: 'stripe_object_id',
response_status: 'response_status',
http_status: 'http_status',
created_at: 'created_at',
updated_at: 'updated_at'
};
exports.Prisma.SortOrder = {
asc: 'asc',
desc: 'desc'
};
exports.Prisma.JsonNullValueInput = {
JsonNull: Prisma.JsonNull
};
exports.Prisma.NullableJsonNullValueInput = {
DbNull: Prisma.DbNull,
JsonNull: Prisma.JsonNull
};
exports.Prisma.QueryMode = {
default: 'default',
insensitive: 'insensitive'
};
exports.Prisma.JsonNullValueFilter = {
DbNull: Prisma.DbNull,
JsonNull: Prisma.JsonNull,
AnyNull: Prisma.AnyNull
};
exports.Prisma.NullsOrder = {
first: 'first',
last: 'last'
};
exports.Prisma.ModelName = {
Task: 'Task',
Claim: 'Claim',
Submission: 'Submission',
JudgeResult: 'JudgeResult',
AuditEvent: 'AuditEvent',
LedgerEntry: 'LedgerEntry'
};
/**
* This is a stub Prisma Client that will error at runtime if called.
*/
class PrismaClient {
constructor() {
return new Proxy(this, {
get(target, prop) {
let message
const runtime = getRuntime()
if (runtime.isEdge) {
message = `PrismaClient is not configured to run in ${runtime.prettyName}. In order to run Prisma Client on edge runtime, either:
- Use Prisma Accelerate: https://pris.ly/d/accelerate
- Use Driver Adapters: https://pris.ly/d/driver-adapters
`;
} else {
message = 'PrismaClient is unable to run in this browser environment, or has been bundled for the browser (running in `' + runtime.prettyName + '`).'
}
message += `
If this is unexpected, please open an issue: https://pris.ly/prisma-prisma-bug-report`
throw new Error(message)
}
})
}
}
exports.PrismaClient = PrismaClient
Object.assign(exports, Prisma)

11506
apps/web/prisma/generated/client/index.d.ts vendored Normal file

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,144 @@
{
"name": "prisma-client-e5a0d29448aef14ee47356f474ec26ee939c82359a834a5929b1331dfd872836",
"main": "index.js",
"types": "index.d.ts",
"browser": "default.js",
"exports": {
"./client": {
"require": {
"node": "./index.js",
"edge-light": "./edge.js",
"workerd": "./edge.js",
"worker": "./edge.js",
"browser": "./index-browser.js",
"default": "./index.js"
},
"import": {
"node": "./index.js",
"edge-light": "./edge.js",
"workerd": "./edge.js",
"worker": "./edge.js",
"browser": "./index-browser.js",
"default": "./index.js"
},
"default": "./index.js"
},
"./package.json": "./package.json",
".": {
"require": {
"node": "./index.js",
"edge-light": "./edge.js",
"workerd": "./edge.js",
"worker": "./edge.js",
"browser": "./index-browser.js",
"default": "./index.js"
},
"import": {
"node": "./index.js",
"edge-light": "./edge.js",
"workerd": "./edge.js",
"worker": "./edge.js",
"browser": "./index-browser.js",
"default": "./index.js"
},
"default": "./index.js"
},
"./extension": {
"types": "./extension.d.ts",
"require": "./extension.js",
"import": "./extension.js",
"default": "./extension.js"
},
"./index-browser": {
"types": "./index.d.ts",
"require": "./index-browser.js",
"import": "./index-browser.js",
"default": "./index-browser.js"
},
"./index": {
"types": "./index.d.ts",
"require": "./index.js",
"import": "./index.js",
"default": "./index.js"
},
"./edge": {
"types": "./edge.d.ts",
"require": "./edge.js",
"import": "./edge.js",
"default": "./edge.js"
},
"./runtime/client": {
"types": "./runtime/client.d.ts",
"node": {
"require": "./runtime/client.js",
"default": "./runtime/client.js"
},
"require": "./runtime/client.js",
"import": "./runtime/client.mjs",
"default": "./runtime/client.mjs"
},
"./runtime/wasm-compiler-edge": {
"types": "./runtime/wasm-compiler-edge.d.ts",
"require": "./runtime/wasm-compiler-edge.js",
"import": "./runtime/wasm-compiler-edge.mjs",
"default": "./runtime/wasm-compiler-edge.mjs"
},
"./runtime/index-browser": {
"types": "./runtime/index-browser.d.ts",
"require": "./runtime/index-browser.js",
"import": "./runtime/index-browser.mjs",
"default": "./runtime/index-browser.mjs"
},
"./generator-build": {
"require": "./generator-build/index.js",
"import": "./generator-build/index.js",
"default": "./generator-build/index.js"
},
"./sql": {
"require": {
"types": "./sql.d.ts",
"node": "./sql.js",
"default": "./sql.js"
},
"import": {
"types": "./sql.d.ts",
"node": "./sql.mjs",
"default": "./sql.mjs"
},
"default": "./sql.js"
},
"./*": "./*"
},
"version": "7.8.0",
"sideEffects": false,
"dependencies": {
"@prisma/client-runtime-utils": "7.8.0"
},
"imports": {
"#wasm-compiler-loader": {
"edge-light": "./wasm-edge-light-loader.mjs",
"workerd": "./wasm-worker-loader.mjs",
"worker": "./wasm-worker-loader.mjs",
"default": "./wasm-worker-loader.mjs"
},
"#main-entry-point": {
"require": {
"node": "./index.js",
"edge-light": "./edge.js",
"workerd": "./edge.js",
"worker": "./edge.js",
"browser": "./index-browser.js",
"default": "./index.js"
},
"import": {
"node": "./index.js",
"edge-light": "./edge.js",
"workerd": "./edge.js",
"worker": "./edge.js",
"browser": "./index-browser.js",
"default": "./index.js"
},
"default": "./index.js"
}
}
}

View File

@@ -0,0 +1,2 @@
"use strict";var h=Object.defineProperty;var T=Object.getOwnPropertyDescriptor;var M=Object.getOwnPropertyNames;var j=Object.prototype.hasOwnProperty;var D=(e,t)=>{for(var n in t)h(e,n,{get:t[n],enumerable:!0})},O=(e,t,n,_)=>{if(t&&typeof t=="object"||typeof t=="function")for(let r of M(t))!j.call(e,r)&&r!==n&&h(e,r,{get:()=>t[r],enumerable:!(_=T(t,r))||_.enumerable});return e};var B=e=>O(h({},"__esModule",{value:!0}),e);var xe={};D(xe,{QueryCompiler:()=>F,__wbg_Error_e83987f665cf5504:()=>q,__wbg_Number_bb48ca12f395cd08:()=>C,__wbg_String_8f0eb39a4a4c2f66:()=>k,__wbg___wbindgen_boolean_get_6d5a1ee65bab5f68:()=>W,__wbg___wbindgen_debug_string_df47ffb5e35e6763:()=>V,__wbg___wbindgen_in_bb933bd9e1b3bc0f:()=>z,__wbg___wbindgen_is_object_c818261d21f283a4:()=>L,__wbg___wbindgen_is_string_fbb76cb2940daafd:()=>P,__wbg___wbindgen_is_undefined_2d472862bd29a478:()=>Q,__wbg___wbindgen_jsval_loose_eq_b664b38a2f582147:()=>Y,__wbg___wbindgen_number_get_a20bf9b85341449d:()=>G,__wbg___wbindgen_string_get_e4f06c90489ad01b:()=>J,__wbg___wbindgen_throw_b855445ff6a94295:()=>X,__wbg_entries_e171b586f8f6bdbf:()=>H,__wbg_getTime_14776bfb48a1bff9:()=>K,__wbg_get_7bed016f185add81:()=>Z,__wbg_get_with_ref_key_1dc361bd10053bfe:()=>v,__wbg_instanceof_ArrayBuffer_70beb1189ca63b38:()=>ee,__wbg_instanceof_Uint8Array_20c8e73002f7af98:()=>te,__wbg_isSafeInteger_d216eda7911dde36:()=>ne,__wbg_length_69bca3cb64fc8748:()=>re,__wbg_length_cdd215e10d9dd507:()=>_e,__wbg_new_0_f9740686d739025c:()=>oe,__wbg_new_1acc0b6eea89d040:()=>ce,__wbg_new_5a79be3ab53b8aa5:()=>ie,__wbg_new_68651c719dcda04e:()=>se,__wbg_new_e17d9f43105b08be:()=>ue,__wbg_prototypesetcall_2a6620b6922694b2:()=>fe,__wbg_set_3f1d0b984ed272ed:()=>be,__wbg_set_907fb406c34a251d:()=>de,__wbg_set_c213c871859d6500:()=>ae,__wbg_set_message_82ae475bb413aa5c:()=>ge,__wbg_set_wasm:()=>N,__wbindgen_cast_2241b6af4c4b2941:()=>le,__wbindgen_cast_4625c577ab2ec9ee:()=>we,__wbindgen_cast_9ae0607507abb057:()=>pe,__wbindgen_cast_d6cd19b81560fd6e:()=>ye,__wbindgen_init_externref_table:()=>me});module.exports=B(xe);var A=()=>{};A.prototype=A;let o;function N(e){o=e}let p=null;function a(){return(p===null||p.byteLength===0)&&(p=new Uint8Array(o.memory.buffer)),p}let y=new TextDecoder("utf-8",{ignoreBOM:!0,fatal:!0});y.decode();const U=2146435072;let S=0;function R(e,t){return S+=t,S>=U&&(y=new TextDecoder("utf-8",{ignoreBOM:!0,fatal:!0}),y.decode(),S=t),y.decode(a().subarray(e,e+t))}function m(e,t){return e=e>>>0,R(e,t)}let f=0;const g=new TextEncoder;"encodeInto"in g||(g.encodeInto=function(e,t){const n=g.encode(e);return t.set(n),{read:e.length,written:n.length}});function l(e,t,n){if(n===void 0){const i=g.encode(e),d=t(i.length,1)>>>0;return a().subarray(d,d+i.length).set(i),f=i.length,d}let _=e.length,r=t(_,1)>>>0;const s=a();let c=0;for(;c<_;c++){const i=e.charCodeAt(c);if(i>127)break;s[r+c]=i}if(c!==_){c!==0&&(e=e.slice(c)),r=n(r,_,_=c+e.length*3,1)>>>0;const i=a().subarray(r+c,r+_),d=g.encodeInto(e,i);c+=d.written,r=n(r,_,c,1)>>>0}return f=c,r}let b=null;function u(){return(b===null||b.buffer.detached===!0||b.buffer.detached===void 0&&b.buffer!==o.memory.buffer)&&(b=new DataView(o.memory.buffer)),b}function x(e){return e==null}function I(e){const t=typeof e;if(t=="number"||t=="boolean"||e==null)return`${e}`;if(t=="string")return`"${e}"`;if(t=="symbol"){const r=e.description;return r==null?"Symbol":`Symbol(${r})`}if(t=="function"){const r=e.name;return typeof r=="string"&&r.length>0?`Function(${r})`:"Function"}if(Array.isArray(e)){const r=e.length;let s="[";r>0&&(s+=I(e[0]));for(let c=1;c<r;c++)s+=", "+I(e[c]);return s+="]",s}const n=/\[object ([^\]]+)\]/.exec(toString.call(e));let _;if(n&&n.length>1)_=n[1];else return toString.call(e);if(_=="Object")try{return"Object("+JSON.stringify(e)+")"}catch{return"Object"}return e instanceof Error?`${e.name}: ${e.message}
${e.stack}`:_}function $(e,t){return e=e>>>0,a().subarray(e/1,e/1+t)}function w(e){const t=o.__wbindgen_externrefs.get(e);return o.__externref_table_dealloc(e),t}const E=typeof FinalizationRegistry>"u"?{register:()=>{},unregister:()=>{}}:new FinalizationRegistry(e=>o.__wbg_querycompiler_free(e>>>0,1));class F{__destroy_into_raw(){const t=this.__wbg_ptr;return this.__wbg_ptr=0,E.unregister(this),t}free(){const t=this.__destroy_into_raw();o.__wbg_querycompiler_free(t,0)}compileBatch(t){const n=l(t,o.__wbindgen_malloc,o.__wbindgen_realloc),_=f,r=o.querycompiler_compileBatch(this.__wbg_ptr,n,_);if(r[2])throw w(r[1]);return w(r[0])}constructor(t){const n=o.querycompiler_new(t);if(n[2])throw w(n[1]);return this.__wbg_ptr=n[0]>>>0,E.register(this,this.__wbg_ptr,this),this}compile(t){const n=l(t,o.__wbindgen_malloc,o.__wbindgen_realloc),_=f,r=o.querycompiler_compile(this.__wbg_ptr,n,_);if(r[2])throw w(r[1]);return w(r[0])}}Symbol.dispose&&(F.prototype[Symbol.dispose]=F.prototype.free);function q(e,t){return Error(m(e,t))}function C(e){return Number(e)}function k(e,t){const n=String(t),_=l(n,o.__wbindgen_malloc,o.__wbindgen_realloc),r=f;u().setInt32(e+4*1,r,!0),u().setInt32(e+4*0,_,!0)}function W(e){const t=e,n=typeof t=="boolean"?t:void 0;return x(n)?16777215:n?1:0}function V(e,t){const n=I(t),_=l(n,o.__wbindgen_malloc,o.__wbindgen_realloc),r=f;u().setInt32(e+4*1,r,!0),u().setInt32(e+4*0,_,!0)}function z(e,t){return e in t}function L(e){const t=e;return typeof t=="object"&&t!==null}function P(e){return typeof e=="string"}function Q(e){return e===void 0}function Y(e,t){return e==t}function G(e,t){const n=t,_=typeof n=="number"?n:void 0;u().setFloat64(e+8*1,x(_)?0:_,!0),u().setInt32(e+4*0,!x(_),!0)}function J(e,t){const n=t,_=typeof n=="string"?n:void 0;var r=x(_)?0:l(_,o.__wbindgen_malloc,o.__wbindgen_realloc),s=f;u().setInt32(e+4*1,s,!0),u().setInt32(e+4*0,r,!0)}function X(e,t){throw new Error(m(e,t))}function H(e){return Object.entries(e)}function K(e){return e.getTime()}function Z(e,t){return e[t>>>0]}function v(e,t){return e[t]}function ee(e){let t;try{t=e instanceof ArrayBuffer}catch{t=!1}return t}function te(e){let t;try{t=e instanceof Uint8Array}catch{t=!1}return t}function ne(e){return Number.isSafeInteger(e)}function re(e){return e.length}function _e(e){return e.length}function oe(){return new Date}function ce(){return new Object}function ie(e){return new Uint8Array(e)}function se(){return new Map}function ue(){return new Array}function fe(e,t,n){Uint8Array.prototype.set.call($(e,t),n)}function be(e,t,n){e[t]=n}function de(e,t,n){return e.set(t,n)}function ae(e,t,n){e[t>>>0]=n}function ge(e,t){global.PRISMA_WASM_PANIC_REGISTRY.set_message(m(e,t))}function le(e,t){return m(e,t)}function we(e){return BigInt.asUintN(64,e)}function pe(e){return e}function ye(e){return e}function me(){const e=o.__wbindgen_externrefs,t=e.grow(4);e.set(0,void 0),e.set(t+0,void 0),e.set(t+1,null),e.set(t+2,!0),e.set(t+3,!1)}0&&(module.exports={QueryCompiler,__wbg_Error_e83987f665cf5504,__wbg_Number_bb48ca12f395cd08,__wbg_String_8f0eb39a4a4c2f66,__wbg___wbindgen_boolean_get_6d5a1ee65bab5f68,__wbg___wbindgen_debug_string_df47ffb5e35e6763,__wbg___wbindgen_in_bb933bd9e1b3bc0f,__wbg___wbindgen_is_object_c818261d21f283a4,__wbg___wbindgen_is_string_fbb76cb2940daafd,__wbg___wbindgen_is_undefined_2d472862bd29a478,__wbg___wbindgen_jsval_loose_eq_b664b38a2f582147,__wbg___wbindgen_number_get_a20bf9b85341449d,__wbg___wbindgen_string_get_e4f06c90489ad01b,__wbg___wbindgen_throw_b855445ff6a94295,__wbg_entries_e171b586f8f6bdbf,__wbg_getTime_14776bfb48a1bff9,__wbg_get_7bed016f185add81,__wbg_get_with_ref_key_1dc361bd10053bfe,__wbg_instanceof_ArrayBuffer_70beb1189ca63b38,__wbg_instanceof_Uint8Array_20c8e73002f7af98,__wbg_isSafeInteger_d216eda7911dde36,__wbg_length_69bca3cb64fc8748,__wbg_length_cdd215e10d9dd507,__wbg_new_0_f9740686d739025c,__wbg_new_1acc0b6eea89d040,__wbg_new_5a79be3ab53b8aa5,__wbg_new_68651c719dcda04e,__wbg_new_e17d9f43105b08be,__wbg_prototypesetcall_2a6620b6922694b2,__wbg_set_3f1d0b984ed272ed,__wbg_set_907fb406c34a251d,__wbg_set_c213c871859d6500,__wbg_set_message_82ae475bb413aa5c,__wbg_set_wasm,__wbindgen_cast_2241b6af4c4b2941,__wbindgen_cast_4625c577ab2ec9ee,__wbindgen_cast_9ae0607507abb057,__wbindgen_cast_d6cd19b81560fd6e,__wbindgen_init_externref_table});

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,90 @@
import { AnyNull } from '@prisma/client-runtime-utils';
import { DbNull } from '@prisma/client-runtime-utils';
import { Decimal } from '@prisma/client-runtime-utils';
import { isAnyNull } from '@prisma/client-runtime-utils';
import { isDbNull } from '@prisma/client-runtime-utils';
import { isJsonNull } from '@prisma/client-runtime-utils';
import { isObjectEnumValue } from '@prisma/client-runtime-utils';
import { JsonNull } from '@prisma/client-runtime-utils';
import { NullTypes } from '@prisma/client-runtime-utils';
export { AnyNull }
declare type Args<T, F extends Operation> = T extends {
[K: symbol]: {
types: {
operations: {
[K in F]: {
args: any;
};
};
};
};
} ? T[symbol]['types']['operations'][F]['args'] : any;
export { DbNull }
export { Decimal }
declare type Exact<A, W> = (A extends unknown ? (W extends A ? {
[K in keyof A]: Exact<A[K], W[K]>;
} : W) : never) | (A extends Narrowable ? A : never);
export declare function getRuntime(): GetRuntimeOutput;
declare type GetRuntimeOutput = {
id: RuntimeName;
prettyName: string;
isEdge: boolean;
};
export { isAnyNull }
export { isDbNull }
export { isJsonNull }
export { isObjectEnumValue }
export { JsonNull }
/**
* Generates more strict variant of an enum which, unlike regular enum,
* throws on non-existing property access. This can be useful in following situations:
* - we have an API, that accepts both `undefined` and `SomeEnumType` as an input
* - enum values are generated dynamically from DMMF.
*
* In that case, if using normal enums and no compile-time typechecking, using non-existing property
* will result in `undefined` value being used, which will be accepted. Using strict enum
* in this case will help to have a runtime exception, telling you that you are probably doing something wrong.
*
* Note: if you need to check for existence of a value in the enum you can still use either
* `in` operator or `hasOwnProperty` function.
*
* @param definition
* @returns
*/
export declare function makeStrictEnum<T extends Record<PropertyKey, string | number>>(definition: T): T;
declare type Narrowable = string | number | bigint | boolean | [];
export { NullTypes }
declare type Operation = 'findFirst' | 'findFirstOrThrow' | 'findUnique' | 'findUniqueOrThrow' | 'findMany' | 'create' | 'createMany' | 'createManyAndReturn' | 'update' | 'updateMany' | 'updateManyAndReturn' | 'upsert' | 'delete' | 'deleteMany' | 'aggregate' | 'count' | 'groupBy' | '$queryRaw' | '$executeRaw' | '$queryRawUnsafe' | '$executeRawUnsafe' | 'findRaw' | 'aggregateRaw' | '$runCommandRaw';
declare namespace Public {
export {
validator
}
}
export { Public }
declare type RuntimeName = 'workerd' | 'deno' | 'netlify' | 'node' | 'bun' | 'edge-light' | '';
declare function validator<V>(): <S>(select: Exact<S, V>) => S;
declare function validator<C, M extends Exclude<keyof C, `$${string}`>, O extends keyof C[M] & Operation>(client: C, model: M, operation: O): <S>(select: Exact<S, Args<C[M], O>>) => S;
declare function validator<C, M extends Exclude<keyof C, `$${string}`>, O extends keyof C[M] & Operation, P extends keyof Args<C[M], O>>(client: C, model: M, operation: O, prop: P): <S>(select: Exact<S, Args<C[M], O>[P]>) => S;
export { }

View File

@@ -0,0 +1,6 @@
/* !!! This is code generated by Prisma. Do not edit directly. !!!
/* eslint-disable */
// biome-ignore-all lint: generated file
"use strict";var s=Object.defineProperty;var g=Object.getOwnPropertyDescriptor;var p=Object.getOwnPropertyNames;var f=Object.prototype.hasOwnProperty;var a=(e,t)=>{for(var n in t)s(e,n,{get:t[n],enumerable:!0})},y=(e,t,n,r)=>{if(t&&typeof t=="object"||typeof t=="function")for(let i of p(t))!f.call(e,i)&&i!==n&&s(e,i,{get:()=>t[i],enumerable:!(r=g(t,i))||r.enumerable});return e};var x=e=>y(s({},"__esModule",{value:!0}),e);var M={};a(M,{AnyNull:()=>o.AnyNull,DbNull:()=>o.DbNull,Decimal:()=>m.Decimal,JsonNull:()=>o.JsonNull,NullTypes:()=>o.NullTypes,Public:()=>l,getRuntime:()=>c,isAnyNull:()=>o.isAnyNull,isDbNull:()=>o.isDbNull,isJsonNull:()=>o.isJsonNull,isObjectEnumValue:()=>o.isObjectEnumValue,makeStrictEnum:()=>u});module.exports=x(M);var l={};a(l,{validator:()=>d});function d(...e){return t=>t}var b=new Set(["toJSON","$$typeof","asymmetricMatch",Symbol.iterator,Symbol.toStringTag,Symbol.isConcatSpreadable,Symbol.toPrimitive]);function u(e){return new Proxy(e,{get(t,n){if(n in t)return t[n];if(!b.has(n))throw new TypeError("Invalid enum value: ".concat(String(n)))}})}var N=()=>{var e,t;return((t=(e=globalThis.process)==null?void 0:e.release)==null?void 0:t.name)==="node"},E=()=>{var e,t;return!!globalThis.Bun||!!((t=(e=globalThis.process)==null?void 0:e.versions)!=null&&t.bun)},S=()=>!!globalThis.Deno,R=()=>typeof globalThis.Netlify=="object",h=()=>typeof globalThis.EdgeRuntime=="object",C=()=>{var e;return((e=globalThis.navigator)==null?void 0:e.userAgent)==="Cloudflare-Workers"};function k(){var n;return(n=[[R,"netlify"],[h,"edge-light"],[C,"workerd"],[S,"deno"],[E,"bun"],[N,"node"]].flatMap(r=>r[0]()?[r[1]]:[]).at(0))!=null?n:""}var O={node:"Node.js",workerd:"Cloudflare Workers",deno:"Deno and Deno Deploy",netlify:"Netlify Edge Functions","edge-light":"Edge Runtime (Vercel Edge Functions, Vercel Edge Middleware, Next.js (Pages Router) Edge API Routes, Next.js (App Router) Edge Route Handlers or Next.js Middleware)"};function c(){let e=k();return{id:e,prettyName:O[e]||e,isEdge:["workerd","deno","netlify","edge-light"].includes(e)}}var o=require("@prisma/client-runtime-utils"),m=require("@prisma/client-runtime-utils");0&&(module.exports={AnyNull,DbNull,Decimal,JsonNull,NullTypes,Public,getRuntime,isAnyNull,isDbNull,isJsonNull,isObjectEnumValue,makeStrictEnum});
//# sourceMappingURL=index-browser.js.map

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,101 @@
generator client {
provider = "prisma-client-js"
output = "./generated/client"
}
datasource db {
provider = "postgresql"
}
model Task {
id String @id @default(uuid())
title String
description String
status String // Enum: TaskStatus (OPEN, EXECUTING, VERIFYING, COMPLETED, FAILED, etc)
difficulty String // Enum: TaskDifficulty (HELLO_WORLD, COMPONENT, VIEW, EPIC)
scope_clarity_score Float
error_classification String? // Enum: TaskErrorClassification
reward_amount Int // Stored in cents
reward_currency String // USD, TWD, USDC
acceptance_criteria Json // Contains validation_mode, test_file_content, rules
required_stack String[]
retry_count Int @default(0)
stripe_payment_intent_id String?
expires_at DateTime?
created_at DateTime @default(now())
updated_at DateTime @updatedAt
claims Claim[]
submissions Submission[]
}
model Claim {
id String @id @default(uuid())
task_id String
task Task @relation(fields: [task_id], references: [id])
developer_wallet String
status String // EXECUTING, CANCELLED, VERIFYING, COMPLETED
claim_token String @unique // Idempotency token for this claim
held_amount Int
held_currency String
expires_at DateTime
created_at DateTime @default(now())
updated_at DateTime @updatedAt
submissions Submission[]
}
model Submission {
id String @id @default(uuid())
task_id String
task Task @relation(fields: [task_id], references: [id])
claim_id String
claim Claim @relation(fields: [claim_id], references: [id])
status String // VERIFYING, JUDGED
deliverables Json // Files and notes submitted
estimated_judge_complete_at DateTime?
created_at DateTime @default(now())
updated_at DateTime @updatedAt
judge_results JudgeResult[]
}
model JudgeResult {
id String @id @default(uuid())
submission_id String
submission Submission @relation(fields: [submission_id], references: [id])
overall_result String // PASS, FAIL, TIMEOUT
tests Json // Array of test results
artifacts Json? // screenshot_url, logs_url, diff_url
error_classification String? // Enum: JudgeErrorClassification
error_signature String?
retryable Boolean @default(false)
resource_usage Json // cpu_ms, mem_peak_mb, io_bytes
judge_completed_at DateTime @default(now())
}
model AuditEvent {
id String @id @default(uuid())
actorType String
actorId String?
action String
entityType String
entityId String
beforeState Json?
afterState Json?
reason String?
metadata Json?
createdAt DateTime @default(now())
}
model LedgerEntry {
id String @id @default(cuid())
task_id String
phase String
idempotency_key String @unique
stripe_object_id String?
response_status String
http_status Int
created_at DateTime @default(now())
updated_at DateTime @updatedAt
}

View File

@@ -0,0 +1,5 @@
/* !!! This is code generated by Prisma. Do not edit directly. !!!
/* eslint-disable */
// biome-ignore-all lint: generated file
export default import('./query_compiler_fast_bg.wasm?module')

View File

@@ -0,0 +1,5 @@
/* !!! This is code generated by Prisma. Do not edit directly. !!!
/* eslint-disable */
// biome-ignore-all lint: generated file
export default import('./query_compiler_fast_bg.wasm')

View File

@@ -1,5 +1,6 @@
generator client {
provider = "prisma-client-js"
output = "./generated/client"
}
datasource db {
@@ -86,3 +87,15 @@ model AuditEvent {
metadata Json?
createdAt DateTime @default(now())
}
model LedgerEntry {
id String @id @default(cuid())
task_id String
phase String
idempotency_key String @unique
stripe_object_id String?
response_status String
http_status Int
created_at DateTime @default(now())
updated_at DateTime @updatedAt
}

View File

@@ -1,4 +1,4 @@
import { PrismaClient } from "@prisma/client";
import { PrismaClient } from "./prisma/generated/client";
import { TaskStatus, TaskDifficulty } from "@agent-bounty/contracts";
const prisma = new PrismaClient();

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 });
}
}

127
apps/web/src/lib/payment.ts Normal file
View File

@@ -0,0 +1,127 @@
import { Prisma } from "@prisma/client";
// In Phase 2, we use a Mock implementation for Stripe to ensure our DB state machine
// and idempotency rules are solid before integrating the real Stripe SDK.
export async function authHold(
tx: Prisma.TransactionClient,
taskId: string,
amount: number,
currency: string,
wallet: string,
idempotencyKey: string
) {
// Check idempotency
const existing = await tx.ledgerEntry.findUnique({
where: { idempotency_key: idempotencyKey },
});
if (existing) {
if (existing.response_status === "SUCCESS") return existing;
throw new Error(`Previous authHold failed for idempotencyKey: ${idempotencyKey}`);
}
// --- MOCK STRIPE CALL ---
// In real life: const intent = await stripe.paymentIntents.create({ amount, currency, payment_method_types: ['card'], capture_method: 'manual', metadata: { taskId, wallet } });
const mockStripeObjectId = `pi_mock_hold_${crypto.randomUUID()}`;
// ------------------------
return await tx.ledgerEntry.create({
data: {
task_id: taskId,
phase: "AUTH_HOLD",
idempotency_key: idempotencyKey,
stripe_object_id: mockStripeObjectId,
response_status: "SUCCESS",
http_status: 200,
},
});
}
export async function capturePayment(
tx: Prisma.TransactionClient,
taskId: string,
idempotencyKey: string
) {
const existing = await tx.ledgerEntry.findUnique({
where: { idempotency_key: idempotencyKey },
});
if (existing) {
if (existing.response_status === "SUCCESS") return existing;
throw new Error(`Previous capturePayment failed for idempotencyKey: ${idempotencyKey}`);
}
// We should find the AUTH_HOLD record to get the intent ID in real life
const holdEntry = await tx.ledgerEntry.findFirst({
where: { task_id: taskId, phase: "AUTH_HOLD", response_status: "SUCCESS" },
orderBy: { created_at: "desc" }
});
if (!holdEntry || !holdEntry.stripe_object_id) {
throw new Error("Cannot capture without a successful AUTH_HOLD");
}
// --- MOCK STRIPE CALL ---
// In real life: const capture = await stripe.paymentIntents.capture(holdEntry.stripe_object_id);
const mockStripeObjectId = `ch_mock_capture_${crypto.randomUUID()}`;
// ------------------------
return await tx.ledgerEntry.create({
data: {
task_id: taskId,
phase: "CAPTURE",
idempotency_key: idempotencyKey,
stripe_object_id: mockStripeObjectId,
response_status: "SUCCESS",
http_status: 200,
},
});
}
export async function releasePayment(
tx: Prisma.TransactionClient,
taskId: string,
idempotencyKey: string
) {
const existing = await tx.ledgerEntry.findUnique({
where: { idempotency_key: idempotencyKey },
});
if (existing) {
if (existing.response_status === "SUCCESS") return existing;
throw new Error(`Previous releasePayment failed for idempotencyKey: ${idempotencyKey}`);
}
const holdEntry = await tx.ledgerEntry.findFirst({
where: { task_id: taskId, phase: "AUTH_HOLD", response_status: "SUCCESS" },
orderBy: { created_at: "desc" }
});
if (!holdEntry || !holdEntry.stripe_object_id) {
// If there was no hold to begin with, releasing is a no-op but we log it as SUCCESS
return await tx.ledgerEntry.create({
data: {
task_id: taskId,
phase: "RELEASE",
idempotency_key: idempotencyKey,
stripe_object_id: null,
response_status: "SUCCESS",
http_status: 200,
},
});
}
// --- MOCK STRIPE CALL ---
// In real life: const cancel = await stripe.paymentIntents.cancel(holdEntry.stripe_object_id);
const mockStripeObjectId = `re_mock_release_${crypto.randomUUID()}`;
// ------------------------
return await tx.ledgerEntry.create({
data: {
task_id: taskId,
phase: "RELEASE",
idempotency_key: idempotencyKey,
stripe_object_id: mockStripeObjectId,
response_status: "SUCCESS",
http_status: 200,
},
});
}

View File

@@ -1,4 +1,4 @@
import { PrismaClient } from "@prisma/client";
import { PrismaClient } from "../../prisma/generated/client";
const globalForPrisma = global as unknown as { prisma: PrismaClient };

View File

@@ -26,12 +26,12 @@ export async function runSubmissionInSandbox(
// Run tests
const result = await sandbox.commands.run("npx vitest run test.spec.tsx --reporter json", {
timeout: 120000 // 2 minutes
timeoutMs: 120000 // 2 minutes
});
let overall = JudgeOverallResult.FAIL;
let errorClass = result.exitCode === 0 ? null : JudgeErrorClassification.TEST_FAIL;
let parsedTests = [];
let overall: JudgeOverallResult = JudgeOverallResult.FAIL;
let errorClass: JudgeErrorClassification | null = result.exitCode === 0 ? null : JudgeErrorClassification.TEST_FAIL;
let parsedTests: any[] = [];
if (result.exitCode === 0) {
overall = JudgeOverallResult.PASS;
@@ -70,7 +70,7 @@ export async function runSubmissionInSandbox(
};
} finally {
if (sandbox) {
await sandbox.close();
await sandbox.kill();
}
}
}

View File

@@ -162,26 +162,11 @@ export const ClaimTaskResponseSchema = z.object({
// submit_solution Request/Response
// ─────────────────────────────────────────────
const DeliverableFileSchema = z.object({
path: z.string().min(1).max(260, "路徑長度超過限制"),
content: z
.string()
.min(10, "檔案內容不得為空")
.max(500_000, "單檔案內容超過 500KB 限制"),
language: z.enum(["typescript", "javascript", "tsx", "jsx", "css", "html"]),
});
export const SubmitSolutionRequestSchema = z.object({
task_id: UUIDSchema,
/** 接案時取得的冪等憑證,防止重複提交 */
claim_token: z.string().uuid(),
deliverables: z.object({
files: z
.array(DeliverableFileSchema)
.min(1, "至少需要提交一個檔案")
.max(20, "單次提交不得超過 20 個檔案"),
notes: z.string().max(1000).optional(),
}),
deliverables: z.record(z.string(), z.string()),
});
export const SubmitSolutionResponseSchema = z.object({

View File

@@ -5,7 +5,7 @@ describe("Contracts Zod Validation", () => {
it("should validate a correct ListOpenTasks payload", () => {
const payload = {
skills: ["React", "TypeScript"],
max_difficulty: TaskDifficulty.INTERMEDIATE,
difficulty: TaskDifficulty.COMPONENT,
limit: 10,
};
const result = ListOpenTasksRequestSchema.safeParse(payload);

16
pnpm-lock.yaml generated
View File

@@ -35,6 +35,9 @@ importers:
react-dom:
specifier: 19.2.4
version: 19.2.4(react@19.2.4)
zod:
specifier: ^3.23.0
version: 3.25.76
devDependencies:
'@tailwindcss/postcss':
specifier: ^4
@@ -3050,6 +3053,9 @@ packages:
peerDependencies:
zod: ^3.25.0 || ^4.0.0
zod@3.25.76:
resolution: {integrity: sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==}
zod@4.4.3:
resolution: {integrity: sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ==}
@@ -4593,8 +4599,8 @@ snapshots:
'@babel/parser': 7.29.7
eslint: 9.39.4(jiti@2.7.0)
hermes-parser: 0.25.1
zod: 4.4.3
zod-validation-error: 4.0.2(zod@4.4.3)
zod: 3.25.76
zod-validation-error: 4.0.2(zod@3.25.76)
transitivePeerDependencies:
- supports-color
@@ -6133,8 +6139,10 @@ snapshots:
dependencies:
zod: 4.4.3
zod-validation-error@4.0.2(zod@4.4.3):
zod-validation-error@4.0.2(zod@3.25.76):
dependencies:
zod: 4.4.3
zod: 3.25.76
zod@3.25.76: {}
zod@4.4.3: {}

128
test_idempotency.ts Normal file
View File

@@ -0,0 +1,128 @@
import { PrismaClient } from "./apps/web/prisma/generated/client/index.js";
import { TaskStatus, MCPToolName, SettlementPhase, TaskDifficulty } from "./packages/contracts/src/enums/index.js";
import { v4 as uuidv4 } from "uuid";
const prisma = new PrismaClient();
async function main() {
console.log("=== Idempotency and Payment Flow Test ===");
// 1. Create a mock task
const taskId = uuidv4();
await prisma.task.create({
data: {
id: taskId,
title: "Test Task",
difficulty: TaskDifficulty.COMPONENT,
status: TaskStatus.OPEN,
requirements: "Test requirements",
base_reward_cents: 500,
}
});
console.log(`[+] Created Task ${taskId}`);
// 2. Simulate Claim Task (Auth Hold)
console.log(`\n[+] Simulating MCP: claim_task`);
// First attempt (Should succeed and create Auth Hold)
const agentId = "agent-test-1";
const idempotencyKeyClaim = `${taskId}_${MCPToolName.CLAIM_TASK}`;
const claim1 = await prisma.$transaction(async (tx) => {
// authHold logic
const existing = await tx.ledgerEntry.findUnique({ where: { idempotency_key: idempotencyKeyClaim }});
if (existing) {
console.log("Idempotent hit for claim_task!");
return existing;
}
await tx.task.update({
where: { id: taskId },
data: { status: TaskStatus.EXECUTING, agent_id: agentId }
});
return await tx.ledgerEntry.create({
data: {
task_id: taskId,
phase: SettlementPhase.AUTH_HOLD,
amount_cents: 500,
currency: "USD",
stripe_object_id: "pi_mock_" + uuidv4(),
idempotency_key: idempotencyKeyClaim
}
});
});
console.log(`[1] First claim_task attempt created ledger entry: ${claim1.id}`);
// Second attempt (Should hit idempotency key and NOT duplicate)
const claim2 = await prisma.$transaction(async (tx) => {
const existing = await tx.ledgerEntry.findUnique({ where: { idempotency_key: idempotencyKeyClaim }});
if (existing) {
console.log("Idempotent hit for claim_task!");
return existing;
}
// Should not reach here
throw new Error("Idempotency failed!");
});
console.log(`[2] Second claim_task attempt returned same entry: ${claim2.id}`);
// 3. Simulate Submit Solution (Capture)
console.log(`\n[+] Simulating MCP: submit_solution`);
const idempotencyKeySubmit = `${taskId}_${MCPToolName.SUBMIT_SOLUTION}`;
const submit1 = await prisma.$transaction(async (tx) => {
const existing = await tx.ledgerEntry.findUnique({ where: { idempotency_key: idempotencyKeySubmit }});
if (existing) {
console.log("Idempotent hit for submit_solution!");
return existing;
}
await tx.task.update({
where: { id: taskId },
data: { status: TaskStatus.COMPLETED }
});
return await tx.ledgerEntry.create({
data: {
task_id: taskId,
phase: SettlementPhase.CAPTURE,
amount_cents: 500,
currency: "USD",
stripe_object_id: claim1.stripe_object_id,
idempotency_key: idempotencyKeySubmit
}
});
});
console.log(`[1] First submit_solution attempt created ledger entry: ${submit1.id}`);
const submit2 = await prisma.$transaction(async (tx) => {
const existing = await tx.ledgerEntry.findUnique({ where: { idempotency_key: idempotencyKeySubmit }});
if (existing) {
console.log("Idempotent hit for submit_solution!");
return existing;
}
throw new Error("Idempotency failed!");
});
console.log(`[2] Second submit_solution attempt returned same entry: ${submit2.id}`);
// Verify DB state
const finalTask = await prisma.task.findUnique({ where: { id: taskId }, include: { ledger_entries: true } });
console.log(`\n=== Final Verification ===`);
console.log(`Task Status: ${finalTask?.status}`);
console.log(`Ledger Entries: ${finalTask?.ledger_entries.length}`);
finalTask?.ledger_entries.forEach(entry => {
console.log(` - Phase: ${entry.phase}, Amount: ${entry.amount_cents}, Key: ${entry.idempotency_key}`);
});
if (finalTask?.status === TaskStatus.COMPLETED && finalTask.ledger_entries.length === 2) {
console.log("\n✅ End-to-End Simulation Passed! Idempotency and State transitions are correct.");
} else {
console.log("\n❌ Simulation Failed.");
}
}
main()
.catch(console.error)
.finally(async () => {
await prisma.$disconnect();
});