Compare commits
1 Commits
feat/ms24-
...
fix/missio
| Author | SHA1 | Date | |
|---|---|---|---|
| a0cc6c3ec7 |
@@ -56,7 +56,6 @@
|
|||||||
"bcryptjs": "^3.0.3",
|
"bcryptjs": "^3.0.3",
|
||||||
"better-auth": "^1.4.17",
|
"better-auth": "^1.4.17",
|
||||||
"bullmq": "^5.67.2",
|
"bullmq": "^5.67.2",
|
||||||
"chokidar": "^4.0.3",
|
|
||||||
"class-transformer": "^0.5.1",
|
"class-transformer": "^0.5.1",
|
||||||
"class-validator": "^0.14.3",
|
"class-validator": "^0.14.3",
|
||||||
"cookie-parser": "^1.4.7",
|
"cookie-parser": "^1.4.7",
|
||||||
|
|||||||
@@ -61,7 +61,6 @@ import { FleetSettingsModule } from "./fleet-settings/fleet-settings.module";
|
|||||||
import { OnboardingModule } from "./onboarding/onboarding.module";
|
import { OnboardingModule } from "./onboarding/onboarding.module";
|
||||||
import { ChatProxyModule } from "./chat-proxy/chat-proxy.module";
|
import { ChatProxyModule } from "./chat-proxy/chat-proxy.module";
|
||||||
import { OrchestratorModule } from "./orchestrator/orchestrator.module";
|
import { OrchestratorModule } from "./orchestrator/orchestrator.module";
|
||||||
import { QueueNotificationsModule } from "./queue-notifications/queue-notifications.module";
|
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [
|
imports: [
|
||||||
@@ -144,7 +143,6 @@ import { QueueNotificationsModule } from "./queue-notifications/queue-notificati
|
|||||||
OnboardingModule,
|
OnboardingModule,
|
||||||
ChatProxyModule,
|
ChatProxyModule,
|
||||||
OrchestratorModule,
|
OrchestratorModule,
|
||||||
QueueNotificationsModule,
|
|
||||||
],
|
],
|
||||||
controllers: [AppController, CsrfController],
|
controllers: [AppController, CsrfController],
|
||||||
providers: [
|
providers: [
|
||||||
|
|||||||
@@ -1,120 +0,0 @@
|
|||||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
||||||
import { NotFoundException } from "@nestjs/common";
|
|
||||||
import type { Response } from "express";
|
|
||||||
import { ConfigService } from "@nestjs/config";
|
|
||||||
import { Test, type TestingModule } from "@nestjs/testing";
|
|
||||||
import { QueueNotificationsController } from "./queue-notifications.controller";
|
|
||||||
import { QueueNotificationsService } from "./queue-notifications.service";
|
|
||||||
import { ApiKeyGuard } from "../common/guards/api-key.guard";
|
|
||||||
|
|
||||||
describe("QueueNotificationsController", () => {
|
|
||||||
let controller: QueueNotificationsController;
|
|
||||||
|
|
||||||
const mockService = {
|
|
||||||
listNotifications: vi.fn(),
|
|
||||||
streamNotifications: vi.fn(),
|
|
||||||
ackNotification: vi.fn(),
|
|
||||||
listTasks: vi.fn(),
|
|
||||||
};
|
|
||||||
|
|
||||||
const mockConfigService = {
|
|
||||||
get: vi.fn().mockReturnValue("coordinator-api-key"),
|
|
||||||
};
|
|
||||||
|
|
||||||
beforeEach(async () => {
|
|
||||||
vi.clearAllMocks();
|
|
||||||
|
|
||||||
const module: TestingModule = await Test.createTestingModule({
|
|
||||||
controllers: [QueueNotificationsController],
|
|
||||||
providers: [
|
|
||||||
{ provide: QueueNotificationsService, useValue: mockService },
|
|
||||||
{ provide: ConfigService, useValue: mockConfigService },
|
|
||||||
],
|
|
||||||
})
|
|
||||||
.overrideGuard(ApiKeyGuard)
|
|
||||||
.useValue({ canActivate: () => true })
|
|
||||||
.compile();
|
|
||||||
|
|
||||||
controller = module.get<QueueNotificationsController>(QueueNotificationsController);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("returns notification objects", async () => {
|
|
||||||
mockService.listNotifications.mockResolvedValue([
|
|
||||||
{
|
|
||||||
id: "notif-1",
|
|
||||||
agent: "mosaic",
|
|
||||||
filename: "notif-1.json",
|
|
||||||
payload: { type: "task.ready" },
|
|
||||||
createdAt: new Date("2026-03-08T22:00:00.000Z"),
|
|
||||||
},
|
|
||||||
]);
|
|
||||||
|
|
||||||
await expect(controller.getNotifications()).resolves.toEqual([
|
|
||||||
expect.objectContaining({
|
|
||||||
id: "notif-1",
|
|
||||||
agent: "mosaic",
|
|
||||||
filename: "notif-1.json",
|
|
||||||
payload: { type: "task.ready" },
|
|
||||||
}),
|
|
||||||
]);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("streams notifications through the response object", async () => {
|
|
||||||
const res = {
|
|
||||||
setHeader: vi.fn(),
|
|
||||||
flushHeaders: vi.fn(),
|
|
||||||
write: vi.fn(),
|
|
||||||
on: vi.fn(),
|
|
||||||
end: vi.fn(),
|
|
||||||
} as unknown as Response;
|
|
||||||
|
|
||||||
mockService.streamNotifications.mockResolvedValue(undefined);
|
|
||||||
|
|
||||||
await controller.streamNotifications(res);
|
|
||||||
|
|
||||||
expect(mockService.streamNotifications).toHaveBeenCalledWith(res);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("acks a notification by id", async () => {
|
|
||||||
mockService.ackNotification.mockResolvedValue({ success: true, id: "notif-2" });
|
|
||||||
|
|
||||||
await expect(controller.ackNotification("notif-2")).resolves.toEqual({
|
|
||||||
success: true,
|
|
||||||
id: "notif-2",
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it("surfaces ack errors", async () => {
|
|
||||||
mockService.ackNotification.mockRejectedValue(new NotFoundException("missing"));
|
|
||||||
|
|
||||||
await expect(controller.ackNotification("missing")).rejects.toThrow(NotFoundException);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("returns parsed queue tasks", async () => {
|
|
||||||
mockService.listTasks.mockResolvedValue([
|
|
||||||
{
|
|
||||||
id: "task-1",
|
|
||||||
project: "mosaic-stack",
|
|
||||||
taskId: "MS24-API-001",
|
|
||||||
status: "pending",
|
|
||||||
description: "Build queue notifications module",
|
|
||||||
},
|
|
||||||
]);
|
|
||||||
|
|
||||||
await expect(controller.getTasks()).resolves.toEqual([
|
|
||||||
{
|
|
||||||
id: "task-1",
|
|
||||||
project: "mosaic-stack",
|
|
||||||
taskId: "MS24-API-001",
|
|
||||||
status: "pending",
|
|
||||||
description: "Build queue notifications module",
|
|
||||||
},
|
|
||||||
]);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("uses ApiKeyGuard at the controller level", () => {
|
|
||||||
const guards = Reflect.getMetadata("__guards__", QueueNotificationsController) as unknown[];
|
|
||||||
|
|
||||||
expect(guards).toContain(ApiKeyGuard);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,36 +0,0 @@
|
|||||||
import { Controller, Get, Param, Post, Res, UseGuards } from "@nestjs/common";
|
|
||||||
import type { Response } from "express";
|
|
||||||
import { SkipCsrf } from "../common/decorators/skip-csrf.decorator";
|
|
||||||
import { ApiKeyGuard } from "../common/guards/api-key.guard";
|
|
||||||
import {
|
|
||||||
QueueNotificationsService,
|
|
||||||
type QueueNotification,
|
|
||||||
type QueueTask,
|
|
||||||
} from "./queue-notifications.service";
|
|
||||||
|
|
||||||
@Controller("queue")
|
|
||||||
@UseGuards(ApiKeyGuard)
|
|
||||||
export class QueueNotificationsController {
|
|
||||||
constructor(private readonly queueNotificationsService: QueueNotificationsService) {}
|
|
||||||
|
|
||||||
@Get("notifications")
|
|
||||||
async getNotifications(): Promise<QueueNotification[]> {
|
|
||||||
return this.queueNotificationsService.listNotifications();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Get("notifications/stream")
|
|
||||||
async streamNotifications(@Res() res: Response): Promise<void> {
|
|
||||||
await this.queueNotificationsService.streamNotifications(res);
|
|
||||||
}
|
|
||||||
|
|
||||||
@SkipCsrf()
|
|
||||||
@Post("notifications/:id/ack")
|
|
||||||
async ackNotification(@Param("id") id: string): Promise<{ success: true; id: string }> {
|
|
||||||
return this.queueNotificationsService.ackNotification(id);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Get("tasks")
|
|
||||||
async getTasks(): Promise<QueueTask[]> {
|
|
||||||
return this.queueNotificationsService.listTasks();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,14 +0,0 @@
|
|||||||
import { Module } from "@nestjs/common";
|
|
||||||
import { ConfigModule } from "@nestjs/config";
|
|
||||||
import { AuthModule } from "../auth/auth.module";
|
|
||||||
import { ApiKeyGuard } from "../common/guards/api-key.guard";
|
|
||||||
import { QueueNotificationsController } from "./queue-notifications.controller";
|
|
||||||
import { QueueNotificationsService } from "./queue-notifications.service";
|
|
||||||
|
|
||||||
@Module({
|
|
||||||
imports: [ConfigModule, AuthModule],
|
|
||||||
controllers: [QueueNotificationsController],
|
|
||||||
providers: [QueueNotificationsService, ApiKeyGuard],
|
|
||||||
exports: [QueueNotificationsService],
|
|
||||||
})
|
|
||||||
export class QueueNotificationsModule {}
|
|
||||||
@@ -1,172 +0,0 @@
|
|||||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
||||||
import { ConfigService } from "@nestjs/config";
|
|
||||||
import { Logger, NotFoundException } from "@nestjs/common";
|
|
||||||
import { mkdtemp, mkdir, rm, writeFile } from "node:fs/promises";
|
|
||||||
import { tmpdir } from "node:os";
|
|
||||||
import { join } from "node:path";
|
|
||||||
import { execFile } from "node:child_process";
|
|
||||||
import { QueueNotificationsService } from "./queue-notifications.service";
|
|
||||||
|
|
||||||
vi.mock("node:child_process", () => ({
|
|
||||||
execFile: vi.fn(),
|
|
||||||
}));
|
|
||||||
|
|
||||||
describe("QueueNotificationsService", () => {
|
|
||||||
let service: QueueNotificationsService;
|
|
||||||
let inboxDir: string;
|
|
||||||
let configService: ConfigService;
|
|
||||||
|
|
||||||
beforeEach(async () => {
|
|
||||||
vi.clearAllMocks();
|
|
||||||
inboxDir = await mkdtemp(join(tmpdir(), "queue-notifications-"));
|
|
||||||
configService = {
|
|
||||||
get: vi.fn((key: string) => {
|
|
||||||
if (key === "MOSAIC_QUEUE_INBOX_DIR") {
|
|
||||||
return inboxDir;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (key === "MOSAIC_QUEUE_CLI") {
|
|
||||||
return "/tmp/mosaic-queue-cli.js";
|
|
||||||
}
|
|
||||||
|
|
||||||
return undefined;
|
|
||||||
}),
|
|
||||||
} as unknown as ConfigService;
|
|
||||||
|
|
||||||
service = new QueueNotificationsService(configService);
|
|
||||||
});
|
|
||||||
|
|
||||||
afterEach(async () => {
|
|
||||||
vi.restoreAllMocks();
|
|
||||||
await rm(inboxDir, { recursive: true, force: true });
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("onModuleInit", () => {
|
|
||||||
it("logs a warning when the inbox directory does not exist", async () => {
|
|
||||||
await rm(inboxDir, { recursive: true, force: true });
|
|
||||||
const warnSpy = vi.spyOn(Logger.prototype, "warn").mockImplementation(() => undefined);
|
|
||||||
|
|
||||||
await service.onModuleInit();
|
|
||||||
|
|
||||||
expect(warnSpy).toHaveBeenCalledWith(
|
|
||||||
expect.stringContaining("Queue notifications inbox directory does not exist")
|
|
||||||
);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("listNotifications", () => {
|
|
||||||
it("returns parsed notifications from agent inbox directories", async () => {
|
|
||||||
await mkdir(join(inboxDir, "mosaic"), { recursive: true });
|
|
||||||
await mkdir(join(inboxDir, "mosaic", "_acked"), { recursive: true });
|
|
||||||
await mkdir(join(inboxDir, "sage"), { recursive: true });
|
|
||||||
await writeFile(
|
|
||||||
join(inboxDir, "mosaic", "notif-1.json"),
|
|
||||||
JSON.stringify({ type: "task.ready", taskId: "MS24-API-001" })
|
|
||||||
);
|
|
||||||
await writeFile(
|
|
||||||
join(inboxDir, "mosaic", "_acked", "notif-ignored.json"),
|
|
||||||
JSON.stringify({ ignored: true })
|
|
||||||
);
|
|
||||||
await writeFile(join(inboxDir, "sage", "notif-2.json"), JSON.stringify({ type: "done" }));
|
|
||||||
|
|
||||||
const notifications = await service.listNotifications();
|
|
||||||
|
|
||||||
expect(notifications).toHaveLength(2);
|
|
||||||
expect(notifications).toEqual(
|
|
||||||
expect.arrayContaining([
|
|
||||||
expect.objectContaining({
|
|
||||||
id: "notif-1",
|
|
||||||
agent: "mosaic",
|
|
||||||
filename: "notif-1.json",
|
|
||||||
payload: { type: "task.ready", taskId: "MS24-API-001" },
|
|
||||||
}),
|
|
||||||
expect.objectContaining({
|
|
||||||
id: "notif-2",
|
|
||||||
agent: "sage",
|
|
||||||
filename: "notif-2.json",
|
|
||||||
payload: { type: "done" },
|
|
||||||
}),
|
|
||||||
])
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("returns an empty array when the inbox directory is missing", async () => {
|
|
||||||
await rm(inboxDir, { recursive: true, force: true });
|
|
||||||
|
|
||||||
await expect(service.listNotifications()).resolves.toEqual([]);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("ackNotification", () => {
|
|
||||||
it("executes the queue CLI with node and ack args", async () => {
|
|
||||||
await mkdir(join(inboxDir, "mosaic"), { recursive: true });
|
|
||||||
await writeFile(join(inboxDir, "mosaic", "notif-3.json"), JSON.stringify({ ok: true }));
|
|
||||||
vi.mocked(execFile).mockImplementation(
|
|
||||||
(
|
|
||||||
_command: string,
|
|
||||||
_args: readonly string[],
|
|
||||||
callback: (error: Error | null, stdout: string, stderr: string) => void
|
|
||||||
) => callback(null, "acked", "")
|
|
||||||
);
|
|
||||||
|
|
||||||
await expect(service.ackNotification("notif-3")).resolves.toEqual({
|
|
||||||
success: true,
|
|
||||||
id: "notif-3",
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(execFile).toHaveBeenCalledWith(
|
|
||||||
"node",
|
|
||||||
["/tmp/mosaic-queue-cli.js", "ack", "notif-3"],
|
|
||||||
expect.any(Function)
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("throws NotFoundException when the notification does not exist", async () => {
|
|
||||||
await expect(service.ackNotification("missing")).rejects.toThrow(NotFoundException);
|
|
||||||
expect(execFile).not.toHaveBeenCalled();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("listTasks", () => {
|
|
||||||
it("parses tab-separated CLI output", async () => {
|
|
||||||
vi.mocked(execFile).mockImplementation(
|
|
||||||
(
|
|
||||||
_command: string,
|
|
||||||
_args: readonly string[],
|
|
||||||
callback: (error: Error | null, stdout: string, stderr: string) => void
|
|
||||||
) =>
|
|
||||||
callback(
|
|
||||||
null,
|
|
||||||
[
|
|
||||||
"task-1\tmosaic-stack/MS24-API-001\t[pending]\tBuild queue notifications module",
|
|
||||||
"task-2\tmosaic-stack/MS24-API-002\t[done]\tWrite tests",
|
|
||||||
].join("\n"),
|
|
||||||
""
|
|
||||||
)
|
|
||||||
);
|
|
||||||
|
|
||||||
await expect(service.listTasks()).resolves.toEqual([
|
|
||||||
{
|
|
||||||
id: "task-1",
|
|
||||||
project: "mosaic-stack",
|
|
||||||
taskId: "MS24-API-001",
|
|
||||||
status: "pending",
|
|
||||||
description: "Build queue notifications module",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "task-2",
|
|
||||||
project: "mosaic-stack",
|
|
||||||
taskId: "MS24-API-002",
|
|
||||||
status: "done",
|
|
||||||
description: "Write tests",
|
|
||||||
},
|
|
||||||
]);
|
|
||||||
|
|
||||||
expect(execFile).toHaveBeenCalledWith(
|
|
||||||
"node",
|
|
||||||
["/tmp/mosaic-queue-cli.js", "list", "mosaic-stack"],
|
|
||||||
expect.any(Function)
|
|
||||||
);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,231 +0,0 @@
|
|||||||
import {
|
|
||||||
Injectable,
|
|
||||||
InternalServerErrorException,
|
|
||||||
Logger,
|
|
||||||
NotFoundException,
|
|
||||||
OnModuleInit,
|
|
||||||
} from "@nestjs/common";
|
|
||||||
import { ConfigService } from "@nestjs/config";
|
|
||||||
import { execFile } from "node:child_process";
|
|
||||||
import { access, readdir, readFile, stat } from "node:fs/promises";
|
|
||||||
import { homedir } from "node:os";
|
|
||||||
import { basename, join } from "node:path";
|
|
||||||
import type { Response } from "express";
|
|
||||||
import chokidar from "chokidar";
|
|
||||||
|
|
||||||
export interface QueueNotification {
|
|
||||||
id: string;
|
|
||||||
agent: string;
|
|
||||||
filename: string;
|
|
||||||
payload: unknown;
|
|
||||||
createdAt: Date;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface QueueTask {
|
|
||||||
id: string;
|
|
||||||
project: string;
|
|
||||||
taskId: string;
|
|
||||||
status: string;
|
|
||||||
description: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Injectable()
|
|
||||||
export class QueueNotificationsService implements OnModuleInit {
|
|
||||||
private readonly logger = new Logger(QueueNotificationsService.name);
|
|
||||||
|
|
||||||
constructor(private readonly configService: ConfigService) {}
|
|
||||||
|
|
||||||
async onModuleInit(): Promise<void> {
|
|
||||||
if (!(await this.inboxDirExists())) {
|
|
||||||
this.logger.warn(`Queue notifications inbox directory does not exist: ${this.getInboxDir()}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async listNotifications(): Promise<QueueNotification[]> {
|
|
||||||
const inboxDir = this.getInboxDir();
|
|
||||||
|
|
||||||
if (!(await this.inboxDirExists())) {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
|
|
||||||
// Paths come from controlled config plus directory entries under the inbox root.
|
|
||||||
// eslint-disable-next-line security/detect-non-literal-fs-filename
|
|
||||||
const agentEntries = await readdir(inboxDir, { withFileTypes: true });
|
|
||||||
const notifications: QueueNotification[] = [];
|
|
||||||
|
|
||||||
for (const agentEntry of agentEntries) {
|
|
||||||
if (!agentEntry.isDirectory() || this.isIgnoredDirectory(agentEntry.name)) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
const agentDir = join(inboxDir, agentEntry.name);
|
|
||||||
// eslint-disable-next-line security/detect-non-literal-fs-filename
|
|
||||||
const files = await readdir(agentDir, { withFileTypes: true });
|
|
||||||
|
|
||||||
for (const fileEntry of files) {
|
|
||||||
if (!fileEntry.isFile() || !fileEntry.name.endsWith(".json")) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
const filePath = join(agentDir, fileEntry.name);
|
|
||||||
const [rawPayload, fileStats] = await Promise.all([
|
|
||||||
// eslint-disable-next-line security/detect-non-literal-fs-filename
|
|
||||||
readFile(filePath, "utf8"),
|
|
||||||
// eslint-disable-next-line security/detect-non-literal-fs-filename
|
|
||||||
stat(filePath),
|
|
||||||
]);
|
|
||||||
|
|
||||||
notifications.push({
|
|
||||||
id: basename(fileEntry.name, ".json"),
|
|
||||||
agent: agentEntry.name,
|
|
||||||
filename: fileEntry.name,
|
|
||||||
payload: JSON.parse(rawPayload) as unknown,
|
|
||||||
createdAt: fileStats.birthtime,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return notifications.sort(
|
|
||||||
(left, right) => right.createdAt.getTime() - left.createdAt.getTime()
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
async streamNotifications(res: Response): Promise<void> {
|
|
||||||
res.setHeader("Content-Type", "text/event-stream");
|
|
||||||
res.setHeader("Cache-Control", "no-cache");
|
|
||||||
res.setHeader("Connection", "keep-alive");
|
|
||||||
res.setHeader("X-Accel-Buffering", "no");
|
|
||||||
|
|
||||||
if (typeof res.flushHeaders === "function") {
|
|
||||||
res.flushHeaders();
|
|
||||||
}
|
|
||||||
|
|
||||||
const emitNotifications = async (): Promise<void> => {
|
|
||||||
try {
|
|
||||||
const notifications = await this.listNotifications();
|
|
||||||
res.write(`data: ${JSON.stringify(notifications)}\n\n`);
|
|
||||||
} catch (error: unknown) {
|
|
||||||
const message = error instanceof Error ? error.message : String(error);
|
|
||||||
res.write(`event: error\n`);
|
|
||||||
res.write(`data: ${JSON.stringify({ error: message })}\n\n`);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
await emitNotifications();
|
|
||||||
|
|
||||||
const watcher = chokidar.watch(this.getInboxDir(), {
|
|
||||||
ignoreInitial: true,
|
|
||||||
persistent: true,
|
|
||||||
ignored: (watchedPath: string) => {
|
|
||||||
return watchedPath.includes("/_acked/") || watchedPath.includes("/_dead-letter/");
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
watcher.on("add", () => {
|
|
||||||
void emitNotifications();
|
|
||||||
});
|
|
||||||
|
|
||||||
watcher.on("unlink", () => {
|
|
||||||
void emitNotifications();
|
|
||||||
});
|
|
||||||
|
|
||||||
res.on("close", () => {
|
|
||||||
void watcher.close();
|
|
||||||
res.end();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
async ackNotification(id: string): Promise<{ success: true; id: string }> {
|
|
||||||
const notification = (await this.listNotifications()).find((entry) => entry.id === id);
|
|
||||||
|
|
||||||
if (!notification) {
|
|
||||||
throw new NotFoundException(`Queue notification ${id} not found`);
|
|
||||||
}
|
|
||||||
|
|
||||||
await this.execQueueCli(["ack", notification.id]);
|
|
||||||
|
|
||||||
return {
|
|
||||||
success: true,
|
|
||||||
id: notification.id,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
async listTasks(): Promise<QueueTask[]> {
|
|
||||||
const stdout = await this.execQueueCli(["list", "mosaic-stack"]);
|
|
||||||
|
|
||||||
return stdout
|
|
||||||
.split(/\r?\n/)
|
|
||||||
.map((line) => line.trim())
|
|
||||||
.filter((line) => line.length > 0)
|
|
||||||
.map((line) => {
|
|
||||||
const [rawId = "", projectTaskId = "", rawStatus = "", description = ""] = line.split("\t");
|
|
||||||
const [project = "", taskId = ""] = projectTaskId.split("/");
|
|
||||||
|
|
||||||
return {
|
|
||||||
id: rawId,
|
|
||||||
project,
|
|
||||||
taskId,
|
|
||||||
status: rawStatus.replace(/^\[/, "").replace(/\]$/, ""),
|
|
||||||
description,
|
|
||||||
};
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
private async execQueueCli(args: string[]): Promise<string> {
|
|
||||||
const cliPath = this.getQueueCliPath();
|
|
||||||
|
|
||||||
return new Promise<string>((resolve, reject) => {
|
|
||||||
execFile("node", [cliPath, ...args], (error, stdout, stderr) => {
|
|
||||||
if (error) {
|
|
||||||
this.logger.error(
|
|
||||||
`Queue CLI command failed: node ${cliPath} ${args.join(" ")} | ${stderr || error.message}`
|
|
||||||
);
|
|
||||||
reject(
|
|
||||||
new InternalServerErrorException(`Queue CLI command failed: ${stderr || error.message}`)
|
|
||||||
);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
resolve(stdout);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
private getInboxDir(): string {
|
|
||||||
return this.expandHomePath(
|
|
||||||
this.configService.get<string>("MOSAIC_QUEUE_INBOX_DIR") ??
|
|
||||||
"~/.openclaw/workspace/agent-inbox"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
private getQueueCliPath(): string {
|
|
||||||
return this.expandHomePath(
|
|
||||||
this.configService.get<string>("MOSAIC_QUEUE_CLI") ?? "~/src/mosaic-queue/dist/cli.js"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
private expandHomePath(value: string): string {
|
|
||||||
if (value === "~") {
|
|
||||||
return homedir();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (value.startsWith("~/")) {
|
|
||||||
return join(homedir(), value.slice(2));
|
|
||||||
}
|
|
||||||
|
|
||||||
return value;
|
|
||||||
}
|
|
||||||
|
|
||||||
private async inboxDirExists(): Promise<boolean> {
|
|
||||||
try {
|
|
||||||
await access(this.getInboxDir());
|
|
||||||
return true;
|
|
||||||
} catch {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private isIgnoredDirectory(name: string): boolean {
|
|
||||||
return name === "_acked" || name === "_dead-letter";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,93 +0,0 @@
|
|||||||
import { type NextRequest, NextResponse } from "next/server";
|
|
||||||
|
|
||||||
const DEFAULT_ORCHESTRATOR_URL = "http://localhost:3001";
|
|
||||||
|
|
||||||
function getOrchestratorUrl(): string {
|
|
||||||
return (
|
|
||||||
process.env.ORCHESTRATOR_URL ??
|
|
||||||
process.env.NEXT_PUBLIC_ORCHESTRATOR_URL ??
|
|
||||||
process.env.NEXT_PUBLIC_API_URL ??
|
|
||||||
DEFAULT_ORCHESTRATOR_URL
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Generic catch-all proxy for orchestrator API routes.
|
|
||||||
*
|
|
||||||
* Forwards any request to /api/orchestrator/<path> → ORCHESTRATOR_URL/<path>
|
|
||||||
* with the ORCHESTRATOR_API_KEY injected server-side so it never reaches the browser.
|
|
||||||
*
|
|
||||||
* Supports GET, POST, PATCH, DELETE, PUT.
|
|
||||||
*
|
|
||||||
* Example:
|
|
||||||
* GET /api/orchestrator/mission-control/sessions
|
|
||||||
* → GET ORCHESTRATOR_URL/api/mission-control/sessions
|
|
||||||
* POST /api/orchestrator/mission-control/sessions/abc/kill
|
|
||||||
* → POST ORCHESTRATOR_URL/api/mission-control/sessions/abc/kill
|
|
||||||
*/
|
|
||||||
async function proxyToOrchestrator(
|
|
||||||
request: NextRequest,
|
|
||||||
context: { params: Promise<{ path: string[] }> }
|
|
||||||
): Promise<NextResponse> {
|
|
||||||
const orchestratorApiKey = process.env.ORCHESTRATOR_API_KEY;
|
|
||||||
if (!orchestratorApiKey) {
|
|
||||||
return NextResponse.json(
|
|
||||||
{ error: "ORCHESTRATOR_API_KEY is not configured on the web server." },
|
|
||||||
{ status: 503 }
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const { path } = await context.params;
|
|
||||||
const upstreamPath = `/${path.join("/")}`;
|
|
||||||
const search = request.nextUrl.search;
|
|
||||||
const upstreamUrl = `${getOrchestratorUrl()}${upstreamPath}${search}`;
|
|
||||||
|
|
||||||
const controller = new AbortController();
|
|
||||||
const timeout = setTimeout(() => {
|
|
||||||
controller.abort();
|
|
||||||
}, 30_000);
|
|
||||||
|
|
||||||
try {
|
|
||||||
const headers: Record<string, string> = {
|
|
||||||
"X-API-Key": orchestratorApiKey,
|
|
||||||
};
|
|
||||||
|
|
||||||
const contentType = request.headers.get("Content-Type");
|
|
||||||
if (contentType) {
|
|
||||||
headers["Content-Type"] = contentType;
|
|
||||||
}
|
|
||||||
|
|
||||||
const hasBody = request.method !== "GET" && request.method !== "HEAD";
|
|
||||||
const body = hasBody ? await request.text() : null;
|
|
||||||
|
|
||||||
const upstream = await fetch(upstreamUrl, {
|
|
||||||
method: request.method,
|
|
||||||
headers,
|
|
||||||
...(body !== null ? { body } : {}),
|
|
||||||
cache: "no-store",
|
|
||||||
signal: controller.signal,
|
|
||||||
});
|
|
||||||
|
|
||||||
const responseText = await upstream.text();
|
|
||||||
return new NextResponse(responseText, {
|
|
||||||
status: upstream.status,
|
|
||||||
headers: {
|
|
||||||
"Content-Type": upstream.headers.get("Content-Type") ?? "application/json",
|
|
||||||
},
|
|
||||||
});
|
|
||||||
} catch (error) {
|
|
||||||
const message =
|
|
||||||
error instanceof Error && error.name === "AbortError"
|
|
||||||
? "Orchestrator request timed out."
|
|
||||||
: "Unable to reach orchestrator.";
|
|
||||||
return NextResponse.json({ error: message }, { status: 502 });
|
|
||||||
} finally {
|
|
||||||
clearTimeout(timeout);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export const GET = proxyToOrchestrator;
|
|
||||||
export const POST = proxyToOrchestrator;
|
|
||||||
export const PATCH = proxyToOrchestrator;
|
|
||||||
export const PUT = proxyToOrchestrator;
|
|
||||||
export const DELETE = proxyToOrchestrator;
|
|
||||||
@@ -158,9 +158,7 @@ async function fetchAuditLog(
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
return await apiGet<AuditLogResponse>(
|
return await apiGet<AuditLogResponse>(`/api/mission-control/audit-log?${params.toString()}`);
|
||||||
`/api/orchestrator/api/mission-control/audit-log?${params.toString()}`
|
|
||||||
);
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (isRateLimitError(error)) {
|
if (isRateLimitError(error)) {
|
||||||
return createEmptyAuditLogResponse(page, "Rate limited - retrying...");
|
return createEmptyAuditLogResponse(page, "Rate limited - retrying...");
|
||||||
|
|||||||
@@ -59,12 +59,9 @@ describe("BargeInInput", (): void => {
|
|||||||
await user.click(screen.getByRole("button", { name: "Send" }));
|
await user.click(screen.getByRole("button", { name: "Send" }));
|
||||||
|
|
||||||
await waitFor((): void => {
|
await waitFor((): void => {
|
||||||
expect(mockApiPost).toHaveBeenCalledWith(
|
expect(mockApiPost).toHaveBeenCalledWith("/api/mission-control/sessions/session-1/inject", {
|
||||||
"/api/orchestrator/api/mission-control/sessions/session-1/inject",
|
content: "execute plan",
|
||||||
{
|
});
|
||||||
content: "execute plan",
|
|
||||||
}
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(onSent).toHaveBeenCalledTimes(1);
|
expect(onSent).toHaveBeenCalledTimes(1);
|
||||||
@@ -86,18 +83,12 @@ describe("BargeInInput", (): void => {
|
|||||||
|
|
||||||
const calls = mockApiPost.mock.calls as [string, unknown?][];
|
const calls = mockApiPost.mock.calls as [string, unknown?][];
|
||||||
|
|
||||||
expect(calls[0]).toEqual([
|
expect(calls[0]).toEqual(["/api/mission-control/sessions/session-2/pause", undefined]);
|
||||||
"/api/orchestrator/api/mission-control/sessions/session-2/pause",
|
|
||||||
undefined,
|
|
||||||
]);
|
|
||||||
expect(calls[1]).toEqual([
|
expect(calls[1]).toEqual([
|
||||||
"/api/orchestrator/api/mission-control/sessions/session-2/inject",
|
"/api/mission-control/sessions/session-2/inject",
|
||||||
{ content: "hello world" },
|
{ content: "hello world" },
|
||||||
]);
|
]);
|
||||||
expect(calls[2]).toEqual([
|
expect(calls[2]).toEqual(["/api/mission-control/sessions/session-2/resume", undefined]);
|
||||||
"/api/orchestrator/api/mission-control/sessions/session-2/resume",
|
|
||||||
undefined,
|
|
||||||
]);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it("submits with Enter and does not submit on Shift+Enter", async (): Promise<void> => {
|
it("submits with Enter and does not submit on Shift+Enter", async (): Promise<void> => {
|
||||||
@@ -114,12 +105,9 @@ describe("BargeInInput", (): void => {
|
|||||||
fireEvent.keyDown(textarea, { key: "Enter", code: "Enter", shiftKey: false });
|
fireEvent.keyDown(textarea, { key: "Enter", code: "Enter", shiftKey: false });
|
||||||
|
|
||||||
await waitFor((): void => {
|
await waitFor((): void => {
|
||||||
expect(mockApiPost).toHaveBeenCalledWith(
|
expect(mockApiPost).toHaveBeenCalledWith("/api/mission-control/sessions/session-3/inject", {
|
||||||
"/api/orchestrator/api/mission-control/sessions/session-3/inject",
|
content: "first",
|
||||||
{
|
});
|
||||||
content: "first",
|
|
||||||
}
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -39,7 +39,7 @@ export function BargeInInput({ sessionId, onSent }: BargeInInputProps): React.JS
|
|||||||
}
|
}
|
||||||
|
|
||||||
const encodedSessionId = encodeURIComponent(sessionId);
|
const encodedSessionId = encodeURIComponent(sessionId);
|
||||||
const baseEndpoint = `/api/orchestrator/api/mission-control/sessions/${encodedSessionId}`;
|
const baseEndpoint = `/api/mission-control/sessions/${encodedSessionId}`;
|
||||||
let didPause = false;
|
let didPause = false;
|
||||||
let didInject = false;
|
let didInject = false;
|
||||||
|
|
||||||
|
|||||||
@@ -177,12 +177,9 @@ describe("GlobalAgentRoster", (): void => {
|
|||||||
fireEvent.click(screen.getByRole("button", { name: "Kill session killme12" }));
|
fireEvent.click(screen.getByRole("button", { name: "Kill session killme12" }));
|
||||||
|
|
||||||
await waitFor((): void => {
|
await waitFor((): void => {
|
||||||
expect(mockApiPost).toHaveBeenCalledWith(
|
expect(mockApiPost).toHaveBeenCalledWith("/api/mission-control/sessions/killme123456/kill", {
|
||||||
"/api/orchestrator/api/mission-control/sessions/killme123456/kill",
|
force: false,
|
||||||
{
|
});
|
||||||
force: false,
|
|
||||||
}
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -83,7 +83,7 @@ function groupByProvider(sessions: MissionControlSession[]): ProviderSessionGrou
|
|||||||
|
|
||||||
async function fetchSessions(): Promise<MissionControlSession[]> {
|
async function fetchSessions(): Promise<MissionControlSession[]> {
|
||||||
const payload = await apiGet<MissionControlSession[] | SessionsPayload>(
|
const payload = await apiGet<MissionControlSession[] | SessionsPayload>(
|
||||||
"/api/orchestrator/api/mission-control/sessions"
|
"/api/mission-control/sessions"
|
||||||
);
|
);
|
||||||
return Array.isArray(payload) ? payload : payload.sessions;
|
return Array.isArray(payload) ? payload : payload.sessions;
|
||||||
}
|
}
|
||||||
@@ -118,12 +118,9 @@ export function GlobalAgentRoster({
|
|||||||
|
|
||||||
const killMutation = useMutation({
|
const killMutation = useMutation({
|
||||||
mutationFn: async (sessionId: string): Promise<string> => {
|
mutationFn: async (sessionId: string): Promise<string> => {
|
||||||
await apiPost<{ message: string }>(
|
await apiPost<{ message: string }>(`/api/mission-control/sessions/${sessionId}/kill`, {
|
||||||
`/api/orchestrator/api/mission-control/sessions/${sessionId}/kill`,
|
force: false,
|
||||||
{
|
});
|
||||||
force: false,
|
|
||||||
}
|
|
||||||
);
|
|
||||||
return sessionId;
|
return sessionId;
|
||||||
},
|
},
|
||||||
onSuccess: (): void => {
|
onSuccess: (): void => {
|
||||||
|
|||||||
@@ -112,20 +112,14 @@ describe("KillAllDialog", (): void => {
|
|||||||
await user.click(screen.getByRole("button", { name: "Kill All Agents" }));
|
await user.click(screen.getByRole("button", { name: "Kill All Agents" }));
|
||||||
|
|
||||||
await waitFor((): void => {
|
await waitFor((): void => {
|
||||||
expect(mockApiPost).toHaveBeenCalledWith(
|
expect(mockApiPost).toHaveBeenCalledWith("/api/mission-control/sessions/internal-1/kill", {
|
||||||
"/api/orchestrator/api/mission-control/sessions/internal-1/kill",
|
force: true,
|
||||||
{
|
});
|
||||||
force: true,
|
|
||||||
}
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(mockApiPost).not.toHaveBeenCalledWith(
|
expect(mockApiPost).not.toHaveBeenCalledWith("/api/mission-control/sessions/external-1/kill", {
|
||||||
"/api/orchestrator/api/mission-control/sessions/external-1/kill",
|
force: true,
|
||||||
{
|
});
|
||||||
force: true,
|
|
||||||
}
|
|
||||||
);
|
|
||||||
expect(onComplete).toHaveBeenCalledTimes(1);
|
expect(onComplete).toHaveBeenCalledTimes(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -147,18 +141,12 @@ describe("KillAllDialog", (): void => {
|
|||||||
await user.click(screen.getByRole("button", { name: "Kill All Agents" }));
|
await user.click(screen.getByRole("button", { name: "Kill All Agents" }));
|
||||||
|
|
||||||
await waitFor((): void => {
|
await waitFor((): void => {
|
||||||
expect(mockApiPost).toHaveBeenCalledWith(
|
expect(mockApiPost).toHaveBeenCalledWith("/api/mission-control/sessions/internal-2/kill", {
|
||||||
"/api/orchestrator/api/mission-control/sessions/internal-2/kill",
|
force: true,
|
||||||
{
|
});
|
||||||
force: true,
|
expect(mockApiPost).toHaveBeenCalledWith("/api/mission-control/sessions/external-2/kill", {
|
||||||
}
|
force: true,
|
||||||
);
|
});
|
||||||
expect(mockApiPost).toHaveBeenCalledWith(
|
|
||||||
"/api/orchestrator/api/mission-control/sessions/external-2/kill",
|
|
||||||
{
|
|
||||||
force: true,
|
|
||||||
}
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -96,12 +96,9 @@ export function KillAllDialog({ sessions, onComplete }: KillAllDialogProps): Rea
|
|||||||
|
|
||||||
const killRequests = targetSessions.map(async (session) => {
|
const killRequests = targetSessions.map(async (session) => {
|
||||||
try {
|
try {
|
||||||
await apiPost<{ message: string }>(
|
await apiPost<{ message: string }>(`/api/mission-control/sessions/${session.id}/kill`, {
|
||||||
`/api/orchestrator/api/mission-control/sessions/${session.id}/kill`,
|
force: true,
|
||||||
{
|
});
|
||||||
force: true,
|
|
||||||
}
|
|
||||||
);
|
|
||||||
return true;
|
return true;
|
||||||
} catch {
|
} catch {
|
||||||
return false;
|
return false;
|
||||||
|
|||||||
@@ -89,7 +89,7 @@ describe("PanelControls", (): void => {
|
|||||||
|
|
||||||
await waitFor((): void => {
|
await waitFor((): void => {
|
||||||
expect(mockApiPost).toHaveBeenCalledWith(
|
expect(mockApiPost).toHaveBeenCalledWith(
|
||||||
"/api/orchestrator/api/mission-control/sessions/session%20with%20space/pause",
|
"/api/mission-control/sessions/session%20with%20space/pause",
|
||||||
undefined
|
undefined
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
@@ -114,12 +114,9 @@ describe("PanelControls", (): void => {
|
|||||||
await user.click(screen.getByRole("button", { name: "Confirm" }));
|
await user.click(screen.getByRole("button", { name: "Confirm" }));
|
||||||
|
|
||||||
await waitFor((): void => {
|
await waitFor((): void => {
|
||||||
expect(mockApiPost).toHaveBeenCalledWith(
|
expect(mockApiPost).toHaveBeenCalledWith("/api/mission-control/sessions/session-4/kill", {
|
||||||
"/api/orchestrator/api/mission-control/sessions/session-4/kill",
|
force: false,
|
||||||
{
|
});
|
||||||
force: false,
|
|
||||||
}
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(onStatusChange).toHaveBeenCalledWith("killed");
|
expect(onStatusChange).toHaveBeenCalledWith("killed");
|
||||||
@@ -140,12 +137,9 @@ describe("PanelControls", (): void => {
|
|||||||
await user.click(screen.getByRole("button", { name: "Confirm" }));
|
await user.click(screen.getByRole("button", { name: "Confirm" }));
|
||||||
|
|
||||||
await waitFor((): void => {
|
await waitFor((): void => {
|
||||||
expect(mockApiPost).toHaveBeenCalledWith(
|
expect(mockApiPost).toHaveBeenCalledWith("/api/mission-control/sessions/session-5/kill", {
|
||||||
"/api/orchestrator/api/mission-control/sessions/session-5/kill",
|
force: true,
|
||||||
{
|
});
|
||||||
force: true,
|
|
||||||
}
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(onStatusChange).toHaveBeenCalledWith("killed");
|
expect(onStatusChange).toHaveBeenCalledWith("killed");
|
||||||
|
|||||||
@@ -50,23 +50,23 @@ export function PanelControls({
|
|||||||
switch (action) {
|
switch (action) {
|
||||||
case "pause":
|
case "pause":
|
||||||
await apiPost<{ message: string }>(
|
await apiPost<{ message: string }>(
|
||||||
`/api/orchestrator/api/mission-control/sessions/${encodeURIComponent(sessionId)}/pause`
|
`/api/mission-control/sessions/${encodeURIComponent(sessionId)}/pause`
|
||||||
);
|
);
|
||||||
return { nextStatus: "paused" };
|
return { nextStatus: "paused" };
|
||||||
case "resume":
|
case "resume":
|
||||||
await apiPost<{ message: string }>(
|
await apiPost<{ message: string }>(
|
||||||
`/api/orchestrator/api/mission-control/sessions/${encodeURIComponent(sessionId)}/resume`
|
`/api/mission-control/sessions/${encodeURIComponent(sessionId)}/resume`
|
||||||
);
|
);
|
||||||
return { nextStatus: "active" };
|
return { nextStatus: "active" };
|
||||||
case "graceful-kill":
|
case "graceful-kill":
|
||||||
await apiPost<{ message: string }>(
|
await apiPost<{ message: string }>(
|
||||||
`/api/orchestrator/api/mission-control/sessions/${encodeURIComponent(sessionId)}/kill`,
|
`/api/mission-control/sessions/${encodeURIComponent(sessionId)}/kill`,
|
||||||
{ force: false }
|
{ force: false }
|
||||||
);
|
);
|
||||||
return { nextStatus: "killed" };
|
return { nextStatus: "killed" };
|
||||||
case "force-kill":
|
case "force-kill":
|
||||||
await apiPost<{ message: string }>(
|
await apiPost<{ message: string }>(
|
||||||
`/api/orchestrator/api/mission-control/sessions/${encodeURIComponent(sessionId)}/kill`,
|
`/api/mission-control/sessions/${encodeURIComponent(sessionId)}/kill`,
|
||||||
{ force: true }
|
{ force: true }
|
||||||
);
|
);
|
||||||
return { nextStatus: "killed" };
|
return { nextStatus: "killed" };
|
||||||
|
|||||||
@@ -316,8 +316,6 @@ services:
|
|||||||
SANDBOX_ENABLED: "true"
|
SANDBOX_ENABLED: "true"
|
||||||
# API key for authenticating requests from the web proxy
|
# API key for authenticating requests from the web proxy
|
||||||
ORCHESTRATOR_API_KEY: ${ORCHESTRATOR_API_KEY}
|
ORCHESTRATOR_API_KEY: ${ORCHESTRATOR_API_KEY}
|
||||||
# Prisma database connection (uses the shared openbrain postgres)
|
|
||||||
DATABASE_URL: postgresql://${POSTGRES_USER}:${POSTGRES_PASSWORD}@openbrain_brain-db:5432/${POSTGRES_DB:-mosaic}
|
|
||||||
volumes:
|
volumes:
|
||||||
- /var/run/docker.sock:/var/run/docker.sock:ro
|
- /var/run/docker.sock:/var/run/docker.sock:ro
|
||||||
- orchestrator_workspace:/workspace
|
- orchestrator_workspace:/workspace
|
||||||
@@ -333,7 +331,6 @@ services:
|
|||||||
start_period: 40s
|
start_period: 40s
|
||||||
networks:
|
networks:
|
||||||
- internal
|
- internal
|
||||||
- openbrain-brain-internal
|
|
||||||
cap_drop:
|
cap_drop:
|
||||||
- ALL
|
- ALL
|
||||||
cap_add:
|
cap_add:
|
||||||
@@ -406,7 +403,6 @@ services:
|
|||||||
networks:
|
networks:
|
||||||
- internal
|
- internal
|
||||||
- traefik-public
|
- traefik-public
|
||||||
- openbrain-brain-internal
|
|
||||||
deploy:
|
deploy:
|
||||||
restart_policy:
|
restart_policy:
|
||||||
condition: on-failure
|
condition: on-failure
|
||||||
|
|||||||
5
pnpm-lock.yaml
generated
5
pnpm-lock.yaml
generated
@@ -162,9 +162,6 @@ importers:
|
|||||||
bullmq:
|
bullmq:
|
||||||
specifier: ^5.67.2
|
specifier: ^5.67.2
|
||||||
version: 5.67.2
|
version: 5.67.2
|
||||||
chokidar:
|
|
||||||
specifier: ^4.0.3
|
|
||||||
version: 4.0.3
|
|
||||||
class-transformer:
|
class-transformer:
|
||||||
specifier: ^0.5.1
|
specifier: ^0.5.1
|
||||||
version: 0.5.1
|
version: 0.5.1
|
||||||
@@ -1628,6 +1625,7 @@ packages:
|
|||||||
|
|
||||||
'@mosaicstack/telemetry-client@0.1.1':
|
'@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}
|
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':
|
'@mrleebo/prisma-ast@0.13.1':
|
||||||
resolution: {integrity: sha512-XyroGQXcHrZdvmrGJvsA9KNeOOgGMg1Vg9OlheUsBOSKznLMDl+YChxbkboRHvtFYJEMRYmlV3uoo/njCw05iw==}
|
resolution: {integrity: sha512-XyroGQXcHrZdvmrGJvsA9KNeOOgGMg1Vg9OlheUsBOSKznLMDl+YChxbkboRHvtFYJEMRYmlV3uoo/njCw05iw==}
|
||||||
@@ -5178,7 +5176,6 @@ packages:
|
|||||||
|
|
||||||
glob@10.5.0:
|
glob@10.5.0:
|
||||||
resolution: {integrity: sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==}
|
resolution: {integrity: sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==}
|
||||||
deprecated: Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me
|
|
||||||
hasBin: true
|
hasBin: true
|
||||||
|
|
||||||
glob@13.0.0:
|
glob@13.0.0:
|
||||||
|
|||||||
Reference in New Issue
Block a user