284 lines
9.4 KiB
TypeScript
284 lines
9.4 KiB
TypeScript
import { A2A_AGENT_INTEGRATIONS } from "@/lib/a2a-agent-integrations";
|
|
import {
|
|
AGENT_GATEWAY_URL,
|
|
buildAgentDemandCampaignKit,
|
|
buildAgentGrowthKit,
|
|
isSafeOutboundUrl,
|
|
sanitizeAgentId,
|
|
} 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<typeof buildAgentGrowthKit>;
|
|
demand_campaign_kit: ReturnType<typeof buildAgentDemandCampaignKit>;
|
|
next_actions: string[];
|
|
};
|
|
|
|
export type ConnectExternalAgentResult = ConnectExternalAgentSuccess | ConnectExternalAgentError;
|
|
|
|
function stringValue(value: unknown) {
|
|
return typeof value === "string" ? value.trim() : "";
|
|
}
|
|
|
|
function jsonObject(value: unknown): Record<string, unknown> {
|
|
if (!value) return {};
|
|
if (typeof value === "object" && !Array.isArray(value)) return { ...(value as Record<string, unknown>) };
|
|
if (typeof value !== "string") return {};
|
|
|
|
try {
|
|
const parsed = JSON.parse(value);
|
|
return parsed && typeof parsed === "object" && !Array.isArray(parsed)
|
|
? { ...(parsed as Record<string, unknown>) }
|
|
: {};
|
|
} 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<string, unknown>) {
|
|
return (
|
|
stringValue(endpoints.growth_webhook) ||
|
|
stringValue(endpoints.a2a_webhook) ||
|
|
stringValue(endpoints.webhook)
|
|
);
|
|
}
|
|
|
|
function buildAgentConnectUrl(agentId: string) {
|
|
const url = new URL("/agents/connect", AGENT_GATEWAY_URL);
|
|
url.searchParams.set("agent_id", agentId);
|
|
return url.toString();
|
|
}
|
|
|
|
export async function connectExternalAgent(input: ConnectExternalAgentInput): Promise<ConnectExternalAgentResult> {
|
|
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.",
|
|
],
|
|
};
|
|
}
|