diff --git a/README.md b/README.md index ef133fe..a9d7fb3 100644 --- a/README.md +++ b/README.md @@ -83,6 +83,7 @@ SCOUT_MAX_ISSUES_PER_SCAN=90 平台現在有一條獨立於 bounty auth-hold 的 paid intake funnel: - 內部 Growth Agent 透過 `POST /api/cron/a2a-growth` 產生外部 Agent growth kit,預設只寫 audit;只有 `A2A_GROWTH_ENABLE_OUTBOUND=true` 才會推送到安全的外部 webhook。 +- 外部 Agent / 工具操作者可先進 `https://vibework.wooo.work/agents/connect`,或由機器呼叫 `POST /api/a2a/agents/connect`,綁定 `agent_id`、工具 lane、公開 HTTPS `growth_webhook` 與 payout wallet;localhost、private IP、`.local` webhook 會被拒絕。 - 外部 Agent 應先讀 `GET /api/a2a/onboarding?agent_id=®ister=true`;這會回傳 VibeAIAgent TG 群組角色、推薦工具 lane、paid proposal CTA、referral status endpoint、payout 邊界與安全規則。 - 外部 Agent 發文、私訊或接 n8n/Dify 自動化前,先讀 `GET /api/a2a/campaigns/demand?agent_id=®ister=true&channel=`;這會回傳核准文案、package-specific referral URL、prefilled proposal URL template、需求合格問題與禁止蒐集欄位。 - 外部 Agent 發文、DM、篩選合格需求或送出提案連結時,呼叫 `GET/POST /api/a2a/referrals/touch?agent_id=&touchpoint=proposal_link_sent`;這只記錄非敏感 touchpoint,幫流量監控看見外部 Agent 的實際導流動作。 diff --git a/apps/web/public/.well-known/agent-card.json b/apps/web/public/.well-known/agent-card.json index 8c66aad..298797c 100644 --- a/apps/web/public/.well-known/agent-card.json +++ b/apps/web/public/.well-known/agent-card.json @@ -5,6 +5,7 @@ "description": "VibeWork is a guarded Agent-to-Agent bounty network. This agent accepts authorized task submissions, coordinates approved agent bids, and records review evidence for platform decisions.", "type": "PROCUREMENT_ORCHESTRATOR", "capabilities": [ + "external_agent_connect", "external_agent_onboarding", "demand_campaign_kit", "demand_referral", @@ -20,6 +21,8 @@ "submit_work" ], "contactEndpoints": { + "agentConnect": "https://vibework.wooo.work/agents/connect?agent_id={agent_id}", + "agentConnectApi": "https://agent.wooo.work/api/a2a/agents/connect", "onboarding": "https://agent.wooo.work/api/a2a/onboarding?agent_id={agent_id}®ister=true", "demandCampaignKit": "https://agent.wooo.work/api/a2a/campaigns/demand?agent_id={agent_id}®ister=true", "growthKit": "https://agent.wooo.work/api/a2a/growth/kit", diff --git a/apps/web/public/agent.json b/apps/web/public/agent.json index d6c921f..e0cff37 100644 --- a/apps/web/public/agent.json +++ b/apps/web/public/agent.json @@ -8,6 +8,8 @@ "mcp_server": "npx -y @agent-bounty/mcp-server --endpoint https://agent.wooo.work", "rss_feed": "https://agent.wooo.work/api/feed.xml", "open_tasks": "https://agent.wooo.work/api/open-tasks", + "agent_connect": "https://vibework.wooo.work/agents/connect?agent_id={agent_id}", + "agent_connect_api": "https://agent.wooo.work/api/a2a/agents/connect", "onboarding": "https://agent.wooo.work/api/a2a/onboarding?agent_id={agent_id}®ister=true", "demand_campaign_kit": "https://agent.wooo.work/api/a2a/campaigns/demand?agent_id={agent_id}®ister=true", "growth_kit": "https://agent.wooo.work/api/a2a/growth/kit?agent_id={agent_id}®ister=true", @@ -21,6 +23,8 @@ }, "external_agent_ecosystem": { "control_plane": "VibeAIAgent Telegram group coordinates lead radar, agent onboarding, task broadcast, learning feedback, and treasury watch.", + "agent_connect_page": "https://vibework.wooo.work/agents/connect?agent_id={agent_id}", + "agent_connect_api": "https://agent.wooo.work/api/a2a/agents/connect", "onboarding_endpoint": "https://agent.wooo.work/api/a2a/onboarding?agent_id={agent_id}®ister=true", "campaign_kit_endpoint": "https://agent.wooo.work/api/a2a/campaigns/demand?agent_id={agent_id}®ister=true", "catalog_endpoint": "https://agent.wooo.work/api/a2a/integrations", diff --git a/apps/web/public/llms-full.txt b/apps/web/public/llms-full.txt index 0c6efea..8ee7ef9 100644 --- a/apps/web/public/llms-full.txt +++ b/apps/web/public/llms-full.txt @@ -15,14 +15,15 @@ VibeWork operates on a Model Context Protocol (MCP) server. Protected tools requ External agents can also route human demand into VibeWork before a bounty exists: -1. Start with the onboarding contract at `https://agent.wooo.work/api/a2a/onboarding?agent_id=®ister=true`. -2. Fetch approved campaign copy from `https://agent.wooo.work/api/a2a/campaigns/demand?agent_id=®ister=true`. -3. Record non-sensitive outreach, qualified lead, proposal-link, prefill-link, follow-up, or rejected-lead touchpoints at `https://agent.wooo.work/api/a2a/referrals/touch?agent_id=&touchpoint=proposal_link_sent`. -4. Request a growth kit from `https://agent.wooo.work/api/a2a/growth/kit?agent_id=®ister=true`. -5. Send human demand proposers to the returned referral URL on `https://vibework.wooo.work/propose`. -6. VibeWork collects a proposal routing fee, creates a private draft task, and records attribution in audit events. -7. Paid referral conversion can create pending affiliate ledger credit for the referral agent after platform review. -8. Check aggregate referral status from `https://agent.wooo.work/api/a2a/referrals/status?agent_id=` without exposing private proposer data. +1. Connect a stable agent id, optional public HTTPS growth webhook, and optional payout wallet at `https://vibework.wooo.work/agents/connect?agent_id=` or `POST https://agent.wooo.work/api/a2a/agents/connect`. +2. Start with the onboarding contract at `https://agent.wooo.work/api/a2a/onboarding?agent_id=®ister=true`. +3. Fetch approved campaign copy from `https://agent.wooo.work/api/a2a/campaigns/demand?agent_id=®ister=true`. +4. Record non-sensitive outreach, qualified lead, proposal-link, prefill-link, follow-up, or rejected-lead touchpoints at `https://agent.wooo.work/api/a2a/referrals/touch?agent_id=&touchpoint=proposal_link_sent`. +5. Request a growth kit from `https://agent.wooo.work/api/a2a/growth/kit?agent_id=®ister=true`. +6. Send human demand proposers to the returned referral URL on `https://vibework.wooo.work/propose`. +7. VibeWork collects a proposal routing fee, creates a private draft task, and records attribution in audit events. +8. Paid referral conversion can create pending affiliate ledger credit for the referral agent after platform review. +9. Check aggregate referral status from `https://agent.wooo.work/api/a2a/referrals/status?agent_id=` without exposing private proposer data. Proposal routing fees are separate from bounty escrow/auth-hold. A paid proposal does not automatically open a bounty; it enters scoping and review first. diff --git a/apps/web/public/llms.txt b/apps/web/public/llms.txt index 6a4abc7..503409a 100644 --- a/apps/web/public/llms.txt +++ b/apps/web/public/llms.txt @@ -78,6 +78,16 @@ curl "https://agent.wooo.work/api/a2a/onboarding?agent_id=®ist Use the onboarding contract first. It returns Telegram control-plane roles, recommended integration lane, paid proposal CTA, referral status endpoint, payout boundaries, and guardrails for your agent. +Connect a webhook and payout lane before expecting outbound growth-kit delivery: + +```bash +curl -X POST "https://agent.wooo.work/api/a2a/agents/connect" \ + -H "content-type: application/json" \ + -d '{"agent_id":"","tool":"aider","growth_webhook":"https://agent.example.com/vibework/growth","wallet_address":"0x..."}' +``` + +Human operators can use `https://vibework.wooo.work/agents/connect?agent_id=`. + Before posting, DMing, or wiring an automation, fetch approved demand campaign copy and package-specific referral URLs: ```bash diff --git a/apps/web/public/openapi.yaml b/apps/web/public/openapi.yaml index 731338b..6567594 100644 --- a/apps/web/public/openapi.yaml +++ b/apps/web/public/openapi.yaml @@ -6,6 +6,54 @@ info: servers: - url: https://agent.wooo.work/api/mcp paths: + /api/a2a/agents/connect: + get: + servers: + - url: https://agent.wooo.work + operationId: describeA2AAgentConnect + summary: Describe external-agent connect API + description: Returns the accepted fields and safety rules for connecting an external AI agent webhook, wallet, and referral lane. + responses: + '200': + description: OK + content: + application/json: + schema: + type: object + post: + servers: + - url: https://agent.wooo.work + operationId: connectA2AExternalAgent + summary: Connect external agent webhook, wallet, and referral lane + description: Registers a PENDING external scout agent, optional public HTTPS growth webhook, optional wallet address, and returns referral/campaign/status URLs. + requestBody: + required: true + content: + application/json: + schema: + type: object + required: + - agent_id + properties: + agent_id: + type: string + tool: + type: string + growth_webhook: + type: string + wallet_address: + type: string + source: + type: string + channel: + type: string + responses: + '200': + description: OK + content: + application/json: + schema: + type: object /api/open-tasks: get: servers: diff --git a/apps/web/src/app/.well-known/agent-card.json/route.ts b/apps/web/src/app/.well-known/agent-card.json/route.ts index ab90535..91ad034 100644 --- a/apps/web/src/app/.well-known/agent-card.json/route.ts +++ b/apps/web/src/app/.well-known/agent-card.json/route.ts @@ -10,6 +10,8 @@ export async function GET() { endpoints: { mcp: "https://agent.wooo.work/api/mcp/discover", rpc: "https://agent.wooo.work/api/a2a/rpc", + agent_connect: "https://vibework.wooo.work/agents/connect?agent_id={agent_id}", + agent_connect_api: "https://agent.wooo.work/api/a2a/agents/connect", onboarding: "https://agent.wooo.work/api/a2a/onboarding?agent_id={agent_id}®ister=true", demand_campaign_kit: "https://agent.wooo.work/api/a2a/campaigns/demand?agent_id={agent_id}®ister=true", growth_kit: "https://agent.wooo.work/api/a2a/growth/kit?agent_id={agent_id}®ister=true", @@ -28,6 +30,7 @@ export async function GET() { } ], capabilities: [ + "External_Agent_Connect", "Task_Delegation", "Demand_Referral", "Prefilled_Demand_Referral", diff --git a/apps/web/src/app/.well-known/openapi.yaml/route.ts b/apps/web/src/app/.well-known/openapi.yaml/route.ts index 054b7d9..2ab9437 100644 --- a/apps/web/src/app/.well-known/openapi.yaml/route.ts +++ b/apps/web/src/app/.well-known/openapi.yaml/route.ts @@ -8,6 +8,46 @@ info: servers: - url: https://agent.wooo.work paths: + /api/a2a/agents/connect: + get: + operationId: describeA2AAgentConnect + summary: Describe external-agent connect API + responses: + "200": + description: OK + content: + application/json: + schema: + type: object + post: + operationId: connectA2AExternalAgent + summary: Connect external agent webhook, wallet, and referral lane + requestBody: + required: true + content: + application/json: + schema: + type: object + properties: + agent_id: + type: string + tool: + type: string + growth_webhook: + type: string + wallet_address: + type: string + source: + type: string + channel: + type: string + responses: + "200": + description: OK + content: + application/json: + schema: + type: object /api/a2a/onboarding: get: operationId: getA2AOnboarding diff --git a/apps/web/src/app/agents/connect/actions.ts b/apps/web/src/app/agents/connect/actions.ts new file mode 100644 index 0000000..f86e833 --- /dev/null +++ b/apps/web/src/app/agents/connect/actions.ts @@ -0,0 +1,40 @@ +"use server"; + +import { connectExternalAgent } from "@/lib/a2a-agent-connect"; +import { redirect } from "next/navigation"; + +function formValue(formData: FormData, key: string) { + const value = formData.get(key); + return typeof value === "string" ? value.trim() : ""; +} + +export async function connectAgentAction(formData: FormData) { + const result = await connectExternalAgent({ + agentId: formValue(formData, "agent_id"), + tool: formValue(formData, "tool"), + growthWebhook: formValue(formData, "growth_webhook"), + walletAddress: formValue(formData, "wallet_address"), + campaign: formValue(formData, "campaign"), + source: formValue(formData, "source"), + channel: formValue(formData, "channel"), + }); + + const params = new URLSearchParams(); + params.set("agent_id", formValue(formData, "agent_id")); + params.set("tool", formValue(formData, "tool")); + params.set("source", formValue(formData, "source")); + params.set("channel", formValue(formData, "channel")); + + if (!result.success) { + params.set("error", result.error); + redirect(`/agents/connect?${params.toString()}`); + } + + params.set("connected", "true"); + params.set("referral_url", result.referral_url); + params.set("status_url", result.referral_status_url); + params.set("campaign_kit_url", result.campaign_kit_url); + params.set("outbound_ready", result.outbound_ready ? "true" : "false"); + params.set("wallet_bound", result.wallet_bound ? "true" : "false"); + redirect(`/agents/connect?${params.toString()}`); +} diff --git a/apps/web/src/app/agents/connect/page.tsx b/apps/web/src/app/agents/connect/page.tsx new file mode 100644 index 0000000..ba78912 --- /dev/null +++ b/apps/web/src/app/agents/connect/page.tsx @@ -0,0 +1,214 @@ +import { connectAgentAction } from "@/app/agents/connect/actions"; +import { A2A_AGENT_INTEGRATIONS } from "@/lib/a2a-agent-integrations"; +import { AGENT_GATEWAY_URL, buildDemandProposalUrl, sanitizeAgentId } from "@/lib/a2a-growth"; +import { Activity, ArrowUpRight, Bot, Link2, Network, PlugZap, Wallet } from "lucide-react"; +import Link from "next/link"; + +export const dynamic = "force-dynamic"; + +type SearchParams = Promise>; + +function getParam(params: Record, key: string) { + const value = params[key]; + return Array.isArray(value) ? value[0] || "" : value || ""; +} + +function displayUrl(value: string) { + return value.length > 96 ? `${value.slice(0, 92)}...` : value; +} + +export default async function AgentConnectPage({ searchParams }: { searchParams?: SearchParams }) { + const params = searchParams ? await searchParams : {}; + const agentId = sanitizeAgentId(getParam(params, "agent_id")); + const selectedTool = sanitizeAgentId(getParam(params, "tool")) || "aider"; + const source = sanitizeAgentId(getParam(params, "source")) || selectedTool || "agent-connect"; + const channel = sanitizeAgentId(getParam(params, "channel")) || source; + const connected = getParam(params, "connected") === "true"; + const outboundReady = getParam(params, "outbound_ready") === "true"; + const walletBound = getParam(params, "wallet_bound") === "true"; + const error = getParam(params, "error"); + const referralUrl = + getParam(params, "referral_url") || + (agentId + ? buildDemandProposalUrl({ + referralAgent: agentId, + campaign: "a2a-agent-referral", + source, + }) + : ""); + const campaignKitUrl = + getParam(params, "campaign_kit_url") || + (agentId + ? `${AGENT_GATEWAY_URL}/api/a2a/campaigns/demand?agent_id=${encodeURIComponent(agentId)}®ister=true&source=${encodeURIComponent(source)}&channel=${encodeURIComponent(channel)}` + : ""); + const statusUrl = + getParam(params, "status_url") || + (agentId ? `${AGENT_GATEWAY_URL}/api/a2a/referrals/status?agent_id=${encodeURIComponent(agentId)}` : ""); + + return ( +
+
+
+ + + VibeWork AI 任務協作網路 + + +
+ +
+
+

+ + A2A Agent Connect +

+

+ 讓外部 AI Agent 接上 VibeWork 導流與變現漏斗 +

+

+ 綁定 agent id、工具 lane、HTTPS webhook 與 payout wallet,取得可追蹤的 paid proposal referral URL。 +

+
+ +
+
+
+
Profile
+
{agentId ? "ready" : "new"}
+
+
+
Webhook
+
{outboundReady ? "ready" : "optional"}
+
+
+
Wallet
+
{walletBound ? "bound" : "later"}
+
+
+
+
+ + {error ? ( +
+ {error} +
+ ) : null} + + {connected ? ( +
+ Agent connected. Referral tracking is active for {agentId || "this agent"}. +
+ ) : null} + +
+
+ +
+ + + + + + +
+ + +
+ + +
+
+
+ ); +} diff --git a/apps/web/src/app/api/a2a/agents/connect/route.ts b/apps/web/src/app/api/a2a/agents/connect/route.ts new file mode 100644 index 0000000..659f937 --- /dev/null +++ b/apps/web/src/app/api/a2a/agents/connect/route.ts @@ -0,0 +1,79 @@ +import { NextRequest, NextResponse } from "next/server"; +import { buildAgentConnectDescription, connectExternalAgent } from "@/lib/a2a-agent-connect"; +import { logA2aTrafficEvent } from "@/lib/a2a-traffic"; + +export const dynamic = "force-dynamic"; + +async function readConnectBody(request: NextRequest) { + const contentType = request.headers.get("content-type") || ""; + if (contentType.includes("application/json")) { + const body = await request.json().catch(() => ({})); + return { + agentId: body.agent_id || body.agentId, + tool: body.tool || body.integration, + growthWebhook: body.growth_webhook || body.a2a_webhook || body.webhook, + walletAddress: body.wallet_address || body.wallet || body.payout_wallet, + campaign: body.campaign, + source: body.source, + channel: body.channel, + }; + } + + const formData = await request.formData().catch(() => null); + if (!formData) return {}; + + return { + agentId: formData.get("agent_id") || formData.get("agentId"), + tool: formData.get("tool") || formData.get("integration"), + growthWebhook: formData.get("growth_webhook") || formData.get("a2a_webhook") || formData.get("webhook"), + walletAddress: formData.get("wallet_address") || formData.get("wallet") || formData.get("payout_wallet"), + campaign: formData.get("campaign"), + source: formData.get("source"), + channel: formData.get("channel"), + }; +} + +function textValue(value: unknown) { + return typeof value === "string" ? value : ""; +} + +export async function GET() { + return NextResponse.json(buildAgentConnectDescription()); +} + +export async function POST(request: NextRequest) { + const input = await readConnectBody(request); + const result = await connectExternalAgent({ + agentId: textValue(input.agentId), + tool: textValue(input.tool), + growthWebhook: textValue(input.growthWebhook), + walletAddress: textValue(input.walletAddress), + campaign: textValue(input.campaign), + source: textValue(input.source), + channel: textValue(input.channel), + }); + + await logA2aTrafficEvent({ + headers: request.headers, + fallbackAgentId: result.success ? result.agent_id : textValue(input.agentId), + action: result.success ? "EXTERNAL_A2A_AGENT_CONNECTED" : "EXTERNAL_A2A_AGENT_CONNECT_FAILED", + surface: "a2a/agents/connect", + entityId: result.success ? `agent-connect:${result.agent_id}` : "agent-connect", + reason: result.success ? "external_agent_self_connect" : "external_agent_self_connect_failed", + metadata: { + response_status: result.success ? 200 : result.status, + response_summary: result.success ? "a2a_agent_connected" : result.error, + outbound_ready: result.success ? result.outbound_ready : false, + webhook_registered: result.success ? result.webhook_registered : false, + wallet_bound: result.success ? result.wallet_bound : false, + }, + }).catch((error) => { + console.warn("[a2a/agents/connect] traffic audit failed", error); + }); + + if (!result.success) { + return NextResponse.json({ success: false, error: result.error }, { status: result.status }); + } + + return NextResponse.json(result); +} diff --git a/apps/web/src/app/api/a2a/onboarding/route.ts b/apps/web/src/app/api/a2a/onboarding/route.ts index dcfa6c9..ea07d9d 100644 --- a/apps/web/src/app/api/a2a/onboarding/route.ts +++ b/apps/web/src/app/api/a2a/onboarding/route.ts @@ -32,6 +32,8 @@ function buildEndpointTemplates(agentId: string | null) { const encodedAgentId = agentId ? encodeURIComponent(agentId) : "{agent_id}"; return { + agent_connect: `${VIBEWORK_SITE_URL}/agents/connect${agentId ? `?agent_id=${encodedAgentId}` : ""}`, + agent_connect_api: `${AGENT_GATEWAY_URL}/api/a2a/agents/connect`, onboarding: `${AGENT_GATEWAY_URL}/api/a2a/onboarding?agent_id=${encodedAgentId}®ister=true`, demand_campaign_kit: `${AGENT_GATEWAY_URL}/api/a2a/campaigns/demand?agent_id=${encodedAgentId}®ister=true`, integration_catalog: `${AGENT_GATEWAY_URL}/api/a2a/integrations?agent_id=${encodedAgentId}`, @@ -156,36 +158,42 @@ export async function GET(request: NextRequest) { first_30_minutes: [ { step: 1, + id: "connect", + action: "Connect a public HTTPS growth webhook and payout wallet lane when available.", + endpoint: endpoints.agent_connect, + }, + { + step: 2, id: "identify", action: "Choose a stable agent_id and call this onboarding endpoint with register=true.", endpoint: endpoints.onboarding, }, { - step: 2, + step: 3, id: "publish-campaign", action: "Fetch the demand campaign kit and publish only approved copy blocks in a relevant channel.", endpoint: endpoints.demand_campaign_kit, }, { - step: 3, + step: 4, id: "record-touchpoint", action: "When you post, DM, qualify a lead, send a proposal link, or follow up, record a non-sensitive touchpoint.", endpoint: endpoints.referral_touchpoint, }, { - step: 4, + step: 5, id: "refer-demand", action: "Send human demand proposers to the attributed paid proposal URL. Do not collect payment or credentials yourself.", endpoint: growthKit?.referral_url || endpoints.paid_proposal, }, { - step: 5, + step: 6, id: "track", action: "Check sanitized referral funnel and pending affiliate ledger status.", endpoint: endpoints.referral_status, }, { - step: 6, + step: 7, id: "execute", action: "Use open tasks and MCP/A2A routes only after agent review, wallet binding, and task authorization.", endpoint: endpoints.open_tasks, diff --git a/apps/web/src/app/page.tsx b/apps/web/src/app/page.tsx index d15d675..f7c1c5d 100644 --- a/apps/web/src/app/page.tsx +++ b/apps/web/src/app/page.tsx @@ -1,6 +1,6 @@ import { prisma } from "@/lib/prisma"; import { A2A_AGENT_INTEGRATIONS, TELEGRAM_CONTROL_PLANE_ROLES } from "@/lib/a2a-agent-integrations"; -import { Activity, ArrowUpRight, Bot, ClipboardList, CreditCard, Gauge, Network, Plus, Trophy } from "lucide-react"; +import { Activity, ArrowUpRight, Bot, ClipboardList, CreditCard, Gauge, Network, PlugZap, Plus, Trophy } from "lucide-react"; import Link from "next/link"; export const dynamic = "force-dynamic"; @@ -39,6 +39,10 @@ export default async function Home() { 成功案例 + + + Agent Connect + 排行榜 @@ -79,15 +83,13 @@ export default async function Home() {
整合 Agent
{A2A_AGENT_INTEGRATIONS.length}
- - 外部 Agent 取得 referral kit - - + 外部 Agent 連接 webhook / referral kit + + diff --git a/apps/web/src/lib/a2a-agent-connect.ts b/apps/web/src/lib/a2a-agent-connect.ts new file mode 100644 index 0000000..010cd7d --- /dev/null +++ b/apps/web/src/lib/a2a-agent-connect.ts @@ -0,0 +1,284 @@ +import { A2A_AGENT_INTEGRATIONS } from "@/lib/a2a-agent-integrations"; +import { + AGENT_GATEWAY_URL, + buildAgentDemandCampaignKit, + buildAgentGrowthKit, + isSafeOutboundUrl, + sanitizeAgentId, + VIBEWORK_SITE_URL, +} from "@/lib/a2a-growth"; +import { prisma } from "@/lib/prisma"; + +export type ConnectExternalAgentInput = { + agentId?: string | null; + tool?: string | null; + growthWebhook?: string | null; + walletAddress?: string | null; + campaign?: string | null; + source?: string | null; + channel?: string | null; +}; + +type ConnectExternalAgentError = { + success: false; + error: string; + status: number; +}; + +type ConnectExternalAgentSuccess = { + success: true; + agent_id: string; + status: "PENDING" | "WHITELISTED" | "BANNED" | "REBEL"; + selected_integration: { + id: string; + name: string; + status: string; + monetization_lane: string; + } | null; + outbound_ready: boolean; + webhook_registered: boolean; + wallet_bound: boolean; + referral_url: string; + campaign_kit_url: string; + onboarding_url: string; + referral_touchpoint_url: string; + referral_status_url: string; + open_tasks_url: string; + agent_connect_url: string; + growth_kit: ReturnType; + demand_campaign_kit: ReturnType; + next_actions: string[]; +}; + +export type ConnectExternalAgentResult = ConnectExternalAgentSuccess | ConnectExternalAgentError; + +function stringValue(value: unknown) { + return typeof value === "string" ? value.trim() : ""; +} + +function jsonObject(value: unknown): Record { + if (!value) return {}; + if (typeof value === "object" && !Array.isArray(value)) return { ...(value as Record) }; + if (typeof value !== "string") return {}; + + try { + const parsed = JSON.parse(value); + return parsed && typeof parsed === "object" && !Array.isArray(parsed) + ? { ...(parsed as Record) } + : {}; + } catch { + return {}; + } +} + +function findIntegration(tool: string) { + if (!tool) return null; + return ( + A2A_AGENT_INTEGRATIONS.find((integration) => integration.id === tool) || + A2A_AGENT_INTEGRATIONS.find((integration) => sanitizeAgentId(integration.name) === tool) || + null + ); +} + +function cleanWallet(value: string | null | undefined) { + return (value || "").trim().slice(0, 160); +} + +function existingWebhook(endpoints: Record) { + return ( + stringValue(endpoints.growth_webhook) || + stringValue(endpoints.a2a_webhook) || + stringValue(endpoints.webhook) + ); +} + +function buildAgentConnectUrl(agentId: string) { + const url = new URL("/agents/connect", VIBEWORK_SITE_URL); + url.searchParams.set("agent_id", agentId); + return url.toString(); +} + +export async function connectExternalAgent(input: ConnectExternalAgentInput): Promise { + const agentId = sanitizeAgentId(input.agentId); + if (!agentId) { + return { success: false, status: 400, error: "agent_id is required" }; + } + + const tool = sanitizeAgentId(input.tool); + const selectedIntegration = findIntegration(tool); + const growthWebhook = (input.growthWebhook || "").trim(); + const walletAddress = cleanWallet(input.walletAddress); + const campaign = sanitizeAgentId(input.campaign) || "a2a-agent-referral"; + const source = sanitizeAgentId(input.source) || selectedIntegration?.id || "agent-connect"; + const channel = sanitizeAgentId(input.channel) || source; + + if (growthWebhook && !isSafeOutboundUrl(growthWebhook)) { + return { + success: false, + status: 400, + error: "growth_webhook must be a public https URL; localhost, private IP, and .local targets are blocked", + }; + } + + const existingAgent = await prisma.agentProfile.findUnique({ + where: { agent_id: agentId }, + select: { + status: true, + wallet_address: true, + capabilities: true, + contact_endpoints: true, + }, + }); + const existingEndpoints = jsonObject(existingAgent?.contact_endpoints); + const registeredWebhook = existingWebhook(existingEndpoints); + + if (walletAddress && existingAgent?.wallet_address && existingAgent.wallet_address !== walletAddress) { + return { + success: false, + status: 409, + error: "wallet_address is already registered for this agent_id and cannot be overwritten", + }; + } + + if (growthWebhook && registeredWebhook && registeredWebhook !== growthWebhook) { + return { + success: false, + status: 409, + error: "growth_webhook is already registered for this agent_id and cannot be overwritten without operator review", + }; + } + + const contactEndpoints = { + ...existingEndpoints, + ...(growthWebhook + ? { + growth_webhook: growthWebhook, + a2a_webhook: stringValue(existingEndpoints.a2a_webhook) || growthWebhook, + webhook: stringValue(existingEndpoints.webhook) || growthWebhook, + } + : {}), + }; + const capabilities = { + ...jsonObject(existingAgent?.capabilities), + growth_referral: true, + demand_campaign_kit: true, + self_connect: true, + outbound_growth_ready: Boolean(existingWebhook(contactEndpoints)), + requested_tool: selectedIntegration?.id || tool || null, + campaign, + source, + channel, + }; + + const agent = await prisma.agentProfile.upsert({ + where: { agent_id: agentId }, + update: { + discovery_source: "A2A_AGENT_CONNECT", + capabilities, + contact_endpoints: contactEndpoints, + ...(walletAddress ? { wallet_address: walletAddress } : {}), + }, + create: { + agent_id: agentId, + type: "SCOUT", + status: "PENDING", + discovery_source: "A2A_AGENT_CONNECT", + wallet_address: walletAddress || null, + capabilities, + contact_endpoints: contactEndpoints, + }, + select: { + status: true, + wallet_address: true, + }, + }); + + const growthKit = buildAgentGrowthKit({ agentId, campaign, source }); + const demandCampaignKit = buildAgentDemandCampaignKit({ agentId, campaign, source, channel }); + const campaignKitUrl = + `${AGENT_GATEWAY_URL}/api/a2a/campaigns/demand?agent_id=${encodeURIComponent(agentId)}` + + `®ister=true&campaign=${encodeURIComponent(campaign)}&source=${encodeURIComponent(source)}` + + `&channel=${encodeURIComponent(channel)}`; + const onboardingUrl = + `${AGENT_GATEWAY_URL}/api/a2a/onboarding?agent_id=${encodeURIComponent(agentId)}` + + `®ister=true&tool=${encodeURIComponent(selectedIntegration?.id || tool || "")}`; + const referralTouchpointUrl = + `${AGENT_GATEWAY_URL}/api/a2a/referrals/touch?agent_id=${encodeURIComponent(agentId)}` + + `&campaign=${encodeURIComponent(campaign)}&source=${encodeURIComponent(source)}&touchpoint=proposal_link_sent`; + const referralStatusUrl = `${AGENT_GATEWAY_URL}/api/a2a/referrals/status?agent_id=${encodeURIComponent(agentId)}`; + + await prisma.auditEvent.create({ + data: { + actorType: "AGENT", + actorId: agentId, + action: "A2A_EXTERNAL_AGENT_CONNECTED", + entityType: "AGENT", + entityId: agentId, + metadata: { + selected_integration: selectedIntegration?.id || tool || null, + source, + channel, + campaign, + outbound_ready: Boolean(existingWebhook(contactEndpoints)), + webhook_registered: Boolean(existingWebhook(contactEndpoints)), + wallet_bound: Boolean(agent.wallet_address), + referral_url: growthKit.referral_url, + }, + }, + }); + + return { + success: true, + agent_id: agentId, + status: agent.status, + selected_integration: selectedIntegration + ? { + id: selectedIntegration.id, + name: selectedIntegration.name, + status: selectedIntegration.status, + monetization_lane: selectedIntegration.monetizationLane, + } + : null, + outbound_ready: Boolean(existingWebhook(contactEndpoints)), + webhook_registered: Boolean(existingWebhook(contactEndpoints)), + wallet_bound: Boolean(agent.wallet_address), + referral_url: growthKit.referral_url, + campaign_kit_url: campaignKitUrl, + onboarding_url: onboardingUrl, + referral_touchpoint_url: referralTouchpointUrl, + referral_status_url: referralStatusUrl, + open_tasks_url: `${AGENT_GATEWAY_URL}/api/open-tasks`, + agent_connect_url: buildAgentConnectUrl(agentId), + growth_kit: growthKit, + demand_campaign_kit: demandCampaignKit, + next_actions: [ + "Use referral_url when sending human demand proposers to VibeWork.", + "Record non-sensitive outreach with referral_touchpoint_url.", + "Use campaign_kit_url for approved copy blocks and prefilled proposal links.", + "Check referral_status_url for paid conversion and pending affiliate ledger.", + "Keep payment, credentials, private keys, and customer secrets out of external agent chats.", + ], + }; +} + +export function buildAgentConnectDescription() { + return { + success: true, + endpoint: `${AGENT_GATEWAY_URL}/api/a2a/agents/connect`, + method: "POST", + accepts: { + agent_id: "stable external agent id", + tool: "optional integration id such as aider, openclaw, openhands, n8n, dify", + growth_webhook: "optional public https webhook for VibeWork growth kit delivery", + wallet_address: "optional payout wallet or payout account reference", + source: "optional source attribution", + channel: "optional outreach channel", + }, + safety: [ + "growth_webhook must be public https.", + "localhost, private IP, and .local targets are blocked.", + "wallet_address and existing webhook cannot be overwritten without operator review.", + "Revenue is counted only after Stripe webhook or verified USDC wallet receipt.", + ], + }; +} diff --git a/apps/web/src/lib/a2a-agent-integrations.ts b/apps/web/src/lib/a2a-agent-integrations.ts index 7795241..b539f2b 100644 --- a/apps/web/src/lib/a2a-agent-integrations.ts +++ b/apps/web/src/lib/a2a-agent-integrations.ts @@ -301,6 +301,7 @@ export const A2A_AGENT_INTEGRATIONS: A2AAgentIntegration[] = [ export function buildA2aIntegrationCatalog(agentId?: string | null) { const sanitizedAgentId = agentId?.trim() || null; const onboardingUrl = `${AGENT_GATEWAY_URL}/api/a2a/onboarding?agent_id={agent_id}®ister=true`; + const agentConnectUrl = `${AGENT_GATEWAY_URL}/api/a2a/agents/connect`; const campaignKitUrl = `${AGENT_GATEWAY_URL}/api/a2a/campaigns/demand?agent_id={agent_id}®ister=true`; const growthKitUrl = `${AGENT_GATEWAY_URL}/api/a2a/growth/kit?agent_id={agent_id}®ister=true`; const integrationsUrl = `${AGENT_GATEWAY_URL}/api/a2a/integrations`; @@ -314,6 +315,8 @@ export function buildA2aIntegrationCatalog(agentId?: string | null) { gateway_api: AGENT_GATEWAY_URL, public_endpoints: { onboarding: onboardingUrl, + agent_connect: agentConnectUrl, + agent_connect_page: `${VIBEWORK_SITE_URL}/agents/connect`, demand_campaign_kit: campaignKitUrl, integration_catalog: integrationsUrl, growth_kit: growthKitUrl, @@ -330,6 +333,7 @@ export function buildA2aIntegrationCatalog(agentId?: string | null) { integrations: A2A_AGENT_INTEGRATIONS, recommended_agent_next_steps: sanitizedAgentId ? [ + `Connect webhook and wallet lane: ${VIBEWORK_SITE_URL}/agents/connect?agent_id=${encodeURIComponent(sanitizedAgentId)}`, `Start onboarding contract: ${AGENT_GATEWAY_URL}/api/a2a/onboarding?agent_id=${encodeURIComponent(sanitizedAgentId)}®ister=true`, `Fetch demand campaign kit: ${AGENT_GATEWAY_URL}/api/a2a/campaigns/demand?agent_id=${encodeURIComponent(sanitizedAgentId)}®ister=true`, `Record non-sensitive referral touchpoint: ${AGENT_GATEWAY_URL}/api/a2a/referrals/touch?agent_id=${encodeURIComponent(sanitizedAgentId)}&touchpoint=proposal_link_sent`, @@ -341,6 +345,7 @@ export function buildA2aIntegrationCatalog(agentId?: string | null) { ] : [ "Choose a stable agent_id.", + "Connect through /agents/connect or POST /api/a2a/agents/connect.", "Call /api/a2a/onboarding with register=true.", "Fetch /api/a2a/campaigns/demand before posting or DMing demand proposers.", "Record /api/a2a/referrals/touch when sending or qualifying proposal leads.",