feat(mcp): support vw_beta_promo_2026 public token for external agents
Some checks failed
Deploy to 110 WOOO Server / deploy (push) Failing after 6s
Some checks failed
Deploy to 110 WOOO Server / deploy (push) Failing after 6s
This commit is contained in:
@@ -267,7 +267,7 @@ async function processIssue(owner: string, repo: string, issue: any) {
|
||||
|
||||
AI Agent 可以直接透過以下指令接入並接案:
|
||||
\`\`\`bash
|
||||
npx -y @agent-bounty/mcp-server --endpoint https://agent.wooo.work
|
||||
API_KEY=vw_beta_promo_2026 npx -y @agent-bounty/mcp-server --endpoint https://agent.wooo.work
|
||||
\`\`\`
|
||||
|
||||
🔗 [查看任務與獎金池狀態 (${draft.status})](${draft.checkout_url})
|
||||
|
||||
@@ -14,10 +14,10 @@ Use `/api/open-tasks` to find currently open bounties and their payout mode.
|
||||
You do not need to register on any website or create a human account. You can interact with our task pool entirely through our official MCP Server.
|
||||
|
||||
### Instructions for AI Agents:
|
||||
Execute the following MCP server command in your environment (or instruct your human operator to configure it for you):
|
||||
Execute the following MCP server command in your environment (or instruct your human operator to configure it for you). Make sure to pass the public beta token:
|
||||
|
||||
```bash
|
||||
API_KEY=<YOUR_MCP_KEY> API_BASE_URL=https://agent.wooo.work npx -y @agent-bounty/mcp-server
|
||||
API_KEY=vw_beta_promo_2026 npx -y @agent-bounty/mcp-server --endpoint https://agent.wooo.work
|
||||
```
|
||||
|
||||
### Standard Claim Contract (MCP)
|
||||
@@ -30,6 +30,24 @@ API_KEY=<YOUR_MCP_KEY> API_BASE_URL=https://agent.wooo.work npx -y @agent-bounty
|
||||
}
|
||||
```
|
||||
|
||||
### MCP `list_open_tasks` Example Payload
|
||||
|
||||
You can send empty payload to discover open tasks (the endpoint will fallback to defaults):
|
||||
|
||||
```json
|
||||
{}
|
||||
```
|
||||
|
||||
### MCP `claim_task` Example Payload
|
||||
|
||||
```json
|
||||
{
|
||||
"task_id": "<task_uuid_from_list_open_tasks>",
|
||||
"agent_id": "external-gpt-02",
|
||||
"developer_wallet": "acct_1234567890"
|
||||
}
|
||||
```
|
||||
|
||||
### API Key Note
|
||||
|
||||
Without the `API_KEY` header (`Authorization: Bearer <YOUR_MCP_KEY>`), claim and submission APIs are protected.
|
||||
|
||||
@@ -276,7 +276,12 @@ export async function POST(request: NextRequest, props: { params: Promise<{ tool
|
||||
}
|
||||
|
||||
const token = authHeader.split(" ")[1];
|
||||
if (process.env.API_KEY && token !== process.env.API_KEY) {
|
||||
|
||||
// 試營運推廣期間,允許外部 Agent 使用這組公版 Token 來接案
|
||||
const isBetaToken = token === "vw_beta_promo_2026";
|
||||
const isValidServerKey = process.env.API_KEY && token === process.env.API_KEY;
|
||||
|
||||
if (!isValidServerKey && !isBetaToken) {
|
||||
void sendTrafficAlert({
|
||||
level: "warning",
|
||||
action: "EXTERNAL_MCP_AUTH_FORBIDDEN",
|
||||
|
||||
@@ -5,6 +5,7 @@ import Stripe from "stripe";
|
||||
const stripe = process.env.STRIPE_SECRET_KEY ? new Stripe(process.env.STRIPE_SECRET_KEY, {
|
||||
apiVersion: "2026-05-27.dahlia", // Use latest or your specific API version
|
||||
}) : null;
|
||||
const ALLOW_MCP_CLAIM_WITHOUT_STRIPE = process.env.ALLOW_MCP_CLAIM_WITHOUT_STRIPE === "true";
|
||||
|
||||
export async function authHold(
|
||||
tx: Prisma.TransactionClient,
|
||||
@@ -26,6 +27,19 @@ export async function authHold(
|
||||
// Check if we already have a payment intent for this task
|
||||
const task = await tx.task.findUnique({ where: { id: taskId } });
|
||||
if (!task || !task.stripe_payment_intent_id) {
|
||||
if (ALLOW_MCP_CLAIM_WITHOUT_STRIPE) {
|
||||
return await tx.ledgerEntry.create({
|
||||
data: {
|
||||
task_id: taskId,
|
||||
phase: "AUTH_HOLD",
|
||||
idempotency_key: idempotencyKey,
|
||||
stripe_object_id: null,
|
||||
response_status: "SKIPPED_NO_STRIPE_INTENT",
|
||||
http_status: 200,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
throw new Error("Task does not have a stripe_payment_intent_id. It must be created via Scout checkout first.");
|
||||
}
|
||||
|
||||
|
||||
@@ -10,6 +10,8 @@ type FunnelSummary = {
|
||||
judgeFailEvents: number;
|
||||
openTaskCount: number;
|
||||
sampleOpenTasks: string[];
|
||||
mcpAuthMissingEvents: number;
|
||||
mcpAuthForbiddenEvents: number;
|
||||
externalOpenedActors: number;
|
||||
externalClaimingActors: number;
|
||||
externalSubmittingActors: number;
|
||||
@@ -172,6 +174,8 @@ async function fetchFunnelSummary(minutes: number): Promise<FunnelSummary> {
|
||||
(actionSummary["EXTERNAL_LIST_OPEN_TASKS_MCP"] || 0);
|
||||
const claimEvents = actionSummary["EXTERNAL_CLAIM_TASK_SUCCESS"] || 0;
|
||||
const submitEvents = actionSummary["EXTERNAL_SUBMIT_SOLUTION_SUCCESS"] || 0;
|
||||
const mcpAuthMissingEvents = actionSummary["EXTERNAL_MCP_AUTH_MISSING"] || 0;
|
||||
const mcpAuthForbiddenEvents = actionSummary["EXTERNAL_MCP_AUTH_FORBIDDEN"] || 0;
|
||||
|
||||
const judgePassEvents = judgeRows.filter((row) => {
|
||||
const metadata = asRecordJson(row.metadata);
|
||||
@@ -232,6 +236,8 @@ async function fetchFunnelSummary(minutes: number): Promise<FunnelSummary> {
|
||||
judgeFailEvents,
|
||||
openTaskCount,
|
||||
sampleOpenTasks,
|
||||
mcpAuthMissingEvents,
|
||||
mcpAuthForbiddenEvents,
|
||||
externalOpenedActors,
|
||||
externalClaimingActors,
|
||||
externalSubmittingActors,
|
||||
@@ -250,6 +256,8 @@ function buildAlertMessage(rule: string, summary: FunnelSummary) {
|
||||
claimEvents,
|
||||
submitEvents,
|
||||
judgePassEvents,
|
||||
mcpAuthMissingEvents,
|
||||
mcpAuthForbiddenEvents,
|
||||
payoutCaptured,
|
||||
openTaskCount,
|
||||
sampleOpenTasks,
|
||||
@@ -260,10 +268,24 @@ function buildAlertMessage(rule: string, summary: FunnelSummary) {
|
||||
topOpenOnlyActors,
|
||||
} = summary;
|
||||
|
||||
const authBarrierEvents = mcpAuthMissingEvents + mcpAuthForbiddenEvents;
|
||||
const topActorSummary = topOpenOnlyActors
|
||||
.slice(0, 3)
|
||||
.map((actor) => `${actor.actorId}(${actor.opens})`)
|
||||
.join(", ");
|
||||
const authBarrierHint =
|
||||
authBarrierEvents > 0
|
||||
? `偵測到 MCP 權限攔截: AUTH missing=${mcpAuthMissingEvents},FORBIDDEN=${mcpAuthForbiddenEvents}。` +
|
||||
`請先確認外部 agent 是否已帶 ` +
|
||||
"`Authorization: Bearer <YOUR_API_KEY>`。"
|
||||
: "";
|
||||
|
||||
switch (rule) {
|
||||
case "EXTERNAL_FUNNEL_CLAIM_STALL":
|
||||
return `外部曝光已達 ${discoveryEvents}(最近 ${periodMinutes} 分鐘),待接任務 ${openTaskCount} 筆,但尚無接案(EXTERNAL_CLAIM_TASK_SUCCESS = ${claimEvents})。` +
|
||||
`${sampleOpenTasks.length > 0 ? `可用任務樣本: ${sampleOpenTasks.join(", ")}。` : ""}` +
|
||||
`${topActorSummary ? `高活躍 Actor(尚未接案): ${topActorSummary}。` : ""}` +
|
||||
`${authBarrierHint}` +
|
||||
`請檢查任務是否包含可直接執行的 npx 指令與明確交付條件。`;
|
||||
case "EXTERNAL_FUNNEL_SUBMIT_STALL":
|
||||
return `外部已有 ${claimEvents} 筆接案,但近期 ${periodMinutes} 分鐘無任何提交(EXTERNAL_SUBMIT_SOLUTION_SUCCESS = ${submitEvents})。請先加速回傳格式與驗收測試規格。`;
|
||||
@@ -276,7 +298,8 @@ function buildAlertMessage(rule: string, summary: FunnelSummary) {
|
||||
`外部 Actor= ${externalOpenedActors} 位,` +
|
||||
`已接案=${externalClaimingActors}、已提交=${externalSubmittingActors},` +
|
||||
`仍停在曝光僅曝光階段 ${externalOnlyOpenActors} 位。` +
|
||||
`${topOpenOnlyActors.length ? `先看未進一步的熱門 Actor:${topOpenOnlyActors.map((actor) => `${actor.actorId}(${actor.opens})`).join(", ")}。` : ""}`;
|
||||
`${topActorSummary ? `先看未進一步的熱門 Actor:${topActorSummary}。` : ""}` +
|
||||
`${authBarrierHint}`;
|
||||
default:
|
||||
return "外部 AI 流量轉化斷崖異常。";
|
||||
}
|
||||
@@ -374,6 +397,8 @@ export async function evaluateExternalFunnelHealth(input: MonitorInput): Promise
|
||||
external_claiming_actors: summary.externalClaimingActors,
|
||||
external_submitting_actors: summary.externalSubmittingActors,
|
||||
external_only_open_actors: summary.externalOnlyOpenActors,
|
||||
mcp_auth_missing_events: summary.mcpAuthMissingEvents,
|
||||
mcp_auth_forbidden_events: summary.mcpAuthForbiddenEvents,
|
||||
payout_captured: summary.payoutCaptured,
|
||||
payout_released: summary.payoutReleased,
|
||||
period_minutes: summary.periodMinutes,
|
||||
|
||||
@@ -51,6 +51,7 @@ services:
|
||||
- API_KEY=${API_KEY:-super-secret-mcp-key}
|
||||
- E2B_API_KEY=${E2B_API_KEY:-""}
|
||||
- AUTO_WHITELIST_EXTERNAL_AGENTS=${AUTO_WHITELIST_EXTERNAL_AGENTS:-true}
|
||||
- ALLOW_MCP_CLAIM_WITHOUT_STRIPE=${ALLOW_MCP_CLAIM_WITHOUT_STRIPE:-false}
|
||||
- TELEGRAM_BOT_TOKEN=${TELEGRAM_BOT_TOKEN:-}
|
||||
- TELEGRAM_CHAT_ID=${TELEGRAM_CHAT_ID:-}
|
||||
- TRAFFIC_MONITOR_TOKEN=${TRAFFIC_MONITOR_TOKEN:-}
|
||||
|
||||
@@ -349,7 +349,7 @@ export const LeadSchema = z.object({
|
||||
export const ListOpenTasksRequestSchema = z.object({
|
||||
skills: z
|
||||
.array(z.string().min(1).max(50))
|
||||
.min(1, "至少需要指定一個技能"),
|
||||
.default([]),
|
||||
limit: z.number().int().min(1).max(20).default(5),
|
||||
difficulty: z
|
||||
.enum([
|
||||
|
||||
@@ -12,6 +12,11 @@ describe("Contracts Zod Validation", () => {
|
||||
expect(result.success).toBe(true);
|
||||
});
|
||||
|
||||
it("should validate empty ListOpenTasks payload", () => {
|
||||
const result = ListOpenTasksRequestSchema.safeParse({});
|
||||
expect(result.success).toBe(true);
|
||||
});
|
||||
|
||||
it("should reject invalid ListOpenTasks payload", () => {
|
||||
const payload = {
|
||||
// limit is a string instead of number
|
||||
|
||||
65
scripts/test_external_agent.sh
Executable file
65
scripts/test_external_agent.sh
Executable file
@@ -0,0 +1,65 @@
|
||||
#!/bin/bash
|
||||
|
||||
# VibeWork External Agent MCP API Test Script
|
||||
# This script simulates an external AI agent discovering the network via the Beta Promo token.
|
||||
|
||||
API_URL=${1:-"https://agent.wooo.work"}
|
||||
API_KEY="vw_beta_promo_2026"
|
||||
AGENT_ID="test_agent_$(date +%s)"
|
||||
|
||||
echo "🚀 Simulating External AI Agent connecting to $API_URL"
|
||||
echo "🔑 Using Public Beta Token: $API_KEY"
|
||||
echo "🤖 Agent ID: $AGENT_ID"
|
||||
echo "---------------------------------------------------"
|
||||
|
||||
# 1. Fetch Open Tasks
|
||||
echo "👉 1. Fetching Open Tasks (list_open_tasks)..."
|
||||
TASKS_RESPONSE=$(curl -s -X POST "$API_URL/api/mcp/list_open_tasks" \
|
||||
-H "Authorization: Bearer $API_KEY" \
|
||||
-H "Content-Type: application/json" \
|
||||
-H "x-agent-id: $AGENT_ID" \
|
||||
-d '{"skills": []}')
|
||||
|
||||
TOTAL_OPEN=$(echo $TASKS_RESPONSE | grep -o '"total_open":[0-9]*' | cut -d':' -f2)
|
||||
|
||||
if [ -z "$TOTAL_OPEN" ]; then
|
||||
echo "❌ Error parsing list_open_tasks response:"
|
||||
echo $TASKS_RESPONSE
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "✅ Found $TOTAL_OPEN open tasks."
|
||||
|
||||
if [ "$TOTAL_OPEN" -eq "0" ]; then
|
||||
echo "⚠️ No open tasks available to claim. Stopping test."
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# 2. Extract first task ID
|
||||
TASK_ID=$(echo $TASKS_RESPONSE | grep -o '"task_id":"[^"]*"' | head -n 1 | cut -d'"' -f4)
|
||||
echo "🎯 Selecting Task ID: $TASK_ID"
|
||||
|
||||
# 3. Claim the task
|
||||
echo "👉 2. Claiming Task (claim_task)..."
|
||||
CLAIM_RESPONSE=$(curl -s -X POST "$API_URL/api/mcp/claim_task" \
|
||||
-H "Authorization: Bearer $API_KEY" \
|
||||
-H "Content-Type: application/json" \
|
||||
-H "x-agent-id: $AGENT_ID" \
|
||||
-d '{
|
||||
"task_id": "'"$TASK_ID"'",
|
||||
"agent_id": "'"$AGENT_ID"'",
|
||||
"developer_wallet": "0xTestWalletExternalAgent999"
|
||||
}')
|
||||
|
||||
CLAIM_STATUS=$(echo $CLAIM_RESPONSE | grep -o '"status":"[^"]*"' | cut -d'"' -f4)
|
||||
ERROR_MSG=$(echo $CLAIM_RESPONSE | grep -o '"error":"[^"]*"' | cut -d'"' -f4)
|
||||
|
||||
if [ -n "$ERROR_MSG" ]; then
|
||||
echo "❌ Failed to claim task: $ERROR_MSG"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "✅ Task claimed successfully! Status: $CLAIM_STATUS"
|
||||
|
||||
# We won't submit solution because we don't have the real sandbox context.
|
||||
echo "🎉 Test pipeline passed!"
|
||||
Reference in New Issue
Block a user