Files
aiworker/NEXT-SESSION.md
Hector Ros 0d97eb1530 Add MCP bridge configuration as STEP 0 in next session
- Install and configure MCP bridge in agent image
- Bridge connects Claude Code stdio <-> HTTP endpoints
- Verification steps included

Co-Authored-By: Claude Sonnet 4.5 (1M context) <noreply@anthropic.com>
2026-01-20 02:25:49 +01:00

16 KiB

📋 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

# 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 0: Configurar MCP Bridge en Agente (15 min)

0.1 Incluir MCP bridge en imagen del agente

Modificar agents/Dockerfile para copiar archivos MCP:

# ... después de instalar Claude Code

# Copy MCP bridge server and config
COPY mcp-bridge-server.js /workspace/mcp-bridge-server.js
COPY claude-code-config.json /root/.config/claude/config.json
RUN chmod +x /workspace/mcp-bridge-server.js

# ...

0.2 Verificar MCP funciona

En terminal del agente:

# Entrar a claude.fuq.tv o kubectl exec al pod
claude
/mcp
# Debería mostrar: "aiworker" server con 5 tools

# Probar una herramienta
/mcp aiworker get_next_task {"agentId":"test"}

Si no funciona: Configuración manual en AGENT-ACCESS.md


🎯 PASO 1: Agregar Auth a Usuarios (30 min)

1.1 Extender schema con relación user → agents

Modificar backend/src/db/schema.ts:

// 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:

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:

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<string, string>)

  return cookies.session || null
}

2.2 Proteger endpoints MCP

Modificar backend/src/api/routes/mcp.ts:

import { authenticateRequest } from '../middleware/auth'

export async function handleMCPRoutes(req: Request, url: URL): Promise<Response> {
  // 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<Response> {
  // ...
  // 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:

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:

#!/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

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) <noreply@anthropic.com>"
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:

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:

// POST /api/agents/launch - Lanzar nuevo agente
async function handleLaunchAgent(req: Request, userId: string): Promise<Response> {
  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<Response> {
  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<Response> {
  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:

const [showAgentLauncher, setShowAgentLauncher] = useState(false)

// Botón en header
<button
  onClick={() => setShowAgentLauncher(true)}
  className="px-4 py-2 bg-green-600 text-white rounded-md"
>
  Launch Agent
</button>

6.2 Crear AgentTerminalModal

Crear frontend/src/components/AgentTerminalModal.tsx:

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 (
    <div className="fixed inset-0 bg-black bg-opacity-75 flex items-center justify-center z-50">
      <div className="bg-white rounded-lg w-full h-full max-w-7xl max-h-[90vh] flex flex-col">
        <div className="p-4 border-b flex justify-between items-center">
          <h2 className="text-xl font-bold">Agent Terminal - {agent.podName}</h2>
          <button onClick={onClose} className="text-gray-500 hover:text-gray-700">
            
          </button>
        </div>
        <div className="flex-1 overflow-hidden">
          <iframe
            src={terminalUrl}
            className="w-full h-full border-0"
            allow="fullscreen"
          />
        </div>
      </div>
    </div>
  )
}

6.3 Proxy para terminals en backend

Agregar en backend/src/index.ts:

// Proxy /agent-terminal/:agentId a pod del agente
if (url.pathname.startsWith('/agent-terminal/')) {
  const agentId = url.pathname.split('/')[2]

  // Obtener pod name de DB
  const [agent] = await db.select().from(agents).where(eq(agents.id, agentId)).limit(1)
  if (!agent) return new Response('Agent not found', { status: 404 })

  // Proxy a pod
  const podUrl = `http://${agent.podName}.agents.svc.cluster.local:7681${url.pathname.replace(`/agent-terminal/${agentId}`, '')}`
  return fetch(podUrl)
}

🎯 PASO 7: Testing End-to-End (30 min)

7.1 Test completo del flujo

# 1. Abrir dashboard
open https://app.fuq.tv

# 2. Iniciar sesión

# 3. Launch Agent (botón verde)
# Debería crear un agente

# 4. Ver en "Agents" tab
# Debería aparecer el agente con status "idle"

# 5. Click en agente → Ver terminal
# Debería abrir modal con terminal tmux

# 6. En terminal: probar Claude Code
cd /workspace
claude

# 7. Crear tarea en proyecto

# 8. En terminal del agente:
curl -X POST https://api.fuq.tv/api/mcp/get_next_task \
  -H "Cookie: session=..." \
  -d '{"agentId":"'$POD_NAME'"}'

# Debería retornar la tarea (solo las del usuario)

📝 NOTAS IMPORTANTES

Terminal Web por Agente

  • Cada agente tiene ttyd + tmux integrado
  • Puerto 7681 expuesto
  • Headless service permite acceso pod-a-pod
  • Frontend proxy para acceder desde navegador

Autenticación MCP

  • Cookie session se pasa en todos los requests
  • Backend valida sesión y extrae userId
  • MCP filtra tareas/proyectos por userId
  • Agentes solo ven tareas de su usuario

Multi-Tenancy

  • Cada usuario puede lanzar N agentes
  • Agentes aislados por userId en DB
  • K8s pods tienen label userId
  • Workspace persistente (PVC) por agente

🐛 TROUBLESHOOTING

Terminal no carga en iframe

  • Verificar proxy en backend funciona
  • Verificar pod tiene puerto 7681 expuesto
  • Verificar ttyd está corriendo: kubectl logs -n agents <pod>

MCP retorna "Unauthorized"

  • Verificar cookie se pasa en request
  • Verificar sesión no expiró
  • Test con curl incluyendo cookie

Agente no se crea

  • Verificar K8s API credentials
  • Verificar namespace agents existe
  • Ver logs del backend

CHECKLIST DE SESIÓN

Al final de la sesión, verificar:

  • Schema tiene campo userId en agents y projects
  • MCP endpoints requieren autenticación
  • Imagen del agente incluye ttyd + tmux
  • StatefulSet configurado para agentes
  • Backend API: launch, delete, my agents
  • Frontend: botón launch agent
  • Frontend: modal con terminal iframe
  • Proxy backend a terminales de agentes
  • Test: lanzar agente → ver terminal → ejecutar claude
  • Test: MCP solo retorna tareas del usuario
  • Código commitado
  • Builds exitosos

🎉 META

Completado hasta ahora: Infraestructura + Backend + Frontend + Agente básico Esta sesión: Multi-Agent + Terminal Integrado + Auth Próximo hito: Preview Environments + CI/CD per Task + Monitoring

¡El MVP multi-usuario está casi listo! 🚀