import { Injectable, NotFoundException } from "@nestjs/common"; import type { LlmProvider } from "@prisma/client"; import { createHash, timingSafeEqual } from "node:crypto"; import { PrismaService } from "../prisma/prisma.service"; import { CryptoService } from "../crypto/crypto.service"; const DEFAULT_GATEWAY_PORT = 18789; const DEFAULT_PRIMARY_MODEL = "openai/gpt-4o-mini"; type ContainerType = "user" | "system"; export interface ContainerTokenValidation { type: ContainerType; id: string; } type OpenClawModelMap = Record>; interface OpenClawProviderConfig { apiKey?: string; baseUrl?: string; models: OpenClawModelMap; } interface OpenClawConfig { gateway: { mode: "local"; port: number; bind: "lan"; auth: { mode: "token" }; http: { endpoints: { chatCompletions: { enabled: true }; }; }; }; agents: { defaults: { model: { primary: string; }; }; }; models: { providers: Record; }; } @Injectable() export class AgentConfigService { constructor( private readonly prisma: PrismaService, private readonly crypto: CryptoService ) {} // Generate complete openclaw.json for a user container async generateUserConfig(userId: string): Promise { const [userAgentConfig, providers, userContainer] = await Promise.all([ this.prisma.userAgentConfig.findUnique({ where: { userId }, }), this.prisma.llmProvider.findMany({ where: { userId, isActive: true, }, orderBy: { createdAt: "asc", }, }), this.prisma.userContainer.findUnique({ where: { userId }, }), ]); if (!userContainer) { throw new NotFoundException(`User container not found for user ${userId}`); } const primaryModel = userAgentConfig?.primaryModel ?? this.resolvePrimaryModelFromProviders(providers) ?? DEFAULT_PRIMARY_MODEL; return this.buildOpenClawConfig(primaryModel, userContainer.gatewayPort, providers); } // Generate config for a system container async generateSystemConfig(containerId: string): Promise { const systemContainer = await this.prisma.systemContainer.findUnique({ where: { id: containerId }, }); if (!systemContainer) { throw new NotFoundException(`System container ${containerId} not found`); } return this.buildOpenClawConfig( systemContainer.primaryModel || DEFAULT_PRIMARY_MODEL, systemContainer.gatewayPort, [] ); } async generateConfigForContainer( type: ContainerType, containerId: string ): Promise { if (type === "system") { return this.generateSystemConfig(containerId); } const userContainer = await this.prisma.userContainer.findUnique({ where: { id: containerId }, select: { userId: true }, }); if (!userContainer) { throw new NotFoundException(`User container ${containerId} not found`); } return this.generateUserConfig(userContainer.userId); } // Validate a container's bearer token async validateContainerToken(token: string): Promise { if (!token) { return null; } const [userContainers, systemContainers] = await Promise.all([ this.prisma.userContainer.findMany({ select: { id: true, gatewayToken: true, }, }), this.prisma.systemContainer.findMany({ select: { id: true, gatewayToken: true, }, }), ]); let match: ContainerTokenValidation | null = null; for (const container of userContainers) { const storedToken = this.decryptContainerToken(container.gatewayToken); if (!match && storedToken && this.tokensEqual(storedToken, token)) { match = { type: "user", id: container.id }; } } for (const container of systemContainers) { const storedToken = this.decryptContainerToken(container.gatewayToken); if (!match && storedToken && this.tokensEqual(storedToken, token)) { match = { type: "system", id: container.id }; } } return match; } private buildOpenClawConfig( primaryModel: string, gatewayPort: number | null, providers: LlmProvider[] ): OpenClawConfig { return { gateway: { mode: "local", port: gatewayPort ?? DEFAULT_GATEWAY_PORT, bind: "lan", auth: { mode: "token" }, http: { endpoints: { chatCompletions: { enabled: true }, }, }, }, agents: { defaults: { model: { primary: primaryModel, }, }, }, models: { providers: this.buildProviderConfig(providers), }, }; } private buildProviderConfig(providers: LlmProvider[]): Record { const providerConfig: Record = {}; for (const provider of providers) { const config: OpenClawProviderConfig = { models: this.extractModels(provider.models), }; const apiKey = this.decryptIfNeeded(provider.apiKey); if (apiKey) { config.apiKey = apiKey; } if (provider.baseUrl) { config.baseUrl = provider.baseUrl; } providerConfig[provider.name] = config; } return providerConfig; } private extractModels(models: unknown): OpenClawModelMap { const modelMap: OpenClawModelMap = {}; if (!Array.isArray(models)) { return modelMap; } for (const modelEntry of models) { if (typeof modelEntry === "string") { modelMap[modelEntry] = {}; continue; } if (this.hasModelId(modelEntry)) { modelMap[modelEntry.id] = {}; } } return modelMap; } private resolvePrimaryModelFromProviders(providers: LlmProvider[]): string | null { for (const provider of providers) { const modelIds = Object.keys(this.extractModels(provider.models)); const firstModelId = modelIds[0]; if (firstModelId) { return `${provider.name}/${firstModelId}`; } } return null; } private decryptIfNeeded(value: string | null | undefined): string | undefined { if (!value) { return undefined; } if (this.crypto.isEncrypted(value)) { return this.crypto.decrypt(value); } return value; } private decryptContainerToken(value: string): string | null { try { return this.decryptIfNeeded(value) ?? null; } catch { return null; } } private tokensEqual(left: string, right: string): boolean { const leftDigest = createHash("sha256").update(left, "utf8").digest(); const rightDigest = createHash("sha256").update(right, "utf8").digest(); return timingSafeEqual(leftDigest, rightDigest); } private hasModelId(modelEntry: unknown): modelEntry is { id: string } { if (typeof modelEntry !== "object" || modelEntry === null || !("id" in modelEntry)) { return false; } return typeof (modelEntry as { id?: unknown }).id === "string"; } }