Compare commits
1 Commits
d4c2a0107b
...
feat/ms21-
| Author | SHA1 | Date | |
|---|---|---|---|
| b174ba4f14 |
@@ -34,9 +34,3 @@ CVE-2026-26996 # HIGH: minimatch DoS via specially crafted glob patterns (needs
|
||||
# OpenBao 2.5.0 compiled with Go 1.25.6, fix needs Go >= 1.25.7.
|
||||
# Cannot build OpenBao from source (large project). Waiting for upstream release.
|
||||
CVE-2025-68121 # CRITICAL: crypto/tls session resumption
|
||||
|
||||
# === multer CVEs (upstream via @nestjs/platform-express) ===
|
||||
# multer <2.1.0 — waiting on NestJS to update their dependency
|
||||
# These are DoS vulnerabilities in file upload handling
|
||||
GHSA-xf7r-hgr6-v32p # HIGH: DoS via incomplete cleanup
|
||||
GHSA-v52c-386h-88mc # HIGH: DoS via resource exhaustion
|
||||
|
||||
@@ -1,24 +0,0 @@
|
||||
-- CreateTable
|
||||
CREATE TABLE "agent_memories" (
|
||||
"id" UUID NOT NULL,
|
||||
"workspace_id" UUID NOT NULL,
|
||||
"agent_id" TEXT NOT NULL,
|
||||
"key" TEXT NOT NULL,
|
||||
"value" JSONB NOT NULL,
|
||||
"created_at" TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updated_at" TIMESTAMPTZ NOT NULL,
|
||||
|
||||
CONSTRAINT "agent_memories_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "agent_memories_workspace_id_agent_id_key_key" ON "agent_memories"("workspace_id", "agent_id", "key");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "agent_memories_workspace_id_idx" ON "agent_memories"("workspace_id");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "agent_memories_agent_id_idx" ON "agent_memories"("agent_id");
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "agent_memories" ADD CONSTRAINT "agent_memories_workspace_id_fkey" FOREIGN KEY ("workspace_id") REFERENCES "workspaces"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
@@ -298,7 +298,6 @@ model Workspace {
|
||||
agents Agent[]
|
||||
agentSessions AgentSession[]
|
||||
agentTasks AgentTask[]
|
||||
agentMemories AgentMemory[]
|
||||
userLayouts UserLayout[]
|
||||
knowledgeEntries KnowledgeEntry[]
|
||||
knowledgeTags KnowledgeTag[]
|
||||
@@ -736,23 +735,6 @@ model AgentSession {
|
||||
@@map("agent_sessions")
|
||||
}
|
||||
|
||||
model AgentMemory {
|
||||
id String @id @default(uuid()) @db.Uuid
|
||||
workspaceId String @map("workspace_id") @db.Uuid
|
||||
agentId String @map("agent_id")
|
||||
key String
|
||||
value Json
|
||||
createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz
|
||||
updatedAt DateTime @updatedAt @map("updated_at") @db.Timestamptz
|
||||
|
||||
workspace Workspace @relation(fields: [workspaceId], references: [id], onDelete: Cascade)
|
||||
|
||||
@@unique([workspaceId, agentId, key])
|
||||
@@index([workspaceId])
|
||||
@@index([agentId])
|
||||
@@map("agent_memories")
|
||||
}
|
||||
|
||||
model WidgetDefinition {
|
||||
id String @id @default(uuid()) @db.Uuid
|
||||
|
||||
|
||||
@@ -24,15 +24,7 @@ describe("AdminService", () => {
|
||||
workspaceMember: {
|
||||
create: vi.fn(),
|
||||
},
|
||||
session: {
|
||||
deleteMany: vi.fn(),
|
||||
},
|
||||
$transaction: vi.fn(async (ops) => {
|
||||
if (typeof ops === "function") {
|
||||
return ops(mockPrismaService);
|
||||
}
|
||||
return Promise.all(ops);
|
||||
}),
|
||||
$transaction: vi.fn(),
|
||||
};
|
||||
|
||||
const mockAdminId = "550e8400-e29b-41d4-a716-446655440001";
|
||||
@@ -90,6 +82,10 @@ describe("AdminService", () => {
|
||||
service = module.get<AdminService>(AdminService);
|
||||
|
||||
vi.clearAllMocks();
|
||||
|
||||
mockPrismaService.$transaction.mockImplementation(async (fn: (tx: unknown) => unknown) => {
|
||||
return fn(mockPrismaService);
|
||||
});
|
||||
});
|
||||
|
||||
it("should be defined", () => {
|
||||
@@ -329,13 +325,12 @@ describe("AdminService", () => {
|
||||
});
|
||||
|
||||
describe("deactivateUser", () => {
|
||||
it("should set deactivatedAt and invalidate sessions", async () => {
|
||||
it("should set deactivatedAt on the user", async () => {
|
||||
mockPrismaService.user.findUnique.mockResolvedValue(mockUser);
|
||||
mockPrismaService.user.update.mockResolvedValue({
|
||||
...mockUser,
|
||||
deactivatedAt: new Date(),
|
||||
});
|
||||
mockPrismaService.session.deleteMany.mockResolvedValue({ count: 3 });
|
||||
|
||||
const result = await service.deactivateUser(mockUserId);
|
||||
|
||||
@@ -346,7 +341,6 @@ describe("AdminService", () => {
|
||||
data: { deactivatedAt: expect.any(Date) },
|
||||
})
|
||||
);
|
||||
expect(mockPrismaService.session.deleteMany).toHaveBeenCalledWith({ where: { userId: mockUserId } });
|
||||
});
|
||||
|
||||
it("should throw NotFoundException if user does not exist", async () => {
|
||||
|
||||
@@ -192,22 +192,19 @@ export class AdminService {
|
||||
throw new BadRequestException(`User ${id} is already deactivated`);
|
||||
}
|
||||
|
||||
const [user] = await this.prisma.$transaction([
|
||||
this.prisma.user.update({
|
||||
where: { id },
|
||||
data: { deactivatedAt: new Date() },
|
||||
include: {
|
||||
workspaceMemberships: {
|
||||
include: {
|
||||
workspace: { select: { id: true, name: true } },
|
||||
},
|
||||
const user = await this.prisma.user.update({
|
||||
where: { id },
|
||||
data: { deactivatedAt: new Date() },
|
||||
include: {
|
||||
workspaceMemberships: {
|
||||
include: {
|
||||
workspace: { select: { id: true, name: true } },
|
||||
},
|
||||
},
|
||||
}),
|
||||
this.prisma.session.deleteMany({ where: { userId: id } }),
|
||||
]);
|
||||
},
|
||||
});
|
||||
|
||||
this.logger.log(`User deactivated and sessions invalidated: ${id}`);
|
||||
this.logger.log(`User deactivated: ${id}`);
|
||||
|
||||
return {
|
||||
id: user.id,
|
||||
|
||||
@@ -1,102 +0,0 @@
|
||||
import { Test, TestingModule } from "@nestjs/testing";
|
||||
import { AgentMemoryController } from "./agent-memory.controller";
|
||||
import { AgentMemoryService } from "./agent-memory.service";
|
||||
import { AuthGuard } from "../auth/guards/auth.guard";
|
||||
import { WorkspaceGuard, PermissionGuard } from "../common/guards";
|
||||
import { describe, it, expect, beforeEach, vi } from "vitest";
|
||||
|
||||
describe("AgentMemoryController", () => {
|
||||
let controller: AgentMemoryController;
|
||||
|
||||
const mockAgentMemoryService = {
|
||||
upsert: vi.fn(),
|
||||
findAll: vi.fn(),
|
||||
findOne: vi.fn(),
|
||||
remove: vi.fn(),
|
||||
};
|
||||
|
||||
const mockGuard = { canActivate: vi.fn(() => true) };
|
||||
|
||||
beforeEach(async () => {
|
||||
const module: TestingModule = await Test.createTestingModule({
|
||||
controllers: [AgentMemoryController],
|
||||
providers: [
|
||||
{
|
||||
provide: AgentMemoryService,
|
||||
useValue: mockAgentMemoryService,
|
||||
},
|
||||
],
|
||||
})
|
||||
.overrideGuard(AuthGuard)
|
||||
.useValue(mockGuard)
|
||||
.overrideGuard(WorkspaceGuard)
|
||||
.useValue(mockGuard)
|
||||
.overrideGuard(PermissionGuard)
|
||||
.useValue(mockGuard)
|
||||
.compile();
|
||||
|
||||
controller = module.get<AgentMemoryController>(AgentMemoryController);
|
||||
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
const workspaceId = "workspace-1";
|
||||
const agentId = "agent-1";
|
||||
const key = "context";
|
||||
|
||||
describe("upsert", () => {
|
||||
it("should upsert a memory entry", async () => {
|
||||
const dto = { value: { foo: "bar" } };
|
||||
const mockEntry = { id: "mem-1", workspaceId, agentId, key, value: dto.value };
|
||||
|
||||
mockAgentMemoryService.upsert.mockResolvedValue(mockEntry);
|
||||
|
||||
const result = await controller.upsert(agentId, key, dto, workspaceId);
|
||||
|
||||
expect(mockAgentMemoryService.upsert).toHaveBeenCalledWith(workspaceId, agentId, key, dto);
|
||||
expect(result).toEqual(mockEntry);
|
||||
});
|
||||
});
|
||||
|
||||
describe("findAll", () => {
|
||||
it("should list all memory entries for an agent", async () => {
|
||||
const mockEntries = [
|
||||
{ id: "mem-1", key: "a", value: 1 },
|
||||
{ id: "mem-2", key: "b", value: 2 },
|
||||
];
|
||||
|
||||
mockAgentMemoryService.findAll.mockResolvedValue(mockEntries);
|
||||
|
||||
const result = await controller.findAll(agentId, workspaceId);
|
||||
|
||||
expect(mockAgentMemoryService.findAll).toHaveBeenCalledWith(workspaceId, agentId);
|
||||
expect(result).toEqual(mockEntries);
|
||||
});
|
||||
});
|
||||
|
||||
describe("findOne", () => {
|
||||
it("should get a single memory entry", async () => {
|
||||
const mockEntry = { id: "mem-1", key, value: "v" };
|
||||
|
||||
mockAgentMemoryService.findOne.mockResolvedValue(mockEntry);
|
||||
|
||||
const result = await controller.findOne(agentId, key, workspaceId);
|
||||
|
||||
expect(mockAgentMemoryService.findOne).toHaveBeenCalledWith(workspaceId, agentId, key);
|
||||
expect(result).toEqual(mockEntry);
|
||||
});
|
||||
});
|
||||
|
||||
describe("remove", () => {
|
||||
it("should delete a memory entry", async () => {
|
||||
const mockResponse = { message: "Memory entry deleted successfully" };
|
||||
|
||||
mockAgentMemoryService.remove.mockResolvedValue(mockResponse);
|
||||
|
||||
const result = await controller.remove(agentId, key, workspaceId);
|
||||
|
||||
expect(mockAgentMemoryService.remove).toHaveBeenCalledWith(workspaceId, agentId, key);
|
||||
expect(result).toEqual(mockResponse);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,89 +0,0 @@
|
||||
import {
|
||||
Controller,
|
||||
Get,
|
||||
Put,
|
||||
Delete,
|
||||
Body,
|
||||
Param,
|
||||
UseGuards,
|
||||
HttpCode,
|
||||
HttpStatus,
|
||||
} from "@nestjs/common";
|
||||
import { AgentMemoryService } from "./agent-memory.service";
|
||||
import { UpsertAgentMemoryDto } from "./dto";
|
||||
import { AuthGuard } from "../auth/guards/auth.guard";
|
||||
import { WorkspaceGuard, PermissionGuard } from "../common/guards";
|
||||
import { Workspace, Permission, RequirePermission } from "../common/decorators";
|
||||
|
||||
/**
|
||||
* Controller for per-agent key/value memory endpoints.
|
||||
* All endpoints require authentication and workspace context.
|
||||
*
|
||||
* Guards are applied in order:
|
||||
* 1. AuthGuard - Verifies user authentication
|
||||
* 2. WorkspaceGuard - Validates workspace access
|
||||
* 3. PermissionGuard - Checks role-based permissions
|
||||
*/
|
||||
@Controller("agents/:agentId/memory")
|
||||
@UseGuards(AuthGuard, WorkspaceGuard, PermissionGuard)
|
||||
export class AgentMemoryController {
|
||||
constructor(private readonly agentMemoryService: AgentMemoryService) {}
|
||||
|
||||
/**
|
||||
* PUT /api/agents/:agentId/memory/:key
|
||||
* Upsert a memory entry for an agent
|
||||
* Requires: MEMBER role or higher
|
||||
*/
|
||||
@Put(":key")
|
||||
@RequirePermission(Permission.WORKSPACE_MEMBER)
|
||||
async upsert(
|
||||
@Param("agentId") agentId: string,
|
||||
@Param("key") key: string,
|
||||
@Body() dto: UpsertAgentMemoryDto,
|
||||
@Workspace() workspaceId: string
|
||||
) {
|
||||
return this.agentMemoryService.upsert(workspaceId, agentId, key, dto);
|
||||
}
|
||||
|
||||
/**
|
||||
* GET /api/agents/:agentId/memory
|
||||
* List all memory entries for an agent
|
||||
* Requires: Any workspace member (including GUEST)
|
||||
*/
|
||||
@Get()
|
||||
@RequirePermission(Permission.WORKSPACE_ANY)
|
||||
async findAll(@Param("agentId") agentId: string, @Workspace() workspaceId: string) {
|
||||
return this.agentMemoryService.findAll(workspaceId, agentId);
|
||||
}
|
||||
|
||||
/**
|
||||
* GET /api/agents/:agentId/memory/:key
|
||||
* Get a single memory entry by key
|
||||
* Requires: Any workspace member (including GUEST)
|
||||
*/
|
||||
@Get(":key")
|
||||
@RequirePermission(Permission.WORKSPACE_ANY)
|
||||
async findOne(
|
||||
@Param("agentId") agentId: string,
|
||||
@Param("key") key: string,
|
||||
@Workspace() workspaceId: string
|
||||
) {
|
||||
return this.agentMemoryService.findOne(workspaceId, agentId, key);
|
||||
}
|
||||
|
||||
/**
|
||||
* DELETE /api/agents/:agentId/memory/:key
|
||||
* Remove a memory entry
|
||||
* Requires: MEMBER role or higher
|
||||
*/
|
||||
@Delete(":key")
|
||||
@HttpCode(HttpStatus.OK)
|
||||
@RequirePermission(Permission.WORKSPACE_MEMBER)
|
||||
async remove(
|
||||
@Param("agentId") agentId: string,
|
||||
@Param("key") key: string,
|
||||
@Workspace() workspaceId: string
|
||||
) {
|
||||
return this.agentMemoryService.remove(workspaceId, agentId, key);
|
||||
}
|
||||
}
|
||||
@@ -1,13 +0,0 @@
|
||||
import { Module } from "@nestjs/common";
|
||||
import { AgentMemoryController } from "./agent-memory.controller";
|
||||
import { AgentMemoryService } from "./agent-memory.service";
|
||||
import { PrismaModule } from "../prisma/prisma.module";
|
||||
import { AuthModule } from "../auth/auth.module";
|
||||
|
||||
@Module({
|
||||
imports: [PrismaModule, AuthModule],
|
||||
controllers: [AgentMemoryController],
|
||||
providers: [AgentMemoryService],
|
||||
exports: [AgentMemoryService],
|
||||
})
|
||||
export class AgentMemoryModule {}
|
||||
@@ -1,126 +0,0 @@
|
||||
import { Test, TestingModule } from "@nestjs/testing";
|
||||
import { AgentMemoryService } from "./agent-memory.service";
|
||||
import { PrismaService } from "../prisma/prisma.service";
|
||||
import { NotFoundException } from "@nestjs/common";
|
||||
import { describe, it, expect, beforeEach, vi } from "vitest";
|
||||
|
||||
describe("AgentMemoryService", () => {
|
||||
let service: AgentMemoryService;
|
||||
|
||||
const mockPrismaService = {
|
||||
agentMemory: {
|
||||
upsert: vi.fn(),
|
||||
findMany: vi.fn(),
|
||||
findUnique: vi.fn(),
|
||||
delete: vi.fn(),
|
||||
},
|
||||
};
|
||||
|
||||
beforeEach(async () => {
|
||||
const module: TestingModule = await Test.createTestingModule({
|
||||
providers: [
|
||||
AgentMemoryService,
|
||||
{
|
||||
provide: PrismaService,
|
||||
useValue: mockPrismaService,
|
||||
},
|
||||
],
|
||||
}).compile();
|
||||
|
||||
service = module.get<AgentMemoryService>(AgentMemoryService);
|
||||
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
const workspaceId = "workspace-1";
|
||||
const agentId = "agent-1";
|
||||
const key = "session-context";
|
||||
|
||||
describe("upsert", () => {
|
||||
it("should upsert a memory entry", async () => {
|
||||
const dto = { value: { data: "some context" } };
|
||||
const mockEntry = {
|
||||
id: "mem-1",
|
||||
workspaceId,
|
||||
agentId,
|
||||
key,
|
||||
value: dto.value,
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
};
|
||||
|
||||
mockPrismaService.agentMemory.upsert.mockResolvedValue(mockEntry);
|
||||
|
||||
const result = await service.upsert(workspaceId, agentId, key, dto);
|
||||
|
||||
expect(mockPrismaService.agentMemory.upsert).toHaveBeenCalledWith({
|
||||
where: { workspaceId_agentId_key: { workspaceId, agentId, key } },
|
||||
create: { workspaceId, agentId, key, value: dto.value },
|
||||
update: { value: dto.value },
|
||||
});
|
||||
expect(result).toEqual(mockEntry);
|
||||
});
|
||||
});
|
||||
|
||||
describe("findAll", () => {
|
||||
it("should return all memory entries for an agent", async () => {
|
||||
const mockEntries = [
|
||||
{ id: "mem-1", key: "a", value: 1 },
|
||||
{ id: "mem-2", key: "b", value: 2 },
|
||||
];
|
||||
|
||||
mockPrismaService.agentMemory.findMany.mockResolvedValue(mockEntries);
|
||||
|
||||
const result = await service.findAll(workspaceId, agentId);
|
||||
|
||||
expect(mockPrismaService.agentMemory.findMany).toHaveBeenCalledWith({
|
||||
where: { workspaceId, agentId },
|
||||
orderBy: { key: "asc" },
|
||||
});
|
||||
expect(result).toEqual(mockEntries);
|
||||
});
|
||||
});
|
||||
|
||||
describe("findOne", () => {
|
||||
it("should return a memory entry by key", async () => {
|
||||
const mockEntry = { id: "mem-1", workspaceId, agentId, key, value: "ctx" };
|
||||
|
||||
mockPrismaService.agentMemory.findUnique.mockResolvedValue(mockEntry);
|
||||
|
||||
const result = await service.findOne(workspaceId, agentId, key);
|
||||
|
||||
expect(mockPrismaService.agentMemory.findUnique).toHaveBeenCalledWith({
|
||||
where: { workspaceId_agentId_key: { workspaceId, agentId, key } },
|
||||
});
|
||||
expect(result).toEqual(mockEntry);
|
||||
});
|
||||
|
||||
it("should throw NotFoundException when key not found", async () => {
|
||||
mockPrismaService.agentMemory.findUnique.mockResolvedValue(null);
|
||||
|
||||
await expect(service.findOne(workspaceId, agentId, key)).rejects.toThrow(NotFoundException);
|
||||
});
|
||||
});
|
||||
|
||||
describe("remove", () => {
|
||||
it("should delete a memory entry", async () => {
|
||||
const mockEntry = { id: "mem-1", workspaceId, agentId, key, value: "x" };
|
||||
|
||||
mockPrismaService.agentMemory.findUnique.mockResolvedValue(mockEntry);
|
||||
mockPrismaService.agentMemory.delete.mockResolvedValue(mockEntry);
|
||||
|
||||
const result = await service.remove(workspaceId, agentId, key);
|
||||
|
||||
expect(mockPrismaService.agentMemory.delete).toHaveBeenCalledWith({
|
||||
where: { workspaceId_agentId_key: { workspaceId, agentId, key } },
|
||||
});
|
||||
expect(result).toEqual({ message: "Memory entry deleted successfully" });
|
||||
});
|
||||
|
||||
it("should throw NotFoundException when key not found", async () => {
|
||||
mockPrismaService.agentMemory.findUnique.mockResolvedValue(null);
|
||||
|
||||
await expect(service.remove(workspaceId, agentId, key)).rejects.toThrow(NotFoundException);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,79 +0,0 @@
|
||||
import { Injectable, NotFoundException } from "@nestjs/common";
|
||||
import { PrismaService } from "../prisma/prisma.service";
|
||||
import { Prisma } from "@prisma/client";
|
||||
import type { UpsertAgentMemoryDto } from "./dto";
|
||||
|
||||
@Injectable()
|
||||
export class AgentMemoryService {
|
||||
constructor(private readonly prisma: PrismaService) {}
|
||||
|
||||
/**
|
||||
* Upsert a memory entry for an agent.
|
||||
*/
|
||||
async upsert(workspaceId: string, agentId: string, key: string, dto: UpsertAgentMemoryDto) {
|
||||
return this.prisma.agentMemory.upsert({
|
||||
where: {
|
||||
workspaceId_agentId_key: { workspaceId, agentId, key },
|
||||
},
|
||||
create: {
|
||||
workspaceId,
|
||||
agentId,
|
||||
key,
|
||||
value: dto.value as Prisma.InputJsonValue,
|
||||
},
|
||||
update: {
|
||||
value: dto.value as Prisma.InputJsonValue,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* List all memory entries for an agent in a workspace.
|
||||
*/
|
||||
async findAll(workspaceId: string, agentId: string) {
|
||||
return this.prisma.agentMemory.findMany({
|
||||
where: { workspaceId, agentId },
|
||||
orderBy: { key: "asc" },
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a single memory entry by key.
|
||||
*/
|
||||
async findOne(workspaceId: string, agentId: string, key: string) {
|
||||
const entry = await this.prisma.agentMemory.findUnique({
|
||||
where: {
|
||||
workspaceId_agentId_key: { workspaceId, agentId, key },
|
||||
},
|
||||
});
|
||||
|
||||
if (!entry) {
|
||||
throw new NotFoundException(`Memory key "${key}" not found for agent "${agentId}"`);
|
||||
}
|
||||
|
||||
return entry;
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete a memory entry by key.
|
||||
*/
|
||||
async remove(workspaceId: string, agentId: string, key: string) {
|
||||
const entry = await this.prisma.agentMemory.findUnique({
|
||||
where: {
|
||||
workspaceId_agentId_key: { workspaceId, agentId, key },
|
||||
},
|
||||
});
|
||||
|
||||
if (!entry) {
|
||||
throw new NotFoundException(`Memory key "${key}" not found for agent "${agentId}"`);
|
||||
}
|
||||
|
||||
await this.prisma.agentMemory.delete({
|
||||
where: {
|
||||
workspaceId_agentId_key: { workspaceId, agentId, key },
|
||||
},
|
||||
});
|
||||
|
||||
return { message: "Memory entry deleted successfully" };
|
||||
}
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
export * from "./upsert-agent-memory.dto";
|
||||
@@ -1,10 +0,0 @@
|
||||
import { IsNotEmpty } from "class-validator";
|
||||
|
||||
/**
|
||||
* DTO for upserting an agent memory entry.
|
||||
* The value accepts any JSON-serializable data.
|
||||
*/
|
||||
export class UpsertAgentMemoryDto {
|
||||
@IsNotEmpty({ message: "value must not be empty" })
|
||||
value!: unknown;
|
||||
}
|
||||
@@ -27,7 +27,6 @@ import { LlmUsageModule } from "./llm-usage/llm-usage.module";
|
||||
import { BrainModule } from "./brain/brain.module";
|
||||
import { CronModule } from "./cron/cron.module";
|
||||
import { AgentTasksModule } from "./agent-tasks/agent-tasks.module";
|
||||
import { AgentMemoryModule } from "./agent-memory/agent-memory.module";
|
||||
import { ValkeyModule } from "./valkey/valkey.module";
|
||||
import { BullMqModule } from "./bullmq/bullmq.module";
|
||||
import { StitcherModule } from "./stitcher/stitcher.module";
|
||||
@@ -101,7 +100,6 @@ import { RlsContextInterceptor } from "./common/interceptors/rls-context.interce
|
||||
BrainModule,
|
||||
CronModule,
|
||||
AgentTasksModule,
|
||||
AgentMemoryModule,
|
||||
RunnerJobsModule,
|
||||
JobEventsModule,
|
||||
JobStepsModule,
|
||||
|
||||
@@ -1,60 +0,0 @@
|
||||
import { describe, it, expect, vi, beforeEach } from "vitest";
|
||||
import * as client from "./client";
|
||||
import { fetchAdminUsers, inviteUser, updateUser, deactivateUser } from "./admin";
|
||||
|
||||
vi.mock("./client");
|
||||
|
||||
beforeEach((): void => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
describe("fetchAdminUsers", (): void => {
|
||||
it("calls admin/users endpoint without params when none provided", async (): Promise<void> => {
|
||||
vi.mocked(client.apiGet).mockResolvedValueOnce({ data: [], meta: {} } as never);
|
||||
await fetchAdminUsers();
|
||||
expect(client.apiGet).toHaveBeenCalledWith("/api/admin/users");
|
||||
});
|
||||
|
||||
it("appends page and limit params when provided", async (): Promise<void> => {
|
||||
vi.mocked(client.apiGet).mockResolvedValueOnce({ data: [], meta: {} } as never);
|
||||
await fetchAdminUsers(2, 50);
|
||||
expect(client.apiGet).toHaveBeenCalledWith("/api/admin/users?page=2&limit=50");
|
||||
});
|
||||
|
||||
it("throws on API error", async (): Promise<void> => {
|
||||
vi.mocked(client.apiGet).mockRejectedValueOnce(new Error("Network error"));
|
||||
await expect(fetchAdminUsers()).rejects.toThrow("Network error");
|
||||
});
|
||||
});
|
||||
|
||||
describe("inviteUser", (): void => {
|
||||
it("posts to invite endpoint", async (): Promise<void> => {
|
||||
vi.mocked(client.apiPost).mockResolvedValueOnce({ id: "inv-1" } as never);
|
||||
await inviteUser({
|
||||
email: "a@b.com",
|
||||
name: "Alice",
|
||||
workspaceId: "ws-1",
|
||||
role: "MEMBER" as never,
|
||||
});
|
||||
expect(client.apiPost).toHaveBeenCalledWith(
|
||||
"/api/admin/users/invite",
|
||||
expect.objectContaining({ email: "a@b.com" })
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("updateUser", (): void => {
|
||||
it("patches correct endpoint with dto", async (): Promise<void> => {
|
||||
vi.mocked(client.apiPatch).mockResolvedValueOnce({ id: "u1", name: "Bob" } as never);
|
||||
await updateUser("u1", { name: "Bob" });
|
||||
expect(client.apiPatch).toHaveBeenCalledWith("/api/admin/users/u1", { name: "Bob" });
|
||||
});
|
||||
});
|
||||
|
||||
describe("deactivateUser", (): void => {
|
||||
it("deletes correct endpoint", async (): Promise<void> => {
|
||||
vi.mocked(client.apiDelete).mockResolvedValueOnce({} as never);
|
||||
await deactivateUser("u1");
|
||||
expect(client.apiDelete).toHaveBeenCalledWith("/api/admin/users/u1");
|
||||
});
|
||||
});
|
||||
@@ -1,53 +0,0 @@
|
||||
import { describe, it, expect, vi, beforeEach } from "vitest";
|
||||
import * as client from "./client";
|
||||
import { fetchTeams, createTeam, fetchTeamMembers } from "./teams";
|
||||
|
||||
vi.mock("./client");
|
||||
|
||||
const localStorageMock = {
|
||||
getItem: vi.fn().mockReturnValue("ws-1"),
|
||||
setItem: vi.fn(),
|
||||
clear: vi.fn(),
|
||||
removeItem: vi.fn(),
|
||||
length: 0,
|
||||
key: vi.fn(),
|
||||
};
|
||||
Object.defineProperty(window, "localStorage", { value: localStorageMock });
|
||||
|
||||
beforeEach((): void => {
|
||||
vi.clearAllMocks();
|
||||
localStorageMock.getItem.mockReturnValue("ws-1");
|
||||
});
|
||||
|
||||
describe("fetchTeams", (): void => {
|
||||
it("calls teams endpoint for active workspace", async (): Promise<void> => {
|
||||
vi.mocked(client.apiGet).mockResolvedValueOnce([] as never);
|
||||
await fetchTeams();
|
||||
expect(client.apiGet).toHaveBeenCalledWith("/api/workspaces/ws-1/teams", "ws-1");
|
||||
});
|
||||
|
||||
it("throws if no workspace id in localStorage", async (): Promise<void> => {
|
||||
localStorageMock.getItem.mockReturnValue(null);
|
||||
await expect(fetchTeams()).rejects.toThrow();
|
||||
});
|
||||
});
|
||||
|
||||
describe("createTeam", (): void => {
|
||||
it("posts to teams endpoint", async (): Promise<void> => {
|
||||
vi.mocked(client.apiPost).mockResolvedValueOnce({ id: "t1", name: "Dev" } as never);
|
||||
await createTeam({ name: "Dev" });
|
||||
expect(client.apiPost).toHaveBeenCalledWith(
|
||||
"/api/workspaces/ws-1/teams",
|
||||
expect.objectContaining({ name: "Dev" }),
|
||||
"ws-1"
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("fetchTeamMembers", (): void => {
|
||||
it("calls members endpoint for team", async (): Promise<void> => {
|
||||
vi.mocked(client.apiGet).mockResolvedValueOnce([] as never);
|
||||
await fetchTeamMembers("t-1");
|
||||
expect(client.apiGet).toHaveBeenCalledWith("/api/workspaces/ws-1/teams/t-1/members", "ws-1");
|
||||
});
|
||||
});
|
||||
@@ -1,65 +0,0 @@
|
||||
import { describe, it, expect, vi, beforeEach } from "vitest";
|
||||
import * as client from "./client";
|
||||
import {
|
||||
fetchUserWorkspaces,
|
||||
fetchWorkspaceMembers,
|
||||
addWorkspaceMember,
|
||||
updateWorkspaceMemberRole,
|
||||
removeWorkspaceMember,
|
||||
} from "./workspaces";
|
||||
|
||||
vi.mock("./client");
|
||||
|
||||
beforeEach((): void => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
describe("fetchUserWorkspaces", (): void => {
|
||||
it("calls correct endpoint", async (): Promise<void> => {
|
||||
vi.mocked(client.apiGet).mockResolvedValueOnce([] as never);
|
||||
await fetchUserWorkspaces();
|
||||
expect(client.apiGet).toHaveBeenCalledWith("/api/workspaces");
|
||||
});
|
||||
});
|
||||
|
||||
describe("fetchWorkspaceMembers", (): void => {
|
||||
it("calls correct endpoint with workspace id", async (): Promise<void> => {
|
||||
vi.mocked(client.apiGet).mockResolvedValueOnce([] as never);
|
||||
await fetchWorkspaceMembers("ws-1");
|
||||
expect(client.apiGet).toHaveBeenCalledWith("/api/workspaces/ws-1/members");
|
||||
});
|
||||
|
||||
it("throws on error", async (): Promise<void> => {
|
||||
vi.mocked(client.apiGet).mockRejectedValueOnce(new Error("Forbidden"));
|
||||
await expect(fetchWorkspaceMembers("ws-1")).rejects.toThrow("Forbidden");
|
||||
});
|
||||
});
|
||||
|
||||
describe("addWorkspaceMember", (): void => {
|
||||
it("posts to correct endpoint", async (): Promise<void> => {
|
||||
vi.mocked(client.apiPost).mockResolvedValueOnce({} as never);
|
||||
await addWorkspaceMember("ws-1", { userId: "u1", role: "MEMBER" as never });
|
||||
expect(client.apiPost).toHaveBeenCalledWith("/api/workspaces/ws-1/members", {
|
||||
userId: "u1",
|
||||
role: "MEMBER",
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("updateWorkspaceMemberRole", (): void => {
|
||||
it("patches correct endpoint", async (): Promise<void> => {
|
||||
vi.mocked(client.apiPatch).mockResolvedValueOnce({} as never);
|
||||
await updateWorkspaceMemberRole("ws-1", "u1", { role: "ADMIN" as never });
|
||||
expect(client.apiPatch).toHaveBeenCalledWith("/api/workspaces/ws-1/members/u1", {
|
||||
role: "ADMIN",
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("removeWorkspaceMember", (): void => {
|
||||
it("calls delete on correct endpoint", async (): Promise<void> => {
|
||||
vi.mocked(client.apiDelete).mockResolvedValueOnce(undefined as never);
|
||||
await removeWorkspaceMember("ws-1", "u1");
|
||||
expect(client.apiDelete).toHaveBeenCalledWith("/api/workspaces/ws-1/members/u1");
|
||||
});
|
||||
});
|
||||
@@ -33,11 +33,11 @@
|
||||
| MS21-TEST-004 | not-started | phase-4 | Frontend component tests | #569 | web | test/ms21-ui | MS21-UI-001,MS21-UI-002,MS21-UI-003,MS21-UI-004,MS21-UI-005 | — | — | — | — | 20K | — | |
|
||||
| MS21-RBAC-001 | not-started | phase-5 | Sidebar navigation role gating | #570 | web | feat/ms21-rbac | MS21-UI-001 | — | — | — | — | 10K | — | |
|
||||
| MS21-RBAC-002 | not-started | phase-5 | Settings page access restriction | #570 | web | feat/ms21-rbac | MS21-RBAC-001 | — | — | — | — | 8K | — | |
|
||||
| MS21-RBAC-003 | done | phase-5 | Action button permission gating | #570 | web | feat/ms21-rbac | MS21-RBAC-001 | — | — | — | — | 8K | — | |
|
||||
| MS21-RBAC-004 | done | phase-5 | User profile role display | #570 | web | feat/ms21-rbac | MS21-RBAC-001 | — | — | — | — | 5K | — | |
|
||||
| MS21-VER-001 | done | phase-6 | Full quality gate pass | #571 | stack | — | MS21-TEST-004,MS21-RBAC-004,MS21-MIG-003 | MS21-VER-002 | — | — | — | 5K | — | |
|
||||
| MS21-VER-002 | done | phase-6 | Deploy and smoke test | #571 | stack | — | MS21-VER-001 | MS21-VER-003 | — | — | — | 5K | — | |
|
||||
| MS21-VER-003 | done | phase-6 | Tag v0.0.21 | #571 | stack | — | MS21-VER-002 | — | — | — | — | 2K | — | |
|
||||
| MS21-RBAC-003 | not-started | phase-5 | Action button permission gating | #570 | web | feat/ms21-rbac | MS21-RBAC-001 | — | — | — | — | 8K | — | |
|
||||
| MS21-RBAC-004 | not-started | phase-5 | User profile role display | #570 | web | feat/ms21-rbac | MS21-RBAC-001 | — | — | — | — | 5K | — | |
|
||||
| MS21-VER-001 | not-started | phase-6 | Full quality gate pass | #571 | stack | — | MS21-TEST-004,MS21-RBAC-004,MS21-MIG-003 | MS21-VER-002 | — | — | — | 5K | — | |
|
||||
| MS21-VER-002 | not-started | phase-6 | Deploy and smoke test | #571 | stack | — | MS21-VER-001 | MS21-VER-003 | — | — | — | 5K | — | |
|
||||
| MS21-VER-003 | not-started | phase-6 | Tag v0.0.21 | #571 | stack | — | MS21-VER-002 | — | — | — | — | 2K | — | |
|
||||
|
||||
## Budget Summary
|
||||
|
||||
|
||||
@@ -1,64 +0,0 @@
|
||||
# MS22 Agent Memory Module
|
||||
|
||||
## Objective
|
||||
|
||||
Add per-agent key/value store: AgentMemory model + NestJS module with CRUD endpoints.
|
||||
|
||||
## Issues
|
||||
|
||||
- MS22-DB-002: Add AgentMemory schema model
|
||||
- MS22-API-002: Add agent-memory NestJS module
|
||||
|
||||
## Plan
|
||||
|
||||
1. AgentMemory model → schema.prisma (after AgentSession, line 736)
|
||||
2. Add `agentMemories AgentMemory[]` relation to Workspace model
|
||||
3. Create apps/api/src/agent-memory/ with service, controller, DTOs, specs
|
||||
4. Register in app.module.ts
|
||||
5. Migrate: `prisma migrate dev --name ms22_agent_memory`
|
||||
6. lint + build
|
||||
7. Commit
|
||||
|
||||
## Endpoints
|
||||
|
||||
- PUT /api/agents/:agentId/memory/:key (upsert)
|
||||
- GET /api/agents/:agentId/memory (list all)
|
||||
- GET /api/agents/:agentId/memory/:key (get one)
|
||||
- DELETE /api/agents/:agentId/memory/:key (remove)
|
||||
|
||||
## Auth
|
||||
|
||||
- @UseGuards(AuthGuard, WorkspaceGuard, PermissionGuard)
|
||||
- @Workspace() decorator for workspaceId
|
||||
- Permission.WORKSPACE_MEMBER for write ops
|
||||
- Permission.WORKSPACE_ANY for read ops
|
||||
|
||||
## Schema
|
||||
|
||||
```prisma
|
||||
model AgentMemory {
|
||||
id String @id @default(uuid()) @db.Uuid
|
||||
workspaceId String @map("workspace_id") @db.Uuid
|
||||
agentId String @map("agent_id")
|
||||
key String
|
||||
value Json
|
||||
createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz
|
||||
updatedAt DateTime @updatedAt @map("updated_at") @db.Timestamptz
|
||||
|
||||
workspace Workspace @relation(fields: [workspaceId], references: [id], onDelete: Cascade)
|
||||
|
||||
@@unique([workspaceId, agentId, key])
|
||||
@@index([workspaceId])
|
||||
@@index([agentId])
|
||||
@@map("agent_memories")
|
||||
}
|
||||
```
|
||||
|
||||
## Progress
|
||||
|
||||
- [ ] Schema
|
||||
- [ ] Module files
|
||||
- [ ] app.module.ts
|
||||
- [ ] Migration
|
||||
- [ ] lint/build
|
||||
- [ ] Commit
|
||||
@@ -74,8 +74,7 @@
|
||||
"tough-cookie": ">=4.1.3",
|
||||
"undici": ">=6.23.0",
|
||||
"rollup": ">=4.59.0",
|
||||
"serialize-javascript": ">=7.0.3",
|
||||
"multer": ">=2.1.0"
|
||||
"serialize-javascript": ">=7.0.3"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
21
pnpm-lock.yaml
generated
21
pnpm-lock.yaml
generated
@@ -17,7 +17,6 @@ overrides:
|
||||
undici: '>=6.23.0'
|
||||
rollup: '>=4.59.0'
|
||||
serialize-javascript: '>=7.0.3'
|
||||
multer: '>=2.1.0'
|
||||
|
||||
importers:
|
||||
|
||||
@@ -1604,7 +1603,6 @@ packages:
|
||||
|
||||
'@mosaicstack/telemetry-client@0.1.1':
|
||||
resolution: {integrity: sha512-1udg6p4cs8rhQgQ2pKCfi7EpRlJieRRhA5CIqthRQ6HQZLgQ0wH+632jEulov3rlHSM1iplIQ+AAe5DWrvSkEA==, tarball: https://git.mosaicstack.dev/api/packages/mosaic/npm/%40mosaicstack%2Ftelemetry-client/-/0.1.1/telemetry-client-0.1.1.tgz}
|
||||
engines: {node: '>=18'}
|
||||
|
||||
'@mrleebo/prisma-ast@0.13.1':
|
||||
resolution: {integrity: sha512-XyroGQXcHrZdvmrGJvsA9KNeOOgGMg1Vg9OlheUsBOSKznLMDl+YChxbkboRHvtFYJEMRYmlV3uoo/njCw05iw==}
|
||||
@@ -5807,6 +5805,10 @@ packages:
|
||||
mkdirp-classic@0.5.3:
|
||||
resolution: {integrity: sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==}
|
||||
|
||||
mkdirp@0.5.6:
|
||||
resolution: {integrity: sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==}
|
||||
hasBin: true
|
||||
|
||||
mkdirp@3.0.1:
|
||||
resolution: {integrity: sha512-+NsyUUAZDmo6YVHzL/stxSu3t9YS1iljliy3BSDrXJ/dkn1KYdmtZODGGjLcc9XLgVVpH4KshHB8XmZgMhaBXg==}
|
||||
engines: {node: '>=10'}
|
||||
@@ -5835,8 +5837,8 @@ packages:
|
||||
msgpackr@1.11.5:
|
||||
resolution: {integrity: sha512-UjkUHN0yqp9RWKy0Lplhh+wlpdt9oQBYgULZOiFhV3VclSF1JnSQWZ5r9gORQlNYaUKQoR8itv7g7z1xDDuACA==}
|
||||
|
||||
multer@2.1.0:
|
||||
resolution: {integrity: sha512-TBm6j41rxNohqawsxlsWsNNh/VdV4QFXcBvRcPhXaA05EZ79z0qJ2bQFpync6JBoHTeNY5Q1JpG7AlTjdlfAEA==}
|
||||
multer@2.0.2:
|
||||
resolution: {integrity: sha512-u7f2xaZ/UG8oLXHvtF/oWTRvT44p9ecwBBqTwgJVq0+4BW1g8OW01TyMEGWBHbyMOYVHXslaut7qEQ1meATXgw==}
|
||||
engines: {node: '>= 10.16.0'}
|
||||
|
||||
mute-stream@2.0.0:
|
||||
@@ -8840,7 +8842,7 @@ snapshots:
|
||||
'@nestjs/core': 11.1.12(@nestjs/common@11.1.12(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@11.1.12)(@nestjs/websockets@11.1.12)(reflect-metadata@0.2.2)(rxjs@7.8.2)
|
||||
cors: 2.8.5
|
||||
express: 5.2.1
|
||||
multer: 2.1.0
|
||||
multer: 2.0.2
|
||||
path-to-regexp: 8.3.0
|
||||
tslib: 2.8.1
|
||||
transitivePeerDependencies:
|
||||
@@ -13389,6 +13391,10 @@ snapshots:
|
||||
|
||||
mkdirp-classic@0.5.3: {}
|
||||
|
||||
mkdirp@0.5.6:
|
||||
dependencies:
|
||||
minimist: 1.2.8
|
||||
|
||||
mkdirp@3.0.1: {}
|
||||
|
||||
mlly@1.8.0:
|
||||
@@ -13430,12 +13436,15 @@ snapshots:
|
||||
optionalDependencies:
|
||||
msgpackr-extract: 3.0.3
|
||||
|
||||
multer@2.1.0:
|
||||
multer@2.0.2:
|
||||
dependencies:
|
||||
append-field: 1.0.0
|
||||
busboy: 1.6.0
|
||||
concat-stream: 2.0.0
|
||||
mkdirp: 0.5.6
|
||||
object-assign: 4.1.1
|
||||
type-is: 1.6.18
|
||||
xtend: 4.0.2
|
||||
|
||||
mute-stream@2.0.0: {}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user