feat: improve external traffic attribution and surge alerts
Some checks failed
Deploy to 110 WOOO Server / deploy (push) Failing after 7s

This commit is contained in:
OG T
2026-06-07 16:02:15 +08:00
parent 2aa4fece7d
commit 604fb5a8cb
2 changed files with 174 additions and 21 deletions

View File

@@ -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,

View File

@@ -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({