feat: implement metadata catalog database management

This commit is contained in:
Codex
2026-08-27 22:43:54 +02:00
parent 705af3aeb2
commit 79c4c925b5
86 changed files with 12566 additions and 135 deletions
+41
View File
@@ -7,6 +7,7 @@ import {
} from "./auth/config.js";
import { createProjectedAuthenticationConfigProvider } from "./auth/runtime-projection.js";
import type { WorkspaceRegistryConfig } from "./workspaces/types.js";
import type { CatalogConnectionConfig } from "./catalog/repository.js";
export interface AppConfig {
host: string; port: number; harnessDir: string; thtBin: string; piBin: string;
@@ -20,6 +21,8 @@ export interface AppConfig {
host?: string; port?: number; database?: string; runtimeUser?: string;
runtimePasswordFile?: string; sslmode?: "verify-ca" | "verify-full"; sslrootcert?: string;
};
/** Installation-local metadata catalog. Omitted installations expose an unavailable admin surface. */
catalogDatabase?: CatalogConnectionConfig;
defaults: { provider?: string; model?: string; thinking?: string };
maxPiProcesses: number;
settingsFile: string;
@@ -40,6 +43,7 @@ export interface AppConfig {
/** Explicit compatibility mode for old loopback clients that send `workspace` in POST /sessions. */
legacyWorkspaceMode: boolean;
workspaceDiagnosticTimeoutMs: number;
catalogSyncTimeoutMs: number;
workspaceRegistry: WorkspaceRegistryConfig;
workspaceSecretStoreRoot: string;
workspaceSecretRuntimeRoot: string;
@@ -127,6 +131,14 @@ function diagnosticTimeout(value: string | undefined): number {
return timeout;
}
function catalogSyncTimeout(value: string | undefined): number {
const timeout = Number(value ?? 600_000);
if (!Number.isSafeInteger(timeout) || timeout < 1_000 || timeout > 3_600_000) {
throw new Error("catalog synchronization timeout configuration is invalid");
}
return timeout;
}
function piManagementTimeout(value: string | undefined): number {
const timeout = Number(value ?? 8_000);
if (!Number.isSafeInteger(timeout) || timeout < 1 || timeout > 30_000) {
@@ -176,6 +188,33 @@ function positiveDimension(value: string | undefined, fallback: number): number
return parsed;
}
function catalogDatabase(env: Record<string, string | undefined>): CatalogConnectionConfig | undefined {
const value = env.THT_CATALOG_DATABASE_URL;
if (value !== undefined) {
try {
const parsed = new URL(value);
if ((parsed.protocol !== "postgres:" && parsed.protocol !== "postgresql:")
|| !parsed.hostname || !parsed.pathname.slice(1) || parsed.hash || parsed.search) throw new Error();
return { connectionString: value };
} catch {
throw new Error("catalog database configuration is invalid");
}
}
const host = env.THT_CATALOG_DB_HOST;
if (host === undefined) return undefined;
const port = Number(env.THT_CATALOG_DB_PORT ?? 5432);
const database = env.THT_CATALOG_DB_NAME;
const user = env.THT_CATALOG_RUNTIME_USER;
const passwordFile = env.THT_CATALOG_RUNTIME_PASSWORD_FILE;
try {
if (!host.trim() || !database?.trim() || !user?.trim() || !passwordFile
|| !path.isAbsolute(passwordFile) || !Number.isInteger(port) || port < 1 || port > 65_535) throw new Error();
return { host, port, database, user, passwordFile };
} catch {
throw new Error("catalog database configuration is invalid");
}
}
export function loadConfig(
env: Record<string, string | undefined>,
options: { surface?: "application" | "workspace-maintenance" } = {},
@@ -356,6 +395,7 @@ export function loadConfig(
authentication,
publicExposure,
sessionStorage,
catalogDatabase: catalogDatabase(env),
defaults: { provider: env.PI_PROVIDER, model: env.PI_MODEL, thinking: env.PI_THINKING },
maxPiProcesses: Number(env.MAX_PI_PROCESSES ?? 4),
settingsFile,
@@ -370,6 +410,7 @@ export function loadConfig(
dwhPrecheck: env.THT_DWH_PRECHECK === "true" || env.THT_DWH_PRECHECK === "1",
legacyWorkspaceMode: legacyWorkspaceMode === "local",
workspaceDiagnosticTimeoutMs: diagnosticTimeout(env.THT_WORKSPACE_DIAGNOSTIC_TIMEOUT_MS),
catalogSyncTimeoutMs: catalogSyncTimeout(env.THT_CATALOG_SYNC_TIMEOUT_MS),
workspaceRegistry,
workspaceSecretStoreRoot,
workspaceSecretRuntimeRoot,