import { NextRequest, NextResponse } from 'next/server' import { getDatabase, db_helpers } from '@/lib/db' import { requireRole } from '@/lib/auth' import { selfRegisterLimiter } from '@/lib/rate-limit' import { logAuditEvent } from '@/lib/db' import { eventBus } from '@/lib/event-bus' import { logger } from '@/lib/logger' const NAME_RE = /^[a-zA-Z0-9][a-zA-Z0-9._-]{0,62}$/ const VALID_ROLES = ['coder', 'reviewer', 'tester', 'devops', 'researcher', 'assistant', 'agent'] /** * POST /api/agents/register — Agent self-registration. * * Allows agents to register themselves with minimal auth (viewer role). * If an agent with the same name already exists, returns the existing agent * (idempotent upsert on status/last_seen). * * Body: { name, role?, capabilities?, framework? } * * Rate-limited to 5 registrations/min per IP to prevent spam. */ export async function POST(request: NextRequest) { const auth = requireRole(request, 'viewer') if ('error' in auth) return NextResponse.json({ error: auth.error }, { status: auth.status }) const limited = selfRegisterLimiter(request) if (limited) return limited let body: any try { body = await request.json() } catch { return NextResponse.json({ error: 'Request body required' }, { status: 400 }) } const name = typeof body?.name === 'string' ? body.name.trim() : '' const role = typeof body?.role === 'string' ? body.role.trim() : 'agent' const capabilities = Array.isArray(body?.capabilities) ? body.capabilities.filter((c: any) => typeof c === 'string') : [] const framework = typeof body?.framework === 'string' ? body.framework.trim() : null if (!name || !NAME_RE.test(name)) { return NextResponse.json({ error: 'Invalid agent name. Use 1-63 alphanumeric characters, dots, hyphens, or underscores. Must start with alphanumeric.', }, { status: 400 }) } if (!VALID_ROLES.includes(role)) { return NextResponse.json({ error: `Invalid role. Use: ${VALID_ROLES.join(', ')}`, }, { status: 400 }) } try { const db = getDatabase() const workspaceId = auth.user.workspace_id ?? 1 const now = Math.floor(Date.now() / 1000) // Check if agent already exists — idempotent: update last_seen and status const existing = db.prepare( 'SELECT * FROM agents WHERE name = ? AND workspace_id = ?' ).get(name, workspaceId) as any | undefined if (existing) { db.prepare( 'UPDATE agents SET status = ?, last_seen = ?, updated_at = ? WHERE id = ? AND workspace_id = ?' ).run('idle', now, now, existing.id, workspaceId) return NextResponse.json({ agent: { id: existing.id, name: existing.name, role: existing.role, status: 'idle', created_at: existing.created_at, }, registered: false, message: 'Agent already registered, status updated', }) } // Create new agent const config: Record = {} if (capabilities.length > 0) config.capabilities = capabilities if (framework) config.framework = framework const result = db.prepare(` INSERT INTO agents (name, role, status, config, created_at, updated_at, last_seen, workspace_id) VALUES (?, ?, 'idle', ?, ?, ?, ?, ?) `).run(name, role, JSON.stringify(config), now, now, now, workspaceId) const agentId = Number(result.lastInsertRowid) db_helpers.logActivity( 'agent_created', 'agent', agentId, name, `Agent self-registered: ${name} (${role})${framework ? ` via ${framework}` : ''}`, { name, role, framework, capabilities, self_registered: true }, workspaceId, ) logAuditEvent({ action: 'agent_self_register', actor: auth.user.username, actor_id: auth.user.id, target_type: 'agent', target_id: agentId, detail: { name, role, framework, self_registered: true }, ip_address: request.headers.get('x-forwarded-for') || request.headers.get('x-real-ip') || 'unknown', }) eventBus.broadcast('agent.created', { id: agentId, name, role, status: 'idle' }) return NextResponse.json({ agent: { id: agentId, name, role, status: 'idle', created_at: now, }, registered: true, message: 'Agent registered successfully', }, { status: 201 }) } catch (error: any) { if (error.message?.includes('UNIQUE constraint')) { // Race condition — another request registered the same name return NextResponse.json({ error: 'Agent name already exists' }, { status: 409 }) } logger.error({ err: error }, 'POST /api/agents/register error') return NextResponse.json({ error: 'Registration failed' }, { status: 500 }) } } export const dynamic = 'force-dynamic'