# 📋 Próxima Sesión - Multi-Agent + Terminal Integrado + Auth **Objetivo**: Integrar terminals web en agentes, autenticación MCP, y gestión multi-agente desde UI **Tiempo estimado**: 3-4 horas **Sesión anterior**: `past-sessions/2026-01-20-frontend-mcp-agents.md` --- ## 🎯 VISIÓN DE ESTA FASE ### Lo que tenemos AHORA: - ✅ Dashboard web en app.fuq.tv - ✅ Backend con MCP endpoints (públicos) - ✅ Un agente deployment (claude-agent) - ✅ Terminal web separado (claude.fuq.tv) - **NO conectado al agente** ### Lo que queremos LOGRAR: - 🎯 **Múltiples agentes** por usuario, gestionados desde UI - 🎯 **Terminal web integrado** EN cada agente (no pod separado) - 🎯 **Ver terminal** de cualquier agente desde app.fuq.tv (iframe/websocket) - 🎯 **MCP autenticado**: cada agente usa token del usuario que lo creó - 🎯 **Agent pool management**: crear, ver, eliminar agentes desde dashboard --- ## ✅ PRE-REQUISITOS ```bash # 1. Sistema actual funcionando curl -s https://app.fuq.tv/ | grep "AiWorker" curl -s https://api.fuq.tv/api/health | grep "ok" # 2. Cluster operativo export KUBECONFIG=~/.kube/aiworker-config kubectl get pods -n control-plane kubectl get pods -n agents # 3. Auth funcionando # Prueba: registra usuario, inicia sesión, crea proyecto ``` --- ## 🎯 PASO 1: Agregar Auth a Usuarios (30 min) ### 1.1 Extender schema con relación user → agents **Modificar** `backend/src/db/schema.ts`: ```typescript // Agregar campo userId a agents table export const agents = mysqlTable('agents', { id: varchar('id', { length: 36 }).primaryKey(), userId: varchar('user_id', { length: 36 }).notNull().references(() => users.id, { onDelete: 'cascade' }), // ... resto de campos }) // Agregar relación export const usersRelations = relations(users, ({ many }) => ({ agents: many(agents), projects: many(projects), })) export const agentsRelations = relations(agents, ({ one }) => ({ user: one(users, { fields: [agents.userId], references: [users.id], }), // ... resto })) ``` Generar migración: ```bash cd backend bun run db:generate # Revisar migración generada bun run db:migrate ``` --- ## 🎯 PASO 2: Auth en MCP Endpoints (45 min) ### 2.1 Crear middleware de auth **Crear** `backend/src/api/middleware/auth.ts`: ```typescript import { db } from '../../db/client' import { users, sessions } from '../../db/schema' import { eq } from 'drizzle-orm' export async function authenticateRequest(req: Request): Promise<{ userId: string } | null> { const sessionId = getSessionIdFromCookie(req) if (!sessionId) return null const [session] = await db .select() .from(sessions) .where(eq(sessions.id, sessionId)) .limit(1) if (!session || session.expiresAt < new Date()) { return null } return { userId: session.userId } } function getSessionIdFromCookie(req: Request): string | null { const cookieHeader = req.headers.get('cookie') if (!cookieHeader) return null const cookies = cookieHeader.split(';').reduce((acc, cookie) => { const [name, value] = cookie.trim().split('=') acc[name] = value return acc }, {} as Record) return cookies.session || null } ``` ### 2.2 Proteger endpoints MCP **Modificar** `backend/src/api/routes/mcp.ts`: ```typescript import { authenticateRequest } from '../middleware/auth' export async function handleMCPRoutes(req: Request, url: URL): Promise { // Autenticar request const auth = await authenticateRequest(req) if (!auth) { return Response.json( { success: false, message: 'Unauthorized' }, { status: 401 } ) } const userId = auth.userId // Pasar userId a todos los handlers... } // Modificar get_next_task para filtrar por usuario async function handleGetNextTask(req: Request, userId: string): Promise { // ... // Filtrar tareas de proyectos del usuario const userProjects = await db.select().from(projects).where(eq(projects.userId, userId)) const projectIds = userProjects.map(p => p.id) const [task] = await db .select() .from(tasks) .where(and( eq(tasks.state, 'backlog'), inArray(tasks.projectId, projectIds) )) .orderBy(tasks.priority, tasks.createdAt) .limit(1) // ... } ``` --- ## 🎯 PASO 3: Integrar Terminal en Imagen del Agente (1 hora) ### 3.1 Modificar Dockerfile del agente **Modificar** `agents/Dockerfile`: ```dockerfile FROM ubuntu:24.04 ENV DEBIAN_FRONTEND=noninteractive # Install system dependencies + ttyd + tmux RUN apt-get update && apt-get install -y \ curl wget git bash unzip \ ca-certificates openssh-client \ python3 python3-pip python3-venv \ build-essential jq \ tmux \ && rm -rf /var/lib/apt/lists/* # Install ttyd (terminal web) RUN curl -L https://github.com/tsl0922/ttyd/releases/download/1.7.4/ttyd.x86_64 -o /usr/local/bin/ttyd \ && chmod +x /usr/local/bin/ttyd # Install Node.js 24.x RUN curl -fsSL https://deb.nodesource.com/setup_24.x | bash - \ && apt-get install -y nodejs \ && rm -rf /var/lib/apt/lists/* # Install Bun RUN curl -fsSL https://bun.sh/install | bash ENV PATH="/root/.bun/bin:$PATH" # Install kubectl RUN curl -LO "https://dl.k8s.io/release/$(curl -L -s https://dl.k8s.io/release/stable.txt)/bin/linux/amd64/kubectl" \ && chmod +x kubectl \ && mv kubectl /usr/local/bin/ # Install Claude Code CLI RUN bun install -g @anthropic-ai/claude-code # Create workspace WORKDIR /workspace # Entrypoint script COPY entrypoint.sh /entrypoint.sh RUN chmod +x /entrypoint.sh EXPOSE 7681 CMD ["/entrypoint.sh"] ``` ### 3.2 Crear entrypoint script **Crear** `agents/entrypoint.sh`: ```bash #!/bin/bash set -e # Configure git git config --global user.name "Claude Agent" git config --global user.email "agent@aiworker.local" # Start tmux session tmux new-session -d -s claude 'bash -c "cd /workspace && exec bash"' # Start ttyd to expose tmux over HTTP on port 7681 exec ttyd -p 7681 -W tmux attach -t claude ``` ### 3.3 Build y push ```bash cd agents git add Dockerfile entrypoint.sh git commit -m "Integrate ttyd+tmux into agent image - Add ttyd for web terminal access - Add tmux for persistent sessions - Expose port 7681 for terminal web - Entrypoint starts tmux + ttyd automatically Co-Authored-By: Claude Sonnet 4.5 (1M context) " git push # Esperar build en Gitea Actions ``` --- ## 🎯 PASO 4: StatefulSet para Agentes (45 min) ### 4.1 Cambiar de Deployment a StatefulSet **Crear** `k8s/agents/statefulset.yaml`: ```yaml apiVersion: v1 kind: Service metadata: name: claude-agent namespace: agents spec: clusterIP: None # Headless service selector: app: claude-agent ports: - name: terminal port: 7681 targetPort: 7681 --- apiVersion: apps/v1 kind: StatefulSet metadata: name: claude-agent namespace: agents spec: serviceName: claude-agent replicas: 0 # Se crearán dinámicamente desde API selector: matchLabels: app: claude-agent template: metadata: labels: app: claude-agent spec: serviceAccountName: agent-sa imagePullSecrets: - name: gitea-registry containers: - name: agent image: git.fuq.tv/admin/aiworker-agent:latest imagePullPolicy: Always ports: - containerPort: 7681 name: terminal env: - name: BACKEND_URL value: "https://api.fuq.tv" - name: MCP_ENDPOINT value: "https://api.fuq.tv/api/mcp" - name: GITEA_URL value: "https://git.fuq.tv" - name: GITEA_TOKEN valueFrom: secretKeyRef: name: agent-secrets key: gitea-token - name: POD_NAME valueFrom: fieldRef: fieldPath: metadata.name - name: USER_ID value: "" # Se inyecta dinámicamente al crear el pod resources: requests: cpu: 500m memory: 1Gi limits: cpu: 2000m memory: 4Gi volumeMounts: - name: workspace mountPath: /workspace volumeClaimTemplates: - metadata: name: workspace spec: accessModes: ["ReadWriteOnce"] storageClassName: longhorn resources: requests: storage: 10Gi ``` --- ## 🎯 PASO 5: Backend API para Gestión de Agentes (1 hora) ### 5.1 Endpoints de agentes autenticados **Modificar** `backend/src/api/routes/agents.ts`: ```typescript // POST /api/agents/launch - Lanzar nuevo agente async function handleLaunchAgent(req: Request, userId: string): Promise { const agentId = randomUUID() const podName = `claude-agent-${userId.slice(0, 8)}-${Date.now()}` // 1. Crear registro en DB await db.insert(agents).values({ id: agentId, userId, podName, k8sNamespace: 'agents', status: 'idle', }) // 2. Crear pod en K8s usando K8s API const k8sConfig = KubeConfig.fromFile('~/.kube/aiworker-config') const k8sApi = k8sConfig.makeApiClient(AppsV1Api) // Escalar StatefulSet o crear Pod individual await k8sApi.createNamespacedPod('agents', { metadata: { name: podName, labels: { app: 'claude-agent', userId: userId, agentId: agentId, }, }, spec: { // ... spec del pod con USER_ID env var }, }) return Response.json({ success: true, data: { agentId, podName }, }) } // DELETE /api/agents/:id - Eliminar agente async function handleDeleteAgent(agentId: string, userId: string): Promise { const [agent] = await db .select() .from(agents) .where(and(eq(agents.id, agentId), eq(agents.userId, userId))) .limit(1) if (!agent) { return Response.json({ success: false, message: 'Agent not found' }, { status: 404 }) } // Eliminar pod de K8s await k8sApi.deleteNamespacedPod(agent.podName, 'agents') // Eliminar de DB await db.delete(agents).where(eq(agents.id, agentId)) return Response.json({ success: true }) } // GET /api/agents/my - Listar mis agentes async function handleMyAgents(userId: string): Promise { const myAgents = await db .select() .from(agents) .where(eq(agents.userId, userId)) return Response.json({ success: true, data: myAgents, }) } ``` --- ## 🎯 PASO 6: Frontend - Ver Agentes y Terminals (1 hora) ### 6.1 Agregar botón "Launch Agent" **Modificar** `frontend/src/pages/Dashboard.tsx`: ```typescript const [showAgentLauncher, setShowAgentLauncher] = useState(false) // Botón en header ``` ### 6.2 Crear AgentTerminalModal **Crear** `frontend/src/components/AgentTerminalModal.tsx`: ```typescript interface AgentTerminalModalProps { agent: Agent isOpen: boolean onClose: () => void } export default function AgentTerminalModal({ agent, isOpen, onClose }: AgentTerminalModalProps) { if (!isOpen) return null // URL del terminal: http://claude-agent-{podName}.agents.svc.cluster.local:7681 const terminalUrl = `/agent-terminal/${agent.id}` return (

Agent Terminal - {agent.podName}