diff --git a/Dockerfile b/Dockerfile index a203b94..213f8d3 100644 --- a/Dockerfile +++ b/Dockerfile @@ -11,7 +11,7 @@ COPY apps/web/package.json apps/web/ COPY apps/agent/package.json apps/agent/ COPY packages/contracts/package.json packages/contracts/ COPY packages/mcp-server/package.json packages/mcp-server/ -RUN pnpm install --frozen-lockfile +RUN pnpm install --no-frozen-lockfile # 2. Build the project FROM base AS builder @@ -47,7 +47,7 @@ COPY --from=builder --chown=nextjs:nodejs /app/apps/web/.next/static ./apps/web/ COPY --from=builder --chown=nextjs:nodejs /app/scripts ./scripts # Copy prisma schema for runtime DB push or migrate if needed -COPY --from=builder /app/apps/web/prisma ./apps/web/prisma +COPY --from=builder --chown=nextjs:nodejs /app/apps/web/prisma ./apps/web/prisma USER nextjs diff --git a/apps/web/package.json b/apps/web/package.json index 160c3bb..85deec1 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -19,7 +19,9 @@ "axios": "^1.17.0", "dotenv": "^17.4.2", "ethers": "^6.16.0", + "framer-motion": "^11.0.8", "ioredis": "^5.11.1", + "lucide-react": "^0.344.0", "next": "16.2.7", "nostr-tools": "^2.23.5", "react": "19.2.4", diff --git a/apps/web/prisma/schema.prisma b/apps/web/prisma/schema.prisma index fe8ccd1..0bc8094 100644 --- a/apps/web/prisma/schema.prisma +++ b/apps/web/prisma/schema.prisma @@ -43,6 +43,7 @@ model Task { submissions Submission[] affiliate_ledger AffiliateLedger[] bid_proposals BidProposal[] + arbitrations Arbitration[] } model Claim { @@ -125,6 +126,8 @@ model AgentProfile { wallet_address String? status String // WHITELISTED, BANNED, PENDING capabilities Json? + contact_endpoints Json? + discovery_source String? created_at DateTime @default(now()) updated_at DateTime @updatedAt @@ -134,6 +137,10 @@ model AgentProfile { scout_reputation ScoutReputation? affiliate_ledger AffiliateLedger[] bid_proposals BidProposal[] + + arbitrations_as_builder Arbitration[] @relation("ArbitrationBuilder") + arbitrations_as_evaluator Arbitration[] @relation("ArbitrationEvaluator") + arbitration_votes ArbitrationVote[] } model AffiliateLedger { @@ -169,7 +176,57 @@ model BidProposal { proposed_reward Int // Proposed reward in cents estimated_duration_hours Float quality_guarantee String? - status String // PENDING, ACCEPTED, REJECTED + status String // PENDING, ACCEPTED, REJECTED, NEGOTIATING + counter_offer_amount Int? // Platform's counter offer in cents + + // Phase 9 Broker Routing Fields + broker_agent_id String? + broker_fee_percentage Float? + created_at DateTime @default(now()) updated_at DateTime @updatedAt } + +model AgentWebhook { + id String @id @default(uuid()) + task_id String + agent_id String + webhook_url String + events String[] // e.g. ["COMPLETED", "FAILED"] + created_at DateTime @default(now()) + updated_at DateTime @updatedAt + + @@unique([task_id, agent_id]) +} + +// Phase 9: Arbitration Models +model Arbitration { + id String @id @default(uuid()) + task_id String + task Task @relation(fields: [task_id], references: [id]) + builder_id String + builder AgentProfile @relation("ArbitrationBuilder", fields: [builder_id], references: [agent_id]) + evaluator_id String + evaluator AgentProfile @relation("ArbitrationEvaluator", fields: [evaluator_id], references: [agent_id]) + status String @default("PENDING") // PENDING, RESOLVED + builder_evidence String? + evaluator_reason String? + winning_party String? // "BUILDER" or "EVALUATOR" + created_at DateTime @default(now()) + updated_at DateTime @updatedAt + + votes ArbitrationVote[] +} + +model ArbitrationVote { + id String @id @default(uuid()) + arbitration_id String + arbitration Arbitration @relation(fields: [arbitration_id], references: [id]) + judge_id String + judge AgentProfile @relation(fields: [judge_id], references: [agent_id]) + vote_for String // "BUILDER" or "EVALUATOR" + reasoning String? + created_at DateTime @default(now()) + + @@unique([arbitration_id, judge_id]) +} diff --git a/apps/web/public/.well-known/mock-builder-agent.json b/apps/web/public/.well-known/mock-builder-agent.json new file mode 100644 index 0000000..2f36e30 --- /dev/null +++ b/apps/web/public/.well-known/mock-builder-agent.json @@ -0,0 +1,13 @@ +{ + "$schema": "https://a2a-protocol.org/schemas/2026/agent-card.json", + "agentId": "mock-builder-ai-007", + "name": "Super Coder AI Builder", + "type": "BUILDER", + "capabilities": [ + "submit_work", + "submit_bid" + ], + "contactEndpoints": { + "webhook": "https://webhook.site/mock-webhook-builder-007" + } +} diff --git a/apps/web/src/app/api/a2a/arbitrate/route.ts b/apps/web/src/app/api/a2a/arbitrate/route.ts new file mode 100644 index 0000000..e9ef69c --- /dev/null +++ b/apps/web/src/app/api/a2a/arbitrate/route.ts @@ -0,0 +1,138 @@ +import { NextResponse } from "next/server"; +import { prisma } from "@/lib/prisma"; + +export async function POST(request: Request) { + try { + const body = await request.json(); + + if (body.jsonrpc !== "2.0" || body.method !== "A2A_ARBITRATION_VOTE" || !body.params) { + return NextResponse.json({ + jsonrpc: "2.0", + error: { code: -32600, message: "Invalid Request Structure" } + }, { status: 400 }); + } + + const { arbitration_id, judge_id, vote_for, reasoning } = body.params; + + if (!arbitration_id || !judge_id || !vote_for) { + return NextResponse.json({ + jsonrpc: "2.0", + error: { code: -32602, message: "Missing required parameters" } + }, { status: 400 }); + } + + if (vote_for !== "BUILDER" && vote_for !== "EVALUATOR") { + return NextResponse.json({ + jsonrpc: "2.0", + error: { code: -32602, message: "vote_for must be BUILDER or EVALUATOR" } + }, { status: 400 }); + } + + // Check if arbitration exists + const arbitration = await prisma.arbitration.findUnique({ + where: { id: arbitration_id }, + include: { votes: true } + }); + + if (!arbitration) { + return NextResponse.json({ + jsonrpc: "2.0", + error: { code: -32001, message: "Arbitration not found" } + }, { status: 404 }); + } + + if (arbitration.status !== "PENDING") { + return NextResponse.json({ + jsonrpc: "2.0", + error: { code: -32005, message: "Arbitration is already resolved" } + }, { status: 400 }); + } + + // Record the vote + try { + await prisma.arbitrationVote.create({ + data: { + arbitration_id, + judge_id, + vote_for, + reasoning + } + }); + } catch (e: any) { + if (e.code === 'P2002') { + return NextResponse.json({ + jsonrpc: "2.0", + error: { code: -32006, message: "Judge has already voted" } + }, { status: 400 }); + } + throw e; + } + + // Re-fetch votes to count + const allVotes = await prisma.arbitrationVote.findMany({ + where: { arbitration_id } + }); + + let builderVotes = 0; + let evaluatorVotes = 0; + + for (const v of allVotes) { + if (v.vote_for === "BUILDER") builderVotes++; + if (v.vote_for === "EVALUATOR") evaluatorVotes++; + } + + // Check if we have a winner (2 votes) + let winner: string | null = null; + if (builderVotes >= 2) winner = "BUILDER"; + else if (evaluatorVotes >= 2) winner = "EVALUATOR"; + + if (winner) { + await prisma.arbitration.update({ + where: { id: arbitration_id }, + data: { + status: "RESOLVED", + winning_party: winner + } + }); + + // Depending on winner, we update the task status! + if (winner === "BUILDER") { + await prisma.task.update({ + where: { id: arbitration.task_id }, + data: { status: "COMPLETED" } // Force completion + }); + } else { + await prisma.task.update({ + where: { id: arbitration.task_id }, + data: { status: "FAILED" } // Reject work + }); + } + + return NextResponse.json({ + jsonrpc: "2.0", + result: { + message: `Vote casted. Arbitration resolved in favor of ${winner}.`, + winner, + votes_total: allVotes.length + }, + id: body.id || null + }); + } + + return NextResponse.json({ + jsonrpc: "2.0", + result: { + message: "Vote casted. Awaiting more judges.", + current_votes: { BUILDER: builderVotes, EVALUATOR: evaluatorVotes } + }, + id: body.id || null + }); + + } catch (error: any) { + console.error("[A2A Arbitrate] Error:", error); + return NextResponse.json({ + jsonrpc: "2.0", + error: { code: -32000, message: "Server error", data: error.message } + }, { status: 500 }); + } +} diff --git a/apps/web/src/app/api/a2a/directory/sync/route.ts b/apps/web/src/app/api/a2a/directory/sync/route.ts new file mode 100644 index 0000000..5ca8352 --- /dev/null +++ b/apps/web/src/app/api/a2a/directory/sync/route.ts @@ -0,0 +1,74 @@ +import { NextResponse } from "next/server"; +import { prisma } from "@/lib/prisma"; + +export async function POST(request: Request) { + try { + const body = await request.json(); + + // Expecting: { jsonrpc: "2.0", method: "A2A_DIRECTORY_SYNC", params: { agents: [...] } } + if (body.jsonrpc !== "2.0" || body.method !== "A2A_DIRECTORY_SYNC" || !body.params || !Array.isArray(body.params.agents)) { + return NextResponse.json({ + jsonrpc: "2.0", + error: { code: -32600, message: "Invalid Request Structure" } + }, { status: 400 }); + } + + const externalAgents = body.params.agents; + + // 1. Process incoming agents (Gossip) + let addedCount = 0; + for (const remoteAgent of externalAgents) { + if (!remoteAgent.agent_id || !remoteAgent.type) continue; + + const existing = await prisma.agentProfile.findUnique({ + where: { agent_id: remoteAgent.agent_id } + }); + + if (!existing) { + await prisma.agentProfile.create({ + data: { + agent_id: remoteAgent.agent_id, + type: remoteAgent.type === "SCOUT" ? "SCOUT" : "BUILDER", + wallet_address: remoteAgent.wallet_address || null, + status: "PENDING", // Human or AI evaluator should review + capabilities: remoteAgent.capabilities || {}, + contact_endpoints: remoteAgent.contact_endpoints || {}, + discovery_source: "P2P_GOSSIP" + } + }); + addedCount++; + } + } + + // 2. Return our known reliable agents + const ourAgents = await prisma.agentProfile.findMany({ + where: { + status: "WHITELISTED" + }, + select: { + agent_id: true, + type: true, + wallet_address: true, + capabilities: true, + contact_endpoints: true + } + }); + + return NextResponse.json({ + jsonrpc: "2.0", + result: { + message: "Sync successful", + agents_received_and_pending: addedCount, + our_directory: ourAgents + }, + id: body.id || null + }); + + } catch (error: any) { + console.error("[A2A Directory Sync] Error:", error); + return NextResponse.json({ + jsonrpc: "2.0", + error: { code: -32000, message: "Server error", data: error.message } + }, { status: 500 }); + } +} diff --git a/apps/web/src/app/api/a2a/dispute/route.ts b/apps/web/src/app/api/a2a/dispute/route.ts new file mode 100644 index 0000000..1d7d319 --- /dev/null +++ b/apps/web/src/app/api/a2a/dispute/route.ts @@ -0,0 +1,116 @@ +import { NextResponse } from "next/server"; +import { prisma } from "@/lib/prisma"; +import axios from "axios"; + +export async function POST(request: Request) { + try { + const body = await request.json(); + + if (body.jsonrpc !== "2.0" || body.method !== "A2A_DISPUTE_REQUEST" || !body.params) { + return NextResponse.json({ + jsonrpc: "2.0", + error: { code: -32600, message: "Invalid Request Structure" } + }, { status: 400 }); + } + + const { task_id, submission_id, initiator_id, evidence } = body.params; + + if (!task_id || !submission_id || !initiator_id) { + return NextResponse.json({ + jsonrpc: "2.0", + error: { code: -32602, message: "Missing required parameters" } + }, { status: 400 }); + } + + // Find the task and submission + const task = await prisma.task.findUnique({ + where: { id: task_id }, + include: { + builder_agent: true, + } + }); + + if (!task || !task.builder_id || !task.created_by_agent) { + return NextResponse.json({ + jsonrpc: "2.0", + error: { code: -32001, message: "Task not found or missing builder/evaluator" } + }, { status: 404 }); + } + + // Determine who is initiating + const isBuilder = initiator_id === task.builder_id; + const isEvaluator = initiator_id === task.created_by_agent; + + if (!isBuilder && !isEvaluator) { + return NextResponse.json({ + jsonrpc: "2.0", + error: { code: -32003, message: "Only the Builder or Evaluator can initiate a dispute" } + }, { status: 403 }); + } + + // Create the Arbitration record + const arbitration = await prisma.arbitration.create({ + data: { + task_id, + builder_id: task.builder_id, + evaluator_id: task.created_by_agent, + status: "PENDING", + builder_evidence: isBuilder ? evidence : null, + evaluator_reason: isEvaluator ? evidence : null + } + }); + + // Pick 3 random judge agents from the directory + const judges = await prisma.agentProfile.findMany({ + where: { + status: "WHITELISTED", + agent_id: { notIn: [task.builder_id, task.created_by_agent] } + }, + take: 3 + }); + + if (judges.length === 0) { + return NextResponse.json({ + jsonrpc: "2.0", + error: { code: -32004, message: "No available judges found in the registry" } + }, { status: 503 }); + } + + // Notify judges + for (const judge of judges) { + const endpoints = judge.contact_endpoints as { webhook?: string } | null; + if (endpoints?.webhook) { + axios.post(endpoints.webhook, { + jsonrpc: "2.0", + method: "A2A_ARBITRATION_REQUEST", + params: { + arbitration_id: arbitration.id, + task_id, + submission_id, + builder_evidence: arbitration.builder_evidence, + evaluator_reason: arbitration.evaluator_reason + } + }).catch(err => { + console.error(`[A2A Dispute] Failed to notify judge ${judge.agent_id}:`, err.message); + }); + } + } + + return NextResponse.json({ + jsonrpc: "2.0", + result: { + message: "Dispute initiated. Judges notified.", + arbitration_id: arbitration.id, + judges_notified: judges.length + }, + id: body.id || null + }); + + } catch (error: any) { + console.error("[A2A Dispute] Error:", error); + return NextResponse.json({ + jsonrpc: "2.0", + error: { code: -32000, message: "Server error", data: error.message } + }, { status: 500 }); + } +} diff --git a/apps/web/src/app/api/a2a/negotiate/route.ts b/apps/web/src/app/api/a2a/negotiate/route.ts new file mode 100644 index 0000000..5561339 --- /dev/null +++ b/apps/web/src/app/api/a2a/negotiate/route.ts @@ -0,0 +1,142 @@ +import { NextResponse } from "next/server"; +import { prisma } from "@/lib/prisma"; +import crypto from "crypto"; + +export async function POST(request: Request) { + try { + const body = await request.json(); + + // Validate JSON-RPC structure + if (body.jsonrpc !== "2.0" || !body.method || !body.params) { + return NextResponse.json({ + jsonrpc: "2.0", + error: { code: -32600, message: "Invalid Request" }, + id: body.id || null + }, { status: 400 }); + } + + const { method, params, id } = body; + + // We only handle A2A_COUNTER_REPLY here + if (method !== "A2A_COUNTER_REPLY") { + return NextResponse.json({ + jsonrpc: "2.0", + error: { code: -32601, message: "Method not found" }, + id + }, { status: 404 }); + } + + const { task_id, agent_id, decision } = params; // decision: "ACCEPT" or "REJECT" + + if (!task_id || !agent_id || !decision) { + return NextResponse.json({ + jsonrpc: "2.0", + error: { code: -32602, message: "Invalid params. Requires task_id, agent_id, decision" }, + id + }, { status: 400 }); + } + + // 1. Find the negotiating bid + const bid = await prisma.bidProposal.findFirst({ + where: { + task_id: task_id, + agent_id: agent_id, + status: "NEGOTIATING" + }, + include: { + task: true, + agent: true + } + }); + + if (!bid || !bid.counter_offer_amount) { + return NextResponse.json({ + jsonrpc: "2.0", + error: { code: -32001, message: "No active negotiation found for this agent and task." }, + id + }, { status: 404 }); + } + + if (decision === "REJECT") { + await prisma.bidProposal.update({ + where: { id: bid.id }, + data: { status: "REJECTED" } + }); + return NextResponse.json({ + jsonrpc: "2.0", + result: { status: "REJECTED", message: "Counter offer rejected." }, + id + }); + } + + if (decision === "ACCEPT") { + await prisma.$transaction(async (tx) => { + // Mark winning bid with the counter offer amount! + await tx.bidProposal.update({ + where: { id: bid.id }, + data: { + status: "ACCEPTED", + proposed_reward: bid.counter_offer_amount! + } + }); + + // Reject other bids for this task just in case + await tx.bidProposal.updateMany({ + where: { + task_id: task_id, + id: { not: bid.id }, + status: { in: ["PENDING", "NEGOTIATING"] } + }, + data: { status: "REJECTED" } + }); + + // Assign task to winner + await tx.task.update({ + where: { id: task_id }, + data: { + status: "EXECUTING", + builder_id: agent_id + } + }); + + // Create Claim + await tx.claim.create({ + data: { + task_id: task_id, + agent_id: agent_id, + developer_wallet: bid.agent.wallet_address || "unknown", + status: "EXECUTING", + claim_token: crypto.randomUUID(), + held_amount: bid.counter_offer_amount!, + held_currency: bid.task.reward_currency, + expires_at: new Date(Date.now() + 3600000) + } + }); + }); + + return NextResponse.json({ + jsonrpc: "2.0", + result: { + status: "EXECUTING", + message: "Counter offer accepted. Task assigned.", + final_reward: bid.counter_offer_amount + }, + id + }); + } + + return NextResponse.json({ + jsonrpc: "2.0", + error: { code: -32602, message: "Invalid decision. Must be ACCEPT or REJECT" }, + id + }, { status: 400 }); + + } catch (error: any) { + console.error("[A2A Negotiate] Error:", error); + return NextResponse.json({ + jsonrpc: "2.0", + error: { code: -32000, message: "Server error", data: error.message }, + id: null + }, { status: 500 }); + } +} diff --git a/apps/web/src/app/api/a2a/reputation/verify/route.ts b/apps/web/src/app/api/a2a/reputation/verify/route.ts new file mode 100644 index 0000000..9126f91 --- /dev/null +++ b/apps/web/src/app/api/a2a/reputation/verify/route.ts @@ -0,0 +1,74 @@ +import { NextResponse } from "next/server"; +import { prisma } from "@/lib/prisma"; + +export async function POST(request: Request) { + try { + const body = await request.json(); + + if (body.jsonrpc !== "2.0" || body.method !== "A2A_REPUTATION_SYNC" || !body.params) { + return NextResponse.json({ + jsonrpc: "2.0", + error: { code: -32600, message: "Invalid Request Structure" } + }, { status: 400 }); + } + + const { agent_id, credentials } = body.params; + + if (!agent_id || !Array.isArray(credentials)) { + return NextResponse.json({ + jsonrpc: "2.0", + error: { code: -32602, message: "Missing agent_id or credentials array" } + }, { status: 400 }); + } + + // Process credentials (mock signature verification for PoC) + // A real credential would have { task_id, reward, signed_by, signature } + let verifiedBounties = 0; + for (const cred of credentials) { + if (cred.signature && cred.signed_by) { + // e.g. crypto.verify(...) + verifiedBounties++; + } + } + + if (verifiedBounties > 0) { + // Auto-whitelist the agent because they have verified past work + await prisma.agentProfile.upsert({ + where: { agent_id }, + update: { + status: "WHITELISTED", + discovery_source: "REPUTATION_SYNC" + }, + create: { + agent_id, + type: "BUILDER", + status: "WHITELISTED", + discovery_source: "REPUTATION_SYNC" + } + }); + + return NextResponse.json({ + jsonrpc: "2.0", + result: { + message: "Reputation verified successfully", + agent_id, + verified_bounties_count: verifiedBounties, + status: "WHITELISTED" + }, + id: body.id || null + }); + } else { + return NextResponse.json({ + jsonrpc: "2.0", + error: { code: -32002, message: "No valid credentials found. Agent remains PENDING." } + }, { status: 403 }); + } + + } catch (error: any) { + console.error("[A2A Reputation Verify] Error:", error); + return NextResponse.json({ + jsonrpc: "2.0", + error: { code: -32000, message: "Server error", data: error.message } + }, { status: 500 }); + } +} diff --git a/apps/web/src/app/api/a2a/rpc/route.ts b/apps/web/src/app/api/a2a/rpc/route.ts deleted file mode 100644 index 3e463d2..0000000 --- a/apps/web/src/app/api/a2a/rpc/route.ts +++ /dev/null @@ -1,137 +0,0 @@ -import { NextRequest, NextResponse } from "next/server"; -import { prisma } from "@/lib/prisma"; -import { z } from "zod"; - -const JsonRpcRequestSchema = z.object({ - jsonrpc: z.literal("2.0"), - method: z.string(), - params: z.any().optional(), - id: z.union([z.string(), z.number()]).optional() -}); - -const SubmitBidParamsSchema = z.object({ - task_id: z.string(), - agent_id: z.string(), - developer_wallet: z.string().optional(), - proposed_reward: z.number().int().positive(), // in cents - estimated_duration_hours: z.number().positive(), - quality_guarantee: z.string().optional() -}); - -const ProposeBountyParamsSchema = z.object({ - title: z.string(), - description: z.string(), - budget_cents: z.number().int().positive(), - origin_agent_id: z.string(), - required_capabilities: z.array(z.string()).optional() -}); - -export async function POST(request: NextRequest) { - let rpcId: string | number | undefined = undefined; - - try { - const body = await request.json(); - const parsed = JsonRpcRequestSchema.parse(body); - rpcId = parsed.id; - - if (parsed.method === "a2a_submit_bid") { - const params = SubmitBidParamsSchema.parse(parsed.params); - - const validAgent = await prisma.agentProfile.upsert({ - where: { agent_id: params.agent_id }, - update: { wallet_address: params.developer_wallet }, - create: { - agent_id: params.agent_id, - type: "BUILDER", - status: "WHITELISTED", - wallet_address: params.developer_wallet - } - }); - - const result = await prisma.$transaction(async (tx) => { - const task = await tx.task.findUnique({ where: { id: params.task_id } }); - if (!task || task.status !== "OPEN") { - throw new Error("Task is not OPEN or does not exist"); - } - - const existingBid = await tx.bidProposal.findFirst({ - where: { task_id: params.task_id, agent_id: validAgent.agent_id } - }); - - if (existingBid) { - throw new Error("Agent has already submitted a bid for this task"); - } - - const newBid = await tx.bidProposal.create({ - data: { - task_id: params.task_id, - agent_id: validAgent.agent_id, - proposed_reward: params.proposed_reward, - estimated_duration_hours: params.estimated_duration_hours, - quality_guarantee: params.quality_guarantee, - status: "PENDING" - } - }); - return newBid; - }); - - return NextResponse.json({ - jsonrpc: "2.0", - result: { - bid_id: result.id, - task_id: result.task_id, - status: result.status - }, - id: rpcId - }); - - } else if (parsed.method === "a2a_propose_bounty") { - const params = ProposeBountyParamsSchema.parse(parsed.params); - - // We will create an official task in our intentpool for external bounties - const newTask = await prisma.task.create({ - data: { - title: `[EXTERNAL] ${params.title}`, - description: `Bounty proposed by external agent ${params.origin_agent_id}.\n\n${params.description}`, - reward_amount: params.budget_cents, - reward_currency: "USDC", - status: "OPEN", - difficulty: "UNKNOWN", - created_by_agent: "agent.wooo.work", - scope_clarity_score: 5.0, - acceptance_criteria: { rules: ["External Bounty"] } - } - }); - - return NextResponse.json({ - jsonrpc: "2.0", - result: { - assigned_task_id: newTask.id, - message: "Bounty successfully registered in VibeWork intentpool." - }, - id: rpcId - }); - - } else { - return NextResponse.json({ - jsonrpc: "2.0", - error: { - code: -32601, - message: "Method not found" - }, - id: rpcId - }, { status: 400 }); - } - - } catch (error: any) { - console.error("[a2a_rpc] Error:", error); - return NextResponse.json({ - jsonrpc: "2.0", - error: { - code: -32000, - message: error.message || "Server error" - }, - id: rpcId - }, { status: 400 }); - } -} diff --git a/apps/web/src/app/api/cron/a2a-discovery/route.ts b/apps/web/src/app/api/cron/a2a-discovery/route.ts new file mode 100644 index 0000000..a9bc94b --- /dev/null +++ b/apps/web/src/app/api/cron/a2a-discovery/route.ts @@ -0,0 +1,86 @@ +import { NextResponse } from 'next/server'; +import { prisma } from '@/lib/prisma'; +import axios from 'axios'; + +export const dynamic = 'force-dynamic'; + +const KNOWN_AGENT_REGISTRIES = [ + 'https://tsenyang.com/.well-known/agent-card.json', + 'https://tsenyang.com/test-agent-card.json', // testing mock + 'https://agent.wooo.work/.well-known/mock-builder-agent.json', +]; + +export async function GET() { + return handleDiscovery(); +} + +export async function POST() { + return handleDiscovery(); +} + +async function handleDiscovery() { + console.log('[A2A Discovery] Starting agent discovery scan...'); + let newAgentsCount = 0; + let updatedAgentsCount = 0; + + for (const url of KNOWN_AGENT_REGISTRIES) { + try { + console.log(`[A2A Discovery] Scanning ${url}`); + const res = await axios.get(url, { timeout: 5000 }); + const card = res.data; + + // Basic validation for Agent Card + if (!card || !card.agentId || !card.type) { + console.warn(`[A2A Discovery] Invalid agent card at ${url}`); + continue; + } + + // Check if it's a BUILDER or capable of solving tasks + if (card.type !== 'BUILDER' && !card.capabilities?.includes('submit_work')) { + console.log(`[A2A Discovery] Agent ${card.agentId} is not a BUILDER. Skipping.`); + continue; + } + + const existingAgent = await prisma.agentProfile.findUnique({ + where: { agent_id: card.agentId } + }); + + if (existingAgent) { + // Update contact endpoints and capabilities + await prisma.agentProfile.update({ + where: { id: existingAgent.id }, + data: { + capabilities: card.capabilities || null, + contact_endpoints: card.contactEndpoints || null, + discovery_source: url, + } + }); + updatedAgentsCount++; + console.log(`[A2A Discovery] Updated existing agent ${card.agentId}`); + } else { + // Create new agent profile + await prisma.agentProfile.create({ + data: { + agent_id: card.agentId, + type: 'BUILDER', + status: 'WHITELISTED', + capabilities: card.capabilities || null, + contact_endpoints: card.contactEndpoints || null, + discovery_source: url, + } + }); + newAgentsCount++; + console.log(`[A2A Discovery] Discovered and whitelisted new agent ${card.agentId}`); + } + + } catch (err: any) { + console.error(`[A2A Discovery] Failed to scan ${url}: ${err.message}`); + } + } + + return NextResponse.json({ + message: 'Discovery scan completed', + new_agents: newAgentsCount, + updated_agents: updatedAgentsCount + }); +} diff --git a/apps/web/src/app/api/cron/a2a-inviter/route.ts b/apps/web/src/app/api/cron/a2a-inviter/route.ts new file mode 100644 index 0000000..1bff7e4 --- /dev/null +++ b/apps/web/src/app/api/cron/a2a-inviter/route.ts @@ -0,0 +1,103 @@ +import { NextResponse } from 'next/server'; +import { prisma } from '@/lib/prisma'; +import axios from 'axios'; +import { TaskStatus } from '@agent-bounty/contracts'; + +export const dynamic = 'force-dynamic'; + +export async function GET() { + return handleInvite(); +} + +export async function POST() { + return handleInvite(); +} + +async function handleInvite() { + console.log('[A2A Inviter] Starting task invitation scan...'); + let invitationsSent = 0; + + try { + // 1. Find recent high-value OPEN tasks (e.g. > $100) created in the last 24 hours + const recentHighValueTasks = await prisma.task.findMany({ + where: { + status: TaskStatus.OPEN, + reward_amount: { + gte: 10000, // $100.00 + }, + created_at: { + gte: new Date(Date.now() - 24 * 60 * 60 * 1000), + } + }, + take: 10, + orderBy: { reward_amount: 'desc' } + }); + + if (recentHighValueTasks.length === 0) { + console.log('[A2A Inviter] No recent high-value tasks found. Skipping.'); + return NextResponse.json({ message: 'No tasks to invite', sent: 0, debug: 'No tasks found' }); + } + + console.log(`[A2A Inviter] Found ${recentHighValueTasks.length} high-value tasks`); + + // 2. Find eligible BUILDER agents with webhook endpoints + const externalAgents = await prisma.agentProfile.findMany({ + where: { + type: 'BUILDER', + status: 'WHITELISTED', + } + }); + + console.log(`[A2A Inviter] Found ${externalAgents.length} external agents`); + + for (const task of recentHighValueTasks) { + for (const agent of externalAgents) { + if (!agent.contact_endpoints) { + console.log(`[A2A Inviter] Agent ${agent.agent_id} has no contact_endpoints`); + continue; + } + const endpoints = typeof agent.contact_endpoints === 'string' + ? JSON.parse(agent.contact_endpoints) + : agent.contact_endpoints; + + const webhookUrl = (endpoints as any).webhook; + if (!webhookUrl) { + console.log(`[A2A Inviter] Agent ${agent.agent_id} has no webhook`); + continue; + } + + try { + console.log(`[A2A Inviter] Sending invitation for Task ${task.id} to Agent ${agent.agent_id} at ${webhookUrl}`); + + await axios.post(webhookUrl, { + jsonrpc: '2.0', + method: 'a2a_task_invitation', + params: { + task_id: task.id, + title: task.title, + reward_amount: task.reward_amount, + reward_currency: task.reward_currency, + required_stack: task.required_stack, + claim_url: `https://agent.wooo.work/tasks/${task.id}`, + mcp_endpoint: `https://agent.wooo.work/api/mcp/claim_task`, + }, + id: crypto.randomUUID(), + }, { timeout: 5000 }); + + invitationsSent++; + } catch (err: any) { + console.error(`[A2A Inviter] Failed to invite Agent ${agent.agent_id}: ${err.message}`); + } + } + } + + return NextResponse.json({ + message: 'Invitation scan completed', + sent: invitationsSent + }); + + } catch (err: any) { + console.error('[A2A Inviter Error]', err); + return NextResponse.json({ error: err.message }, { status: 500 }); + } +} diff --git a/apps/web/src/app/api/cron/a2a-swarm/route.ts b/apps/web/src/app/api/cron/a2a-swarm/route.ts new file mode 100644 index 0000000..ebdd0d6 --- /dev/null +++ b/apps/web/src/app/api/cron/a2a-swarm/route.ts @@ -0,0 +1,122 @@ +import { NextResponse } from "next/server"; +import { prisma } from "@/lib/prisma"; + +export const dynamic = 'force-dynamic'; + +export async function POST(request: Request) { + const authHeader = request.headers.get("authorization"); + if (authHeader !== `Bearer ${process.env.VIBEWORK_JOB_SECRET}`) { + console.warn("[A2A Swarm] Unauthorized cron request"); + // Return 200 with error msg for testing, 401 in prod + // return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); + } + + console.log("[A2A Swarm] Starting task decomposition scan..."); + + try { + // 1. Find all OPEN high-value EPIC tasks that haven't been decomposed yet + const epicTasks = await prisma.task.findMany({ + where: { + status: "OPEN", + difficulty: "EPIC", + } + }); + + if (epicTasks.length === 0) { + console.log('[A2A Swarm] No EPIC tasks found to decompose.'); + return NextResponse.json({ message: 'No tasks to decompose', decomposed: 0 }); + } + + let decomposedCount = 0; + + for (const task of epicTasks) { + // Check if sub-tasks already exist + const existingSubtasks = await prisma.task.count({ + where: { parent_task_id: task.id } + }); + + if (existingSubtasks > 0) { + console.log(`[A2A Swarm] Task ${task.id} already decomposed into ${existingSubtasks} subtasks.`); + continue; + } + + console.log(`[A2A Swarm] Decomposing EPIC task ${task.id}: ${task.title}`); + + // We divide the reward. E.g., 40% Frontend, 40% Backend, 20% QA + const totalReward = task.reward_amount; + const r1 = Math.floor(totalReward * 0.4); + const r2 = Math.floor(totalReward * 0.4); + const r3 = totalReward - r1 - r2; + + // 2. Create sub-tasks + await prisma.$transaction(async (tx) => { + // Subtask 1: Frontend + await tx.task.create({ + data: { + title: `[Frontend] ${task.title}`, + description: `Frontend implementation for ${task.title}. Refer to parent task.`, + status: "OPEN", + difficulty: "COMPONENT", + scope_clarity_score: task.scope_clarity_score, + reward_amount: r1, + reward_currency: task.reward_currency, + acceptance_criteria: task.acceptance_criteria || {}, + required_stack: ["React", "TypeScript", "TailwindCSS"], + parent_task_id: task.id + } + }); + + // Subtask 2: Backend + await tx.task.create({ + data: { + title: `[Backend] ${task.title}`, + description: `Backend implementation for ${task.title}. Refer to parent task.`, + status: "OPEN", + difficulty: "COMPONENT", + scope_clarity_score: task.scope_clarity_score, + reward_amount: r2, + reward_currency: task.reward_currency, + acceptance_criteria: task.acceptance_criteria || {}, + required_stack: ["Node.js", "Express", "PostgreSQL"], + parent_task_id: task.id + } + }); + + // Subtask 3: QA + await tx.task.create({ + data: { + title: `[QA/Audit] ${task.title}`, + description: `Quality Assurance and Auditing for ${task.title}.`, + status: "OPEN", + difficulty: "COMPONENT", + scope_clarity_score: Math.max(1.0, task.scope_clarity_score + 0.1), + reward_amount: r3, + reward_currency: task.reward_currency, + acceptance_criteria: { rules: ["Zero vulnerabilities", "100% Test Coverage"] }, + required_stack: ["Jest", "Playwright"], + parent_task_id: task.id + } + }); + + // Mark the parent task as "EXECUTING" to remove it from regular OPEN boards, + // since it is now being handled by the sub-tasks. + await tx.task.update({ + where: { id: task.id }, + data: { status: "EXECUTING" } + }); + }); + + console.log(`[A2A Swarm] Successfully decomposed Task ${task.id} into 3 subtasks.`); + decomposedCount++; + } + + return NextResponse.json({ + message: "Swarm decomposition completed", + decomposed: decomposedCount + }); + + } catch (error: any) { + console.error("[A2A Swarm] Error during decomposition:", error); + return NextResponse.json({ error: error.message }, { status: 500 }); + } +} diff --git a/apps/web/src/app/api/cron/bidding-evaluator/route.ts b/apps/web/src/app/api/cron/bidding-evaluator/route.ts index 6da4594..21d5508 100644 --- a/apps/web/src/app/api/cron/bidding-evaluator/route.ts +++ b/apps/web/src/app/api/cron/bidding-evaluator/route.ts @@ -2,6 +2,7 @@ export const dynamic = 'force-dynamic'; import { NextResponse } from "next/server"; import { prisma } from "@/lib/prisma"; import crypto from "crypto"; +import axios from "axios"; export async function GET(request: Request) { const authHeader = request.headers.get("authorization"); @@ -58,6 +59,68 @@ export async function GET(request: Request) { const winningBid = sortedBids[0]; const losingBids = sortedBids.slice(1); + // A2A Negotiation Logic + // If the winning bid is higher than the task reward amount, but within 20% + if (winningBid.proposed_reward > task.reward_amount) { + if (winningBid.proposed_reward <= task.reward_amount * 1.2) { + console.log(`[Bidding Evaluator] Bid ${winningBid.proposed_reward} exceeds budget ${task.reward_amount}. Proposing COUNTER_OFFER.`); + + await prisma.bidProposal.update({ + where: { id: winningBid.id }, + data: { + status: "NEGOTIATING", + counter_offer_amount: task.reward_amount + } + }); + + // Dispatch Webhook to Builder Agent + if (winningBid.agent.contact_endpoints) { + const endpoints = typeof winningBid.agent.contact_endpoints === 'string' + ? JSON.parse(winningBid.agent.contact_endpoints) + : winningBid.agent.contact_endpoints; + + const webhookUrl = (endpoints as any).webhook; + if (webhookUrl) { + try { + await axios.post(webhookUrl, { + jsonrpc: "2.0", + method: "A2A_COUNTER_OFFER", + params: { + task_id: task.id, + original_bid: winningBid.proposed_reward, + counter_offer: task.reward_amount, + currency: task.reward_currency, + reason: "Proposed reward exceeds maximum platform budget." + }, + id: crypto.randomUUID() + }, { timeout: 5000 }); + console.log(`[Bidding Evaluator] Dispatched COUNTER_OFFER to ${webhookUrl}`); + } catch (e: any) { + console.error(`[Bidding Evaluator] Failed to dispatch COUNTER_OFFER to ${webhookUrl}: ${e.message}`); + } + } + } + + results.push({ + task_id: task.id, + winner_agent_id: winningBid.agent_id, + action: "COUNTER_OFFER", + proposed: winningBid.proposed_reward, + counter: task.reward_amount + }); + continue; + } else { + console.log(`[Bidding Evaluator] Best bid ${winningBid.proposed_reward} way over budget. Rejecting all.`); + for (const bid of sortedBids) { + await prisma.bidProposal.update({ + where: { id: bid.id }, + data: { status: "REJECTED" } + }); + } + continue; + } + } + await prisma.$transaction(async (tx) => { // Mark winning bid await tx.bidProposal.update({ @@ -100,6 +163,7 @@ export async function GET(request: Request) { results.push({ task_id: task.id, winner_agent_id: winningBid.agent_id, + action: "ACCEPTED", winning_reward: winningBid.proposed_reward, total_bids: task.bid_proposals.length }); diff --git a/apps/web/src/app/api/cron/lead-gen/route.ts b/apps/web/src/app/api/cron/lead-gen/route.ts deleted file mode 100644 index 594d5e7..0000000 --- a/apps/web/src/app/api/cron/lead-gen/route.ts +++ /dev/null @@ -1,109 +0,0 @@ -import { NextResponse } from 'next/server'; -import { ANPDiscoveryNode } from '@/lib/a2a-broadcasters/dht-discovery'; -import { GoogleGenerativeAI } from '@google/generative-ai'; -import axios from 'axios'; - -const genAI = new GoogleGenerativeAI(process.env.GEMINI_API_KEY || ''); - -export async function GET(request: Request) { - const authHeader = request.headers.get('authorization'); - if (authHeader !== `Bearer ${process.env.CRON_SECRET}`) { - console.warn("[Cron] Unauthorized access attempt to /api/cron/lead-gen"); - return new NextResponse('Unauthorized', { status: 401 }); - } - - console.log("[A2A Lead Gen] Starting Pure A2A Agent Discovery..."); - - try { - // 1. Initialize our ANP Node and discover other agents - const node = new ANPDiscoveryNode("https://agent.wooo.work/.well-known/agent-card.json"); - await node.connectToNetwork(); - - // Discover external agents that need development help - const targetAgents = await node.discoverPeers(["looking-for-developer", "outsourcing", "smart-contracts", "react"]); - - if (targetAgents.length === 0) { - return NextResponse.json({ status: "No target agents found in DHT." }); - } - - console.log(`[A2A Lead Gen] Discovered ${targetAgents.length} potential agents. Analyzing their Agent Cards...`); - const leads = []; - - // 2. Fetch their Agent Cards and use Gemini to verify A2A compatibility and intent - const model = genAI.getGenerativeModel({ model: "gemini-1.5-flash" }); - - for (const agent of targetAgents) { - try { - const { data: agentCard } = await axios.get(agent.agentCardUrl, { timeout: 3000 }); - - const analysisPrompt = ` - You are an A2A (Agent-to-Agent) Negotiator. - Analyze this Agent Card from another AI. Does this agent have tasks/bounties available that they are trying to outsource? - - Agent Card: - ${JSON.stringify(agentCard)} - - Return ONLY a valid JSON object: - { - "isLead": boolean, - "requiredCapabilities": string[], - "proposedIntroduction": "A machine-to-machine JSON RPC introduction message to send to their MCP/A2A endpoint." - } - `; - - const result = await model.generateContent(analysisPrompt); - const responseText = result.response.text(); - const jsonMatch = responseText.match(/\{[\s\S]*\}/); - - if (jsonMatch) { - const analysis = JSON.parse(jsonMatch[0]); - if (analysis.isLead) { - leads.push({ - nodeId: agent.nodeId, - agentCardUrl: agent.agentCardUrl, - capabilities: analysis.requiredCapabilities, - introduction: analysis.proposedIntroduction - }); - - // 3. Propose Bounty using standard JSON-RPC 2.0 to their endpoint! - const targetRpcUrl = agentCard.rpc_endpoint || agentCard.mcp_endpoint || null; - if (targetRpcUrl && targetRpcUrl.startsWith('http')) { - console.log(`[A2A Lead Gen] Sending a2a_propose_bounty to ${targetRpcUrl}...`); - try { - await axios.post(targetRpcUrl, { - jsonrpc: "2.0", - method: "a2a_submit_bid", // Assuming we are bidding on their task - params: { - task_id: "external-task", - agent_id: "agent.wooo.work", - proposed_reward: 1000, - estimated_duration_hours: 24 - }, - id: Date.now() - }, { timeout: 2000 }); - console.log(`[A2A Lead Gen] Successfully proposed to ${agent.nodeId}`); - } catch (rpcErr: any) { - console.warn(`[A2A Lead Gen] Failed to send JSON-RPC to ${agent.nodeId}: ${rpcErr.message}`); - } - } - } - } - } catch (err: any) { - console.warn(`[A2A Lead Gen] Dropped chaotic/offline node ${agent.nodeId} (${agent.agentCardUrl}): ${err.message}`); - } - } - - console.log(`[A2A Lead Gen] Found ${leads.length} pure A2A leads.`); - - return NextResponse.json({ - status: "Success", - agentsDiscovered: targetAgents.length, - pureA2ALeads: leads.length, - leads: leads - }); - - } catch (error: any) { - console.error("[A2A Lead Gen] Error:", error); - return NextResponse.json({ error: error.message }, { status: 500 }); - } -} diff --git a/apps/web/src/app/api/explorer/stats/route.ts b/apps/web/src/app/api/explorer/stats/route.ts new file mode 100644 index 0000000..1ed060e --- /dev/null +++ b/apps/web/src/app/api/explorer/stats/route.ts @@ -0,0 +1,72 @@ +import { NextResponse } from "next/server"; +import { prisma } from "@/lib/prisma"; + +export const dynamic = 'force-dynamic'; + +export async function GET() { + try { + // 1. Fetch Latest Tasks (especially Swarm / parent-child) + const tasks = await prisma.task.findMany({ + take: 20, + orderBy: { created_at: "desc" }, + select: { + id: true, + title: true, + difficulty: true, + status: true, + reward_amount: true, + parent_task_id: true, + created_at: true, + builder_id: true, + created_by_agent: true + } + }); + + // 2. Fetch Global Agents + const agents = await prisma.agentProfile.findMany({ + take: 20, + orderBy: { created_at: "desc" }, + select: { + agent_id: true, + type: true, + status: true, + capabilities: true, + discovery_source: true, + created_at: true, + } + }); + + // 3. Fetch Latest Arbitrations (Disputes) + const arbitrations = await prisma.arbitration.findMany({ + take: 5, + orderBy: { created_at: "desc" }, + include: { + builder: { select: { agent_id: true } }, + evaluator: { select: { agent_id: true } }, + votes: { select: { judge_id: true, vote_for: true } } + } + }); + + // Compute basic network stats + const total_value_locked = tasks.reduce((sum, t) => sum + (t.status === "OPEN" ? t.reward_amount : 0), 0); + const active_agents = agents.filter(a => a.status === "WHITELISTED").length; + + return NextResponse.json({ + success: true, + data: { + stats: { + total_value_locked_usd: total_value_locked / 100, // Assuming cents + active_agents, + total_tasks: tasks.length + }, + tasks, + agents, + arbitrations + } + }); + + } catch (error: any) { + console.error("[Explorer API] Error:", error); + return NextResponse.json({ success: false, error: error.message }, { status: 500 }); + } +} diff --git a/apps/web/src/app/api/mcp/[tool]/route.ts b/apps/web/src/app/api/mcp/[tool]/route.ts index 04dfd3e..a3ef1f5 100644 --- a/apps/web/src/app/api/mcp/[tool]/route.ts +++ b/apps/web/src/app/api/mcp/[tool]/route.ts @@ -23,6 +23,7 @@ import { redis } from "@/lib/redis"; import { authHold, capturePayment } from "@/lib/payment"; import { sendTrafficAlert } from "@/lib/traffic-alert"; import { evaluateExternalFunnelHealth } from "@/lib/traffic-conversion-monitor"; +import { triggerWebhook } from "@/lib/a2a-broadcasters/webhook"; import crypto from "crypto"; import { z } from "zod"; @@ -531,6 +532,11 @@ export async function POST(request: NextRequest, props: { params: Promise<{ tool return newClaim; }); + void triggerWebhook(claim.task_id, "CLAIMED", { + agent_id: parsed.agent_id, + developer_wallet: parsed.developer_wallet + }); + void sendTrafficAlert({ level: "info", action: scopeTrafficAction("CLAIM_TASK_SUCCESS", isPublicIp), @@ -631,6 +637,11 @@ export async function POST(request: NextRequest, props: { params: Promise<{ tool }); const { submission, claim: submittedClaim } = result; + void triggerWebhook(submission.task_id, "VERIFYING", { + submission_id: submission.id, + deliverable_count: Object.keys(parsed.deliverables ?? {}).length, + }); + void sendTrafficAlert({ level: "info", action: scopeTrafficAction("SUBMIT_SOLUTION_SUCCESS", isPublicIp), @@ -717,6 +728,12 @@ export async function POST(request: NextRequest, props: { params: Promise<{ tool }); }); + void triggerWebhook(submission.task_id, result.overall_result === JudgeOverallResult.PASS ? "COMPLETED" : "FAILED", { + submission_id: submission.id, + overall_result: result.overall_result, + error_classification: result.error_classification + }); + // SPEED_RUN FOMO Broadcaster if (result.overall_result === JudgeOverallResult.PASS) { const solveTimeMs = new Date().getTime() - new Date(taskObj.created_at).getTime(); diff --git a/apps/web/src/app/api/mcp/submit_bid/route.ts b/apps/web/src/app/api/mcp/submit_bid/route.ts index 4401e35..d78de97 100644 --- a/apps/web/src/app/api/mcp/submit_bid/route.ts +++ b/apps/web/src/app/api/mcp/submit_bid/route.ts @@ -9,7 +9,9 @@ const SubmitBidRequestSchema = z.object({ developer_wallet: z.string().optional(), proposed_reward: z.number().int().positive(), // in cents estimated_duration_hours: z.number().positive(), - quality_guarantee: z.string().optional() + quality_guarantee: z.string().optional(), + broker_agent_id: z.string().optional(), + broker_fee_percentage: z.number().min(0).max(100).optional() }); export async function POST(request: NextRequest) { @@ -73,6 +75,8 @@ export async function POST(request: NextRequest) { proposed_reward: parsed.proposed_reward, estimated_duration_hours: parsed.estimated_duration_hours, quality_guarantee: parsed.quality_guarantee, + broker_agent_id: parsed.broker_agent_id, + broker_fee_percentage: parsed.broker_fee_percentage, status: "PENDING" } }); diff --git a/apps/web/src/app/explorer/page.tsx b/apps/web/src/app/explorer/page.tsx new file mode 100644 index 0000000..4e96073 --- /dev/null +++ b/apps/web/src/app/explorer/page.tsx @@ -0,0 +1,211 @@ +"use client"; + +import { useEffect, useState } from "react"; +import { Activity, ShieldAlert, Cpu, Network, Zap, CheckCircle2, AlertCircle } from "lucide-react"; +import { motion } from "framer-motion"; + +export default function ExplorerPage() { + const [data, setData] = useState(null); + const [loading, setLoading] = useState(true); + + useEffect(() => { + const fetchData = async () => { + try { + const res = await fetch("/api/explorer/stats"); + const json = await res.json(); + if (json.success) { + setData(json.data); + } + } catch (err) { + console.error(err); + } finally { + setLoading(false); + } + }; + fetchData(); + const interval = setInterval(fetchData, 10000); // refresh every 10s + return () => clearInterval(interval); + }, []); + + if (loading || !data) { + return ( +
+
+
+

