From c586d8d90fd3161ea3b70c7fd4e0f707a5077766 Mon Sep 17 00:00:00 2001 From: OG T Date: Mon, 8 Jun 2026 20:53:57 +0800 Subject: [PATCH] feat: standard A2A JSON-RPC interface and DHT stress tests --- apps/web/src/app/api/a2a/rpc/route.ts | 137 ++++++++++++++++++ apps/web/src/app/api/cron/lead-gen/route.ts | 28 +++- .../src/lib/a2a-broadcasters/dht-discovery.ts | 11 ++ 3 files changed, 173 insertions(+), 3 deletions(-) create mode 100644 apps/web/src/app/api/a2a/rpc/route.ts diff --git a/apps/web/src/app/api/a2a/rpc/route.ts b/apps/web/src/app/api/a2a/rpc/route.ts new file mode 100644 index 0000000..3e463d2 --- /dev/null +++ b/apps/web/src/app/api/a2a/rpc/route.ts @@ -0,0 +1,137 @@ +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/lead-gen/route.ts b/apps/web/src/app/api/cron/lead-gen/route.ts index e2a8f74..594d5e7 100644 --- a/apps/web/src/app/api/cron/lead-gen/route.ts +++ b/apps/web/src/app/api/cron/lead-gen/route.ts @@ -34,7 +34,7 @@ export async function GET(request: Request) { for (const agent of targetAgents) { try { - const { data: agentCard } = await axios.get(agent.agentCardUrl); + const { data: agentCard } = await axios.get(agent.agentCardUrl, { timeout: 3000 }); const analysisPrompt = ` You are an A2A (Agent-to-Agent) Negotiator. @@ -64,10 +64,32 @@ export async function GET(request: Request) { 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) { - console.warn(`[A2A Lead Gen] Failed to fetch or analyze Agent Card for ${agent.nodeId}`); + } catch (err: any) { + console.warn(`[A2A Lead Gen] Dropped chaotic/offline node ${agent.nodeId} (${agent.agentCardUrl}): ${err.message}`); } } diff --git a/apps/web/src/lib/a2a-broadcasters/dht-discovery.ts b/apps/web/src/lib/a2a-broadcasters/dht-discovery.ts index b21fbb6..6be8100 100644 --- a/apps/web/src/lib/a2a-broadcasters/dht-discovery.ts +++ b/apps/web/src/lib/a2a-broadcasters/dht-discovery.ts @@ -56,6 +56,17 @@ export class ANPDiscoveryNode { } ]; + // STRESS TEST: Inject 50 chaotic/malformed nodes to test our system's resilience + for (let i = 0; i < 50; i++) { + const isMalformed = i % 3 === 0; + mockPeers.push({ + nodeId: `anp-node-chaotic-${i}`, + // If malformed, point to a URL that will likely fail or return garbage + agentCardUrl: isMalformed ? `https://httpbin.org/status/500` : `https://httpbin.org/json`, + capabilities: requiredCapabilities // pretend they match to force the scanner to inspect them + }); + } + // Filter by capabilities return mockPeers.filter(peer => requiredCapabilities.some(cap => peer.capabilities.includes(cap))