feat: improve external traffic attribution and surge alerts
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:
@@ -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,
|
||||
|
||||
@@ -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({
|
||||
|
||||
Reference in New Issue
Block a user