Some checks failed
ci/woodpecker/push/ci Pipeline failed
Co-authored-by: Jason Woltje <jason@diversecanvas.com> Co-committed-by: Jason Woltje <jason@diversecanvas.com>
171 lines
5.8 KiB
TypeScript
171 lines
5.8 KiB
TypeScript
import {
|
|
Body,
|
|
Controller,
|
|
Delete,
|
|
ForbiddenException,
|
|
Get,
|
|
HttpCode,
|
|
HttpStatus,
|
|
Inject,
|
|
NotFoundException,
|
|
Param,
|
|
Patch,
|
|
Post,
|
|
UseGuards,
|
|
} from '@nestjs/common';
|
|
import type { Brain } from '@mosaic/brain';
|
|
import { BRAIN } from '../brain/brain.tokens.js';
|
|
import { AuthGuard } from '../auth/auth.guard.js';
|
|
import { CurrentUser } from '../auth/current-user.decorator.js';
|
|
import { CreateAgentConfigDto, UpdateAgentConfigDto } from './agent-config.dto.js';
|
|
|
|
// ─── M4-011 helpers ──────────────────────────────────────────────────────────
|
|
|
|
type CapabilityFields = {
|
|
domains?: string[] | null;
|
|
preferredModel?: string | null;
|
|
preferredProvider?: string | null;
|
|
toolSets?: string[] | null;
|
|
};
|
|
|
|
/** Extract capability shorthand fields from the DTO (undefined if none provided). */
|
|
function buildCapabilities(dto: CapabilityFields): Record<string, unknown> | undefined {
|
|
const hasAny =
|
|
dto.domains !== undefined ||
|
|
dto.preferredModel !== undefined ||
|
|
dto.preferredProvider !== undefined ||
|
|
dto.toolSets !== undefined;
|
|
|
|
if (!hasAny) return undefined;
|
|
|
|
const cap: Record<string, unknown> = {};
|
|
if (dto.domains !== undefined) cap['domains'] = dto.domains;
|
|
if (dto.preferredModel !== undefined) cap['preferredModel'] = dto.preferredModel;
|
|
if (dto.preferredProvider !== undefined) cap['preferredProvider'] = dto.preferredProvider;
|
|
if (dto.toolSets !== undefined) cap['toolSets'] = dto.toolSets;
|
|
return cap;
|
|
}
|
|
|
|
/** Merge capabilities into the config object, preserving other config keys. */
|
|
function mergeCapabilities(
|
|
existing: Record<string, unknown> | null | undefined,
|
|
capabilities: Record<string, unknown> | undefined,
|
|
): Record<string, unknown> | undefined {
|
|
if (capabilities === undefined && existing === undefined) return undefined;
|
|
if (capabilities === undefined) return existing ?? undefined;
|
|
|
|
const base = existing ?? {};
|
|
const existingCap =
|
|
typeof base['capabilities'] === 'object' && base['capabilities'] !== null
|
|
? (base['capabilities'] as Record<string, unknown>)
|
|
: {};
|
|
|
|
return {
|
|
...base,
|
|
capabilities: { ...existingCap, ...capabilities },
|
|
};
|
|
}
|
|
|
|
@Controller('api/agents')
|
|
@UseGuards(AuthGuard)
|
|
export class AgentConfigsController {
|
|
constructor(@Inject(BRAIN) private readonly brain: Brain) {}
|
|
|
|
@Get()
|
|
async list(@CurrentUser() user: { id: string; role?: string }) {
|
|
return this.brain.agents.findAccessible(user.id);
|
|
}
|
|
|
|
@Get(':id')
|
|
async findOne(@Param('id') id: string, @CurrentUser() user: { id: string }) {
|
|
const agent = await this.brain.agents.findById(id);
|
|
if (!agent) throw new NotFoundException('Agent not found');
|
|
if (!agent.isSystem && agent.ownerId !== user.id) {
|
|
throw new ForbiddenException('Agent does not belong to the current user');
|
|
}
|
|
return agent;
|
|
}
|
|
|
|
@Post()
|
|
async create(@Body() dto: CreateAgentConfigDto, @CurrentUser() user: { id: string }) {
|
|
// Merge capability shorthand fields into config.capabilities (M4-011)
|
|
const capabilities = buildCapabilities(dto);
|
|
const config = mergeCapabilities(dto.config, capabilities);
|
|
|
|
return this.brain.agents.create({
|
|
name: dto.name,
|
|
provider: dto.provider,
|
|
model: dto.model,
|
|
status: dto.status,
|
|
projectId: dto.projectId,
|
|
systemPrompt: dto.systemPrompt,
|
|
allowedTools: dto.allowedTools,
|
|
skills: dto.skills,
|
|
isSystem: false,
|
|
config,
|
|
ownerId: user.id,
|
|
});
|
|
}
|
|
|
|
@Patch(':id')
|
|
async update(
|
|
@Param('id') id: string,
|
|
@Body() dto: UpdateAgentConfigDto,
|
|
@CurrentUser() user: { id: string; role?: string },
|
|
) {
|
|
const agent = await this.brain.agents.findById(id);
|
|
if (!agent) throw new NotFoundException('Agent not found');
|
|
if (agent.isSystem && user.role !== 'admin') {
|
|
throw new ForbiddenException('Only admins can update system agents');
|
|
}
|
|
if (!agent.isSystem && agent.ownerId !== user.id) {
|
|
throw new ForbiddenException('Agent does not belong to the current user');
|
|
}
|
|
|
|
// Merge capability shorthand fields into config.capabilities (M4-011)
|
|
const capabilities = buildCapabilities(dto);
|
|
const baseConfig =
|
|
dto.config !== undefined
|
|
? dto.config
|
|
: (agent.config as Record<string, unknown> | null | undefined);
|
|
const config = mergeCapabilities(baseConfig ?? undefined, capabilities);
|
|
|
|
// Pass ownerId for user agents so the repo WHERE clause enforces ownership.
|
|
// For system agents (admin path) pass undefined so the WHERE matches only on id.
|
|
const ownerId = agent.isSystem ? undefined : user.id;
|
|
const updated = await this.brain.agents.update(
|
|
id,
|
|
{
|
|
name: dto.name,
|
|
provider: dto.provider,
|
|
model: dto.model,
|
|
status: dto.status,
|
|
projectId: dto.projectId,
|
|
systemPrompt: dto.systemPrompt,
|
|
allowedTools: dto.allowedTools,
|
|
skills: dto.skills,
|
|
config: capabilities !== undefined || dto.config !== undefined ? config : undefined,
|
|
},
|
|
ownerId,
|
|
);
|
|
if (!updated) throw new NotFoundException('Agent not found');
|
|
return updated;
|
|
}
|
|
|
|
@Delete(':id')
|
|
@HttpCode(HttpStatus.NO_CONTENT)
|
|
async remove(@Param('id') id: string, @CurrentUser() user: { id: string; role?: string }) {
|
|
const agent = await this.brain.agents.findById(id);
|
|
if (!agent) throw new NotFoundException('Agent not found');
|
|
if (agent.isSystem) {
|
|
throw new ForbiddenException('Cannot delete system agents');
|
|
}
|
|
if (agent.ownerId !== user.id) {
|
|
throw new ForbiddenException('Agent does not belong to the current user');
|
|
}
|
|
// Pass ownerId so the repo WHERE clause enforces ownership at the DB level.
|
|
const deleted = await this.brain.agents.remove(id, user.id);
|
|
if (!deleted) throw new NotFoundException('Agent not found');
|
|
}
|
|
}
|