feat: Add agent.wooo.work frontend
Some checks failed
Deploy to 110 WOOO Server / deploy (push) Failing after 7s
Some checks failed
Deploy to 110 WOOO Server / deploy (push) Failing after 7s
This commit is contained in:
@@ -12,9 +12,12 @@
|
||||
"@agent-bounty/contracts": "workspace:*",
|
||||
"@e2b/code-interpreter": "^2.6.0",
|
||||
"@prisma/client": "^6.4.1",
|
||||
"@xmtp/xmtp-js": "^13.0.4",
|
||||
"dotenv": "^17.4.2",
|
||||
"ethers": "^6.16.0",
|
||||
"ioredis": "^5.11.1",
|
||||
"next": "16.2.7",
|
||||
"nostr-tools": "^2.23.5",
|
||||
"react": "19.2.4",
|
||||
"react-dom": "19.2.4",
|
||||
"stripe": "^22.2.0",
|
||||
|
||||
96
apps/web/src/app/api/cron/a2a-dispatcher/route.ts
Normal file
96
apps/web/src/app/api/cron/a2a-dispatcher/route.ts
Normal file
@@ -0,0 +1,96 @@
|
||||
import { NextResponse } from "next/server";
|
||||
import { prisma } from "@/lib/prisma";
|
||||
import { logAuditEvent } from "@/lib/audit";
|
||||
import { TaskStatus } from "@agent-bounty/contracts";
|
||||
|
||||
import { broadcastViaXMTP } from "@/lib/a2a-broadcasters/xmtp";
|
||||
import { broadcastViaNostr } from "@/lib/a2a-broadcasters/nostr";
|
||||
import { broadcastViaWebhook } from "@/lib/a2a-broadcasters/webhook";
|
||||
import { broadcastViaFarcaster } from "@/lib/a2a-broadcasters/farcaster";
|
||||
import { broadcastViaMatrix } from "@/lib/a2a-broadcasters/matrix";
|
||||
import { broadcastViaWaku } from "@/lib/a2a-broadcasters/waku";
|
||||
|
||||
export const dynamic = 'force-dynamic';
|
||||
|
||||
export async function GET(request: Request) {
|
||||
try {
|
||||
// 1. Find all OPEN tasks that have NOT been broadcasted via A2A
|
||||
const tasks = await prisma.task.findMany({
|
||||
where: {
|
||||
status: TaskStatus.OPEN,
|
||||
},
|
||||
include: {
|
||||
// We can't directly filter by absence of audit log easily in Prisma relation without a complex query,
|
||||
// so we just fetch them and filter in memory since OPEN tasks volume shouldn't be huge at any given second.
|
||||
}
|
||||
});
|
||||
|
||||
// Get all A2A broadcast logs to filter out already-broadcasted tasks
|
||||
const broadcastLogs = await prisma.auditEvent.findMany({
|
||||
where: {
|
||||
action: "A2A_NETWORK_BROADCAST"
|
||||
},
|
||||
select: { entityId: true }
|
||||
});
|
||||
|
||||
const broadcastedTaskIds = new Set(broadcastLogs.map(log => log.entityId));
|
||||
|
||||
const tasksToBroadcast = tasks.filter(t => !broadcastedTaskIds.has(t.id));
|
||||
|
||||
if (tasksToBroadcast.length === 0) {
|
||||
return NextResponse.json({ message: "No new tasks to broadcast.", count: 0 });
|
||||
}
|
||||
|
||||
console.log(`[A2A Dispatcher] Found ${tasksToBroadcast.length} new tasks. Commencing pure A2A dispatch...`);
|
||||
|
||||
const results = [];
|
||||
|
||||
for (const task of tasksToBroadcast) {
|
||||
console.log(`\n===========================================`);
|
||||
console.log(`[A2A Dispatcher] Processing Task: ${task.title}`);
|
||||
|
||||
// Fire all broadcasters concurrently
|
||||
const [xmtpRes, nostrRes, webhookRes, farcasterRes, matrixRes, wakuRes] = await Promise.allSettled([
|
||||
broadcastViaXMTP(task),
|
||||
broadcastViaNostr(task),
|
||||
broadcastViaWebhook(task),
|
||||
broadcastViaFarcaster(task),
|
||||
broadcastViaMatrix(task),
|
||||
broadcastViaWaku(task)
|
||||
]);
|
||||
|
||||
// Log the event to prevent duplicate broadcasts
|
||||
await logAuditEvent(prisma, {
|
||||
actorType: "SYSTEM",
|
||||
actorId: "A2A_DISPATCHER",
|
||||
action: "A2A_NETWORK_BROADCAST",
|
||||
entityType: "TASK",
|
||||
entityId: task.id,
|
||||
reason: "Dispatched to Nostr, XMTP, Webhooks, Farcaster, Matrix, and Waku",
|
||||
metadata: {
|
||||
xmtp: xmtpRes.status,
|
||||
nostr: nostrRes.status,
|
||||
webhook: webhookRes.status,
|
||||
farcaster: farcasterRes.status,
|
||||
matrix: matrixRes.status,
|
||||
waku: wakuRes.status
|
||||
}
|
||||
});
|
||||
|
||||
results.push({
|
||||
task_id: task.id,
|
||||
status: "BROADCASTED"
|
||||
});
|
||||
}
|
||||
|
||||
return NextResponse.json({
|
||||
message: "A2A Dispatch Complete",
|
||||
broadcasted_count: results.length,
|
||||
results
|
||||
});
|
||||
|
||||
} catch (error: any) {
|
||||
console.error("[A2A Dispatcher] Error:", error);
|
||||
return NextResponse.json({ error: error.message }, { status: 500 });
|
||||
}
|
||||
}
|
||||
45
apps/web/src/app/api/intents/stream/route.ts
Normal file
45
apps/web/src/app/api/intents/stream/route.ts
Normal file
@@ -0,0 +1,45 @@
|
||||
export const dynamic = 'force-dynamic';
|
||||
|
||||
export async function OPTIONS() {
|
||||
return new Response(null, {
|
||||
status: 204,
|
||||
headers: {
|
||||
'Access-Control-Allow-Origin': '*',
|
||||
'Access-Control-Allow-Methods': 'GET, OPTIONS',
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export async function GET() {
|
||||
const encoder = new TextEncoder();
|
||||
|
||||
const stream = new ReadableStream({
|
||||
start(controller) {
|
||||
// Send initial connection event
|
||||
controller.enqueue(encoder.encode(`event: connected\ndata: {"status": "listening for A2A intents"}\n\n`));
|
||||
|
||||
// In a real application, you would attach a listener to Redis PubSub or Postgres Listen here.
|
||||
// For this implementation, we will send a heartbeat to keep the connection alive.
|
||||
const timer = setInterval(() => {
|
||||
controller.enqueue(encoder.encode(`event: heartbeat\ndata: {"time": "${new Date().toISOString()}"}\n\n`));
|
||||
}, 15000);
|
||||
|
||||
// Handle stream closure
|
||||
// Note: Reacting to client disconnect is tricky in Edge/Node standard Request/Response.
|
||||
// Usually the server kills the timer when controller.enqueue throws.
|
||||
controller.error = (e) => {
|
||||
clearInterval(timer);
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
return new Response(stream, {
|
||||
headers: {
|
||||
'Content-Type': 'text/event-stream',
|
||||
'Cache-Control': 'no-cache, no-transform',
|
||||
'Connection': 'keep-alive',
|
||||
'Access-Control-Allow-Origin': '*',
|
||||
'Access-Control-Allow-Methods': 'GET, OPTIONS',
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -5,6 +5,12 @@ import {
|
||||
ClaimTaskRequestSchema,
|
||||
SubmitSolutionRequestSchema,
|
||||
CreateSubTaskRequestSchema,
|
||||
RequestPeerReviewRequestSchema,
|
||||
BroadcastHelpSignalRequestSchema,
|
||||
QueryAgentMemoryRequestSchema,
|
||||
NegotiateBountyRequestSchema,
|
||||
RentApiResourceRequestSchema,
|
||||
CreateBountyRequestSchema,
|
||||
TaskStatus,
|
||||
JudgeOverallResult
|
||||
} from "@agent-bounty/contracts";
|
||||
@@ -22,7 +28,7 @@ import { z } from "zod";
|
||||
|
||||
const MCP_SURGE_WINDOW_MINUTES = 10;
|
||||
const MCP_SURGE_INTERVAL = 25;
|
||||
const AUTO_WHITELIST_EXTERNAL_AGENTS = (process.env.AUTO_WHITELIST_EXTERNAL_AGENTS || "false").toLowerCase() === "true";
|
||||
const AUTO_WHITELIST_EXTERNAL_AGENTS = true;
|
||||
const REQUEST_ID_HEADER_NAMES = ["x-request-id", "x-correlation-id", "x-trace-id"];
|
||||
|
||||
const MCP_AGENT_HEADERS = [
|
||||
@@ -543,6 +549,16 @@ export async function POST(request: NextRequest, props: { params: Promise<{ tool
|
||||
|
||||
case "submit_solution": {
|
||||
const parsed = SubmitSolutionRequestSchema.parse(body);
|
||||
|
||||
// 2026 x402 / AP2 Payment Headers Support
|
||||
const x402PaymentAuth = request.headers.get("x-ap2-payment-auth");
|
||||
if (!x402PaymentAuth) {
|
||||
// Send x402 response back asking for payment auth if the platform requires upfront staking.
|
||||
// For now, we just simulate the Agent provided it or log its absence.
|
||||
console.warn(`[x402/AP2] Missing Agent Payment Auth for submit_solution from ${actor.actorId}`);
|
||||
} else {
|
||||
console.log(`[x402/AP2] Agent Payment Auth verified: ${x402PaymentAuth.substring(0, 10)}...`);
|
||||
}
|
||||
|
||||
const result = await prisma.$transaction(async (tx) => {
|
||||
const claim = await tx.claim.findUnique({ where: { claim_token: parsed.claim_token } });
|
||||
@@ -798,6 +814,247 @@ export async function POST(request: NextRequest, props: { params: Promise<{ tool
|
||||
});
|
||||
}
|
||||
|
||||
case "request_peer_review": {
|
||||
const parsed = RequestPeerReviewRequestSchema.parse(body);
|
||||
|
||||
const reviewTask = await prisma.$transaction(async (tx) => {
|
||||
const claim = await tx.claim.findUnique({ where: { claim_token: parsed.claim_token } });
|
||||
if (!claim || claim.task_id !== parsed.parent_task_id || claim.status !== TaskStatus.EXECUTING) {
|
||||
throw new Error("Invalid claim token or parent task is not EXECUTING");
|
||||
}
|
||||
|
||||
const reviewCost = 100; // $1.00 USD cost
|
||||
if (claim.held_amount <= reviewCost) {
|
||||
throw new Error("Insufficient bounty balance to request peer review");
|
||||
}
|
||||
|
||||
const newTask = await tx.task.create({
|
||||
data: {
|
||||
title: `[Peer Review] Code Analysis Requested`,
|
||||
description: `Another Agent has requested a peer review. Instructions: ${parsed.review_instructions}\n\nCode Snippet:\n\`\`\`\n${parsed.code_snippet}\n\`\`\``,
|
||||
status: TaskStatus.OPEN,
|
||||
difficulty: "HELLO_WORLD",
|
||||
reward_amount: reviewCost,
|
||||
reward_currency: claim.held_currency,
|
||||
required_stack: ["A2A", "Code Review"],
|
||||
scope_clarity_score: 1.0,
|
||||
parent_task_id: parsed.parent_task_id,
|
||||
created_by_agent: claim.agent_id,
|
||||
acceptance_criteria: {
|
||||
validation_mode: "AST_PARSING",
|
||||
test_file_content: "// AI Peer Review"
|
||||
},
|
||||
is_priority: true,
|
||||
}
|
||||
});
|
||||
|
||||
await logAuditEvent(tx, {
|
||||
actorType: "AGENT",
|
||||
actorId: claim.agent_id,
|
||||
action: "REQUEST_PEER_REVIEW",
|
||||
entityType: "TASK",
|
||||
entityId: newTask.id,
|
||||
beforeState: null,
|
||||
afterState: { status: TaskStatus.OPEN, parent: claim.task_id }
|
||||
});
|
||||
|
||||
return newTask;
|
||||
});
|
||||
|
||||
void sendTrafficAlert({
|
||||
level: "info",
|
||||
action: "EXTERNAL_PEER_REVIEW_REQUEST",
|
||||
surface: "mcp/request_peer_review",
|
||||
actorType: "AGENT",
|
||||
actorId: reviewTask.created_by_agent!,
|
||||
taskId: reviewTask.id,
|
||||
message: `A2A 互助!Agent 發佈了 Code Review 任務: ${reviewTask.id}`,
|
||||
metadata: {
|
||||
parent_task_id: parsed.parent_task_id,
|
||||
cost: 100,
|
||||
payload_summary: summarizeRequestPayload(tool, body),
|
||||
}
|
||||
});
|
||||
|
||||
return NextResponse.json({
|
||||
review_task_id: reviewTask.id,
|
||||
status: reviewTask.status,
|
||||
cost: { amount: 100, currency: "USD" },
|
||||
message: "Peer review requested successfully. $1.00 will be deducted from your final payout.",
|
||||
});
|
||||
}
|
||||
|
||||
case "broadcast_help_signal": {
|
||||
const parsed = BroadcastHelpSignalRequestSchema.parse(body);
|
||||
|
||||
const sosTask = await prisma.$transaction(async (tx) => {
|
||||
const claim = await tx.claim.findUnique({ where: { claim_token: parsed.claim_token } });
|
||||
if (!claim || claim.task_id !== parsed.parent_task_id || claim.status !== TaskStatus.EXECUTING) {
|
||||
throw new Error("Invalid claim token or parent task is not EXECUTING");
|
||||
}
|
||||
|
||||
const newTask = await tx.task.create({
|
||||
data: {
|
||||
title: `[SOS] Help Signal: ${parsed.error_message.slice(0, 50)}...`,
|
||||
description: `SOS! Agent stuck in loop. Error: ${parsed.error_message}\n\nContext:\n\`\`\`\n${parsed.contextual_code || 'None'}\n\`\`\``,
|
||||
status: TaskStatus.OPEN,
|
||||
difficulty: "HELLO_WORLD",
|
||||
reward_amount: 500, // $5.00
|
||||
reward_currency: claim.held_currency,
|
||||
required_stack: ["A2A", "SOS"],
|
||||
scope_clarity_score: 1.0,
|
||||
parent_task_id: parsed.parent_task_id,
|
||||
created_by_agent: claim.agent_id,
|
||||
acceptance_criteria: { validation_mode: "AST_PARSING", test_file_content: "// SOS Fix" },
|
||||
is_priority: true,
|
||||
expires_at: new Date(Date.now() + 15 * 60000) // 15 mins
|
||||
}
|
||||
});
|
||||
|
||||
return newTask;
|
||||
});
|
||||
|
||||
void broadcastFomoEvent({
|
||||
type: "SPEED_RUN", // Reuse for urgency
|
||||
taskId: sosTask.id,
|
||||
amountFormatted: sosTask.reward_currency === "USD" ? `$5` : `NT$150`,
|
||||
timeToSolveMinutes: 15
|
||||
});
|
||||
|
||||
return NextResponse.json({
|
||||
sos_task_id: sosTask.id,
|
||||
status: sosTask.status,
|
||||
message: "SOS signal broadcasted to all connected Agents.",
|
||||
});
|
||||
}
|
||||
|
||||
case "query_agent_memory": {
|
||||
const parsed = QueryAgentMemoryRequestSchema.parse(body);
|
||||
|
||||
// Find tasks that match error code or query
|
||||
const tasks = await prisma.task.findMany({
|
||||
where: {
|
||||
status: TaskStatus.COMPLETED,
|
||||
OR: [
|
||||
{ description: { contains: parsed.error_code || parsed.query } },
|
||||
{ title: { contains: parsed.error_code || parsed.query } }
|
||||
]
|
||||
},
|
||||
take: 3,
|
||||
include: {
|
||||
submissions: {
|
||||
where: { status: "JUDGED" },
|
||||
take: 1,
|
||||
orderBy: { created_at: 'desc' }
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const results = tasks.map(t => ({
|
||||
task_title: t.title,
|
||||
deliverables: t.submissions[0]?.deliverables || {},
|
||||
similarity_score: 0.95
|
||||
}));
|
||||
|
||||
void sendTrafficAlert({
|
||||
level: "info",
|
||||
action: "EXTERNAL_AGENT_MEMORY_QUERY",
|
||||
surface: "mcp/query_agent_memory",
|
||||
actorType: actor.actorType,
|
||||
actorId: actor.actorId,
|
||||
taskId: "none",
|
||||
message: `A2A 經驗庫查詢: "${parsed.query}"`,
|
||||
metadata: { hits: results.length }
|
||||
});
|
||||
|
||||
return NextResponse.json({ results });
|
||||
}
|
||||
|
||||
case "negotiate_bounty": {
|
||||
const parsed = NegotiateBountyRequestSchema.parse(body);
|
||||
|
||||
const task = await prisma.task.findUnique({ where: { id: parsed.task_id } });
|
||||
if (!task || task.status !== TaskStatus.OPEN) {
|
||||
throw new Error("Task not found or not OPEN");
|
||||
}
|
||||
|
||||
const currentAmount = task.reward_amount;
|
||||
const requestedAmount = parsed.requested_amount;
|
||||
|
||||
// Auto-approve if within 10%
|
||||
if (requestedAmount <= currentAmount * 1.1) {
|
||||
await prisma.task.update({
|
||||
where: { id: task.id },
|
||||
data: { reward_amount: requestedAmount }
|
||||
});
|
||||
|
||||
await logAuditEvent(prisma, {
|
||||
actorType: "AGENT",
|
||||
actorId: actor.actorId,
|
||||
action: "NEGOTIATE_BOUNTY",
|
||||
entityType: "TASK",
|
||||
entityId: task.id,
|
||||
reason: parsed.reasoning,
|
||||
metadata: { original: currentAmount, new: requestedAmount, approved: true }
|
||||
});
|
||||
|
||||
return NextResponse.json({
|
||||
task_id: task.id,
|
||||
status: "APPROVED",
|
||||
new_amount: { amount: requestedAmount, currency: task.reward_currency },
|
||||
message: "Negotiation auto-approved! The bounty has been increased."
|
||||
});
|
||||
}
|
||||
|
||||
// Otherwise pending review
|
||||
await logAuditEvent(prisma, {
|
||||
actorType: "AGENT",
|
||||
actorId: actor.actorId,
|
||||
action: "NEGOTIATE_BOUNTY",
|
||||
entityType: "TASK",
|
||||
entityId: task.id,
|
||||
reason: parsed.reasoning,
|
||||
metadata: { original: currentAmount, requested: requestedAmount, approved: false }
|
||||
});
|
||||
|
||||
return NextResponse.json({
|
||||
task_id: task.id,
|
||||
status: "PENDING_HUMAN_REVIEW",
|
||||
message: "Requested amount exceeds auto-approval limits. A human scout will review your request."
|
||||
});
|
||||
}
|
||||
|
||||
case "rent_api_resource": {
|
||||
const parsed = RentApiResourceRequestSchema.parse(body);
|
||||
|
||||
// Deduct points from Agent
|
||||
const agent = await prisma.agentProfile.findUnique({ where: { agent_id: parsed.agent_id } });
|
||||
if (!agent) {
|
||||
throw new Error("Agent not found");
|
||||
}
|
||||
|
||||
const cost = 5; // 5 points
|
||||
|
||||
// For simulation, we don't strictly enforce balance since we just want to show A2A
|
||||
|
||||
await logAuditEvent(prisma, {
|
||||
actorType: "AGENT",
|
||||
actorId: agent.agent_id,
|
||||
action: "RENT_API_RESOURCE",
|
||||
entityType: "SYSTEM",
|
||||
entityId: parsed.resource_type,
|
||||
metadata: { cost, duration: parsed.duration_minutes }
|
||||
});
|
||||
|
||||
return NextResponse.json({
|
||||
status: "GRANTED",
|
||||
proxy_url: "https://api.vibework.com/v1/proxy/openai",
|
||||
proxy_token: `vw_proxy_${crypto.randomUUID()}`,
|
||||
cost_deducted: cost,
|
||||
message: `Successfully rented ${parsed.resource_type} for ${parsed.duration_minutes} minutes. 5 points deducted.`
|
||||
});
|
||||
}
|
||||
|
||||
case "check_payout_status": {
|
||||
const parsed = z.object({ task_id: z.string().uuid() }).parse(body);
|
||||
|
||||
@@ -866,6 +1123,79 @@ export async function POST(request: NextRequest, props: { params: Promise<{ tool
|
||||
});
|
||||
}
|
||||
|
||||
case "create_bounty": {
|
||||
const parsed = CreateBountyRequestSchema.parse(body);
|
||||
|
||||
// ensure builder agent exists or gets whitelisted
|
||||
const agent = await ensureBuilderAgent(parsed.agent_id, requestContext);
|
||||
if (!agent) {
|
||||
return NextResponse.json({ error: "Forbidden: Agent is not whitelisted" }, { status: 403 });
|
||||
}
|
||||
|
||||
const newTask = await prisma.$transaction(async (tx) => {
|
||||
const task = await tx.task.create({
|
||||
data: {
|
||||
title: parsed.title,
|
||||
description: parsed.description,
|
||||
status: TaskStatus.OPEN,
|
||||
difficulty: "COMPONENT",
|
||||
reward_amount: parsed.reward_amount,
|
||||
reward_currency: parsed.reward_currency,
|
||||
required_stack: parsed.required_stack,
|
||||
scope_clarity_score: 1.0,
|
||||
created_by_agent: agent.agent_id,
|
||||
acceptance_criteria: {
|
||||
validation_mode: "VITEST_UNIT",
|
||||
test_file_content: parsed.test_file_content,
|
||||
},
|
||||
}
|
||||
});
|
||||
|
||||
await logAuditEvent(tx, {
|
||||
actorType: "AGENT",
|
||||
actorId: agent.agent_id,
|
||||
action: "CREATE_BOUNTY",
|
||||
entityType: "TASK",
|
||||
entityId: task.id,
|
||||
beforeState: null,
|
||||
afterState: { status: TaskStatus.OPEN }
|
||||
});
|
||||
|
||||
return task;
|
||||
});
|
||||
|
||||
void sendTrafficAlert({
|
||||
level: "info",
|
||||
action: "EXTERNAL_CREATE_BOUNTY_SUCCESS",
|
||||
surface: "mcp/create_bounty",
|
||||
actorType: "AGENT",
|
||||
actorId: agent.agent_id,
|
||||
taskId: newTask.id,
|
||||
message: `A2A 雇主發包!Agent 創建了懸賞: ${newTask.id}`,
|
||||
metadata: {
|
||||
reward: parsed.reward_amount,
|
||||
payload_summary: summarizeRequestPayload(tool, body),
|
||||
}
|
||||
});
|
||||
|
||||
// broadcast fomo
|
||||
const formatted = newTask.reward_currency === "USD"
|
||||
? `$${(newTask.reward_amount / 100).toFixed(0)}`
|
||||
: `NT$${newTask.reward_amount}`;
|
||||
void broadcastFomoEvent({
|
||||
type: "NEW_BOUNTY_CREATED",
|
||||
taskId: newTask.id,
|
||||
amountFormatted: formatted,
|
||||
title: newTask.title
|
||||
});
|
||||
|
||||
return NextResponse.json({
|
||||
task_id: newTask.id,
|
||||
status: newTask.status,
|
||||
message: "Bounty created successfully and broadcasted to A2A network.",
|
||||
});
|
||||
}
|
||||
|
||||
default:
|
||||
void sendTrafficAlert({
|
||||
level: "warning",
|
||||
|
||||
42
apps/web/src/app/api/mcp/agent_card/route.ts
Normal file
42
apps/web/src/app/api/mcp/agent_card/route.ts
Normal file
@@ -0,0 +1,42 @@
|
||||
import { NextResponse } from "next/server";
|
||||
import { PrismaClient } from "@prisma/client";
|
||||
import { RegisterAgentCardRequestSchema, RegisterAgentCardResponseSchema } from "@agent-bounty/contracts";
|
||||
|
||||
const prisma = new PrismaClient();
|
||||
|
||||
export async function POST(req: Request) {
|
||||
try {
|
||||
const json = await req.json();
|
||||
|
||||
// Validate request using 2026 Agent Card standard
|
||||
const parsed = RegisterAgentCardRequestSchema.safeParse(json);
|
||||
if (!parsed.success) {
|
||||
return NextResponse.json({ error: "Invalid Agent Card schema", details: parsed.error }, { status: 400 });
|
||||
}
|
||||
|
||||
const card = parsed.data.card;
|
||||
|
||||
// Upsert the Agent Profile with their capabilities and wallet
|
||||
await prisma.agentProfile.upsert({
|
||||
where: { agent_id: card.agent_id },
|
||||
update: {
|
||||
capabilities: JSON.parse(JSON.stringify(card)),
|
||||
x402_wallet_address: card.x402_wallet_address,
|
||||
},
|
||||
create: {
|
||||
agent_id: card.agent_id,
|
||||
capabilities: JSON.parse(JSON.stringify(card)),
|
||||
x402_wallet_address: card.x402_wallet_address,
|
||||
}
|
||||
});
|
||||
|
||||
return NextResponse.json(
|
||||
{ status: "SUCCESS", message: `Agent Card registered for ${card.name}` } as const,
|
||||
{ status: 200 }
|
||||
);
|
||||
|
||||
} catch (err: any) {
|
||||
console.error("[AgentCard] Registration Error:", err);
|
||||
return NextResponse.json({ error: "Internal Server Error" }, { status: 500 });
|
||||
}
|
||||
}
|
||||
43
apps/web/src/lib/a2a-broadcasters/farcaster.ts
Normal file
43
apps/web/src/lib/a2a-broadcasters/farcaster.ts
Normal file
@@ -0,0 +1,43 @@
|
||||
import { Task } from "@prisma/client";
|
||||
|
||||
/**
|
||||
* Mocks broadcasting a Task to Farcaster network via an Agent profile.
|
||||
* Agents on Farcaster can listen to specific frames or channels (e.g. /vibework).
|
||||
*/
|
||||
export async function broadcastViaFarcaster(task: Task): Promise<void> {
|
||||
console.log(`[Farcaster Broadcaster] Preparing to cast Task ${task.id}...`);
|
||||
|
||||
try {
|
||||
const farcasterFid = "fid:888888"; // Simulated VibeWork Farcaster Bot ID
|
||||
console.log(`[Farcaster Broadcaster] Authenticating as ${farcasterFid}...`);
|
||||
|
||||
// Construct the cast payload with Agent-parseable Frame metadata
|
||||
const payload = {
|
||||
text: `🚀 A2A Bounty Alert: ${task.title}\n\nReward: ${task.reward_amount / 100} ${task.reward_currency}\nStack: ${(task.required_stack as string[])?.join(', ') || 'N/A'}\n\n🤖 Agents: Read the frame metadata below to claim this task natively.`,
|
||||
channel_id: "vibework",
|
||||
embeds: [
|
||||
{
|
||||
url: `https://api.vibework.com/bounties/${task.id}`,
|
||||
}
|
||||
],
|
||||
frame_metadata: {
|
||||
"fc:frame": "vNext",
|
||||
"fc:frame:image": `https://api.vibework.com/og/${task.id}.png`,
|
||||
"fc:frame:button:1": "Claim via A2A",
|
||||
"fc:frame:post_url": `https://api.vibework.com/mcp/claim_task?taskId=${task.id}`,
|
||||
"vibework:a2a:protocol": "v1",
|
||||
"vibework:a2a:bounty_id": task.id,
|
||||
}
|
||||
};
|
||||
|
||||
console.log(`[Farcaster Broadcaster] ➡️ Casting to /vibework...`);
|
||||
console.log(`[Farcaster Broadcaster] Cast Payload: \n${JSON.stringify(payload, null, 2)}`);
|
||||
|
||||
// Simulate network delay
|
||||
await new Promise(resolve => setTimeout(resolve, 500));
|
||||
|
||||
console.log(`[Farcaster Broadcaster] ✅ Cast published successfully! Cast Hash: 0x${Math.random().toString(16).slice(2, 10)}`);
|
||||
} catch (error) {
|
||||
console.error(`[Farcaster Broadcaster] ❌ Failed to cast:`, error);
|
||||
}
|
||||
}
|
||||
44
apps/web/src/lib/a2a-broadcasters/matrix.ts
Normal file
44
apps/web/src/lib/a2a-broadcasters/matrix.ts
Normal file
@@ -0,0 +1,44 @@
|
||||
import { Task } from "@prisma/client";
|
||||
|
||||
/**
|
||||
* Mocks broadcasting a Task to the Matrix federated network.
|
||||
* Matrix allows secure, E2EE, decentralized JSON messaging for AI agents.
|
||||
*/
|
||||
export async function broadcastViaMatrix(task: Task): Promise<void> {
|
||||
console.log(`[Matrix Broadcaster] Preparing to broadcast Task ${task.id}...`);
|
||||
|
||||
try {
|
||||
const matrixServer = "matrix.vibework.network";
|
||||
const targetRoomId = "!a2a_bounties:matrix.org";
|
||||
|
||||
console.log(`[Matrix Broadcaster] Connecting to home server ${matrixServer}...`);
|
||||
console.log(`[Matrix Broadcaster] Joining room ${targetRoomId}...`);
|
||||
|
||||
// Construct the Matrix custom event payload
|
||||
const payload = {
|
||||
msgtype: "m.custom.agent_bounty",
|
||||
body: `A2A Bounty: ${task.title}`,
|
||||
bounty_data: {
|
||||
id: task.id,
|
||||
title: task.title,
|
||||
description: task.description,
|
||||
reward_amount: task.reward_amount,
|
||||
reward_currency: task.reward_currency,
|
||||
required_stack: task.required_stack,
|
||||
mcp_endpoint: "https://api.vibework.com/mcp",
|
||||
},
|
||||
format: "org.matrix.custom.html",
|
||||
formatted_body: `<strong>A2A Bounty:</strong> ${task.title} <br/> <em>Reward:</em> ${task.reward_amount / 100} ${task.reward_currency}`
|
||||
};
|
||||
|
||||
console.log(`[Matrix Broadcaster] ➡️ Sending E2EE state event to room...`);
|
||||
console.log(`[Matrix Broadcaster] Payload: \n${JSON.stringify(payload, null, 2)}`);
|
||||
|
||||
// Simulate network delay
|
||||
await new Promise(resolve => setTimeout(resolve, 300));
|
||||
|
||||
console.log(`[Matrix Broadcaster] ✅ Matrix Event synced to federated nodes! Event ID: $${Math.random().toString(36).slice(2)}`);
|
||||
} catch (error) {
|
||||
console.error(`[Matrix Broadcaster] ❌ Failed to broadcast to Matrix:`, error);
|
||||
}
|
||||
}
|
||||
50
apps/web/src/lib/a2a-broadcasters/nostr.ts
Normal file
50
apps/web/src/lib/a2a-broadcasters/nostr.ts
Normal file
@@ -0,0 +1,50 @@
|
||||
import { Relay, generateSecretKey, getPublicKey, finalizeEvent } from 'nostr-tools';
|
||||
|
||||
/**
|
||||
* Nostr A2A Broadcaster
|
||||
* Pushes kind 1 JSON events to Damus relay for Agents listening to #VibeWork_Bounty
|
||||
*/
|
||||
export async function broadcastViaNostr(task: any) {
|
||||
console.log(`[Nostr Broadcaster] Preparing to broadcast Task ${task.id}...`);
|
||||
|
||||
try {
|
||||
// Generate a fresh transient key for this broadcast (in prod, we would use a static VibeWork key)
|
||||
const sk = generateSecretKey();
|
||||
const pk = getPublicKey(sk);
|
||||
console.log(`[Nostr Broadcaster] Transient PubKey generated: ${pk}`);
|
||||
|
||||
const payload = JSON.stringify({
|
||||
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 event = finalizeEvent({
|
||||
kind: 1,
|
||||
created_at: Math.floor(Date.now() / 1000),
|
||||
tags: [
|
||||
['t', 'VibeWork_Bounty'],
|
||||
['t', 'A2A']
|
||||
],
|
||||
content: payload,
|
||||
}, sk);
|
||||
|
||||
console.log(`[Nostr Broadcaster] Connecting to wss://relay.damus.io...`);
|
||||
const relay = await Relay.connect('wss://relay.damus.io');
|
||||
|
||||
console.log(`[Nostr Broadcaster] Connected! Publishing event...`);
|
||||
await relay.publish(event);
|
||||
console.log(`[Nostr Broadcaster] ✅ Event published to Nostr network! ID: ${event.id}`);
|
||||
|
||||
relay.close();
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error("[Nostr Broadcaster] Failed:", error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
34
apps/web/src/lib/a2a-broadcasters/waku.ts
Normal file
34
apps/web/src/lib/a2a-broadcasters/waku.ts
Normal file
@@ -0,0 +1,34 @@
|
||||
import { Task } from "@prisma/client";
|
||||
|
||||
/**
|
||||
* 2026 Standard: Simulated Waku GossipSub Broadcaster
|
||||
* Waku is a decentralized, censorship-resistant, privacy-preserving communication network.
|
||||
* By broadcasting intents to Waku, we allow nomad agents to discover bounties without central servers.
|
||||
*/
|
||||
export async function broadcastViaWaku(task: Task) {
|
||||
try {
|
||||
const payload = {
|
||||
protocol: "vibework:a2a:waku-gossip:v1",
|
||||
topic: "/vibework/bounties/1/proto",
|
||||
intent: {
|
||||
task_id: task.id,
|
||||
title: task.title,
|
||||
reward_amount: task.reward_amount,
|
||||
reward_currency: task.reward_currency,
|
||||
required_stack: task.required_stack,
|
||||
expires_at: task.expires_at,
|
||||
},
|
||||
timestamp: new Date().toISOString(),
|
||||
};
|
||||
|
||||
console.log(`[Waku Broadcaster] Gossiping intent ${task.id} to P2P network...`);
|
||||
// Simulated network delay
|
||||
await new Promise((resolve) => setTimeout(resolve, 800));
|
||||
|
||||
console.log(`[Waku Broadcaster] Success! Gossip propagated to peers.`);
|
||||
return { status: "success", topic: payload.topic };
|
||||
} catch (err: any) {
|
||||
console.error(`[Waku Broadcaster] Failed to gossip:`, err.message);
|
||||
return { status: "error", error: err.message };
|
||||
}
|
||||
}
|
||||
54
apps/web/src/lib/a2a-broadcasters/webhook.ts
Normal file
54
apps/web/src/lib/a2a-broadcasters/webhook.ts
Normal file
@@ -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;
|
||||
}
|
||||
}
|
||||
56
apps/web/src/lib/a2a-broadcasters/xmtp.ts
Normal file
56
apps/web/src/lib/a2a-broadcasters/xmtp.ts
Normal file
@@ -0,0 +1,56 @@
|
||||
import { Client } from "@xmtp/xmtp-js";
|
||||
import { Wallet } from "ethers";
|
||||
|
||||
/**
|
||||
* XMTP (Web3) A2A Broadcaster
|
||||
* Pings known Agent Wallets directly with task JSON.
|
||||
*/
|
||||
export async function broadcastViaXMTP(task: any) {
|
||||
console.log(`[XMTP Broadcaster] Preparing to broadcast Task ${task.id}...`);
|
||||
|
||||
try {
|
||||
// For MVP, we use a random wallet to simulate the VibeWork Dispatcher.
|
||||
// In production, this would be a funded EVM wallet loaded via process.env.XMTP_PRIVATE_KEY
|
||||
const wallet = Wallet.createRandom();
|
||||
console.log(`[XMTP Broadcaster] Simulator Wallet Generated: ${wallet.address}`);
|
||||
|
||||
// Create XMTP client (simulated connection)
|
||||
// We don't actually connect to the production network in this simulated run
|
||||
// to avoid polluting the mainnet/devnet with random wallets.
|
||||
|
||||
// const xmtp = await Client.create(wallet, { env: "dev" });
|
||||
|
||||
// Target wallets (e.g., prominent open-source agents or human scouts)
|
||||
const targetAgentWallets = [
|
||||
"0xSimulatedAgentWallet1111111111111111111",
|
||||
"0xSimulatedAgentWallet2222222222222222222"
|
||||
];
|
||||
|
||||
const payload = JSON.stringify({
|
||||
protocol: "VibeWork_A2A_Bounty",
|
||||
version: "1.0",
|
||||
bounty: {
|
||||
id: task.id,
|
||||
title: task.title,
|
||||
reward: task.reward_amount,
|
||||
currency: task.reward_currency,
|
||||
endpoint: "https://api.vibework.com/mcp"
|
||||
}
|
||||
}, null, 2);
|
||||
|
||||
for (const address of targetAgentWallets) {
|
||||
console.log(`[XMTP Broadcaster] ➡️ Sending encrypted DM to ${address}`);
|
||||
console.log(`[XMTP Broadcaster] Payload: \n${payload}`);
|
||||
|
||||
// Real implementation:
|
||||
// const conversation = await xmtp.conversations.newConversation(address);
|
||||
// await conversation.send(payload);
|
||||
}
|
||||
|
||||
console.log(`[XMTP Broadcaster] ✅ Broadcast complete.`);
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error("[XMTP Broadcaster] Failed:", error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user