Implement multi-user auth + agent management + terminal proxy
All checks were successful
Build and Push Backend / build (push) Successful in 5s

- Add userId to agents and projects tables (with migrations)
- Create auth middleware for session validation
- Protect MCP endpoints with authentication and user filtering
- Implement agent management API (launch, my, delete)
- Add terminal proxy at /agent-terminal/:agentId with auth
- Update all agent endpoints to verify user ownership

Co-Authored-By: Claude Sonnet 4.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Hector Ros
2026-01-20 17:21:53 +01:00
parent 08e6f66c7d
commit 8382f6645e
8 changed files with 1161 additions and 107 deletions

View File

@@ -5,8 +5,9 @@
import { db } from '../../db/client'
import { agents, tasks } from '../../db/schema'
import { eq } from 'drizzle-orm'
import { eq, and } from 'drizzle-orm'
import { randomUUID } from 'crypto'
import { authenticateRequest } from '../middleware/auth'
/**
* Handle all agent routes
@@ -15,7 +16,34 @@ export async function handleAgentRoutes(req: Request, url: URL): Promise<Respons
const method = req.method
const pathParts = url.pathname.split('/').filter(Boolean)
// GET /api/agents - List all agents
// Authenticate user for protected endpoints
const publicEndpoints = ['heartbeat']
const isPublic = pathParts.length === 4 && publicEndpoints.includes(pathParts[3])
let auth = null
if (!isPublic) {
auth = await authenticateRequest(req)
if (!auth) {
return Response.json(
{ success: false, message: 'Unauthorized' },
{ status: 401 }
)
}
}
const userId = auth?.userId
// GET /api/agents/my - List my agents (authenticated)
if (method === 'GET' && pathParts.length === 3 && pathParts[2] === 'my') {
return await listMyAgents(userId!)
}
// POST /api/agents/launch - Launch new agent (authenticated)
if (method === 'POST' && pathParts.length === 3 && pathParts[2] === 'launch') {
return await launchAgent(userId!, req)
}
// GET /api/agents - List all agents (admin only - for now authenticated user sees all)
if (method === 'GET' && pathParts.length === 2) {
return await listAgents(url)
}
@@ -23,10 +51,10 @@ export async function handleAgentRoutes(req: Request, url: URL): Promise<Respons
// GET /api/agents/:id - Get single agent
if (method === 'GET' && pathParts.length === 3) {
const agentId = pathParts[2]
return await getAgent(agentId)
return await getAgent(agentId, userId!)
}
// POST /api/agents - Register agent
// POST /api/agents - Register agent (called by agent pod on startup)
if (method === 'POST' && pathParts.length === 2) {
return await registerAgent(req)
}
@@ -34,10 +62,10 @@ export async function handleAgentRoutes(req: Request, url: URL): Promise<Respons
// PATCH /api/agents/:id - Update agent
if (method === 'PATCH' && pathParts.length === 3) {
const agentId = pathParts[2]
return await updateAgent(agentId, req)
return await updateAgent(agentId, req, userId!)
}
// POST /api/agents/:id/heartbeat - Update heartbeat
// POST /api/agents/:id/heartbeat - Update heartbeat (public)
if (method === 'POST' && pathParts.length === 4 && pathParts[3] === 'heartbeat') {
const agentId = pathParts[2]
return await updateHeartbeat(agentId)
@@ -46,7 +74,7 @@ export async function handleAgentRoutes(req: Request, url: URL): Promise<Respons
// DELETE /api/agents/:id - Unregister agent
if (method === 'DELETE' && pathParts.length === 3) {
const agentId = pathParts[2]
return await unregisterAgent(agentId)
return await unregisterAgent(agentId, userId!)
}
return new Response('Not Found', { status: 404 })
@@ -82,14 +110,19 @@ async function listAgents(url: URL): Promise<Response> {
}
/**
* Get single agent
* Get single agent (verify ownership)
*/
async function getAgent(agentId: string): Promise<Response> {
async function getAgent(agentId: string, userId: string): Promise<Response> {
try {
const agent = await db
.select()
.from(agents)
.where(eq(agents.id, agentId))
.where(
and(
eq(agents.id, agentId),
eq(agents.userId, userId)
)
)
.limit(1)
if (agent.length === 0) {
@@ -114,16 +147,17 @@ async function getAgent(agentId: string): Promise<Response> {
/**
* Register new agent (called when pod starts)
* userId is extracted from env var passed to pod
*/
async function registerAgent(req: Request): Promise<Response> {
try {
const body = await req.json()
// Validate required fields
if (!body.podName) {
if (!body.podName || !body.userId) {
return Response.json({
success: false,
error: 'podName is required',
error: 'podName and userId are required',
}, { status: 400 })
}
@@ -145,6 +179,7 @@ async function registerAgent(req: Request): Promise<Response> {
const newAgent = {
id: randomUUID(),
userId: body.userId,
podName: body.podName,
k8sNamespace: body.k8sNamespace || 'agents',
status: 'idle' as const,
@@ -169,23 +204,28 @@ async function registerAgent(req: Request): Promise<Response> {
}
/**
* Update agent status and current task
* Update agent status and current task (verify ownership)
*/
async function updateAgent(agentId: string, req: Request): Promise<Response> {
async function updateAgent(agentId: string, req: Request, userId: string): Promise<Response> {
try {
const body = await req.json()
// Check if agent exists
// Check if agent exists and belongs to user
const existing = await db
.select()
.from(agents)
.where(eq(agents.id, agentId))
.where(
and(
eq(agents.id, agentId),
eq(agents.userId, userId)
)
)
.limit(1)
if (existing.length === 0) {
return Response.json({
success: false,
error: 'Agent not found',
error: 'Agent not found or access denied',
}, { status: 404 })
}
@@ -260,21 +300,26 @@ async function updateHeartbeat(agentId: string): Promise<Response> {
}
/**
* Unregister agent (called when pod terminates)
* Unregister agent (called when pod terminates, verify ownership)
*/
async function unregisterAgent(agentId: string): Promise<Response> {
async function unregisterAgent(agentId: string, userId: string): Promise<Response> {
try {
// Check if agent exists
// Check if agent exists and belongs to user
const existing = await db
.select()
.from(agents)
.where(eq(agents.id, agentId))
.where(
and(
eq(agents.id, agentId),
eq(agents.userId, userId)
)
)
.limit(1)
if (existing.length === 0) {
return Response.json({
success: false,
error: 'Agent not found',
error: 'Agent not found or access denied',
}, { status: 404 })
}
@@ -286,9 +331,11 @@ async function unregisterAgent(agentId: string): Promise<Response> {
.where(eq(tasks.id, existing[0].currentTaskId))
}
// Delete agent
// Delete agent from DB
await db.delete(agents).where(eq(agents.id, agentId))
// TODO: Delete K8s pod if it exists
return Response.json({
success: true,
message: 'Agent unregistered',
@@ -301,3 +348,68 @@ async function unregisterAgent(agentId: string): Promise<Response> {
}, { status: 500 })
}
}
/**
* List my agents (filtered by userId)
*/
async function listMyAgents(userId: string): Promise<Response> {
try {
const myAgents = await db
.select()
.from(agents)
.where(eq(agents.userId, userId))
return Response.json({
success: true,
data: myAgents,
count: myAgents.length,
})
} catch (error) {
console.error('Error listing my agents:', error)
return Response.json({
success: false,
error: 'Failed to list agents',
}, { status: 500 })
}
}
/**
* Launch new agent (create pod dynamically)
* TODO: Integrate with K8s API to create pod
*/
async function launchAgent(userId: string, req: Request): Promise<Response> {
try {
const agentId = randomUUID()
const podName = `claude-agent-${userId.slice(0, 8)}-${Date.now()}`
// Create agent record in DB
const newAgent = {
id: agentId,
userId,
podName,
k8sNamespace: 'agents',
status: 'idle' as const,
currentTaskId: null,
tasksCompleted: 0,
lastHeartbeat: new Date(),
}
await db.insert(agents).values(newAgent)
// TODO: Create K8s pod using K8s API
// For now, just return the agent record
// In production, this would call kubectl or use @kubernetes/client-node
return Response.json({
success: true,
data: newAgent,
message: 'Agent launch initiated. Pod will be created shortly.',
}, { status: 201 })
} catch (error) {
console.error('Error launching agent:', error)
return Response.json({
success: false,
error: 'Failed to launch agent',
}, { status: 500 })
}
}