diff --git a/apps/api/src/app.module.ts b/apps/api/src/app.module.ts index 3013608..bddc35b 100644 --- a/apps/api/src/app.module.ts +++ b/apps/api/src/app.module.ts @@ -49,6 +49,7 @@ import { PersonalitiesModule } from "./personalities/personalities.module"; import { WorkspacesModule } from "./workspaces/workspaces.module"; import { AdminModule } from "./admin/admin.module"; import { AgentTemplateModule } from "./agent-template/agent-template.module"; +import { UserAgentModule } from "./user-agent/user-agent.module"; import { TeamsModule } from "./teams/teams.module"; import { ImportModule } from "./import/import.module"; import { ConversationArchiveModule } from "./conversation-archive/conversation-archive.module"; @@ -131,6 +132,7 @@ import { OrchestratorModule } from "./orchestrator/orchestrator.module"; WorkspacesModule, AdminModule, AgentTemplateModule, + UserAgentModule, TeamsModule, ImportModule, ConversationArchiveModule, diff --git a/apps/api/src/user-agent/dto/create-user-agent.dto.ts b/apps/api/src/user-agent/dto/create-user-agent.dto.ts new file mode 100644 index 0000000..35b1b76 --- /dev/null +++ b/apps/api/src/user-agent/dto/create-user-agent.dto.ts @@ -0,0 +1,43 @@ +import { IsString, IsBoolean, IsOptional, IsArray, MinLength } from "class-validator"; + +export class CreateUserAgentDto { + @IsString() + @MinLength(1) + templateId?: string; + + @IsString() + @MinLength(1) + name!: string; + + @IsString() + @MinLength(1) + displayName!: string; + + @IsString() + @MinLength(1) + role!: string; + + @IsString() + @MinLength(1) + personality!: string; + + @IsString() + @IsOptional() + primaryModel?: string; + + @IsArray() + @IsOptional() + fallbackModels?: string[]; + + @IsArray() + @IsOptional() + toolPermissions?: string[]; + + @IsString() + @IsOptional() + discordChannel?: string; + + @IsBoolean() + @IsOptional() + isActive?: boolean; +} diff --git a/apps/api/src/user-agent/dto/update-user-agent.dto.ts b/apps/api/src/user-agent/dto/update-user-agent.dto.ts new file mode 100644 index 0000000..b8164d0 --- /dev/null +++ b/apps/api/src/user-agent/dto/update-user-agent.dto.ts @@ -0,0 +1,4 @@ +import { PartialType } from "@nestjs/mapped-types"; +import { CreateUserAgentDto } from "./create-user-agent.dto"; + +export class UpdateUserAgentDto extends PartialType(CreateUserAgentDto) {} diff --git a/apps/api/src/user-agent/user-agent.controller.ts b/apps/api/src/user-agent/user-agent.controller.ts new file mode 100644 index 0000000..d5923de --- /dev/null +++ b/apps/api/src/user-agent/user-agent.controller.ts @@ -0,0 +1,60 @@ +import { + Controller, + Get, + Post, + Patch, + Delete, + Body, + Param, + UseGuards, + ParseUUIDPipe, +} from "@nestjs/common"; +import { UserAgentService } from "./user-agent.service"; +import { CreateUserAgentDto } from "./dto/create-user-agent.dto"; +import { UpdateUserAgentDto } from "./dto/update-user-agent.dto"; +import { AuthGuard } from "../auth/guards/auth.guard"; +import { CurrentUser } from "../auth/decorators/current-user.decorator"; +import type { AuthUser } from "@mosaic/shared"; + +@Controller("agents") +@UseGuards(AuthGuard) +export class UserAgentController { + constructor(private readonly userAgentService: UserAgentService) {} + + @Get() + findAll(@CurrentUser() user: AuthUser) { + return this.userAgentService.findAll(user.id); + } + + @Get(":id") + findOne(@CurrentUser() user: AuthUser, @Param("id", ParseUUIDPipe) id: string) { + return this.userAgentService.findOne(user.id, id); + } + + @Post() + create(@CurrentUser() user: AuthUser, @Body() dto: CreateUserAgentDto) { + return this.userAgentService.create(user.id, dto); + } + + @Post("from-template/:templateId") + createFromTemplate( + @CurrentUser() user: AuthUser, + @Param("templateId", ParseUUIDPipe) templateId: string + ) { + return this.userAgentService.createFromTemplate(user.id, templateId); + } + + @Patch(":id") + update( + @CurrentUser() user: AuthUser, + @Param("id", ParseUUIDPipe) id: string, + @Body() dto: UpdateUserAgentDto + ) { + return this.userAgentService.update(user.id, id, dto); + } + + @Delete(":id") + remove(@CurrentUser() user: AuthUser, @Param("id", ParseUUIDPipe) id: string) { + return this.userAgentService.remove(user.id, id); + } +} diff --git a/apps/api/src/user-agent/user-agent.module.ts b/apps/api/src/user-agent/user-agent.module.ts new file mode 100644 index 0000000..c22fa67 --- /dev/null +++ b/apps/api/src/user-agent/user-agent.module.ts @@ -0,0 +1,12 @@ +import { Module } from "@nestjs/common"; +import { UserAgentService } from "./user-agent.service"; +import { UserAgentController } from "./user-agent.controller"; +import { PrismaModule } from "../prisma/prisma.module"; + +@Module({ + imports: [PrismaModule], + controllers: [UserAgentController], + providers: [UserAgentService], + exports: [UserAgentService], +}) +export class UserAgentModule {} diff --git a/apps/api/src/user-agent/user-agent.service.ts b/apps/api/src/user-agent/user-agent.service.ts new file mode 100644 index 0000000..ca88c92 --- /dev/null +++ b/apps/api/src/user-agent/user-agent.service.ts @@ -0,0 +1,122 @@ +import { + Injectable, + NotFoundException, + ConflictException, + ForbiddenException, +} from "@nestjs/common"; +import { PrismaService } from "../prisma/prisma.service"; +import { CreateUserAgentDto } from "./dto/create-user-agent.dto"; +import { UpdateUserAgentDto } from "./dto/update-user-agent.dto"; + +@Injectable() +export class UserAgentService { + constructor(private readonly prisma: PrismaService) {} + + async findAll(userId: string) { + return this.prisma.userAgent.findMany({ + where: { userId }, + orderBy: { createdAt: "asc" }, + }); + } + + async findOne(userId: string, id: string) { + const agent = await this.prisma.userAgent.findUnique({ where: { id } }); + if (!agent) throw new NotFoundException(`UserAgent ${id} not found`); + if (agent.userId !== userId) throw new ForbiddenException("Access denied to this agent"); + return agent; + } + + async findByName(userId: string, name: string) { + const agent = await this.prisma.userAgent.findUnique({ + where: { userId_name: { userId, name } }, + }); + if (!agent) throw new NotFoundException(`UserAgent "${name}" not found for user`); + return agent; + } + + async create(userId: string, dto: CreateUserAgentDto) { + // Check for unique name within user scope + const existing = await this.prisma.userAgent.findUnique({ + where: { userId_name: { userId, name: dto.name } }, + }); + if (existing) + throw new ConflictException(`UserAgent "${dto.name}" already exists for this user`); + + // If templateId provided, verify it exists + if (dto.templateId) { + const template = await this.prisma.agentTemplate.findUnique({ + where: { id: dto.templateId }, + }); + if (!template) throw new NotFoundException(`AgentTemplate ${dto.templateId} not found`); + } + + return this.prisma.userAgent.create({ + data: { + userId, + templateId: dto.templateId ?? null, + name: dto.name, + displayName: dto.displayName, + role: dto.role, + personality: dto.personality, + primaryModel: dto.primaryModel ?? null, + fallbackModels: dto.fallbackModels ?? ([] as string[]), + toolPermissions: dto.toolPermissions ?? ([] as string[]), + discordChannel: dto.discordChannel ?? null, + isActive: dto.isActive ?? true, + }, + }); + } + + async createFromTemplate(userId: string, templateId: string) { + const template = await this.prisma.agentTemplate.findUnique({ + where: { id: templateId }, + }); + if (!template) throw new NotFoundException(`AgentTemplate ${templateId} not found`); + + // Check for unique name within user scope + const existing = await this.prisma.userAgent.findUnique({ + where: { userId_name: { userId, name: template.name } }, + }); + if (existing) + throw new ConflictException(`UserAgent "${template.name}" already exists for this user`); + + return this.prisma.userAgent.create({ + data: { + userId, + templateId: template.id, + name: template.name, + displayName: template.displayName, + role: template.role, + personality: template.personality, + primaryModel: template.primaryModel, + fallbackModels: template.fallbackModels as string[], + toolPermissions: template.toolPermissions as string[], + discordChannel: template.discordChannel, + isActive: template.isActive, + }, + }); + } + + async update(userId: string, id: string, dto: UpdateUserAgentDto) { + const agent = await this.findOne(userId, id); + + // If name is being changed, check for uniqueness + if (dto.name && dto.name !== agent.name) { + const existing = await this.prisma.userAgent.findUnique({ + where: { userId_name: { userId, name: dto.name } }, + }); + if (existing) + throw new ConflictException(`UserAgent "${dto.name}" already exists for this user`); + } + + return this.prisma.userAgent.update({ + where: { id }, + data: dto, + }); + } + + async remove(userId: string, id: string) { + await this.findOne(userId, id); + return this.prisma.userAgent.delete({ where: { id } }); + } +} diff --git a/docs/MISSION-MANIFEST.md b/docs/MISSION-MANIFEST.md index 1860eef..daec8f5 100644 --- a/docs/MISSION-MANIFEST.md +++ b/docs/MISSION-MANIFEST.md @@ -24,14 +24,14 @@ ## Milestones -| # | ID | Name | Status | Tasks | Notes | -| --- | ------------- | ------------- | ---------- | -------------- | --------------------- | -| 1 | schema-seed | Schema+Seed | ✅ done | P2-001, P2-002 | PRs #675, #677 merged | -| 2 | admin-crud | Admin CRUD | ✅ done | P2-003 | PR #678 merged | -| 3 | user-crud | User CRUD | 🔄 next | P2-004 | Depends on M2 | -| 4 | agent-routing | Agent Routing | ⬜ pending | P2-005, P2-006 | Depends on M3 | -| 5 | discord-ui | Discord+UI | ⬜ pending | P2-007, P2-008 | Depends on M4 | -| 6 | verification | Verification | ⬜ pending | P2-009, P2-010 | Final gate | +| # | ID | Name | Status | Tasks | Notes | +| --- | ------------- | ------------- | -------------- | -------------- | --------------------- | +| 1 | schema-seed | Schema+Seed | ✅ done | P2-001, P2-002 | PRs #675, #677 merged | +| 2 | admin-crud | Admin CRUD | ✅ done | P2-003 | PR #678 merged | +| 3 | user-crud | User CRUD | 🔄 in-progress | P2-004 | Depends on M2 | +| 4 | agent-routing | Agent Routing | ⬜ pending | P2-005, P2-006 | Depends on M3 | +| 5 | discord-ui | Discord+UI | ⬜ pending | P2-007, P2-008 | Depends on M4 | +| 6 | verification | Verification | ⬜ pending | P2-009, P2-010 | Final gate | ## Task Summary @@ -42,7 +42,7 @@ See `docs/TASKS.md` — MS22 Phase 2 section for full task details. | P2-001 Schema | ✅ done | #675 | AgentTemplate + UserAgent | | P2-002 Seed | ✅ done | #677 | jarvis/builder/medic templates | | P2-003 Admin CRUD | ✅ done | #678 | /admin/agent-templates | -| P2-004 User CRUD | ⬜ not-started | — | | +| P2-004 User CRUD | 🔄 in-progress | — | | | P2-005 Status endpoints | ⬜ not-started | — | | | P2-006 Chat routing | ⬜ not-started | — | | | P2-007 Discord routing | ⬜ not-started | — | | @@ -54,14 +54,15 @@ See `docs/TASKS.md` — MS22 Phase 2 section for full task details. | Phase | Est | Used | | ----------------- | -------- | -------------------- | -| Schema+Seed+CRUD | 30K | ~10K (done directly) | +| Schema+Seed+CRUD | 30K | ~15K (done directly) | | User CRUD+Routing | 40K | — | | Discord+UI | 30K | — | | Verification | 10K | — | -| **Total** | **110K** | **~10K** | +| **Total** | **110K** | **~15K** | ## Session Log -| Date | Work Done | -| ---------- | ------------------------------------------------------------------ | -| 2026-03-04 | P2-001..003 shipped; CI fix; postgres rebuilt; mission initialized | +| Date | Work Done | +| ---------- | ----------------------------------------------------------------------------------------------- | +| 2026-03-04 | Session 2: Fixed CI security audit (multer override), merged PR #678, starting P2-004 User CRUD | +| 2026-03-04 | P2-001..003 shipped; CI fix; postgres rebuilt; mission initialized | diff --git a/docs/TASKS.md b/docs/TASKS.md index c22773d..4f00287 100644 --- a/docs/TASKS.md +++ b/docs/TASKS.md @@ -97,9 +97,9 @@ PRD: `docs/PRD-MS22-P2-AGENT-FLEET.md` | Task ID | Status | Phase | Description | Issue | Scope | Branch | Depends On | Blocks | Assigned Worker | Started | Completed | Est Tokens | Act Tokens | Notes | | ----------- | ----------- | -------- | -------------------------------------------- | -------- | ----- | --------------------------- | ------------- | ------------- | --------------- | ---------- | ---------- | ---------- | ---------- | -------------- | | MS22-P2-001 | done | p2-fleet | Prisma schema: AgentTemplate, UserAgent | TASKS:P2 | api | feat/ms22-p2-agent-schema | MS22-P1a | P2-002,P2-003 | orchestrator | 2026-03-04 | 2026-03-04 | 10K | 3K | PR #675 merged | -| MS22-P2-002 | not-started | p2-fleet | Seed default agents (jarvis, builder, medic) | TASKS:P2 | api | feat/ms22-p2-agent-seed | P2-001 | P2-004 | — | — | — | 5K | — | | -| MS22-P2-003 | not-started | p2-fleet | Agent template CRUD endpoints (admin) | TASKS:P2 | api | feat/ms22-p2-agent-api | P2-001 | P2-005 | — | — | — | 15K | — | | -| MS22-P2-004 | not-started | p2-fleet | User agent CRUD endpoints | TASKS:P2 | api | feat/ms22-p2-agent-api | P2-002,P2-003 | P2-006 | — | — | — | 15K | — | | +| MS22-P2-002 | done | p2-fleet | Seed default agents (jarvis, builder, medic) | TASKS:P2 | api | feat/ms22-p2-agent-seed | P2-001 | P2-004 | orchestrator | 2026-03-04 | 2026-03-04 | 5K | 2K | PR #677 merged | +| MS22-P2-003 | done | p2-fleet | Agent template CRUD endpoints (admin) | TASKS:P2 | api | feat/ms22-p2-agent-crud | P2-001 | P2-005 | orchestrator | 2026-03-04 | 2026-03-04 | 15K | 5K | PR #678 merged | +| MS22-P2-004 | in-progress | p2-fleet | User agent CRUD endpoints | TASKS:P2 | api | feat/ms22-p2-user-agents | P2-002,P2-003 | P2-006 | orchestrator | 2026-03-04 | — | 15K | — | | | MS22-P2-005 | not-started | p2-fleet | Agent status endpoints | TASKS:P2 | api | feat/ms22-p2-agent-api | P2-003 | P2-008 | — | — | — | 10K | — | | | MS22-P2-006 | not-started | p2-fleet | Agent chat routing (select agent by name) | TASKS:P2 | api | feat/ms22-p2-agent-routing | P2-004 | P2-007 | — | — | — | 15K | — | | | MS22-P2-007 | not-started | p2-fleet | Discord channel → agent routing | TASKS:P2 | api | feat/ms22-p2-discord-router | P2-006 | P2-009 | — | — | — | 15K | — | | diff --git a/docs/scratchpads/ms22-p2-named-agent-fleet-20260304.md b/docs/scratchpads/ms22-p2-named-agent-fleet-20260304.md new file mode 100644 index 0000000..42b3194 --- /dev/null +++ b/docs/scratchpads/ms22-p2-named-agent-fleet-20260304.md @@ -0,0 +1,23 @@ +# Mission Scratchpad — MS22-P2 Named Agent Fleet + +> Append-only log. NEVER delete entries. NEVER overwrite sections. +> This is the orchestrator's working memory across sessions. + +## Original Mission Prompt + +``` +(Paste the mission prompt here on first session) +``` + +## Planning Decisions + +## Session Log + +| Session | Date | Milestone | Tasks Done | Outcome | +| ------- | ---------- | ----------- | ---------------------- | ---------------------------------------------------------------------------------------- | +| 2 | 2026-03-04 | M3-UserCRUD | P2-004 in-progress | Fixed CI security audit (multer>=2.1.1), merged PR #678 (Admin CRUD), starting User CRUD | +| 1 | 2026-03-04 | M1+M2 | P2-001, P2-002, P2-003 | Schema, seed, and Admin CRUD complete | + +## Open Questions + +## Corrections