From 604fb5a8cbef311b053d3ffce2398de3d1a5e157 Mon Sep 17 00:00:00 2001 From: OG T Date: Sun, 7 Jun 2026 16:02:15 +0800 Subject: [PATCH] feat: improve external traffic attribution and surge alerts --- apps/web/src/app/api/mcp/[tool]/route.ts | 70 ++++++++++++- apps/web/src/app/api/open-tasks/route.ts | 125 +++++++++++++++++++---- 2 files changed, 174 insertions(+), 21 deletions(-) diff --git a/apps/web/src/app/api/mcp/[tool]/route.ts b/apps/web/src/app/api/mcp/[tool]/route.ts index b6ee355..46a6d5a 100644 --- a/apps/web/src/app/api/mcp/[tool]/route.ts +++ b/apps/web/src/app/api/mcp/[tool]/route.ts @@ -16,6 +16,40 @@ import { sendTrafficAlert } from "@/lib/traffic-alert"; import crypto from "crypto"; import { z } from "zod"; +const MCP_SURGE_WINDOW_MINUTES = 10; +const MCP_SURGE_INTERVAL = 25; + +const MCP_AGENT_HEADERS = [ + "x-agent-id", + "x-agent-name", + "x-client-id", + "x-request-id", + "x-mcp-agent-id", + "x-openai-agent", +]; + +function normalizeActorId(value: string, fallback: string) { + const normalized = value.trim().toLowerCase().replace(/[^a-z0-9._:-]+/g, "_").replace(/_+/g, "_"); + return normalized.slice(0, 64) || fallback; +} + +function resolveActorFromMcpRequest(request: NextRequest) { + for (const headerName of MCP_AGENT_HEADERS) { + const headerValue = request.headers.get(headerName); + if (headerValue) { + return { + actorType: "AGENT" as const, + actorId: `agent:${normalizeActorId(headerValue, "agent")}`, + }; + } + } + + return { + actorType: "AGENT" as const, + actorId: "mcp-anonymous", + }; +} + export async function POST(request: NextRequest, props: { params: Promise<{ tool: string }> }) { const params = await props.params; const tool = params.tool; @@ -37,6 +71,7 @@ export async function POST(request: NextRequest, props: { params: Promise<{ tool switch (tool) { case "list_open_tasks": { ListOpenTasksRequestSchema.parse(body); + const actor = resolveActorFromMcpRequest(request); const tasks = await prisma.task.findMany({ where: { status: TaskStatus.OPEN }, @@ -62,16 +97,47 @@ export async function POST(request: NextRequest, props: { params: Promise<{ tool level: "info", action: "EXTERNAL_LIST_OPEN_TASKS_MCP", surface: "mcp/list_open_tasks", - actorType: "USER", - actorId: "mcp-anonymous", + actorType: actor.actorType, + actorId: actor.actorId, taskId: "open-tasks", message: "外部 MCP 查詢任務列表", metadata: { count: formattedTasks.length, source_tool: tool, + user_agent: request.headers.get("user-agent") ?? "unknown", + source_ip: + request.headers.get("x-forwarded-for")?.split(",")[0]?.trim() ?? + request.headers.get("x-real-ip") ?? + "unknown", }, }); + void prisma.auditEvent.count({ + where: { + createdAt: { + gte: new Date(Date.now() - MCP_SURGE_WINDOW_MINUTES * 60 * 1000), + }, + action: "EXTERNAL_LIST_OPEN_TASKS_MCP", + }, + }).then((eventCount) => { + if (eventCount > 0 && eventCount % MCP_SURGE_INTERVAL === 0) { + void sendTrafficAlert({ + level: "warning", + action: "EXTERNAL_LIST_OPEN_TASKS_SURGE", + surface: "mcp/list_open_tasks", + actorType: "SYSTEM", + actorId: "traffic-monitor", + taskId: "open-tasks", + message: `MCP list_open_tasks spike detected: ${eventCount} hits in ${MCP_SURGE_WINDOW_MINUTES}m`, + metadata: { + alert_window_minutes: MCP_SURGE_WINDOW_MINUTES, + event_count: eventCount, + source_tool: tool, + }, + }); + } + }).catch(() => {}); + return NextResponse.json({ tasks: formattedTasks, total_open: formattedTasks.length, diff --git a/apps/web/src/app/api/open-tasks/route.ts b/apps/web/src/app/api/open-tasks/route.ts index e9cc1fb..0ebefdc 100644 --- a/apps/web/src/app/api/open-tasks/route.ts +++ b/apps/web/src/app/api/open-tasks/route.ts @@ -1,6 +1,7 @@ import { NextResponse } from "next/server"; import { prisma } from "@/lib/prisma"; import { TaskStatus } from "@agent-bounty/contracts"; +import { sendTrafficAlert } from "@/lib/traffic-alert"; export const dynamic = "force-dynamic"; @@ -18,6 +19,69 @@ const getPayoutMode = (task: { return "PAYMENT_PENDING"; }; +const AI_USER_AGENT_HINTS = [ + "gpt", + "chatgpt", + "openai", + "anthropic", + "claude", + "perplexity", + "llm", + "mcp", + "autogpt", + "agent", + "assistant", + "gemini", + "cursor", + "copilot", +]; + +function normalizeActorId(value: string, fallback: string) { + const cleaned = value.trim().toLowerCase().replace(/[^a-z0-9._:-]+/g, "_").replace(/_+/g, "_"); + return cleaned.slice(0, 64) || fallback; +} + +function resolveSourceIp(request: Request) { + return ( + request.headers.get("x-forwarded-for")?.split(",")[0]?.trim() ?? + request.headers.get("x-real-ip") ?? + "unknown" + ); +} + +function isLikelyAIAgentUserAgent(userAgent: string) { + const normalized = userAgent.toLowerCase(); + return AI_USER_AGENT_HINTS.some((token) => normalized.includes(token)); +} + +function resolveExternalActor(request: Request) { + const headerActorId = + request.headers.get("x-agent-id") ?? + request.headers.get("x-agent-name") ?? + request.headers.get("x-ai-agent-id") ?? + request.headers.get("x-ai-id"); + + if (headerActorId) { + return { + actorType: "AGENT" as const, + actorId: `agent:${normalizeActorId(headerActorId, "agent-client")}`, + }; + } + + const userAgent = request.headers.get("user-agent") ?? "unknown"; + if (isLikelyAIAgentUserAgent(userAgent)) { + return { + actorType: "AGENT" as const, + actorId: `agent:${normalizeActorId(userAgent, "agent")}`, + }; + } + + return { + actorType: "USER" as const, + actorId: `open-tasks:${resolveSourceIp(request)}`, + }; +} + export async function GET(request: Request) { const tasks = await prisma.task.findMany({ where: { status: TaskStatus.OPEN }, @@ -56,27 +120,50 @@ export async function GET(request: Request) { task_url: `https://agent.wooo.work/tasks/${task.id}`, })); - const sourceIp = - request.headers.get("x-forwarded-for")?.split(",")[0]?.trim() ?? - request.headers.get("x-real-ip") ?? - "unknown"; + const actor = resolveExternalActor(request); - void prisma.auditEvent.create({ - data: { - actorType: "USER", - actorId: `open-tasks:${sourceIp}`, - action: "EXTERNAL_LIST_OPEN_TASKS", - entityType: "TASK", - entityId: "open-tasks", - afterState: { - total_open: publicPayload.length, - }, - reason: "external-discovery", - metadata: { - source: "public-open-tasks", - user_agent: request.headers.get("user-agent") ?? "unknown", - }, + void sendTrafficAlert({ + level: "info", + action: "EXTERNAL_LIST_OPEN_TASKS", + surface: "public-open-tasks", + actorType: actor.actorType, + actorId: actor.actorId, + taskId: "open-tasks", + message: `External discovery call for open tasks (${publicPayload.length} items)`, + metadata: { + source: "public-open-tasks", + task_count: publicPayload.length, + source_ip: resolveSourceIp(request), + user_agent: request.headers.get("user-agent") ?? "unknown", }, + }); + + // Light-weight surge signal: when this endpoint is hit in large bursts, + // emit a warning once every 25 requests in a 10-minute window. + const surgeWindow = 10; + const surgeWindowStart = new Date(Date.now() - surgeWindow * 60 * 1000); + void prisma.auditEvent.count({ + where: { + createdAt: { gte: surgeWindowStart }, + action: "EXTERNAL_LIST_OPEN_TASKS", + }, + }).then((eventCount) => { + if (eventCount > 0 && eventCount % 25 === 0) { + void sendTrafficAlert({ + level: "warning", + action: "EXTERNAL_LIST_OPEN_TASKS_SURGE", + surface: "public-open-tasks", + actorType: "SYSTEM", + actorId: "traffic-monitor", + taskId: "open-tasks", + message: `Open tasks discovery surge detected: ${eventCount} hits in ${surgeWindow}m`, + metadata: { + alert_window_minutes: surgeWindow, + event_count: eventCount, + surface: "public-open-tasks", + }, + }); + } }).catch(() => {}); return NextResponse.json({