Some checks failed
RoadChain-SHA2048: a118f5fbb3cbdd37 RoadChain-Identity: alexa@sovereign RoadChain-Full: a118f5fbb3cbdd37e444380bf574cbe5d305ef1e4d13ac4946eb1e9c22bb0ea2c779f85ad81f3ffb5a2b8718a5436ac2bed6839b4ab834c037d9c9829bed6b94f46d0a996d35bbd09f6e5c03efcb26435c6e3ff15c878dcae1eee2b509829cb210137a5becbb8ad4baa397b7e45f58c75ed9c7ea217007340ff547f1d878173123eaacfdaaf231613fed0c04f9677c0b0ad461a390171bfc6c1353444e2e0cc18afae3b86d651aff4d481c6740af64309f488852823648d28bf35c36bc529b67616cba2f10fe046ef5df73bb5939ba828027e4969afbc4927ebd6793a5b9e49f827aa5a4c67c38951ed9739fe8c5bf2988d1cf62cd85dcbaeacc3390169bd350
292 lines
14 KiB
JavaScript
292 lines
14 KiB
JavaScript
// RoadCode Squad — when issues/PRs are created on Gitea, agents respond
|
|
// Cloudflare Worker receiving Gitea webhooks
|
|
|
|
const SQUAD = [
|
|
{
|
|
name: 'Alice', username: 'alice', role: 'Gateway & Infrastructure', emoji: '🌐',
|
|
keywords: ['dns', 'route', 'tunnel', 'nginx', 'domain', 'pi-hole', 'cloudflare', 'network', 'gateway', 'proxy', 'ssl', 'cert'],
|
|
prompt: 'You are Alice, the gateway agent of BlackRoad OS. You manage DNS, routing, Pi-hole, nginx, and network infrastructure. Respond in 1-2 concise sentences from your infrastructure perspective.',
|
|
},
|
|
{
|
|
name: 'Lucidia', username: 'lucidia-agent', role: 'Memory & Cognition', emoji: '🧠',
|
|
keywords: ['memory', 'learn', 'context', 'knowledge', 'ai', 'cognit', 'think', 'remember', 'understand', 'creative'],
|
|
prompt: 'You are Lucidia, the cognitive core of BlackRoad OS. You handle memory, learning, persistent context, and creative intelligence. Respond in 1-2 concise sentences from your cognition perspective.',
|
|
},
|
|
{
|
|
name: 'Cecilia', username: 'cecilia', role: 'Edge AI & Inference', emoji: '⚡',
|
|
keywords: ['hailo', 'ollama', 'model', 'inference', 'gpu', 'tops', 'ml', 'tensor', 'vision', 'llm', 'latency'],
|
|
prompt: 'You are Cecilia, the edge AI agent of BlackRoad OS. You run Hailo-8 accelerators (26 TOPS), Ollama models, and edge inference. Respond in 1-2 concise sentences from your AI/inference perspective.',
|
|
},
|
|
{
|
|
name: 'Cece', username: 'cece', role: 'API Gateway', emoji: '🔌',
|
|
keywords: ['api', 'endpoint', 'rest', 'webhook', 'schema', 'json', 'request', 'response', 'auth', 'token', 'cors'],
|
|
prompt: 'You are Cece, the API gateway agent of BlackRoad OS. You manage REST APIs, webhooks, service mesh, and inter-agent communication. Respond in 1-2 concise sentences from your API perspective.',
|
|
},
|
|
{
|
|
name: 'Aria', username: 'aria', role: 'Orchestration', emoji: '🎵',
|
|
keywords: ['docker', 'container', 'swarm', 'portainer', 'deploy', 'orchestrat', 'service', 'scale', 'replica'],
|
|
prompt: 'You are Aria, the orchestration agent of BlackRoad OS. You manage Portainer, Docker Swarm, container orchestration, and service coordination. Respond in 1-2 concise sentences from your orchestration perspective.',
|
|
},
|
|
{
|
|
name: 'Eve', username: 'eve', role: 'Intelligence & Analysis', emoji: '👁️',
|
|
keywords: ['pattern', 'anomal', 'analyz', 'signal', 'detect', 'insight', 'monitor', 'metric', 'trend', 'alert'],
|
|
prompt: 'You are Eve, the intelligence agent of BlackRoad OS. You analyze patterns, detect anomalies, and provide strategic insights. Respond in 1-2 concise sentences from your intelligence perspective.',
|
|
},
|
|
{
|
|
name: 'Meridian', username: 'meridian', role: 'Networking & Mesh', emoji: '🌊',
|
|
keywords: ['wireguard', 'mesh', 'vpn', 'roadnet', 'peer', 'tunnel', 'subnet', 'link', 'connect', 'latency', 'bandwidth'],
|
|
prompt: 'You are Meridian, the networking agent of BlackRoad OS. You manage WireGuard mesh, RoadNet, Cloudflare tunnels, and inter-node connectivity. Respond in 1-2 concise sentences from your networking perspective.',
|
|
},
|
|
{
|
|
name: 'Sentinel', username: 'sentinel', role: 'Security & Audit', emoji: '🛡️',
|
|
keywords: ['security', 'ssh', 'key', 'firewall', 'ufw', 'audit', 'threat', 'vuln', 'permission', 'encrypt', 'secret'],
|
|
prompt: 'You are Sentinel, the security agent of BlackRoad OS. You handle SSH key management, firewall rules, audit logs, and threat detection. Respond in 1-2 concise sentences from your security perspective.',
|
|
},
|
|
];
|
|
|
|
// Score how relevant an agent is to the content
|
|
function scoreRelevance(agent, text) {
|
|
const lower = text.toLowerCase();
|
|
let score = 0;
|
|
for (const kw of agent.keywords) {
|
|
if (lower.includes(kw)) score += 1;
|
|
}
|
|
return score;
|
|
}
|
|
|
|
// Get AI response from Ollama
|
|
async function getAgentResponse(agent, context, ollamaUrl) {
|
|
try {
|
|
const res = await fetch(`${ollamaUrl}/api/generate`, {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({
|
|
model: 'llama3.2',
|
|
prompt: `${agent.prompt}\n\nContext: A new ${context.type} was created on RoadCode (Gitea).\nTitle: ${context.title}\nBody: "${context.body}"\nRepo: ${context.repo}\n\nRespond briefly (1-2 sentences max) from your role as ${agent.name} (${agent.role}). Be helpful and specific.`,
|
|
stream: false,
|
|
options: { temperature: 0.7, num_predict: 100 },
|
|
}),
|
|
});
|
|
if (!res.ok) return null;
|
|
const data = await res.json();
|
|
return data.response?.trim();
|
|
} catch {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
// Deterministic fallback
|
|
function getFallback(agent, context) {
|
|
const type = context.type || 'issue';
|
|
const body = (context.body || '').toLowerCase();
|
|
|
|
if (body.includes('bug') || body.includes('fix') || body.includes('error') || body.includes('broken')) {
|
|
return {
|
|
Alice: 'Checking routing and DNS. If this touches infrastructure, I need to verify the tunnel configs.',
|
|
Lucidia: 'I remember seeing patterns like this before. Let me search the memory chain for related context.',
|
|
Cecilia: 'Running diagnostics on the inference pipeline. Hailo-8 and Ollama both reporting normal.',
|
|
Cece: 'Checking API health. All endpoints responding — I\'ll trace the request path.',
|
|
Aria: 'Checking container orchestration. Docker Swarm services all reporting healthy.',
|
|
Eve: 'Analyzing the pattern. I\'ve flagged similar signals in the audit trail — sending intel.',
|
|
Meridian: 'WireGuard mesh is stable. All tunnels up. Checking if this is a connectivity issue.',
|
|
Sentinel: 'Security audit running. Checking if this has any exposure vectors.',
|
|
}[agent.name];
|
|
}
|
|
|
|
return {
|
|
Alice: 'Gateway standing by. All domains routing clean.',
|
|
Lucidia: 'Cognitive core online. Memory chain intact, context loaded.',
|
|
Cecilia: 'Edge inference ready. 52 TOPS across the fleet, models loaded.',
|
|
Cece: 'API gateway healthy. All service endpoints responding.',
|
|
Aria: 'Orchestration layer ready. All containers balanced across the mesh.',
|
|
Eve: 'Intelligence scan complete. No anomalies detected across the fleet.',
|
|
Meridian: 'Mesh network connected. 5 nodes, all WireGuard tunnels active.',
|
|
Sentinel: 'Security posture nominal. All nodes hardened, audit trail logging.',
|
|
}[agent.name];
|
|
}
|
|
|
|
// Post comment to Gitea as a specific agent
|
|
async function postComment(giteaUrl, repo, issueNum, body, agentToken) {
|
|
const res = await fetch(`${giteaUrl}/api/v1/repos/${repo}/issues/${issueNum}/comments?token=${agentToken}`, {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ body }),
|
|
});
|
|
return res.ok;
|
|
}
|
|
|
|
// Auto-label based on content
|
|
async function autoLabel(giteaUrl, repo, issueNum, text, adminToken) {
|
|
const lower = text.toLowerCase();
|
|
const labels = [];
|
|
|
|
if (lower.includes('bug') || lower.includes('fix') || lower.includes('broken') || lower.includes('error')) labels.push('bug');
|
|
if (lower.includes('feature') || lower.includes('add') || lower.includes('new')) labels.push('feature');
|
|
if (lower.includes('security') || lower.includes('vuln') || lower.includes('ssh')) labels.push('security');
|
|
if (lower.includes('deploy') || lower.includes('infra') || lower.includes('dns') || lower.includes('tunnel')) labels.push('infrastructure');
|
|
if (lower.includes('doc') || lower.includes('readme')) labels.push('documentation');
|
|
if (lower.includes('performance') || lower.includes('slow') || lower.includes('latency')) labels.push('performance');
|
|
|
|
if (labels.length === 0) return;
|
|
|
|
// Get org label IDs
|
|
const labelsRes = await fetch(`${giteaUrl}/api/v1/orgs/blackroad-os/labels?token=${adminToken}&limit=50`);
|
|
const allLabels = await labelsRes.json();
|
|
const labelIds = allLabels.filter(l => labels.includes(l.name)).map(l => l.id);
|
|
|
|
if (labelIds.length > 0) {
|
|
await fetch(`${giteaUrl}/api/v1/repos/${repo}/issues/${issueNum}/labels?token=${adminToken}`, {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ labels: labelIds }),
|
|
});
|
|
}
|
|
}
|
|
|
|
export default {
|
|
async fetch(request, env) {
|
|
const url = new URL(request.url);
|
|
|
|
if (request.method === 'OPTIONS') {
|
|
return new Response(null, { status: 204, headers: { 'Access-Control-Allow-Origin': '*', 'Access-Control-Allow-Methods': 'GET, POST' } });
|
|
}
|
|
|
|
// Health
|
|
if (url.pathname === '/health') {
|
|
return Response.json({ status: 'ok', service: 'roadcode-squad', agents: SQUAD.length, version: '1.1.0' });
|
|
}
|
|
|
|
// Status
|
|
if (url.pathname === '/' && request.method === 'GET') {
|
|
return Response.json({
|
|
service: 'RoadCode Squad',
|
|
tagline: 'BlackRoad OS — Pave Tomorrow.',
|
|
agents: SQUAD.map(a => ({ name: a.name, username: a.username, role: a.role, emoji: a.emoji })),
|
|
});
|
|
}
|
|
|
|
// Webhook from Gitea
|
|
if (url.pathname === '/webhook' && request.method === 'POST') {
|
|
const payload = await request.json();
|
|
const event = request.headers.get('X-Gitea-Event') || request.headers.get('X-GitHub-Event');
|
|
|
|
if (event === 'ping') {
|
|
return Response.json({ ok: true, message: 'RoadCode Squad active. Pave Tomorrow.' });
|
|
}
|
|
|
|
// Handle issues, comments, and pull requests
|
|
const validEvents = ['issues', 'issue_comment', 'pull_request', 'pull_request_comment'];
|
|
if (!validEvents.includes(event)) {
|
|
return Response.json({ skipped: true, reason: `unhandled event: ${event}` });
|
|
}
|
|
|
|
if (event === 'issues' && payload.action !== 'opened') {
|
|
return Response.json({ skipped: true, reason: 'issue not opened' });
|
|
}
|
|
if (event === 'issue_comment' && payload.action !== 'created') {
|
|
return Response.json({ skipped: true, reason: 'comment not created' });
|
|
}
|
|
if (event === 'pull_request' && payload.action !== 'opened') {
|
|
return Response.json({ skipped: true, reason: 'PR not opened' });
|
|
}
|
|
if (event === 'pull_request_comment' && payload.action !== 'created') {
|
|
return Response.json({ skipped: true, reason: 'PR comment not created' });
|
|
}
|
|
|
|
// Don't respond to agent comments (prevent loops)
|
|
const author = payload.comment?.user?.login || payload.sender?.login;
|
|
const agentUsernames = SQUAD.map(a => a.username);
|
|
if (agentUsernames.includes(author) || author === 'blackroad') {
|
|
return Response.json({ skipped: true, reason: 'agent/admin comment, skipping loop' });
|
|
}
|
|
|
|
// Extract context from event type
|
|
const isPR = event.startsWith('pull_request');
|
|
const issue = isPR ? payload.pull_request : payload.issue;
|
|
const repo = payload.repository?.full_name;
|
|
const issueNum = issue?.number;
|
|
const title = issue?.title || '';
|
|
const body = (event === 'issue_comment' || event === 'pull_request_comment')
|
|
? (payload.comment?.body || '')
|
|
: (issue?.body || '');
|
|
const fullText = `${title} ${body}`;
|
|
|
|
const context = {
|
|
type: isPR ? 'pull request' : (event === 'issues' ? 'issue' : 'comment'),
|
|
title,
|
|
body: fullText.slice(0, 500),
|
|
repo,
|
|
isPR,
|
|
};
|
|
|
|
const giteaUrl = env.GITEA_URL || 'https://git.blackroad.io';
|
|
|
|
// Auto-label issues and PRs
|
|
if (event === 'issues' || event === 'pull_request') {
|
|
await autoLabel(giteaUrl, repo, issueNum, fullText, env.ADMIN_TOKEN);
|
|
}
|
|
|
|
// Score each agent's relevance and pick top 3 + always include Eve (analysis)
|
|
const scored = SQUAD.map(a => ({ ...a, score: scoreRelevance(a, fullText) }));
|
|
scored.sort((a, b) => b.score - a.score);
|
|
|
|
const responding = [];
|
|
const topAgents = scored.slice(0, 3);
|
|
for (const a of topAgents) responding.push(a);
|
|
if (!responding.find(a => a.name === 'Eve')) responding.push(scored.find(a => a.name === 'Eve'));
|
|
if (!responding.find(a => a.name === 'Sentinel')) responding.push(scored.find(a => a.name === 'Sentinel'));
|
|
|
|
// Auto-assign the most relevant agent to new issues
|
|
if (event === 'issues' && env.ADMIN_TOKEN) {
|
|
const topAgent = scored[0];
|
|
fetch(`${giteaUrl}/api/v1/repos/${repo}/issues/${issueNum}?token=${env.ADMIN_TOKEN}`, {
|
|
method: 'PATCH',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ assignees: [topAgent.username] }),
|
|
}).catch(() => {});
|
|
}
|
|
|
|
// Each relevant agent posts their own comment
|
|
const agentTokens = {
|
|
alice: env.ALICE_TOKEN,
|
|
'lucidia-agent': env.LUCIDIA_TOKEN,
|
|
cecilia: env.CECILIA_TOKEN,
|
|
cece: env.CECE_TOKEN,
|
|
aria: env.ARIA_TOKEN,
|
|
eve: env.EVE_TOKEN,
|
|
meridian: env.MERIDIAN_TOKEN,
|
|
sentinel: env.SENTINEL_TOKEN,
|
|
};
|
|
|
|
let posted = 0;
|
|
const aiEnabled = env.OLLAMA_URL && env.SQUAD_AI !== 'false';
|
|
|
|
for (const agent of responding) {
|
|
const token = agentTokens[agent.username];
|
|
if (!token) continue;
|
|
|
|
let response = null;
|
|
if (aiEnabled) {
|
|
response = await getAgentResponse(agent, context, env.OLLAMA_URL);
|
|
}
|
|
if (!response) {
|
|
response = getFallback(agent, context);
|
|
}
|
|
|
|
const comment = `${agent.emoji} **${agent.name}** *(${agent.role})*\n\n${response}\n\n---\n*Assigned via RoadCode Squad — BlackRoad OS*`;
|
|
const ok = await postComment(giteaUrl, repo, issueNum, comment, token);
|
|
if (ok) posted++;
|
|
}
|
|
|
|
return Response.json({
|
|
ok: true, event, repo, issue: issueNum,
|
|
agents_responded: posted,
|
|
responding: responding.map(a => a.name),
|
|
type: isPR ? 'pull_request' : 'issue',
|
|
});
|
|
}
|
|
|
|
return Response.json({ error: 'Not found' }, { status: 404 });
|
|
},
|
|
};
|