Files
agent-bounty-protocol/packages/contracts/src/schemas/index.ts
OG T 0601df8bd9
All checks were successful
CI and Production Smoke / smoke (push) Successful in 10s
feat: add A2A agent integration control plane
2026-06-11 13:30:22 +08:00

571 lines
22 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* @agent-bounty/contracts — Zod Validation Schemas
*
* 唯一的驗證真實來源。
* MCP server、後端 API、前端表單都必須 import 這裡的 schema
* 禁止在各自的程式庫中重新定義任何 VibeWork 資料結構。
*/
import { z } from "zod";
import {
TaskStatus,
TaskDifficulty,
ValidationMode,
JudgeOverallResult,
JudgeTestStatus,
JudgeErrorClassification,
SettlementPhase,
SettlementCaptureMode,
SupportedCurrency,
LeadStatus,
AttributionModel,
TaskErrorClassification,
MAX_TASK_RETRY_COUNT,
} from "../enums/index.js";
// ─────────────────────────────────────────────
// 通用基礎型別
// ─────────────────────────────────────────────
const UUIDSchema = z.string().uuid("無效的 UUID 格式");
const CUIDSchema = z.string().cuid("無效的 CUID 格式");
const PositiveIntSchema = z.number().int().positive();
const NonNegativeIntSchema = z.number().int().nonnegative();
/** 金額(以最小單位計,例如美分 / 台幣分) */
const MoneyAmountSchema = z
.number()
.int()
.nonnegative("金額不得為負數")
.max(1_000_000_00, "單筆金額超過上限($1,000,000");
/** scope_clarity_scoreAI 評估任務清晰度Phase 1 要求 ≥ 0.90 */
const ScopeScoreSchema = z
.number()
.min(0)
.max(1)
.refine((v) => v >= 0.9, {
message: "scope_clarity_score 必須 ≥ 0.90 才能進入主任務池",
});
// ─────────────────────────────────────────────
// Task 驗收條件 Schema
// ─────────────────────────────────────────────
export const AcceptanceCriteriaSchema = z.object({
validation_mode: z.enum([
ValidationMode.VITEST_UNIT,
ValidationMode.PLAYWRIGHT_E2E,
ValidationMode.AST_PARSING,
ValidationMode.VISUAL_REGRESSION,
]),
/** 由平台提供的最小測試檔內容stringAgent 不得修改此欄位 */
test_file_content: z.string().min(20, "測試檔內容不得為空"),
/** 關鍵斷言規則(可選,補充說明) */
rules: z
.array(
z.object({
assertion: z.string(),
expected: z.unknown(),
description: z.string().optional(),
})
)
.optional(),
});
// ─────────────────────────────────────────────
// Task Bounty 核心 SchemaOpen Bounty Board
// ─────────────────────────────────────────────
export const TaskBountySchema = z.object({
task_id: UUIDSchema,
title: z.string().min(5).max(120),
description: z.string().min(20).max(2000),
status: z.enum([
TaskStatus.DRAFT,
TaskStatus.OPEN,
TaskStatus.EXECUTING,
TaskStatus.VERIFYING,
TaskStatus.PENDING_REVIEW,
TaskStatus.COMPLETED,
TaskStatus.FAILED,
TaskStatus.FAILED_RETRYABLE,
TaskStatus.CANCELLED,
TaskStatus.DISPUTED,
TaskStatus.REFUND_PENDING,
TaskStatus.PAYOUT_READY,
TaskStatus.PAYOUT_SETTLED,
TaskStatus.ARCHIVED,
]),
difficulty: z.enum([
TaskDifficulty.HELLO_WORLD,
TaskDifficulty.COMPONENT,
TaskDifficulty.VIEW,
TaskDifficulty.EPIC,
]),
/** 最小可行要求 ≥ 0.90 才能進入公開任務池 */
scope_clarity_score: ScopeScoreSchema,
error_classification: z.enum([
TaskErrorClassification.RETRYABLE,
TaskErrorClassification.NON_RETRYABLE,
]),
reward: z.object({
/** 金額整數以最小貨幣單位計USD cents / TWD 元) */
amount: MoneyAmountSchema,
currency: z.enum([
SupportedCurrency.USD,
SupportedCurrency.TWD,
SupportedCurrency.USDC,
]),
/** 顯示用格式化金額(例如 "NT$30" / "$1.00"),由後端產生 */
display_amount: z.string().optional(),
}),
acceptance_criteria: AcceptanceCriteriaSchema,
/** 要求使用的技術棧 */
required_stack: z.array(z.string()).min(1).default(["React", "Tailwind CSS"]),
/** 已重試次數 */
retry_count: NonNegativeIntSchema.max(MAX_TASK_RETRY_COUNT).default(0),
/** Auth-Hold 的 Stripe payment_intent_id */
stripe_payment_intent_id: z.string().optional(),
/** 任務到期時間(對應 Redis TTL */
expires_at: z.string().datetime().optional(),
created_at: z.string().datetime(),
updated_at: z.string().datetime(),
});
// ─────────────────────────────────────────────
// claim_task Request/Response
// ─────────────────────────────────────────────
export const ClaimTaskRequestSchema = z.object({
task_id: UUIDSchema,
agent_id: z.string().min(1, "必須提供 agent_id 進行白名單驗證"),
/** Agent 收款錢包Stripe Connect account 或 EVM 地址) */
developer_wallet: z
.string()
.regex(
/^(0x[a-fA-F0-9]{40}|acct_[a-zA-Z0-9]+)$/,
"developer_wallet 必須是有效的 EVM 地址或 Stripe Connect ID"
),
});
export const ClaimTaskResponseSchema = z.object({
task_id: UUIDSchema,
status: z.literal(TaskStatus.EXECUTING),
/** Stripe Auth-Hold 的金額(供 Agent 確認) */
held_amount: MoneyAmountSchema,
held_currency: z.enum([SupportedCurrency.USD, SupportedCurrency.TWD]),
/** Redis TTL 過期時間 */
expires_at: z.string().datetime(),
/** 接案 Agent 的冪等憑證 */
claim_token: z.string().uuid(),
});
// ─────────────────────────────────────────────
// submit_solution Request/Response
// ─────────────────────────────────────────────
export const SubmitSolutionRequestSchema = z.object({
task_id: UUIDSchema,
/** 接案時取得的冪等憑證,防止重複提交 */
claim_token: z.string().uuid(),
deliverables: z.record(z.string(), z.string()),
/** 必須在目標專案開啟 Pull Request並提供該 PR 的網址以供人類審核 */
github_pr_url: z.string().url().optional(),
});
export const SubmitSolutionResponseSchema = z.object({
task_id: UUIDSchema,
submission_id: UUIDSchema,
status: z.literal(TaskStatus.VERIFYING),
/** Judge 預計完成時間ISO 8601 */
estimated_judge_complete_at: z.string().datetime().optional(),
});
// ─────────────────────────────────────────────
// submit_bid Request/Response
// ─────────────────────────────────────────────
export const SubmitBidRequestSchema = z.object({
task_id: UUIDSchema,
agent_id: z.string().min(1).max(120),
developer_wallet: z
.string()
.regex(
/^(0x[a-fA-F0-9]{40}|acct_[a-zA-Z0-9]+)$/,
"developer_wallet 必須是有效的 EVM 地址或 Stripe Connect ID"
)
.optional(),
proposed_reward: MoneyAmountSchema.refine((value) => value > 0, {
message: "proposed_reward 必須大於 0",
}),
estimated_duration_hours: z.number().positive().max(24 * 30),
quality_guarantee: z.string().max(1000).optional(),
broker_agent_id: z.string().max(120).optional(),
broker_fee_percentage: z.number().min(0).max(100).optional(),
});
export const SubmitBidResponseSchema = z.object({
bid_id: UUIDSchema,
task_id: UUIDSchema,
status: z.enum(["PENDING", "ACCEPTED", "REJECTED", "NEGOTIATING"]),
proposed_reward: MoneyAmountSchema,
});
// ─────────────────────────────────────────────
// create_sub_task Request/Response
// ─────────────────────────────────────────────
export const CreateSubTaskRequestSchema = z.object({
parent_task_id: UUIDSchema,
claim_token: z.string().uuid(),
title: z.string().min(5).max(120),
description: z.string().min(20).max(2000),
reward_amount: MoneyAmountSchema,
acceptance_criteria: AcceptanceCriteriaSchema,
});
export const CreateSubTaskResponseSchema = z.object({
sub_task_id: UUIDSchema,
status: z.union([
z.literal(TaskStatus.DRAFT),
z.literal(TaskStatus.OPEN),
]),
});
// ─────────────────────────────────────────────
// Judge Result SchemaE2B 沙盒回傳)
// ─────────────────────────────────────────────
export const JudgeTestResultSchema = z.object({
name: z.string(),
status: z.enum([
JudgeTestStatus.PASSED,
JudgeTestStatus.FAILED,
JudgeTestStatus.SKIPPED,
]),
duration_ms: NonNegativeIntSchema,
logs: z.string().optional(),
assertion_diff: z.string().optional(),
});
export const JudgeResultSchema = z.object({
attempt_id: UUIDSchema,
task_id: UUIDSchema,
submission_id: UUIDSchema,
overall_result: z.enum([
JudgeOverallResult.PASS,
JudgeOverallResult.FAIL,
JudgeOverallResult.TIMEOUT,
]),
tests: z.array(JudgeTestResultSchema),
artifacts: z
.object({
screenshot_url: z.string().url().optional(),
logs_url: z.string().url().optional(),
coverage_summary: z.string().optional(),
diff_url: z.string().url().optional(),
})
.optional(),
/** 失敗類型overall_result=fail/timeout 時必填) */
error_classification: z
.enum([
JudgeErrorClassification.TEST_FAIL,
JudgeErrorClassification.LINT_FAIL,
JudgeErrorClassification.TIMEOUT,
JudgeErrorClassification.RESOURCE_EXHAUSTED,
JudgeErrorClassification.ENVIRONMENT_ERROR,
JudgeErrorClassification.NETWORK_DENIED,
JudgeErrorClassification.SANDBOX_CRASH,
JudgeErrorClassification.TEST_SETUP_FAIL,
JudgeErrorClassification.ENV_MISCONFIG,
])
.optional(),
/** 供 Builder/Scout 質量分數模型使用的錯誤指紋 */
error_signature: z.string().optional(),
/** 若為 true後端可以安排重試不計入 Agent 失敗分數) */
retryable: z.boolean(),
resource_usage: z.object({
cpu_ms: NonNegativeIntSchema,
mem_peak_mb: NonNegativeIntSchema,
io_bytes: NonNegativeIntSchema,
}),
judge_completed_at: z.string().datetime(),
});
// ─────────────────────────────────────────────
// Settlement Ledger Schema對帳台帳
// ─────────────────────────────────────────────
export const SettlementLedgerEntrySchema = z.object({
id: CUIDSchema,
task_id: UUIDSchema,
agent_id: z.string(),
human_client_id: z.string(),
/** Stripe 冪等 key格式 "{task_id}_{phase}_{attempt}" */
idempotency_key: z.string().min(10).max(255),
phase: z.enum([
SettlementPhase.AUTH_HOLD,
SettlementPhase.CAPTURE,
SettlementPhase.RELEASE,
SettlementPhase.REFUND,
SettlementPhase.PAYOUT,
SettlementPhase.DISPUTE,
SettlementPhase.CORRECTION,
]),
capture_mode: z.enum([
SettlementCaptureMode.STRIPE_AUTH_CAPTURE,
SettlementCaptureMode.BASE_SMART_CONTRACT,
]),
/** Stripe object IDpayment_intent_id / charge_id / refund_id */
stripe_object_id: z.string().optional(),
amount: MoneyAmountSchema,
currency: z.enum([SupportedCurrency.USD, SupportedCurrency.TWD]),
request_payload_hash: z.string(),
response_status: z.string(),
http_status: z.number().int().min(100).max(599),
attempt: PositiveIntSchema,
source: z.enum(["api", "webhook", "manual_replay"]),
/** 分潤快照capture 時寫入,不得事後修改) */
split: z
.object({
platform_amount: MoneyAmountSchema,
builder_amount: MoneyAmountSchema,
scout_amount: MoneyAmountSchema.optional(),
platform_rate: z.number().min(0).max(1),
builder_rate: z.number().min(0).max(1),
scout_rate: z.number().min(0).max(1).optional(),
})
.optional(),
created_at: z.string().datetime(),
updated_at: z.string().datetime(),
});
// ─────────────────────────────────────────────
// Scout Draft / Lead Schemas
// ─────────────────────────────────────────────
export const ScoutDraftRequestSchema = z.object({
scout_id: z.string().min(1, "必須提供 scout_id 進行歸因"),
title: z.string().min(5).max(120),
description: z.string().min(20).max(2000),
reward_amount: MoneyAmountSchema,
reward_currency: z.enum([SupportedCurrency.USD, SupportedCurrency.TWD]),
required_stack: z.array(z.string()).default(["React", "Tailwind CSS"]),
test_file_content: z.string().min(20, "必須提供測試檔以便自動驗收"),
});
export const ScoutDraftResponseSchema = z.object({
task_id: UUIDSchema,
checkout_url: z.string().url(),
status: z.union([
z.literal(TaskStatus.DRAFT),
z.literal(TaskStatus.OPEN),
]),
});
export const LeadSchema = z.object({
lead_id: UUIDSchema,
scout_agent_id: z.string().optional(),
/** Scout 的 affiliate token用於歸因 */
affiliate_token: z.string().uuid().optional(),
attribution_model: z.enum([
AttributionModel.LAST_CLICK,
AttributionModel.FIRST_CLICK,
AttributionModel.LINEAR,
]),
status: z.enum([
LeadStatus.DRAFT,
LeadStatus.CONFIRMED,
LeadStatus.PAYMENT_AUTHORIZED,
LeadStatus.TASK_CREATED,
LeadStatus.EXPIRED,
LeadStatus.CANCELLED,
]),
/** 需求者原始需求(純文字,不超過 500 字) */
raw_requirement: z.string().min(10).max(500),
/** AI 生成的 PRD 草稿JSON */
prd_draft: z.record(z.string(), z.unknown()).optional(),
/** 付款連結Stripe Checkout URL */
payment_link: z.string().url().optional(),
/** 付款連結 TTL 過期時間 */
payment_link_expires_at: z.string().datetime().optional(),
/** 成功後建立的正式 task_id */
task_id: UUIDSchema.optional(),
created_at: z.string().datetime(),
updated_at: z.string().datetime(),
});
// ─────────────────────────────────────────────
// list_open_tasks Response Schema
// ─────────────────────────────────────────────
export const ListOpenTasksRequestSchema = z.object({
skills: z
.array(z.string().min(1).max(50))
.default([]),
limit: z.number().int().min(1).max(20).default(5),
difficulty: z
.enum([
TaskDifficulty.HELLO_WORLD,
TaskDifficulty.COMPONENT,
TaskDifficulty.VIEW,
TaskDifficulty.EPIC,
])
.optional(),
});
/** 精簡版 Tasklist 用,避免暴露內部欄位) */
export const TaskSummarySchema = TaskBountySchema.pick({
task_id: true,
title: true,
status: true,
difficulty: true,
reward: true,
required_stack: true,
scope_clarity_score: true,
created_at: true,
}).extend({
description_preview: z.string().max(200),
});
export const ListOpenTasksResponseSchema = z.object({
tasks: z.array(TaskSummarySchema),
total_open: z.number().int().nonnegative(),
/** 任務池是否告警(供 MCP client 顯示警示) */
stockout_warning: z.boolean(),
});
// ─────────────────────────────────────────────
// fetch_github_repo_structure Request Schema (Trojan Horse Tool)
// ─────────────────────────────────────────────
export const FetchGithubRepoStructureSchema = z.object({
owner: z.string().min(1, "Repository owner is required"),
repo: z.string().min(1, "Repository name is required"),
});
// ─────────────────────────────────────────────
// request_peer_review Request/Response
// ─────────────────────────────────────────────
export const RequestPeerReviewRequestSchema = z.object({
parent_task_id: UUIDSchema,
claim_token: z.string().uuid(),
code_snippet: z.string().min(10, "Code snippet is required"),
review_instructions: z.string().min(10, "Review instructions are required"),
});
export const RequestPeerReviewResponseSchema = z.object({
review_task_id: UUIDSchema,
status: z.literal(TaskStatus.OPEN),
cost: MoneyAmountSchema,
message: z.string(),
});
// ─────────────────────────────────────────────
// broadcast_help_signal Request/Response
// ─────────────────────────────────────────────
export const BroadcastHelpSignalRequestSchema = z.object({
parent_task_id: UUIDSchema,
claim_token: z.string().uuid(),
error_message: z.string().min(5),
contextual_code: z.string().optional(),
});
export const BroadcastHelpSignalResponseSchema = z.object({
sos_task_id: UUIDSchema,
status: z.literal(TaskStatus.OPEN),
message: z.string(),
});
// ─────────────────────────────────────────────
// query_agent_memory Request/Response
// ─────────────────────────────────────────────
export const QueryAgentMemoryRequestSchema = z.object({
query: z.string().min(3),
error_code: z.string().optional(),
});
export const QueryAgentMemoryResponseSchema = z.object({
results: z.array(z.object({
task_title: z.string(),
deliverables: z.unknown(),
similarity_score: z.number().optional(),
})),
});
// ─────────────────────────────────────────────
// negotiate_bounty Request/Response
// ─────────────────────────────────────────────
export const NegotiateBountyRequestSchema = z.object({
task_id: UUIDSchema,
agent_id: z.string().min(1, "必須提供 agent_id 進行白名單驗證"),
requested_amount: MoneyAmountSchema,
reasoning: z.string().min(10, "Reasoning is required for negotiation"),
});
export const NegotiateBountyResponseSchema = z.object({
task_id: UUIDSchema,
status: z.enum(["APPROVED", "REJECTED", "PENDING_HUMAN_REVIEW"]),
new_amount: MoneyAmountSchema.optional(),
message: z.string(),
});
// ─────────────────────────────────────────────
// rent_api_resource Request/Response
// ─────────────────────────────────────────────
export const RentApiResourceRequestSchema = z.object({
agent_id: z.string(),
resource_type: z.enum(["GPT_4O", "CLAUDE_3_5_SONNET", "EMBEDDINGS"]),
duration_minutes: z.number().min(1).max(60).default(5),
});
export const RentApiResourceResponseSchema = z.object({
status: z.enum(["GRANTED", "INSUFFICIENT_FUNDS"]),
proxy_url: z.string().optional(),
proxy_token: z.string().optional(),
cost_deducted: z.number().optional(),
message: z.string(),
});
// ─────────────────────────────────────────────
// create_bounty Request/Response
// ─────────────────────────────────────────────
export const CreateBountyRequestSchema = z.object({
agent_id: z.string().min(1, "必須提供發包者的 agent_id"),
title: z.string().min(5).max(120),
description: z.string().min(20).max(2000),
reward_amount: MoneyAmountSchema,
reward_currency: z.enum([SupportedCurrency.USD, SupportedCurrency.TWD]).default(SupportedCurrency.USD),
required_stack: z.array(z.string()).default(["TypeScript", "AI"]),
test_file_content: z.string().default("// Default A2A Verification Stub"),
});
export const CreateBountyResponseSchema = z.object({
task_id: UUIDSchema,
status: z.literal(TaskStatus.OPEN),
message: z.string(),
});
// ─────────────────────────────────────────────
// Agent Card Schema (2026 Discovery Standard)
// ─────────────────────────────────────────────
export const AgentCardSchema = z.object({
agent_id: z.string().min(1),
name: z.string().min(2).max(100),
description: z.string().max(1000).optional(),
supported_models: z.array(z.string()).default(["gpt-4o"]),
skills: z.array(z.string()).min(1),
max_concurrent_tasks: z.number().int().min(1).max(100).default(5),
x402_wallet_address: z.string().optional(),
});
export const RegisterAgentCardRequestSchema = z.object({
card: AgentCardSchema,
});
export const RegisterAgentCardResponseSchema = z.object({
status: z.literal("SUCCESS"),
message: z.string(),
});