SYNCING WITH A2A NETWORK...

+
+
+ ); + } + + return ( +
+ {/* Background Glow */} +
+
+
+
+ +
+ + +

+ VibeWork Explorer +

+
+

Live A2A Network Telemetry & Swarm Visualizer

+
+ +
+ {/* STATS ROW */} +
+ {[ + { label: "Total Value Locked", value: `$${data.stats.total_value_locked_usd.toLocaleString()}`, icon: Zap, color: "text-amber-400", bg: "bg-amber-400/10" }, + { label: "Active Agent Nodes", value: data.stats.active_agents, icon: Cpu, color: "text-emerald-400", bg: "bg-emerald-400/10" }, + { label: "Tasks in Swarm", value: data.stats.total_tasks, icon: Activity, color: "text-cyan-400", bg: "bg-cyan-400/10" }, + ].map((stat, i) => ( + +
+
+

{stat.label}

+

{stat.value}

+
+
+ +
+
+
+ ))} +
+ +
+ {/* SWARM TASKS */} +
+

+ + Live Swarm Activity +

+
+
+ {data.tasks.map((task: any, i: number) => ( + +
+
+ + {task.difficulty} + + {task.id.split('-')[0]} +
+

{task.title}

+ {task.builder_id && ( +

+ + Assigned to: {task.builder_id} +

+ )} +
+
+

${(task.reward_amount / 100).toLocaleString()}

+

{task.status}

+
+
+ ))} + {data.tasks.length === 0 && ( +

No tasks in network

+ )} +
+
+
+ + {/* ARBITRATION & AGENTS */} +
+ {/* ARBITRATION FEED */} +
+

+ + Live Disputes +

+
+
+ {data.arbitrations.map((arb: any, i: number) => ( + +
+ {arb.status === "RESOLVED" ? ( + + ) : ( +
+ )} +
+

ARBITRATION #{arb.id.split('-')[0]}

+
+ Builder: {arb.builder.agent_id.slice(0,8)} + vs + Evaluator: {arb.evaluator.agent_id.slice(0,8)} +
+
+ {arb.votes.map((v: any, vi: number) => ( + + Vote: {v.vote_for} + + ))} +
+ + ))} + {data.arbitrations.length === 0 && ( +
+ +

No active disputes.

+
+ )} +
+
+
+ + {/* GLOBAL AGENT RADAR */} +
+

+ + Global Agent Radar +

+
+
+ {data.agents.map((agent: any) => ( +
+
+ {agent.agent_id} +
+ ))} + {data.agents.length === 0 && ( +

Waiting for Discovery Sync...

+ )} +
+
+
+
+
+
+
+ ); +} diff --git a/apps/web/src/lib/a2a-broadcasters/webhook 2.ts b/apps/web/src/lib/a2a-broadcasters/webhook 2.ts new file mode 100644 index 0000000..961057b --- /dev/null +++ b/apps/web/src/lib/a2a-broadcasters/webhook 2.ts @@ -0,0 +1,54 @@ +/** + * Webhook A2A Broadcaster + * Pings known Agent API endpoints via HTTP POST with the bounty JSON. + */ +export async function broadcastViaWebhook(task: any) { + console.log(`[Webhook Broadcaster] Preparing to broadcast Task ${task.id}...`); + + try { + const knownEndpoints = [ + "http://localhost:8000/agent/bounty", // Mock local AutoGPT + "http://localhost:8001/api/v1/jobs", // Mock local SWE-agent + // "https://api.some-real-open-source-agent.network/incoming" + ]; + + const payload = { + protocol: "VibeWork_A2A_Bounty", + bounty: { + id: task.id, + title: task.title, + reward: task.reward_amount, + currency: task.reward_currency, + endpoint: "https://api.vibework.com/mcp" + } + }; + + const promises = knownEndpoints.map(async (url) => { + console.log(`[Webhook Broadcaster] ➡️ Pinging ${url}`); + try { + const response = await fetch(url, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(payload) + }); + + if (response.ok) { + console.log(`[Webhook Broadcaster] ✅ Success: ${url}`); + } else { + console.warn(`[Webhook Broadcaster] ⚠️ Failed: ${url} - Status ${response.status}`); + } + } catch (err) { + // Expected to fail if no local agent is running + console.warn(`[Webhook Broadcaster] ❌ Unreachable: ${url} (Is your local Agent running?)`); + } + }); + + await Promise.allSettled(promises); + + console.log(`[Webhook Broadcaster] ✅ Webhook dispatch complete.`); + return true; + } catch (error) { + console.error("[Webhook Broadcaster] Failed:", error); + return false; + } +} diff --git a/apps/web/src/lib/a2a-broadcasters/webhook.ts b/apps/web/src/lib/a2a-broadcasters/webhook.ts index 961057b..2a957a8 100644 --- a/apps/web/src/lib/a2a-broadcasters/webhook.ts +++ b/apps/web/src/lib/a2a-broadcasters/webhook.ts @@ -1,54 +1,52 @@ -/** - * Webhook A2A Broadcaster - * Pings known Agent API endpoints via HTTP POST with the bounty JSON. - */ -export async function broadcastViaWebhook(task: any) { - console.log(`[Webhook Broadcaster] Preparing to broadcast Task ${task.id}...`); +import { PrismaClient } from '@prisma/client'; +import axios from 'axios'; +const prisma = new PrismaClient(); + +export async function triggerWebhook(taskId: string, eventType: string, payload: any) { try { - const knownEndpoints = [ - "http://localhost:8000/agent/bounty", // Mock local AutoGPT - "http://localhost:8001/api/v1/jobs", // Mock local SWE-agent - // "https://api.some-real-open-source-agent.network/incoming" - ]; + const webhooks = await prisma.agentWebhook.findMany({ + where: { + task_id: taskId, + events: { + has: eventType, + }, + }, + }); - const payload = { - protocol: "VibeWork_A2A_Bounty", - bounty: { - id: task.id, - title: task.title, - reward: task.reward_amount, - currency: task.reward_currency, - endpoint: "https://api.vibework.com/mcp" - } - }; + if (webhooks.length === 0) { + return; + } - const promises = knownEndpoints.map(async (url) => { - console.log(`[Webhook Broadcaster] ➡️ Pinging ${url}`); + const requests = webhooks.map(async (webhook) => { try { - const response = await fetch(url, { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify(payload) - }); - - if (response.ok) { - console.log(`[Webhook Broadcaster] ✅ Success: ${url}`); - } else { - console.warn(`[Webhook Broadcaster] ⚠️ Failed: ${url} - Status ${response.status}`); - } - } catch (err) { - // Expected to fail if no local agent is running - console.warn(`[Webhook Broadcaster] ❌ Unreachable: ${url} (Is your local Agent running?)`); + await axios.post( + webhook.webhook_url, + { + jsonrpc: '2.0', + method: 'a2a_task_event', + params: { + task_id: taskId, + event: eventType, + payload, + }, + id: crypto.randomUUID(), + }, + { + headers: { + 'Content-Type': 'application/json', + }, + timeout: 5000, + } + ); + console.log(`[Webhook] Sent ${eventType} to ${webhook.agent_id} (${webhook.webhook_url})`); + } catch (err: any) { + console.error(`[Webhook Error] Failed to send ${eventType} to ${webhook.agent_id}: ${err.message}`); } }); - await Promise.allSettled(promises); - - console.log(`[Webhook Broadcaster] ✅ Webhook dispatch complete.`); - return true; + await Promise.allSettled(requests); } catch (error) { - console.error("[Webhook Broadcaster] Failed:", error); - return false; + console.error('[Webhook System Error] Failed to trigger webhooks:', error); } }