import { Controller, Get, Post, Put, Delete, Body, Param, NotFoundException, BadRequestException, } from '@nestjs/common'; import * as crypto from 'crypto'; import { AgentInstanceRepository } from '../../../infrastructure/repositories/agent-instance.repository'; import { AgentInstanceDeployService } from '../../../infrastructure/services/agent-instance-deploy.service'; import { AgentInstance } from '../../../domain/entities/agent-instance.entity'; import { ConfigService } from '@nestjs/config'; @Controller('api/v1/agent/instances') export class AgentInstanceController { private readonly claudeApiKey: string; constructor( private readonly instanceRepo: AgentInstanceRepository, private readonly deployService: AgentInstanceDeployService, private readonly configService: ConfigService, ) { this.claudeApiKey = this.configService.get('ANTHROPIC_API_KEY', ''); } @Get() async list() { return this.instanceRepo.findAll(); } @Get(':id') async getOne(@Param('id') id: string) { const inst = await this.instanceRepo.findById(id); if (!inst) throw new NotFoundException(`Instance ${id} not found`); return this.sanitize(inst); } @Post() async create(@Body() body: { name: string; agentType?: string; userId: string; // Pool-server deployment (no server creds needed) usePool?: boolean; // User-owned server deployment serverHost?: string; sshPort?: number; sshUser?: string; sshKey?: string; // plaintext private key, only used if usePool=false }) { if (!body.name || !body.userId) { throw new BadRequestException('name and userId are required'); } const instance = new AgentInstance(); instance.id = crypto.randomUUID(); instance.userId = body.userId; instance.name = body.name; instance.agentType = body.agentType ?? 'openclaw'; instance.containerName = `openclaw-${instance.id.slice(0, 8)}`; instance.status = 'deploying'; instance.config = {}; instance.hostPort = 0; // Will be set by deploy service if (body.usePool !== false) { await this.instanceRepo.save(instance); this.deployService.deployFromPool(instance, this.claudeApiKey) .then(() => this.instanceRepo.save(instance)) .catch(async (err) => { instance.status = 'error'; instance.errorMessage = err.message; await this.instanceRepo.save(instance); }); } else { if (!body.serverHost || !body.sshUser || !body.sshKey) { throw new BadRequestException('serverHost, sshUser, sshKey required for user-owned server'); } instance.serverHost = body.serverHost; instance.sshPort = body.sshPort ?? 22; instance.sshUser = body.sshUser; await this.instanceRepo.save(instance); this.deployService.deployToUserServer(instance, body.sshKey, this.claudeApiKey) .then(() => this.instanceRepo.save(instance)) .catch(async (err) => { instance.status = 'error'; instance.errorMessage = err.message; await this.instanceRepo.save(instance); }); } return this.sanitize(instance); } @Put(':id/stop') async stop(@Param('id') id: string) { const inst = await this.instanceRepo.findById(id); if (!inst) throw new NotFoundException(`Instance ${id} not found`); await this.deployService.stopInstance(inst); inst.status = 'stopped'; return this.sanitize(await this.instanceRepo.save(inst)); } @Put(':id/name') async rename(@Param('id') id: string, @Body() body: { name: string }) { const inst = await this.instanceRepo.findById(id); if (!inst) throw new NotFoundException(`Instance ${id} not found`); inst.name = body.name; return this.sanitize(await this.instanceRepo.save(inst)); } @Delete(':id') async remove(@Param('id') id: string) { const inst = await this.instanceRepo.findById(id); if (!inst) throw new NotFoundException(`Instance ${id} not found`); await this.deployService.removeInstance(inst); inst.status = 'removed'; await this.instanceRepo.save(inst); return { message: 'Instance removed', id }; } private sanitize(inst: AgentInstance) { const { openclawToken, openclawTokenIv, ...safe } = inst; return { ...safe, hasToken: !!openclawToken }; } }