Document completed session: - Frontend with hierarchical navigation + task deletion - MCP HTTP endpoints (5 tools) - Agent with Ubuntu 24.04 + Node 24 + Bun + tooling - Terminal web at claude.fuq.tv - All deployed and functional Prepare next session: Multi-Agent + Terminal Integrated + Auth - Integrate ttyd+tmux into each agent - View agent terminals from app.fuq.tv - MCP authentication by user - Launch/delete agents from UI Co-Authored-By: Claude Sonnet 4.5 (1M context) <noreply@anthropic.com>
612 lines
15 KiB
Markdown
612 lines
15 KiB
Markdown
# 📋 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<string, string>)
|
|
|
|
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<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`:
|
|
|
|
```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) <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`:
|
|
|
|
```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<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`:
|
|
|
|
```typescript
|
|
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`:
|
|
|
|
```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 (
|
|
<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`:
|
|
|
|
```typescript
|
|
// 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
|
|
|
|
```bash
|
|
# 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! 🚀**
|