From c2f9b03973c622890c8e109f43a70bc3027ae1ae Mon Sep 17 00:00:00 2001 From: mptyl Date: Mon, 10 Aug 2026 21:09:37 +0200 Subject: [PATCH] refactor: remove legacy workspace runtime branches --- backend/src/workspaces/bindings.ts | 102 +- backend/src/workspaces/contracts.ts | 59 +- .../workspaces/deprecated-v2-descriptor.ts | 52 - backend/src/workspaces/diagnostics.ts | 909 +++--------- backend/src/workspaces/migrate-v2-qdrant.ts | 35 +- backend/src/workspaces/runtime-renderer.ts | 193 +-- backend/test/routes-workspaces.test.ts | 9 +- .../test/workspace-runtime-handoff.test.ts | 21 - .../test/workspace-runtime-renderer.test.ts | 255 +--- backend/test/workspaces-bindings.test.ts | 261 +--- backend/test/workspaces-contracts.test.ts | 304 +--- backend/test/workspaces-diagnostics.test.ts | 1291 +++++------------ .../workspaces-runtime-v3-boundaries.test.ts | 70 + backend/test/workspaces-schema.test.ts | 36 - 14 files changed, 906 insertions(+), 2691 deletions(-) delete mode 100644 backend/src/workspaces/deprecated-v2-descriptor.ts create mode 100644 backend/test/workspaces-runtime-v3-boundaries.test.ts diff --git a/backend/src/workspaces/bindings.ts b/backend/src/workspaces/bindings.ts index dbc4784b..9d74f7e0 100644 --- a/backend/src/workspaces/bindings.ts +++ b/backend/src/workspaces/bindings.ts @@ -1,15 +1,12 @@ import { constants, realpathSync, statSync, accessSync } from "node:fs"; import { isAbsolute, relative } from "node:path"; -import { buildInstallationContract, type InstallationRole, type InstallationSuffix } from "./contracts.js"; +import { buildInstallationContract, type InstallationSuffix } from "./contracts.js"; import { DWH_TRANSPORTS, - VECTOR_TRANSPORTS, validateWorkspaceDescriptor, type DwhTransport, - type VectorTransport, type WorkspaceDescriptor, } from "./schema.js"; -import type { DeprecatedV2Descriptor } from "./deprecated-v2-descriptor.js"; export interface ResolvedEvidenceBinding { values: Record; @@ -18,42 +15,26 @@ export interface ResolvedEvidenceBinding { export interface RuntimeBindings { dwh: ResolvedBinding; - vector: ResolvedBinding; - vectorWriter: ResolvedBinding; - embedding: ResolvedBinding; evidence: ResolvedEvidenceBinding; } export interface ResolvedBinding { - transport: DwhTransport | VectorTransport; + transport: DwhTransport; values: Record; missing: string[]; } -const REQUIRED_SUFFIXES: Record<"DWH" | "VECTOR", Record> = { - DWH: { - postgres_direct: ["HOST", "PORT", "USER", "PASSWORD_FILE"], - rest_api: ["BASE_URL", "API_KEY_FILE"], - ssh_tunnel: [ - "USER", "PASSWORD_FILE", "SSH_HOST", "SSH_PORT", "SSH_USER", - "SSH_PRIVATE_KEY_FILE", "SSH_KNOWN_HOSTS_FILE", "SSH_TARGET_HOST", "SSH_TARGET_PORT", - ], - }, - VECTOR: { - pgvector_direct: ["HOST", "PORT", "USER", "PASSWORD_FILE"], - rest_api: ["BASE_URL", "API_KEY_FILE"], - ssh_tunnel: [ - "USER", "PASSWORD_FILE", "SSH_HOST", "SSH_PORT", "SSH_USER", - "SSH_PRIVATE_KEY_FILE", "SSH_KNOWN_HOSTS_FILE", "SSH_TARGET_HOST", "SSH_TARGET_PORT", - ], - }, +const REQUIRED_SUFFIXES: Record = { + postgres_direct: ["HOST", "PORT", "USER", "PASSWORD_FILE"], + rest_api: ["BASE_URL", "API_KEY_FILE"], + ssh_tunnel: [ + "USER", "PASSWORD_FILE", "SSH_HOST", "SSH_PORT", "SSH_USER", + "SSH_PRIVATE_KEY_FILE", "SSH_KNOWN_HOSTS_FILE", "SSH_TARGET_HOST", "SSH_TARGET_PORT", + ], }; -const EMBEDDING_REQUIRED_SUFFIXES: readonly InstallationSuffix[] = ["BASE_URL"]; - -function isTransport(value: string | undefined): value is DwhTransport | VectorTransport { - return value !== undefined - && ([...DWH_TRANSPORTS, ...VECTOR_TRANSPORTS] as readonly string[]).includes(value); +function isTransport(value: string | undefined): value is DwhTransport { + return value !== undefined && (DWH_TRANSPORTS as readonly string[]).includes(value); } function isInside(path: string, root: string): boolean { @@ -76,18 +57,23 @@ function safeSecretFilePath(path: string, secretRoots: readonly string[]): strin } } +function requireSupportedDescriptor(workspace: unknown): void { + if (typeof workspace !== "object" || workspace === null) { + throw new Error("Workspace bindings support only workspace schema version 3"); + } + const metadata = Reflect.get(workspace, "workspace"); + if (typeof metadata !== "object" || metadata === null + || Reflect.get(metadata, "schema_version") !== 3) { + throw new Error("Workspace bindings support only workspace schema version 3"); + } +} + function requiredSuffixes( workspace: WorkspaceDescriptor, - role: Exclude, - transport: DwhTransport | VectorTransport, + transport: DwhTransport, ): readonly InstallationSuffix[] { - if (role === "EMBEDDING") return EMBEDDING_REQUIRED_SUFFIXES; - if (role === "VECTOR_WRITER") return ["API_KEY_FILE"]; - const required = REQUIRED_SUFFIXES[role][transport] ?? []; - const diagnostic = role === "DWH" - ? workspace.diagnostics?.dwh_rest - : (workspace as unknown as DeprecatedV2Descriptor).diagnostics?.vector_rest?.metadata; - return transport === "rest_api" && diagnostic?.auth === "none" + const required = REQUIRED_SUFFIXES[transport]; + return transport === "rest_api" && workspace.diagnostics?.dwh_rest?.auth === "none" ? required.filter((suffix) => suffix !== "API_KEY_FILE") : required; } @@ -98,37 +84,29 @@ function requiredSuffixes( */ export function resolveBinding( workspace: WorkspaceDescriptor, - role: Exclude, + role: "DWH", env: NodeJS.ProcessEnv, secretRoots: readonly string[], ): ResolvedBinding { + requireSupportedDescriptor(workspace); const descriptor = validateWorkspaceDescriptor(workspace); - if (descriptor.workspace.schema_version === 3 && role !== "DWH") { - return { transport: "rest_api", values: {}, missing: [] }; - } - const contract = buildInstallationContract(descriptor); - const legacy = descriptor as unknown as DeprecatedV2Descriptor; const variables = contract.variables.filter((variable) => variable.role === role); const transportVariable = variables.find((variable) => variable.suffix === "TRANSPORT"); - const supported = role === "DWH" - ? descriptor.dwh.supported_transports - : role === "VECTOR" - ? legacy.semantic_index.vector_store.supported_transports - : ["rest_api"] as const; + const supported = descriptor.dwh.supported_transports; const selectedValue = transportVariable ? env[transportVariable.name] : undefined; const selectedTransport = isTransport(selectedValue) ? selectedValue : supported[0]; const missing: string[] = []; - if (transportVariable && (!isTransport(selectedValue) || !supported.includes(selectedTransport as never))) { + if (transportVariable && (!isTransport(selectedValue) || !supported.includes(selectedTransport))) { missing.push(transportVariable.name); } - const required = new Set(requiredSuffixes(descriptor, role, selectedTransport)); + const required = new Set(requiredSuffixes(descriptor, selectedTransport)); const values: Record = {}; for (const variable of variables) { if (variable.suffix === "TRANSPORT") continue; - if (variable.transports && !variable.transports.includes(selectedTransport as never)) continue; + if (variable.transports && !variable.transports.includes(selectedTransport)) continue; const value = env[variable.name]; const present = value !== undefined && value.trim() !== ""; @@ -149,12 +127,13 @@ export function resolveEvidenceBinding( env: NodeJS.ProcessEnv, secretRoots: readonly string[], ): ResolvedEvidenceBinding { + requireSupportedDescriptor(workspace); const descriptor = validateWorkspaceDescriptor(workspace); const variables = buildInstallationContract(descriptor).variables .filter((variable) => variable.role === "EVIDENCE"); if (variables.length === 0) return { values: {}, missing: [] }; - const source = "evidence" in descriptor ? descriptor.evidence?.source : undefined; + const source = descriptor.evidence?.source; const required = new Set( source?.type === "http" ? ["SIGNED_URLS_FILE"] @@ -176,29 +155,22 @@ export function resolveEvidenceBinding( return { values, missing }; } -/** Resolve all runtime roles together so optional writer credentials cannot be smuggled into reader bindings. */ +/** Resolve the complete schema-v3 runtime binding set. */ export function resolveRuntimeBindings( workspace: WorkspaceDescriptor, env: NodeJS.ProcessEnv, secretRoots: readonly string[], ): RuntimeBindings { + requireSupportedDescriptor(workspace); const descriptor = validateWorkspaceDescriptor(workspace); return { dwh: resolveBinding(descriptor, "DWH", env, secretRoots), - vector: resolveBinding(descriptor, "VECTOR", env, secretRoots), - vectorWriter: resolveBinding(descriptor, "VECTOR_WRITER", env, secretRoots), - embedding: resolveBinding(descriptor, "EMBEDDING", env, secretRoots), evidence: resolveEvidenceBinding(descriptor, env, secretRoots), }; } -/** - * SSH bindings are currently probe-only: diagnostics owns a short-lived tunnel, while the - * session runtime has no tunnel owner. Keep activation fail-closed until that lifecycle exists. - */ +/** SSH bindings remain diagnostic-only until the session runtime owns a long-lived tunnel. */ export function supportsSessionRuntime(bindings: RuntimeBindings): boolean { - return bindings.dwh.transport !== "ssh_tunnel" - && bindings.vector.transport !== "ssh_tunnel" - && bindings.evidence.missing.length === 0; + return bindings.dwh.transport !== "ssh_tunnel" && bindings.evidence.missing.length === 0; } diff --git a/backend/src/workspaces/contracts.ts b/backend/src/workspaces/contracts.ts index e535caa9..e13baf17 100644 --- a/backend/src/workspaces/contracts.ts +++ b/backend/src/workspaces/contracts.ts @@ -1,8 +1,7 @@ import { validateWorkspaceDescriptor } from "./schema.js"; -import type { DwhTransport, VectorTransport, WorkspaceDescriptor } from "./schema.js"; -import type { DeprecatedV2Descriptor } from "./deprecated-v2-descriptor.js"; +import type { DwhTransport, WorkspaceDescriptor } from "./schema.js"; -export type InstallationRole = "DWH" | "VECTOR" | "VECTOR_WRITER" | "EMBEDDING" | "EVIDENCE"; +export type InstallationRole = "DWH" | "EVIDENCE"; export type InstallationSuffix = | "TRANSPORT" | "HOST" @@ -24,7 +23,7 @@ export type InstallationSuffix = | "SECRET_KEY_FILE" | "SESSION_TOKEN_FILE"; -type ConnectorTransport = DwhTransport | VectorTransport; +type ConnectorTransport = DwhTransport; export interface InstallationVariable { name: string; @@ -67,12 +66,6 @@ const SSH_SUFFIXES: readonly InstallationSuffix[] = [ "SSH_TARGET_PORT", ]; -const EMBEDDING_SUFFIXES: readonly InstallationSuffix[] = [ - "BASE_URL", - "API_KEY_FILE", - "TLS_CA_FILE", -]; - function namespaceFor(workspace: WorkspaceDescriptor): string { return workspace.workspace.id.replaceAll("-", "_").toUpperCase(); } @@ -94,11 +87,10 @@ function createVariable( function connectorVariables( namespace: string, - role: "DWH" | "VECTOR", - transports: readonly ConnectorTransport[], + transports: readonly DwhTransport[], ): InstallationVariable[] { - const suffixTransports = new Map(); - const add = (suffixes: readonly InstallationSuffix[], transport: ConnectorTransport) => { + const suffixTransports = new Map(); + const add = (suffixes: readonly InstallationSuffix[], transport: DwhTransport) => { for (const suffix of suffixes) { const applicable = suffixTransports.get(suffix) ?? []; applicable.push(transport); @@ -107,7 +99,7 @@ function connectorVariables( }; for (const transport of transports) { - if (transport === "postgres_direct" || transport === "pgvector_direct") { + if (transport === "postgres_direct") { add(DIRECT_SUFFIXES, transport); } else if (transport === "rest_api") { add(REST_SUFFIXES, transport); @@ -117,9 +109,9 @@ function connectorVariables( } return [ - createVariable(namespace, role, "TRANSPORT", transports), + createVariable(namespace, "DWH", "TRANSPORT", transports), ...[...suffixTransports.entries()].map(([suffix, applicable]) => ( - createVariable(namespace, role, suffix, applicable) + createVariable(namespace, "DWH", suffix, applicable) )), ]; } @@ -143,30 +135,27 @@ function evidenceVariables( return []; } +function requireSupportedDescriptor(workspace: unknown): void { + if (typeof workspace !== "object" || workspace === null) { + throw new Error("Installation contract supports only workspace schema version 3"); + } + const metadata = Reflect.get(workspace, "workspace"); + if (typeof metadata !== "object" || metadata === null + || Reflect.get(metadata, "schema_version") !== 3) { + throw new Error("Installation contract supports only workspace schema version 3"); + } +} + export function buildInstallationContract(workspace: WorkspaceDescriptor): InstallationContract { + requireSupportedDescriptor(workspace); const descriptor = validateWorkspaceDescriptor(workspace); const namespace = namespaceFor(descriptor); - const schemaVersion = Number(descriptor.workspace.schema_version); - const legacy = descriptor as unknown as DeprecatedV2Descriptor; return { workspaceId: descriptor.workspace.id, namespace, variables: [ - ...connectorVariables(namespace, "DWH", descriptor.dwh.supported_transports), - ...(schemaVersion === 2 - ? connectorVariables( - namespace, - "VECTOR", - legacy.semantic_index.vector_store.supported_transports, - ) - : []), - ...(schemaVersion === 2 && legacy.semantic_index.vector_writer - ? [createVariable(namespace, "VECTOR_WRITER", "API_KEY_FILE")] - : []), - ...(schemaVersion === 2 - ? EMBEDDING_SUFFIXES.map((suffix) => createVariable(namespace, "EMBEDDING", suffix)) - : []), + ...connectorVariables(namespace, descriptor.dwh.supported_transports), ...evidenceVariables(namespace, descriptor), ], }; @@ -276,10 +265,10 @@ export function renderWorkspaceDocs(workspace: WorkspaceDescriptor): { envExampl "", "Use the following UI fields as installation bindings. Secret fields always contain file paths, never secret values.", "", - ...(["DWH", "VECTOR", "VECTOR_WRITER", "EMBEDDING", "EVIDENCE"] as const) + ...(["DWH", "EVIDENCE"] as const) .filter((role) => variablesByRole.has(role)) .flatMap((role) => [ - `## ${role === "DWH" ? "Data warehouse" : role === "VECTOR" ? "Vector store" : role === "VECTOR_WRITER" ? "Vector writer" : role === "EMBEDDING" ? "Embedding service" : "Evidence"}`, + `## ${role === "DWH" ? "Data warehouse" : "Evidence"}`, "", ...(variablesByRole.get(role) ?? []).map((variable) => ( `- \`${variable.name}\`${variable.transports ? ` (for: ${variable.transports.join(", ")})` : ""}` diff --git a/backend/src/workspaces/deprecated-v2-descriptor.ts b/backend/src/workspaces/deprecated-v2-descriptor.ts deleted file mode 100644 index dd5fe61e..00000000 --- a/backend/src/workspaces/deprecated-v2-descriptor.ts +++ /dev/null @@ -1,52 +0,0 @@ -import type { - CanonicalDiagnostics, - DwhTransport, - VectorTransport, -} from "./schema.js"; - -/** - * Temporary compile-time shape for legacy runtime branches that will be removed separately. - * It is deliberately not part of the accepted workspace descriptor schema. - */ -export interface DeprecatedV2Descriptor { - workspace: { - schema_version: 2; - id: string; - name: string; - description?: string; - language: "en" | "it"; - }; - dwh: { - engine: "postgres"; - database: string; - schema: string; - port?: number; - timeout_ms?: number; - supported_transports: DwhTransport[]; - }; - semantic_index: { - vector_store: { - engine: "pgvector"; - database: string; - schema: string; - collection: string; - dimensions: number; - distance: "cosine" | "l2" | "inner_product"; - port?: number; - timeout_ms?: number; - supported_transports: VectorTransport[]; - }; - vector_writer?: Record; - embedding: { - provider: "ollama_compatible" | "openai_compatible"; - model: string; - dimensions: number; - timeout_ms?: number; - }; - }; - llm_policy: { - default?: `${string}/${string}`; - allowed: `${string}/${string}`[]; - }; - diagnostics?: CanonicalDiagnostics; -} diff --git a/backend/src/workspaces/diagnostics.ts b/backend/src/workspaces/diagnostics.ts index 5fa4fde2..bff93020 100644 --- a/backend/src/workspaces/diagnostics.ts +++ b/backend/src/workspaces/diagnostics.ts @@ -1,9 +1,4 @@ -import { randomUUID } from "node:crypto"; -import { readFile, realpath } from "node:fs/promises"; -import { createConnection } from "node:net"; -import { createServer } from "node:net"; -import { once } from "node:events"; -import { spawn } from "node:child_process"; +import { readFile } from "node:fs/promises"; import { Client } from "pg"; import { MAX_WORKSPACE_DIAGNOSTIC_TIMEOUT_MS } from "../config.js"; import { buildInstallationContract } from "./contracts.js"; @@ -14,7 +9,6 @@ import { type RestDiagnosticRequest, type WorkspaceDescriptor, } from "./schema.js"; -import type { DeprecatedV2Descriptor } from "./deprecated-v2-descriptor.js"; import type { WorkspaceErrorCode } from "./types.js"; import type { SemanticRuntimeConfig } from "./runtime-renderer.js"; @@ -31,12 +25,9 @@ export interface WorkspaceDiagnostics { diagnostics: Diagnostic[]; } -type ConnectorRole = "dwh" | "vector"; - interface DiagnosticResource { database?: string; schema?: string; - collection?: string; } type RestConnectorDiagnostic = RestDiagnosticRequest & { @@ -44,8 +35,8 @@ type RestConnectorDiagnostic = RestDiagnosticRequest & { }; export interface ConnectorDiagnosticRequest { - role: ConnectorRole; - transport: "postgres_direct" | "pgvector_direct" | "rest_api" | "ssh_tunnel"; + role: "dwh"; + transport: "postgres_direct" | "rest_api"; host?: string; port?: number; baseUrl?: string; @@ -66,59 +57,24 @@ export interface ConnectorDiagnosticResult { resource: DiagnosticResource; } -export interface SshTunnelRequest { - sshHost: string; - sshPort: number; - sshUser: string; - privateKeyFile: string; - knownHostsFile: string; - targetHost: string; - targetPort: number; - localHost: "127.0.0.1"; - localPort: 0; +export interface QdrantDiagnosticRequest { + baseUrl: string; + collection: string; timeoutMs: number; signal: AbortSignal; } -export interface LoopbackTunnel { - host: "127.0.0.1"; - port: number; -} - -export interface VectorDiagnosticRequest { - transport?: "pgvector_direct" | "rest_api" | "ssh_tunnel"; - baseUrl?: string; - credentialFile?: string; - tlsCaFile?: string; - tlsServername?: string; - diagnostic?: RestDiagnosticRequest & { - response: { collection: string; dimensions: string; distance: string }; - }; +export interface QdrantDiagnosticResult { collection: string; dimensions?: number; - distance?: "cosine" | "l2" | "inner_product"; - host?: string; - port?: number; - user?: string; - resource?: DiagnosticResource; - timeoutMs: number; - signal: AbortSignal; -} - -export interface VectorDiagnosticResult { - collection?: string; - dimensions?: number; - distance?: "cosine" | "l2" | "inner_product"; + distance?: string; } export interface EmbeddingDiagnosticRequest { baseUrl: string; - credentialFile?: string; - tlsCaFile?: string; model: string; timeoutMs: number; signal: AbortSignal; - diagnostic?: RestDiagnosticRequest & { response: { model: string; dimensions: string } }; } export interface EmbeddingDiagnosticResult { @@ -126,20 +82,6 @@ export interface EmbeddingDiagnosticResult { dimensions?: number; } -export interface WriteDiagnosticRecordRequest { - collection: string; - id: string; - dimensions: number; - timeoutMs: number; - signal: AbortSignal; - credentialFile?: string; - tlsCaFile?: string; - baseUrl?: string; - diagnostic?: RestDiagnosticRequest & { - response: { operation: string }; - }; -} - export interface DirectProtocolFactory { probe(request: ConnectorDiagnosticRequest): Promise; } @@ -151,68 +93,35 @@ export interface DatabaseDiagnosticClient { export interface DatabaseDiagnosticClientFactory { connect(request: { - host: string; port: number; database: string; user: string; credentialFile: string; tlsCaFile?: string; tlsServername?: string; signal: AbortSignal; + host: string; + port: number; + database: string; + user: string; + credentialFile: string; + tlsCaFile?: string; + tlsServername?: string; + signal: AbortSignal; }): Promise; } -export interface SshProcessFactory { - start(request: SshTunnelRequest, args: readonly string[]): Promise<{ - tunnel: LoopbackTunnel; - close(): Promise; - }>; -} - export interface ConcreteDiagnosticAdapterDependencies { directProtocol?: DirectProtocolFactory; - sshProcess?: SshProcessFactory; databaseClient?: DatabaseDiagnosticClientFactory; - sshSpawn?: (args: readonly string[]) => { kill(signal?: NodeJS.Signals): boolean; once?(event: "error" | "exit", listener: (...args: any[]) => void): unknown; stderr?: { on(event: "data", listener: (data: Buffer | string) => void): unknown; off?(event: "data", listener: (data: Buffer | string) => void): unknown } }; - reserveLoopbackPort?: () => Promise; - sshForwardConfirmed?: (tunnel: LoopbackTunnel, signal: AbortSignal) => Promise; } -/** - * Adapters own protocol-specific I/O. They receive only binding file paths, never secret - * contents, and return metadata only; response bodies must stay inside the adapter. - */ +/** Adapters retain only diagnostic metadata and never return credential contents or bodies. */ export interface DiagnosticAdapters { probeConnector(request: ConnectorDiagnosticRequest): Promise; - withSshTunnel( - request: SshTunnelRequest, - probe: (tunnel: LoopbackTunnel) => Promise, - ): Promise; - inspectVector(request: VectorDiagnosticRequest): Promise; + inspectQdrant(request: QdrantDiagnosticRequest): Promise; probeEmbedding(request: EmbeddingDiagnosticRequest): Promise; - writeDiagnosticRecord(request: WriteDiagnosticRecordRequest): Promise; - removeDiagnosticRecord(request: WriteDiagnosticRecordRequest): Promise; } export const DEFAULT_WORKSPACE_DIAGNOSTIC_TIMEOUT_MS = 5_000; -async function connectTcp(host: string, port: number, signal: AbortSignal): Promise { - const socket = createConnection({ host, port }); - const abort = () => socket.destroy(); - signal.addEventListener("abort", abort, { once: true }); - try { - await Promise.race([once(socket, "connect"), once(socket, "error").then(([error]) => Promise.reject(error))]); - } finally { - signal.removeEventListener("abort", abort); - socket.destroy(); - } -} - async function secretPresent(file: string): Promise { return (await readFile(file, "utf8")).trim().length > 0; } -async function sameSecretFile(first: string, second: string): Promise { - try { - return await realpath(first) === await realpath(second); - } catch { - return first === second; - } -} - async function restHeaders( diagnostic: RestDiagnosticRequest, credentialFile: string | undefined, @@ -223,21 +132,6 @@ async function restHeaders( return diagnostic.auth === "bearer" ? { authorization: `Bearer ${secret}` } : { "x-api-key": secret }; } -async function reserveLoopbackPort(): Promise { - const server = createServer(); - await new Promise((resolve, reject) => { - server.once("error", reject); - server.listen(0, "127.0.0.1", resolve); - }); - try { - const address = server.address(); - if (!address || typeof address === "string") throw new Error("SSH tunnel port unavailable"); - return address.port; - } finally { - await new Promise((resolve, reject) => server.close((error) => error ? reject(error) : resolve())); - } -} - /** * Concrete production adapters deliberately retain only probe metadata. Protocol failures and * response bodies are discarded at this boundary; callers receive fixed diagnostics instead. @@ -245,66 +139,22 @@ async function reserveLoopbackPort(): Promise { export function createConcreteDiagnosticAdapters( dependencies: ConcreteDiagnosticAdapterDependencies = {}, ): DiagnosticAdapters { - const spawnSsh = dependencies.sshSpawn ?? ((args: readonly string[]) => spawn("ssh", [...args], { stdio: ["ignore", "ignore", "pipe"] })); - const reserveSshPort = dependencies.reserveLoopbackPort ?? reserveLoopbackPort; - const sshProcess = dependencies.sshProcess ?? { - async start(request: SshTunnelRequest, args: readonly string[]) { - const port = await reserveSshPort(); - const resolvedArgs = args.map((argument) => argument === `127.0.0.1:0:${request.targetHost}:${request.targetPort}` - ? `127.0.0.1:${port}:${request.targetHost}:${request.targetPort}` : argument); - const child = spawnSsh(resolvedArgs); - const abort = () => { child.kill("SIGTERM"); }; - request.signal.addEventListener("abort", abort, { once: true }); - const tunnel = { host: "127.0.0.1" as const, port }; - try { - await withTimeout(request.timeoutMs, async (signal) => { - await Promise.race([ - dependencies.sshForwardConfirmed - ? dependencies.sshForwardConfirmed(tunnel, signal) - : new Promise((resolve, reject) => { - let stderrBuffer = ""; - const confirmation = new RegExp(`Local forwarding listening on 127\\.0\\.0\\.1 port ${port}\\.?`); - const confirm = (data: Buffer | string) => { - stderrBuffer = `${stderrBuffer}${data.toString()}`.slice(-4096); - const lines = stderrBuffer.split(/\r?\n/); - stderrBuffer = lines.pop() ?? ""; - if (lines.some((line) => confirmation.test(line))) { - child.stderr?.off?.("data", confirm); - resolve(); - } - }; - if (!child.stderr) return reject(new Error("SSH tunnel readiness failed")); - child.stderr.on("data", confirm); - signal.addEventListener("abort", () => reject(new Error("SSH tunnel readiness failed")), { once: true }); - }), - new Promise((_resolve, reject) => { - child.once?.("error", () => reject(new Error("SSH tunnel readiness failed"))); - child.once?.("exit", () => reject(new Error("SSH tunnel readiness failed"))); - }), - ]); - }); - } catch (error) { - request.signal.removeEventListener("abort", abort); - child.kill("SIGTERM"); - throw error; - } - return { - tunnel, - async close() { - request.signal.removeEventListener("abort", abort); - const exited = child.once - ? new Promise((resolve) => child.once?.("exit", resolve)) - : Promise.resolve(); - child.kill("SIGTERM"); - await withTimeout(request.timeoutMs, () => exited).catch(() => undefined); - }, - }; - }, - }; const databaseClient = dependencies.databaseClient ?? { - async connect(request: { host: string; port: number; database: string; user: string; credentialFile: string; tlsCaFile?: string; tlsServername?: string; signal: AbortSignal }) { + async connect(request: { + host: string; + port: number; + database: string; + user: string; + credentialFile: string; + tlsCaFile?: string; + tlsServername?: string; + signal: AbortSignal; + }) { const client = new Client({ - host: request.host, port: request.port, database: request.database, user: request.user, + host: request.host, + port: request.port, + database: request.database, + user: request.user, password: (await readFile(request.credentialFile, "utf8")).trim(), ssl: { ...(request.tlsCaFile ? { ca: await readFile(request.tlsCaFile, "utf8") } : {}), @@ -317,7 +167,13 @@ export function createConcreteDiagnosticAdapters( request.signal.addEventListener("abort", abort, { once: true }); try { await client.connect(); - return { query: async (sql: string, values: readonly unknown[]) => await client.query(sql, [...values]), end: async () => { request.signal.removeEventListener("abort", abort); await client.end(); } }; + return { + query: async (sql: string, values: readonly unknown[]) => await client.query(sql, [...values]), + end: async () => { + request.signal.removeEventListener("abort", abort); + await client.end(); + }, + }; } catch (error) { request.signal.removeEventListener("abort", abort); await client.end().catch(() => undefined); @@ -335,24 +191,40 @@ export function createConcreteDiagnosticAdapters( const schema = request.resource.schema; if (!database || !schema) throw new Error("direct probe failed"); const client = await databaseClient.connect({ - host: request.host, port: request.port, database, user: request.user, - credentialFile: request.credentialFile, tlsCaFile: request.tlsCaFile, - tlsServername: request.tlsServername, signal: request.signal, + host: request.host, + port: request.port, + database, + user: request.user, + credentialFile: request.credentialFile, + tlsCaFile: request.tlsCaFile, + tlsServername: request.tlsServername, + signal: request.signal, }); try { - const result = await client.query("SELECT current_database() AS database, current_schema() AS schema", []); + const result = await client.query( + "SELECT current_database() AS database, current_schema() AS schema", + [], + ); const row = result.rows[0]; if (row?.database !== database || row.schema !== schema) throw new Error("direct probe failed"); - return { resolved: true, tlsVerified: true, authenticated: true, resource: request.resource }; + return { + resolved: true, + tlsVerified: true, + authenticated: true, + resource: request.resource, + }; } finally { await client.end().catch(() => undefined); } }, }; + return { async probeConnector(request) { if (request.transport === "rest_api") { - if (!request.baseUrl || !request.diagnostic || request.tlsCaFile) throw new Error("REST probe failed"); + if (!request.baseUrl || !request.diagnostic || request.tlsCaFile) { + throw new Error("REST probe failed"); + } const endpoint = resolveDiagnosticUrl(request.baseUrl, request.diagnostic.path); const response = await fetch(endpoint.toString(), { method: request.diagnostic.method, @@ -361,11 +233,13 @@ export function createConcreteDiagnosticAdapters( redirect: "error", }); if (!response.ok) throw new Error("REST probe failed"); - if (request.diagnostic && "response" in request.diagnostic) { + if ("response" in request.diagnostic) { const payload = await response.json().catch(() => undefined) as Record | undefined; - const declared = request.diagnostic.response as { database?: string; schema?: string }; - if (!payload || (declared.database && payload[declared.database] !== request.resource.database) - || (declared.schema && payload[declared.schema] !== request.resource.schema)) throw new Error("REST probe failed"); + const declared = request.diagnostic.response; + if (!payload || (declared?.database && payload[declared.database] !== request.resource.database) + || (declared?.schema && payload[declared.schema] !== request.resource.schema)) { + throw new Error("REST probe failed"); + } } return { resolved: true, @@ -374,191 +248,56 @@ export function createConcreteDiagnosticAdapters( resource: request.resource, }; } + if (request.transport !== "postgres_direct") throw new Error("direct probe failed"); return await directProtocol.probe(request); }, - async withSshTunnel(request, probe) { - // The image supplies OpenSSH for the registry's SSH implementation. This adapter refuses - // an unverified host rather than falling back to an unsafe SSH option; the route-level - // tunnel owner supplies the process lifecycle in the next registry task. - if (!request.knownHostsFile || !(await secretPresent(request.privateKeyFile))) { - throw new Error("SSH probe failed"); - } - const args = [ - "-N", "-v", "-o", "BatchMode=yes", "-o", "ExitOnForwardFailure=yes", "-o", "StrictHostKeyChecking=yes", - "-o", `UserKnownHostsFile=${request.knownHostsFile}`, "-i", request.privateKeyFile, - "-p", String(request.sshPort), "-L", `127.0.0.1:0:${request.targetHost}:${request.targetPort}`, - `${request.sshUser}@${request.sshHost}`, - ]; - const tunnel = await sshProcess.start(request, args); - try { - return await probe(tunnel.tunnel); - } finally { - await tunnel.close().catch(() => undefined); - } - }, - async inspectVector(request) { - if (request.transport === "rest_api" && request.baseUrl && request.diagnostic === undefined) { - const response = await fetch(new URL(`/collections/${request.collection}`, `${request.baseUrl}/`).toString(), { - method: "GET", - signal: request.signal, - redirect: "error", - }); - const payload = await response.json().catch(() => undefined) as { - result?: { config?: { params?: { vectors?: { size?: unknown; distance?: unknown } } } }; - } | undefined; - const size = payload?.result?.config?.params?.vectors?.size; - const distance = payload?.result?.config?.params?.vectors?.distance; - if (!response.ok || !Number.isInteger(size) || typeof distance !== "string") { - throw new Error("vector metadata adapter is unavailable"); - } - return { - collection: request.collection, - dimensions: size as number, - distance: distance.toLowerCase() as VectorDiagnosticResult["distance"], - }; - } - if (request.transport === "pgvector_direct" || request.transport === "ssh_tunnel") { - const resource = request.resource; - if (!request.host || !request.port || !request.user || !request.credentialFile - || !resource?.database || !resource.schema || !(await secretPresent(request.credentialFile))) { - throw new Error("vector metadata adapter is unavailable"); - } - const client = await databaseClient.connect({ - host: request.host, port: request.port, database: resource.database, user: request.user, - credentialFile: request.credentialFile, tlsCaFile: request.tlsCaFile, - tlsServername: request.tlsServername, signal: request.signal, - }); - try { - const metadata = await client.query( - "SELECT a.atttypmod - 4 AS dimensions, CASE WHEN pg_get_indexdef(i.indexrelid) LIKE '%vector_cosine_ops%' THEN 'cosine' WHEN pg_get_indexdef(i.indexrelid) LIKE '%vector_l2_ops%' THEN 'l2' WHEN pg_get_indexdef(i.indexrelid) LIKE '%vector_ip_ops%' THEN 'inner_product' END AS distance FROM pg_attribute a JOIN pg_class c ON c.oid = a.attrelid JOIN pg_namespace n ON n.oid = c.relnamespace JOIN pg_index i ON i.indrelid = c.oid AND a.attnum = ANY(i.indkey) WHERE n.nspname = $1 AND c.relname = $2 AND a.attnum > 0 AND NOT a.attisdropped AND a.atttypid = (SELECT oid FROM pg_type WHERE typname = 'vector') ORDER BY i.indexrelid LIMIT 1", - [resource.schema, request.collection], - ); - const row = metadata.rows[0]; - if (!row || !Number.isInteger(row.dimensions) || (row.distance !== "cosine" && row.distance !== "l2" && row.distance !== "inner_product")) throw new Error("vector metadata adapter is unavailable"); - return { collection: request.collection, dimensions: row.dimensions as number, distance: row.distance as VectorDiagnosticResult["distance"] }; - } finally { - await client.end().catch(() => undefined); - } - } - if (request.transport !== "rest_api" || !request.baseUrl || !request.diagnostic || request.tlsCaFile) { - throw new Error("vector metadata adapter is unavailable"); - } - const response = await fetch(resolveDiagnosticUrl(request.baseUrl, request.diagnostic.path).toString(), { - method: request.diagnostic.method, - headers: await restHeaders(request.diagnostic, request.credentialFile), - signal: request.signal, - redirect: "error", - }); - const payload = await response.json().catch(() => undefined) as Record | undefined; - const fields = request.diagnostic.response; - if (!response.ok || !payload || typeof payload[fields.collection] !== "string" - || !Number.isInteger(payload[fields.dimensions]) || typeof payload[fields.distance] !== "string") { - throw new Error("vector metadata adapter is unavailable"); + async inspectQdrant(request) { + const response = await fetch( + new URL(`/collections/${request.collection}`, `${request.baseUrl}/`).toString(), + { method: "GET", signal: request.signal, redirect: "error" }, + ); + const payload = await response.json().catch(() => undefined) as { + result?: { config?: { params?: { vectors?: { size?: unknown; distance?: unknown } } } }; + } | undefined; + const size = payload?.result?.config?.params?.vectors?.size; + const distance = payload?.result?.config?.params?.vectors?.distance; + if (!response.ok || !Number.isInteger(size) || typeof distance !== "string" + || distance.length === 0) { + throw new Error("Qdrant metadata probe failed"); } return { - collection: payload[fields.collection] as string, - dimensions: payload[fields.dimensions] as number, - distance: payload[fields.distance] as VectorDiagnosticResult["distance"], + collection: request.collection, + dimensions: size as number, + distance: distance.toLowerCase(), }; }, async probeEmbedding(request) { - if (!request.diagnostic && !request.tlsCaFile) { - const response = await fetch(new URL("/api/embed", `${request.baseUrl}/`).toString(), { - method: "POST", - headers: { "content-type": "application/json" }, - body: JSON.stringify({ model: request.model, input: "diagnostic" }), - signal: request.signal, - redirect: "error", - }); - const payload = await response.json().catch(() => undefined) as { - embeddings?: unknown[]; - } | undefined; - const vector = Array.isArray(payload?.embeddings) ? payload?.embeddings[0] : undefined; - if (!response.ok || !Array.isArray(vector)) throw new Error("embedding probe failed"); - return { available: true, dimensions: vector.length }; - } - if (!request.diagnostic || request.tlsCaFile) throw new Error("embedding probe failed"); - const response = await fetch(resolveDiagnosticUrl(request.baseUrl, request.diagnostic.path).toString(), { - method: request.diagnostic.method, - headers: await restHeaders(request.diagnostic, request.credentialFile), + const response = await fetch(new URL("/api/embed", `${request.baseUrl}/`).toString(), { + method: "POST", + headers: { "content-type": "application/json" }, + body: JSON.stringify({ model: request.model, input: "diagnostic" }), signal: request.signal, redirect: "error", }); - const payload = await response.json().catch(() => undefined) as Record | undefined; - if (!response.ok || !payload || payload[request.diagnostic.response.model] !== request.model - || !Number.isInteger(payload[request.diagnostic.response.dimensions])) throw new Error("embedding probe failed"); - return { available: true, dimensions: payload[request.diagnostic.response.dimensions] as number }; - }, - async writeDiagnosticRecord(request) { - if (!request.baseUrl || !request.diagnostic || request.tlsCaFile) throw new Error("vector write adapter is unavailable"); - const response = await fetch(resolveDiagnosticUrl(request.baseUrl, request.diagnostic.path).toString(), { - method: request.diagnostic.method, - headers: { - ...await restHeaders(request.diagnostic, request.credentialFile), - "content-type": "application/json", - }, - body: JSON.stringify({ operation: "create", id: request.id, collection: request.collection, dimensions: request.dimensions }), - signal: request.signal, - redirect: "error", - }); - const payload = await response.json().catch(() => undefined) as Record | undefined; - if (!response.ok || !payload || payload[request.diagnostic.response.operation] !== "create") { - throw new Error("vector write adapter is unavailable"); - } - }, - async removeDiagnosticRecord(request) { - if (!request.baseUrl || !request.diagnostic || request.tlsCaFile) throw new Error("vector write adapter is unavailable"); - const response = await fetch(resolveDiagnosticUrl(request.baseUrl, request.diagnostic.path).toString(), { - method: request.diagnostic.method, - headers: { - ...await restHeaders(request.diagnostic, request.credentialFile), - "content-type": "application/json", - }, - body: JSON.stringify({ operation: "remove", id: request.id, collection: request.collection }), - signal: request.signal, - redirect: "error", - }); - const payload = await response.json().catch(() => undefined) as Record | undefined; - if (!response.ok || !payload || payload[request.diagnostic.response.operation] !== "remove") { - throw new Error("vector write adapter is unavailable"); - } + const payload = await response.json().catch(() => undefined) as { + embeddings?: unknown[]; + } | undefined; + const vector = Array.isArray(payload?.embeddings) ? payload.embeddings[0] : undefined; + if (!response.ok || !Array.isArray(vector)) throw new Error("embedding probe failed"); + return { available: true, dimensions: vector.length }; }, }; } -export function createProductionWorkspaceDiagnoser( - timeoutMs: number, - adapters: DiagnosticAdapters = createConcreteDiagnosticAdapters(), - semanticRuntime: SemanticRuntimeConfig = { - internalQdrantUrl: "http://qdrant:6333", - internalEmbeddingUrl: "http://embedding:11434", - internalEmbeddingModel: "qwen3-embedding:0.6b", - internalEmbeddingDimensions: 1024, - }, -) { - const legacyDiagnoser = createWorkspaceDiagnoser(adapters, { timeoutMs }); - return async ( - workspace: WorkspaceDescriptor, - bindings: RuntimeBindings, - options: { writeProbe: boolean }, - ): Promise => { - const descriptor = validateWorkspaceDescriptor(workspace); - if (Number(descriptor.workspace.schema_version) !== 3) { - return await legacyDiagnoser(descriptor, bindings, options); - } - return await diagnoseSchemaV3Workspace( - descriptor as Extract, - bindings, - adapters, - timeoutMs, - semanticRuntime, - ); - }; +function configuredTimeout(value: number | undefined): number { + return Math.min( + Math.max(1, value ?? DEFAULT_WORKSPACE_DIAGNOSTIC_TIMEOUT_MS), + MAX_WORKSPACE_DIAGNOSTIC_TIMEOUT_MS, + ); } -function boundedTimeout(value: number | undefined, fallback: number): number { - const selected = value ?? fallback; - return Math.min(Math.max(1, selected), fallback, MAX_WORKSPACE_DIAGNOSTIC_TIMEOUT_MS); +function boundedTimeout(value: number | undefined, ceiling: number): number { + return Math.min(Math.max(1, value ?? ceiling), ceiling, MAX_WORKSPACE_DIAGNOSTIC_TIMEOUT_MS); } async function withTimeout(timeoutMs: number, operation: (signal: AbortSignal) => Promise): Promise { @@ -582,24 +321,14 @@ function sameResource(expected: DiagnosticResource, actual: DiagnosticResource): return Object.entries(expected).every(([key, value]) => actual[key as keyof DiagnosticResource] === value); } -function hasRequiredConnectorChecks(result: ConnectorDiagnosticResult, resource: DiagnosticResource): boolean { - return result.resolved && result.tlsVerified && result.authenticated && sameResource(resource, result.resource); -} - -function hasMatchingVectorMetadata( - actual: VectorDiagnosticResult, - expected: { collection: string; dimensions: number; distance: "cosine" | "l2" | "inner_product" }, +function hasRequiredConnectorChecks( + result: ConnectorDiagnosticResult, + resource: DiagnosticResource, ): boolean { - return actual.collection === expected.collection - && actual.dimensions === expected.dimensions - && actual.distance === expected.distance; -} - -function hasMatchingEmbeddingMetadata( - actual: EmbeddingDiagnosticResult, - expected: { dimensions: number }, -): boolean { - return actual.available && actual.dimensions === expected.dimensions; + return result.resolved + && result.tlsVerified + && result.authenticated + && sameResource(resource, result.resource); } function diagnosticError(code: WorkspaceErrorCode, field?: string): Diagnostic { @@ -617,15 +346,11 @@ function diagnosticError(code: WorkspaceErrorCode, field?: string): Diagnostic { }; } -function bindingName( - workspace: WorkspaceDescriptor | DeprecatedV2Descriptor, - role: "DWH" | "VECTOR" | "VECTOR_WRITER" | "EMBEDDING", - suffix: string, -): string { - const entry = buildInstallationContract(workspace as WorkspaceDescriptor).variables.find((variable) => ( - variable.role === role && variable.suffix === suffix +function bindingName(workspace: WorkspaceDescriptor, suffix: string): string { + const entry = buildInstallationContract(workspace).variables.find((variable) => ( + variable.role === "DWH" && variable.suffix === suffix )); - if (!entry) throw new Error(`workspace contract is missing ${role}_${suffix}`); + if (!entry) throw new Error(`workspace contract is missing DWH_${suffix}`); return entry.name; } @@ -634,8 +359,19 @@ function numericBinding(binding: Record, name: string): number | return Number.isInteger(value) && value > 0 && value <= 65_535 ? value : undefined; } -async function diagnoseSchemaV3Workspace( - descriptor: Extract, +function requireSupportedDescriptor(workspace: unknown): void { + if (typeof workspace !== "object" || workspace === null) { + throw new Error("Workspace diagnoser supports only workspace schema version 3"); + } + const metadata = Reflect.get(workspace, "workspace"); + if (typeof metadata !== "object" || metadata === null + || Reflect.get(metadata, "schema_version") !== 3) { + throw new Error("Workspace diagnoser supports only workspace schema version 3"); + } +} + +async function diagnoseValidatedWorkspace( + descriptor: WorkspaceDescriptor, bindings: RuntimeBindings, adapters: DiagnosticAdapters, timeoutMs: number, @@ -654,19 +390,14 @@ async function diagnoseSchemaV3Workspace( ...[...bindings.dwh.missing].sort().map((field) => diagnosticError("binding_missing", field)), ...evidenceDiagnostics, ]; - if (diagnostics.length > 0) { - return { activatable: false, diagnostics }; - } + if (diagnostics.length > 0) return { activatable: false, diagnostics }; const dwhTimeout = boundedTimeout(descriptor.dwh.timeout_ms, timeoutMs); - const vectorTimeout = timeoutMs; - const embeddingTimeout = timeoutMs; let activatable = true; - const dwhValues = bindings.dwh.values; - const dwhField = (suffix: string) => bindingName(descriptor, "DWH", suffix); + const dwhField = (suffix: string) => bindingName(descriptor, suffix); const dwhResource = { database: descriptor.dwh.database, schema: descriptor.dwh.schema }; - let dwhRequest: ConnectorDiagnosticRequest | SshTunnelRequest | undefined; + let dwhRequest: ConnectorDiagnosticRequest | undefined; if (bindings.dwh.transport === "rest_api") { const diagnostic = descriptor.diagnostics?.dwh_rest; const baseUrl = dwhValues[dwhField("BASE_URL")]; @@ -705,32 +436,9 @@ async function diagnoseSchemaV3Workspace( signal: new AbortController().signal, }; } - } else { - const sshHost = dwhValues[dwhField("SSH_HOST")]; - const sshPort = numericBinding(dwhValues, dwhField("SSH_PORT")); - const sshUser = dwhValues[dwhField("SSH_USER")]; - const privateKeyFile = dwhValues[dwhField("SSH_PRIVATE_KEY_FILE")]; - const knownHostsFile = dwhValues[dwhField("SSH_KNOWN_HOSTS_FILE")]; - const targetHost = dwhValues[dwhField("SSH_TARGET_HOST")]; - const targetPort = numericBinding(dwhValues, dwhField("SSH_TARGET_PORT")); - if (sshHost && sshPort && sshUser && privateKeyFile && knownHostsFile && targetHost && targetPort) { - dwhRequest = { - sshHost, - sshPort, - sshUser, - privateKeyFile, - knownHostsFile, - targetHost, - targetPort, - localHost: "127.0.0.1", - localPort: 0, - timeoutMs: dwhTimeout, - signal: new AbortController().signal, - }; - } } - if (!dwhRequest || "sshHost" in dwhRequest) { + if (!dwhRequest) { diagnostics.push(diagnosticError("workspace_not_activatable")); return { activatable: false, diagnostics }; } @@ -751,16 +459,16 @@ async function diagnoseSchemaV3Workspace( } try { - const vector = await withTimeout(vectorTimeout, (signal) => adapters.inspectVector({ - transport: "rest_api", + const vector = await withTimeout(timeoutMs, (signal) => adapters.inspectQdrant({ baseUrl: semanticRuntime.internalQdrantUrl, collection: descriptor.semantic_index.vector_store.collection, - dimensions: descriptor.semantic_index.vector_store.dimensions, - distance: descriptor.semantic_index.vector_store.distance, - timeoutMs: vectorTimeout, + timeoutMs, signal, })); - if (!hasMatchingVectorMetadata(vector, descriptor.semantic_index.vector_store)) { + const expected = descriptor.semantic_index.vector_store; + if (vector.collection !== expected.collection + || vector.dimensions !== expected.dimensions + || vector.distance !== expected.distance) { diagnostics.push(diagnosticError("semantic_index_incompatible")); activatable = false; } @@ -770,19 +478,16 @@ async function diagnoseSchemaV3Workspace( } try { - const embedding = await withTimeout(embeddingTimeout, (signal) => adapters.probeEmbedding({ + const embedding = await withTimeout(timeoutMs, (signal) => adapters.probeEmbedding({ baseUrl: semanticRuntime.internalEmbeddingUrl, model: semanticRuntime.internalEmbeddingModel, - timeoutMs: embeddingTimeout, + timeoutMs, signal, })); - if ( - semanticRuntime.internalEmbeddingModel !== descriptor.semantic_index.embedding.model + if (semanticRuntime.internalEmbeddingModel !== descriptor.semantic_index.embedding.model || semanticRuntime.internalEmbeddingDimensions !== descriptor.semantic_index.embedding.dimensions - || !hasMatchingEmbeddingMetadata(embedding, { - dimensions: descriptor.semantic_index.embedding.dimensions, - }) - ) { + || !embedding.available + || embedding.dimensions !== descriptor.semantic_index.embedding.dimensions) { diagnostics.push(diagnosticError("semantic_index_incompatible")); activatable = false; } @@ -793,326 +498,48 @@ async function diagnoseSchemaV3Workspace( return { activatable, - diagnostics: diagnostics.length > 0 ? diagnostics : [{ level: "info", code: "binding_ok", message: "Installation bindings and diagnostics succeeded." }], + diagnostics: diagnostics.length > 0 + ? diagnostics + : [{ + level: "info", + code: "binding_ok", + message: "Installation bindings and diagnostics succeeded.", + }], }; } -function diagnosticsForMissingBindings( - workspace: DeprecatedV2Descriptor, - bindings: RuntimeBindings, -): Diagnostic[] { - const missing = new Set([ - ...bindings.dwh.missing, - ...bindings.vector.missing, - ...bindings.embedding.missing, - ]); - const knownHosts = [ - bindings.dwh.transport === "ssh_tunnel" ? bindingName(workspace, "DWH", "SSH_KNOWN_HOSTS_FILE") : undefined, - bindings.vector.transport === "ssh_tunnel" ? bindingName(workspace, "VECTOR", "SSH_KNOWN_HOSTS_FILE") : undefined, - ].filter((field): field is string => field !== undefined); - const ordered = [...new Set([...knownHosts.filter((field) => missing.has(field)), ...[...missing].sort()])]; - return ordered.map((field) => diagnosticError("binding_missing", field)); -} - -function connectorRequest( - workspace: DeprecatedV2Descriptor, - role: ConnectorRole, - bindings: RuntimeBindings, - timeoutMs: number, -): ConnectorDiagnosticRequest | SshTunnelRequest | undefined { - const binding = role === "dwh" ? bindings.dwh : bindings.vector; - const contractRole = role === "dwh" ? "DWH" : "VECTOR"; - const values = binding.values; - const resource: DiagnosticResource = role === "dwh" - ? { database: workspace.dwh.database, schema: workspace.dwh.schema } - : { - database: workspace.semantic_index.vector_store.database, - schema: workspace.semantic_index.vector_store.schema, - collection: workspace.semantic_index.vector_store.collection, - }; - const field = (suffix: string) => bindingName(workspace, contractRole, suffix); - if (binding.transport === "rest_api") { - const baseUrl = values[field("BASE_URL")]; - const diagnostic = role === "dwh" - ? workspace.diagnostics?.dwh_rest - : workspace.diagnostics?.vector_rest?.metadata; - if (baseUrl === undefined || diagnostic === undefined) return undefined; - const credentialFile = diagnostic.auth === "none" ? undefined : values[field("API_KEY_FILE")]; - if (diagnostic.auth !== "none" && credentialFile === undefined) return undefined; - return { - role, - transport: "rest_api", - baseUrl, - credentialFile, - tlsCaFile: values[field("TLS_CA_FILE")], - resource, - timeoutMs, - signal: new AbortController().signal, - diagnostic, - }; - } - - if (binding.transport === "ssh_tunnel") { - const sshHost = values[field("SSH_HOST")]; - const sshPort = numericBinding(values, field("SSH_PORT")); - const sshUser = values[field("SSH_USER")]; - const privateKeyFile = values[field("SSH_PRIVATE_KEY_FILE")]; - const knownHostsFile = values[field("SSH_KNOWN_HOSTS_FILE")]; - const targetHost = values[field("SSH_TARGET_HOST")]; - const targetPort = numericBinding(values, field("SSH_TARGET_PORT")); - if (!sshHost || !sshPort || !sshUser || !privateKeyFile || !knownHostsFile || !targetHost || !targetPort) return undefined; - return { - sshHost, sshPort, sshUser, privateKeyFile, knownHostsFile, targetHost, targetPort, - localHost: "127.0.0.1", localPort: 0, timeoutMs, signal: new AbortController().signal, - }; - } - - const credentialFile = values[field("PASSWORD_FILE")]; - if (credentialFile === undefined) return undefined; - - const host = values[field("HOST")]; - const port = numericBinding(values, field("PORT")); - const user = values[field("USER")]; - if (!host || !port || !user) return undefined; - return { - role, - transport: binding.transport, - host, - port, - user, - credentialFile, - tlsCaFile: values[field("TLS_CA_FILE")], - resource, - timeoutMs, - signal: new AbortController().signal, - }; -} - -function tunnelProbeRequest( - workspace: DeprecatedV2Descriptor, - role: ConnectorRole, - bindings: RuntimeBindings, - timeoutMs: number, - tunnel: LoopbackTunnel, - signal: AbortSignal, -): ConnectorDiagnosticRequest { - const binding = role === "dwh" ? bindings.dwh : bindings.vector; - const contractRole = role === "dwh" ? "DWH" : "VECTOR"; - const password = binding.values[bindingName(workspace, contractRole, "PASSWORD_FILE")]; - const user = binding.values[bindingName(workspace, contractRole, "USER")]; - if (!password || !user) throw new Error("missing SSH connector credentials"); - return { - role, - transport: "ssh_tunnel", - host: tunnel.host, - port: tunnel.port, - user, - credentialFile: password, - tlsCaFile: binding.values[bindingName(workspace, contractRole, "TLS_CA_FILE")], - tlsServername: binding.values[bindingName(workspace, contractRole, "SSH_TARGET_HOST")], - resource: role === "dwh" - ? { database: workspace.dwh.database, schema: workspace.dwh.schema } - : { - database: workspace.semantic_index.vector_store.database, - schema: workspace.semantic_index.vector_store.schema, - collection: workspace.semantic_index.vector_store.collection, - }, - timeoutMs, - signal, - }; -} +const DEFAULT_SEMANTIC_RUNTIME: SemanticRuntimeConfig = { + internalQdrantUrl: "http://qdrant:6333", + internalEmbeddingUrl: "http://embedding:11434", + internalEmbeddingModel: "qwen3-embedding:0.6b", + internalEmbeddingDimensions: 1024, +}; export function createWorkspaceDiagnoser( adapters: DiagnosticAdapters, - options: { timeoutMs?: number } = {}, + options: { timeoutMs?: number; semanticRuntime?: SemanticRuntimeConfig } = {}, ) { - const fallbackTimeout = boundedTimeout(options.timeoutMs, DEFAULT_WORKSPACE_DIAGNOSTIC_TIMEOUT_MS); - - return async function diagnoseWorkspace( + const timeoutMs = configuredTimeout(options.timeoutMs); + const semanticRuntime = options.semanticRuntime ?? DEFAULT_SEMANTIC_RUNTIME; + return async function diagnose( workspace: WorkspaceDescriptor, bindings: RuntimeBindings, - options: { writeProbe: boolean }, + _options: { writeProbe: boolean }, ): Promise { + requireSupportedDescriptor(workspace); const descriptor = validateWorkspaceDescriptor(workspace); - if (Number(descriptor.workspace.schema_version) !== 2) { - return { activatable: false, diagnostics: [diagnosticError("workspace_not_activatable")] }; - } - const canonical = descriptor as unknown as DeprecatedV2Descriptor; - const diagnostics = diagnosticsForMissingBindings(canonical, bindings); - if (diagnostics.length > 0) return { activatable: false, diagnostics }; - - const dwhTimeout = boundedTimeout(canonical.dwh.timeout_ms, fallbackTimeout); - const vectorTimeout = boundedTimeout(canonical.semantic_index.vector_store.timeout_ms, fallbackTimeout); - const embeddingTimeout = boundedTimeout(canonical.semantic_index.embedding.timeout_ms, fallbackTimeout); - let tunneledVectorMetadata: VectorDiagnosticResult | undefined; - - for (const role of ["dwh", "vector"] as const) { - const timeoutMs = role === "dwh" ? dwhTimeout : vectorTimeout; - const request = connectorRequest(canonical, role, bindings, timeoutMs); - if (!request) { - diagnostics.push(diagnosticError("binding_missing")); - continue; - } - try { - const result = "sshHost" in request - ? await withTimeout(timeoutMs, (signal) => adapters.withSshTunnel( - { ...request, signal }, - async (tunnel) => { - const tunneledRequest = tunnelProbeRequest(canonical, role, bindings, timeoutMs, tunnel, signal); - const connector = await adapters.probeConnector(tunneledRequest); - if (role === "vector") { - tunneledVectorMetadata = await adapters.inspectVector({ - transport: "ssh_tunnel", host: tunneledRequest.host, port: tunneledRequest.port, - user: tunneledRequest.user, credentialFile: tunneledRequest.credentialFile, - tlsCaFile: tunneledRequest.tlsCaFile, tlsServername: tunneledRequest.tlsServername, - resource: tunneledRequest.resource, - collection: canonical.semantic_index.vector_store.collection, - dimensions: canonical.semantic_index.vector_store.dimensions, - distance: canonical.semantic_index.vector_store.distance, - timeoutMs: vectorTimeout, signal, - }); - } - return connector; - }, - )) - : await withTimeout(timeoutMs, (signal) => adapters.probeConnector({ ...request, signal })); - const resource = role === "dwh" - ? { database: canonical.dwh.database, schema: canonical.dwh.schema } - : { - database: canonical.semantic_index.vector_store.database, - schema: canonical.semantic_index.vector_store.schema, - collection: canonical.semantic_index.vector_store.collection, - }; - if (!hasRequiredConnectorChecks(result, resource)) diagnostics.push(diagnosticError("connector_unavailable")); - else diagnostics.push({ level: "info", code: "binding_ok", message: `${role === "dwh" ? "DWH" : "Vector"} binding diagnostic passed.` }); - } catch { - diagnostics.push(diagnosticError("connector_unavailable")); - } - } - - if (!diagnostics.some((diagnostic) => diagnostic.level === "error")) { - try { - const vectorBinding = bindings.vector; - const vectorRest = canonical.diagnostics?.vector_rest?.metadata; - const vectorDirect = connectorRequest(canonical, "vector", bindings, vectorTimeout); - const directVectorRequest = vectorDirect && !("sshHost" in vectorDirect) ? vectorDirect : undefined; - const vector = tunneledVectorMetadata ?? await withTimeout(vectorTimeout, (signal) => adapters.inspectVector({ - transport: vectorBinding.transport === "pgvector_direct" || vectorBinding.transport === "rest_api" - || vectorBinding.transport === "ssh_tunnel" ? vectorBinding.transport : undefined, - baseUrl: vectorBinding.values[bindingName(canonical, "VECTOR", "BASE_URL")], - credentialFile: vectorBinding.values[bindingName(canonical, "VECTOR", "API_KEY_FILE")], - tlsCaFile: vectorBinding.values[bindingName(canonical, "VECTOR", "TLS_CA_FILE")], - diagnostic: vectorRest, - collection: canonical.semantic_index.vector_store.collection, - dimensions: canonical.semantic_index.vector_store.dimensions, - distance: canonical.semantic_index.vector_store.distance, - ...(directVectorRequest ? { - host: directVectorRequest.host, - port: directVectorRequest.port, - user: directVectorRequest.user, - credentialFile: directVectorRequest.credentialFile, - tlsCaFile: directVectorRequest.tlsCaFile, - resource: directVectorRequest.resource, - } : {}), - timeoutMs: vectorTimeout, - signal, - })); - const expected = canonical.semantic_index.vector_store; - if ( - vector.collection !== expected.collection - || vector.dimensions !== expected.dimensions - || vector.distance !== expected.distance - ) diagnostics.push(diagnosticError("semantic_index_incompatible")); - } catch { - diagnostics.push(diagnosticError("connector_unavailable")); - } - } - - if (!diagnostics.some((diagnostic) => diagnostic.level === "error")) { - try { - const embedding = await withTimeout(embeddingTimeout, (signal) => adapters.probeEmbedding({ - baseUrl: bindings.embedding.values[bindingName(canonical, "EMBEDDING", "BASE_URL")] ?? "", - credentialFile: bindings.embedding.values[bindingName(canonical, "EMBEDDING", "API_KEY_FILE")], - tlsCaFile: bindings.embedding.values[bindingName(canonical, "EMBEDDING", "TLS_CA_FILE")], - model: canonical.semantic_index.embedding.model, - timeoutMs: embeddingTimeout, - signal, - diagnostic: canonical.diagnostics?.embedding, - })); - if (!embedding.available || embedding.dimensions !== canonical.semantic_index.embedding.dimensions) { - diagnostics.push(diagnosticError("semantic_index_incompatible")); - } - } catch { - diagnostics.push(diagnosticError("connector_unavailable")); - } - } - - if ( - options.writeProbe - && canonical.semantic_index.vector_writer - && canonical.diagnostics?.vector_rest?.reversible_probe - && bindings.vector.transport === "rest_api" - && !diagnostics.some((diagnostic) => diagnostic.level === "error") - ) { - const credentialFile = bindings.vectorWriter.values[bindingName(canonical, "VECTOR_WRITER", "API_KEY_FILE")]; - if (!credentialFile) return { activatable: true, diagnostics }; - const readerCredentialFile = bindings.vector.values[bindingName(canonical, "VECTOR", "API_KEY_FILE")]; - if (readerCredentialFile && await sameSecretFile(credentialFile, readerCredentialFile)) { - diagnostics.push(diagnosticError("binding_missing", bindingName(canonical, "VECTOR_WRITER", "API_KEY_FILE"))); - return { activatable: false, diagnostics }; - } - const request: WriteDiagnosticRecordRequest = { - collection: canonical.semantic_index.vector_store.collection, - id: `diagnostic:${randomUUID()}`, - dimensions: canonical.semantic_index.vector_store.dimensions, - timeoutMs: vectorTimeout, - signal: new AbortController().signal, - credentialFile, - tlsCaFile: bindings.vector.values[bindingName(canonical, "VECTOR", "TLS_CA_FILE")], - baseUrl: bindings.vector.values[bindingName(canonical, "VECTOR", "BASE_URL")], - diagnostic: canonical.diagnostics.vector_rest.reversible_probe, - }; - let writeStarted = false; - let cleanupAttempted = false; - let cleanupFailed = false; - try { - writeStarted = true; - await withTimeout(vectorTimeout, (signal) => adapters.writeDiagnosticRecord({ ...request, signal })); - cleanupAttempted = true; - await withTimeout(vectorTimeout, (signal) => adapters.removeDiagnosticRecord({ ...request, signal })); - } catch { - cleanupFailed = true; - } finally { - if (writeStarted && (!cleanupAttempted || cleanupFailed)) { - try { - await withTimeout(vectorTimeout, (signal) => adapters.removeDiagnosticRecord({ ...request, signal })); - } catch { - cleanupFailed = true; - } - } - } - if (cleanupFailed) { - diagnostics.push(diagnosticError("connector_unavailable")); - } - } - - // The concrete SSH adapter deliberately owns only a bounded diagnostic tunnel and closes it - // in `finally`. Until a session runtime owns an equivalent long-lived tunnel, a successful - // probe is connectivity evidence only and must never be advertised as activatable. - if ( - (bindings.dwh.transport === "ssh_tunnel" || bindings.vector.transport === "ssh_tunnel") - && !diagnostics.some((diagnostic) => diagnostic.level === "error") - ) { - diagnostics.push(diagnosticError("workspace_not_activatable")); - } - - return { - activatable: !diagnostics.some((diagnostic) => diagnostic.level === "error"), - diagnostics, - }; + return await diagnoseValidatedWorkspace(descriptor, bindings, adapters, timeoutMs, semanticRuntime); }; } +export function createProductionWorkspaceDiagnoser( + timeoutMs: number, + adapters: DiagnosticAdapters = createConcreteDiagnosticAdapters(), + semanticRuntime: SemanticRuntimeConfig = DEFAULT_SEMANTIC_RUNTIME, +) { + return createWorkspaceDiagnoser(adapters, { timeoutMs, semanticRuntime }); +} + export const diagnoseWorkspace = createProductionWorkspaceDiagnoser( DEFAULT_WORKSPACE_DIAGNOSTIC_TIMEOUT_MS, ); diff --git a/backend/src/workspaces/migrate-v2-qdrant.ts b/backend/src/workspaces/migrate-v2-qdrant.ts index 5fd6c40d..99b39c72 100644 --- a/backend/src/workspaces/migrate-v2-qdrant.ts +++ b/backend/src/workspaces/migrate-v2-qdrant.ts @@ -1,8 +1,37 @@ -import { validateOperationalWorkspace, type WorkspaceV3 } from "./schema.js"; -import type { DeprecatedV2Descriptor } from "./deprecated-v2-descriptor.js"; +import { + validateOperationalWorkspace, + type CanonicalDiagnostics, + type DwhTransport, + type WorkspaceV3, +} from "./schema.js"; + +/** Legacy input exists only at the migration boundary and is never an accepted runtime descriptor. */ +interface WorkspaceV2MigrationInput { + workspace: { + schema_version: 2; + id: string; + name: string; + description?: string; + language: "en" | "it"; + }; + dwh: { + engine: "postgres"; + database: string; + schema: string; + port?: number; + timeout_ms?: number; + supported_transports: DwhTransport[]; + }; + semantic_index: unknown; + llm_policy: { + default?: `${string}/${string}`; + allowed: `${string}/${string}`[]; + }; + diagnostics?: CanonicalDiagnostics; +} export function migrateWorkspaceV2ToV3( - legacy: DeprecatedV2Descriptor, + legacy: WorkspaceV2MigrationInput, collection: string, ): WorkspaceV3 { return validateOperationalWorkspace({ diff --git a/backend/src/workspaces/runtime-renderer.ts b/backend/src/workspaces/runtime-renderer.ts index 6fa159d8..f6499f89 100644 --- a/backend/src/workspaces/runtime-renderer.ts +++ b/backend/src/workspaces/runtime-renderer.ts @@ -1,12 +1,7 @@ import { basename, join } from "node:path"; import { stringify } from "yaml"; import { buildInstallationContract } from "./contracts.js"; -import { - validateWorkspaceDescriptor, - type WorkspaceDescriptor, - type WorkspaceV3, -} from "./schema.js"; -import type { DeprecatedV2Descriptor } from "./deprecated-v2-descriptor.js"; +import { validateWorkspaceDescriptor, type WorkspaceDescriptor } from "./schema.js"; import type { ResolvedBinding, ResolvedEvidenceBinding, RuntimeBindings } from "./bindings.js"; export type { RuntimeBindings } from "./bindings.js"; @@ -45,10 +40,6 @@ const DEFAULT_SEMANTIC_RUNTIME: SemanticRuntimeConfig = { internalEmbeddingDimensions: 1024, }; -function seconds(timeoutMs: number | undefined): number | undefined { - return timeoutMs === undefined ? undefined : Math.max(1, Math.ceil(timeoutMs / 1_000)); -} - function bindingValue(binding: ResolvedBinding, name: string): string | undefined { return binding.values[name]; } @@ -59,7 +50,7 @@ function requireBinding(binding: ResolvedBinding, name: string): string { return value; } -function legacyDirectConnection( +function directConnection( binding: ResolvedBinding, names: { host: string; port: string; user: string; passwordFile: string; tlsCaFile: string }, identity: { database: string; schema: string }, @@ -77,7 +68,7 @@ function legacyDirectConnection( return connection; } -function legacyRestEndpoint( +function restEndpoint( binding: ResolvedBinding, names: { baseUrl: string; apiKeyFile: string; tlsCaFile: string }, requiresCredential: boolean, @@ -115,7 +106,7 @@ function requireEvidenceBinding(binding: ResolvedEvidenceBinding, name: string): } function renderEvidence( - workspace: WorkspaceV3, + workspace: WorkspaceDescriptor, binding: ResolvedEvidenceBinding, context: RuntimeRenderContext, bindingName: (suffix: string) => string, @@ -188,6 +179,7 @@ function renderEvidence( }; } + function placeholderConnection(identity: { database: string; schema: string }): Record { return { host: "localhost", @@ -200,7 +192,18 @@ function placeholderConnection(identity: { database: string; schema: string }): }; } -/** Render the compatibility fields consumed by the current Python harness. */ +function requireSupportedDescriptor(workspace: unknown): void { + if (typeof workspace !== "object" || workspace === null) { + throw new Error("Runtime renderer supports only workspace schema version 3"); + } + const metadata = Reflect.get(workspace, "workspace"); + if (typeof metadata !== "object" || metadata === null + || Reflect.get(metadata, "schema_version") !== 3) { + throw new Error("Runtime renderer supports only workspace schema version 3"); + } +} + +/** Render the schema-v3 compatibility fields consumed by the current Python harness. */ export function renderRuntimeConfig( workspace: WorkspaceDescriptor, bindings: RuntimeBindings, @@ -209,119 +212,36 @@ export function renderRuntimeConfig( installation: RuntimeInstallationOverlay = {}, semanticRuntime: SemanticRuntimeConfig = DEFAULT_SEMANTIC_RUNTIME, ): string { + requireSupportedDescriptor(workspace); const descriptor = validateWorkspaceDescriptor(workspace); const contract = buildInstallationContract(descriptor); - const name = (role: "DWH" | "VECTOR" | "EMBEDDING" | "EVIDENCE", suffix: string) => { + const name = (role: "DWH" | "EVIDENCE", suffix: string) => { const variable = contract.variables.find((entry) => entry.role === role && entry.suffix === suffix); if (!variable) throw new Error(`workspace contract is missing ${role}_${suffix}`); return variable.name; }; - if (Number(descriptor.workspace.schema_version) !== 2) { - if (Number(descriptor.workspace.schema_version) === 1) { - throw new Error("Workspace descriptor requires explicit migration to schema version 2"); - } - const canonicalV3 = descriptor as WorkspaceV3; - const renderedEvidence = canonicalV3.evidence === undefined - ? undefined - : renderEvidence( - canonicalV3, - bindings.evidence, - requireRuntimeRenderContext(identity), - (suffix) => name("EVIDENCE", suffix), - ); - if (bindings.dwh.missing.length > 0) { - throw new Error("runtime configuration requires complete bindings"); - } - - const dwhRest = bindings.dwh.transport === "rest_api"; - const dwhIdentity = { database: descriptor.dwh.database, schema: descriptor.dwh.schema }; - const database = bindings.dwh.transport === "postgres_direct" - ? { ...legacyDirectConnection(bindings.dwh, { - host: name("DWH", "HOST"), - port: name("DWH", "PORT"), - user: name("DWH", "USER"), - passwordFile: name("DWH", "PASSWORD_FILE"), - tlsCaFile: name("DWH", "TLS_CA_FILE"), - }, dwhIdentity), transport: "direct" } - : placeholderConnection(dwhIdentity); - const renderedV3: Record = { - ...(identity ? { - runtime_identity: { - workspace_id: identity.workspaceId, - workspace_revision: identity.workspaceRevision, - source_identity: `workspace://${identity.workspaceId}`, - }, - } : {}), - ...(installation.session_storage === undefined - ? {} : { session_storage: installation.session_storage }), - ...(installation.profile === undefined ? {} : { profile: installation.profile }), - language: descriptor.workspace.language, - database, - resources: { - vector: { - engine: "qdrant", - base_url: semanticRuntime.internalQdrantUrl, - collection: descriptor.semantic_index.vector_store.collection, - }, - embeddings: { - provider: "ollama_internal", - base_url: semanticRuntime.internalEmbeddingUrl, - model: semanticRuntime.internalEmbeddingModel, - dimensions: semanticRuntime.internalEmbeddingDimensions, - }, - }, - roots: paths, - paths, - ...(renderedEvidence ?? {}), - }; - if (bindings.dwh.transport === "postgres_direct") { - renderedV3.dwh = { type: "postgres_direct", connection: database }; - } else if (dwhRest) { - renderedV3.rest = legacyRestEndpoint(bindings.dwh, { - baseUrl: name("DWH", "BASE_URL"), - apiKeyFile: name("DWH", "API_KEY_FILE"), - tlsCaFile: name("DWH", "TLS_CA_FILE"), - }, descriptor.diagnostics?.dwh_rest?.auth !== "none"); - renderedV3.database = placeholderConnection(dwhIdentity); - renderedV3.dwh = { type: "thoth_rest", database: dwhIdentity, endpoint: renderedV3.rest }; - } else { - throw new Error("ssh_tunnel runtime configuration requires a diagnostic tunnel"); - } - return stringify(renderedV3, { lineWidth: 0, sortMapEntries: false }); - } - - const canonical = descriptor as unknown as DeprecatedV2Descriptor; - if ([...bindings.dwh.missing, ...bindings.vector.missing, ...bindings.embedding.missing].length > 0) { + const renderedEvidence = descriptor.evidence === undefined + ? undefined + : renderEvidence( + descriptor, + bindings.evidence, + requireRuntimeRenderContext(identity), + (suffix) => name("EVIDENCE", suffix), + ); + if (bindings.dwh.missing.length > 0) { throw new Error("runtime configuration requires complete bindings"); } - const dwhIdentity = { database: canonical.dwh.database, schema: canonical.dwh.schema }; - const vectorIdentity = { - database: canonical.semantic_index.vector_store.database ?? canonical.dwh.database, - schema: canonical.semantic_index.vector_store.schema ?? canonical.dwh.schema, - }; - const dwhDirect = bindings.dwh.transport === "postgres_direct"; - const vectorDirect = bindings.vector.transport === "pgvector_direct"; - const database = dwhDirect - ? { ...legacyDirectConnection(bindings.dwh, { - host: name("DWH", "HOST"), port: name("DWH", "PORT"), user: name("DWH", "USER"), - passwordFile: name("DWH", "PASSWORD_FILE"), tlsCaFile: name("DWH", "TLS_CA_FILE"), + const dwhIdentity = { database: descriptor.dwh.database, schema: descriptor.dwh.schema }; + const database = bindings.dwh.transport === "postgres_direct" + ? { ...directConnection(bindings.dwh, { + host: name("DWH", "HOST"), + port: name("DWH", "PORT"), + user: name("DWH", "USER"), + passwordFile: name("DWH", "PASSWORD_FILE"), + tlsCaFile: name("DWH", "TLS_CA_FILE"), }, dwhIdentity), transport: "direct" } : placeholderConnection(dwhIdentity); - const vectorDb = vectorDirect - ? legacyDirectConnection(bindings.vector, { - host: name("VECTOR", "HOST"), port: name("VECTOR", "PORT"), user: name("VECTOR", "USER"), - passwordFile: name("VECTOR", "PASSWORD_FILE"), tlsCaFile: name("VECTOR", "TLS_CA_FILE"), - }, vectorIdentity) - : placeholderConnection(vectorIdentity); - const embedding: Record = { - base_url: requireBinding(bindings.embedding, name("EMBEDDING", "BASE_URL")), - model: canonical.semantic_index.embedding.model, - dim: canonical.semantic_index.embedding.dimensions, - }; - const embeddingTimeout = seconds(canonical.semantic_index.embedding.timeout_ms); - if (embeddingTimeout !== undefined) embedding.timeout = embeddingTimeout; - const rendered: Record = { ...(identity ? { runtime_identity: { @@ -333,37 +253,38 @@ export function renderRuntimeConfig( ...(installation.session_storage === undefined ? {} : { session_storage: installation.session_storage }), ...(installation.profile === undefined ? {} : { profile: installation.profile }), - language: canonical.workspace.language, + language: descriptor.workspace.language, database, - vector_db: vectorDb, - embeddings: embedding, + resources: { + vector: { + engine: "qdrant", + base_url: semanticRuntime.internalQdrantUrl, + collection: descriptor.semantic_index.vector_store.collection, + }, + embeddings: { + provider: "ollama_internal", + base_url: semanticRuntime.internalEmbeddingUrl, + model: semanticRuntime.internalEmbeddingModel, + dimensions: semanticRuntime.internalEmbeddingDimensions, + }, + }, roots: paths, paths, + ...(renderedEvidence ?? {}), }; - if (dwhDirect) { + if (bindings.dwh.transport === "postgres_direct") { rendered.dwh = { type: "postgres_direct", connection: database }; } else if (bindings.dwh.transport === "rest_api") { - const rest = legacyRestEndpoint(bindings.dwh, { - baseUrl: name("DWH", "BASE_URL"), apiKeyFile: name("DWH", "API_KEY_FILE"), + const rest = restEndpoint(bindings.dwh, { + baseUrl: name("DWH", "BASE_URL"), + apiKeyFile: name("DWH", "API_KEY_FILE"), tlsCaFile: name("DWH", "TLS_CA_FILE"), - }, canonical.diagnostics?.dwh_rest?.auth !== "none"); + }, descriptor.diagnostics?.dwh_rest?.auth !== "none"); rendered.rest = rest; + rendered.database = placeholderConnection(dwhIdentity); rendered.dwh = { type: "thoth_rest", database: dwhIdentity, endpoint: rest }; } else { throw new Error("ssh_tunnel runtime configuration requires a diagnostic tunnel"); } - if (vectorDirect) { - rendered.vectors = { type: "pgvector_direct", connection: vectorDb }; - } else if (bindings.vector.transport === "rest_api") { - const vectorRest = legacyRestEndpoint(bindings.vector, { - baseUrl: name("VECTOR", "BASE_URL"), apiKeyFile: name("VECTOR", "API_KEY_FILE"), - tlsCaFile: name("VECTOR", "TLS_CA_FILE"), - }, canonical.diagnostics?.vector_rest?.metadata.auth !== "none"); - rendered.vector_rest = vectorRest; - rendered.vectors = { type: "thoth_vector_http", reader: vectorRest }; - } else { - throw new Error("ssh_tunnel runtime configuration requires a diagnostic tunnel"); - } - return stringify(rendered, { lineWidth: 0, sortMapEntries: false }); } diff --git a/backend/test/routes-workspaces.test.ts b/backend/test/routes-workspaces.test.ts index d9aa77cc..645a2d18 100644 --- a/backend/test/routes-workspaces.test.ts +++ b/backend/test/routes-workspaces.test.ts @@ -287,11 +287,10 @@ test("runs diagnostics for a schema v3 workspace without external semantic bindi expect(testResult.statusCode).toBe(200); expect(testResult.json()).toMatchObject({ activatable: true, diagnostics: [] }); - expect(diagnose).toHaveBeenCalledWith(workspace, expect.objectContaining({ - vector: expect.objectContaining({ missing: [], values: {} }), - vectorWriter: expect.objectContaining({ missing: [], values: {} }), - embedding: expect.objectContaining({ missing: [], values: {} }), - }), { writeProbe: false }); + expect(diagnose).toHaveBeenCalledWith(workspace, { + dwh: expect.objectContaining({ transport: "postgres_direct" }), + evidence: { missing: [], values: {} }, + }, { writeProbe: false }); }); test("reports missing Evidence binding through the real test route without changing registry revision", async () => { diff --git a/backend/test/workspace-runtime-handoff.test.ts b/backend/test/workspace-runtime-handoff.test.ts index 517a5160..c252250d 100644 --- a/backend/test/workspace-runtime-handoff.test.ts +++ b/backend/test/workspace-runtime-handoff.test.ts @@ -55,18 +55,6 @@ function evidenceWorkspace(source: string, policy = ""): string { ${source}${policy}`; } -const migrationRequiredWorkspace = canonicalWorkspace - .replace("schema_version: 3", "schema_version: 2") - .replace( - " engine: qdrant\n collection: psd-clinical", - " engine: pgvector\n database: analytics\n schema: vectors\n collection: documents", - ) - .replace(" dimensions: 1024", " dimensions: 768") - .replace(" distance: cosine", " distance: cosine\n supported_transports: [rest_api]") - .replace(" provider: ollama_internal", " provider: ollama_compatible") - .replace(" model: qwen3-embedding:0.6b", " model: nomic-embed-text") - .replace(" dimensions: 1024", " dimensions: 768"); - afterEach(() => { vi.unstubAllEnvs(); roots.splice(0).forEach((root) => rmSync(root, { recursive: true, force: true })); @@ -377,15 +365,6 @@ test("static S3 Evidence resolves only configured secret-root file paths", async } }); -test("ThtRunner refuses to render a migration-required registry snapshot", async () => { - const f = await fixture(migrationRequiredWorkspace); - const runner = runnerFor(f); - - expect(() => runner.acquireWorkspaceRuntime(f.revision.snapshotPath)).toThrow( - "Workspace descriptor requires explicit migration to schema version 3", - ); -}); - test("local GET sessions mine uses the real canonical handoff and returns an empty inventory", async () => { const f = await fixture(); const app = buildApp(loadConfig({ diff --git a/backend/test/workspace-runtime-renderer.test.ts b/backend/test/workspace-runtime-renderer.test.ts index 9945d725..bb1fce1b 100644 --- a/backend/test/workspace-runtime-renderer.test.ts +++ b/backend/test/workspace-runtime-renderer.test.ts @@ -12,32 +12,6 @@ import { import { supportsSessionRuntime } from "../src/workspaces/bindings.js"; import { parseWorkspaceYaml } from "../src/workspaces/schema.js"; -const workspace = parseWorkspaceYaml(`workspace: - schema_version: 2 - id: psd-clinical - name: Policlinico San Donato - language: it -dwh: - engine: postgres - database: postgres - schema: datawarehouse - supported_transports: [postgres_direct, rest_api] -semantic_index: - vector_store: - engine: pgvector - database: postgres - schema: vectors - collection: clinical_documents - dimensions: 768 - distance: cosine - supported_transports: [pgvector_direct, rest_api] - embedding: - provider: ollama_compatible - model: nomic-embed-text-v2-moe - dimensions: 768 -llm_policy: - allowed: [zai/glm-5.2] -`); const workspaceV3 = parseWorkspaceYaml(`workspace: schema_version: 3 id: psd-clinical @@ -47,7 +21,7 @@ dwh: engine: postgres database: postgres schema: datawarehouse - supported_transports: [postgres_direct, rest_api] + supported_transports: [postgres_direct, rest_api, ssh_tunnel] semantic_index: vector_store: engine: qdrant @@ -59,6 +33,7 @@ semantic_index: model: qwen3-embedding:0.6b dimensions: 1024 llm_policy: + default: zai/glm-5.2 allowed: [zai/glm-5.2] `); const paths: RuntimePaths = { @@ -72,31 +47,6 @@ const semanticRuntime: SemanticRuntimeConfig = { internalEmbeddingModel: "qwen3-embedding:0.6b", internalEmbeddingDimensions: 1024, }; -const legacyWorkspace = parseWorkspaceYaml(`workspace: - schema_version: 1 - id: psd-clinical - name: Policlinico San Donato - language: it -dwh: - engine: postgres - database: postgres - schema: datawarehouse - supported_transports: [postgres_direct] -semantic_index: - vector_store: - engine: pgvector - collection: clinical_documents - dimensions: 768 - distance: cosine - supported_transports: [pgvector_direct] - embedding: - provider: ollama_compatible - model: nomic-embed-text-v2-moe - dimensions: 768 -llm_policy: - allowed: [zai/glm-5.2] -`); - const directBindings: RuntimeBindings = { dwh: { transport: "postgres_direct", @@ -109,44 +59,13 @@ const directBindings: RuntimeBindings = { THT_WS_PSD_CLINICAL_DWH_TLS_CA_FILE: "/run/secrets/dwh-ca.pem", }, }, - vector: { - transport: "pgvector_direct", - missing: [], - values: { - THT_WS_PSD_CLINICAL_VECTOR_HOST: "vector.internal", - THT_WS_PSD_CLINICAL_VECTOR_PORT: "5432", - THT_WS_PSD_CLINICAL_VECTOR_USER: "vector_reader", - THT_WS_PSD_CLINICAL_VECTOR_PASSWORD_FILE: "/run/secrets/vector-password", - THT_WS_PSD_CLINICAL_VECTOR_TLS_CA_FILE: "/run/secrets/vector-ca.pem", - }, - }, - vectorWriter: { transport: "rest_api", missing: [], values: {} }, - embedding: { - transport: "rest_api", - missing: [], - values: { THT_WS_PSD_CLINICAL_EMBEDDING_BASE_URL: "http://embedding.internal:11434" }, - }, evidence: { missing: [], values: {} }, }; -test("runtime support stays fail-closed for either SSH connector", () => { - expect(supportsSessionRuntime(directBindings)).toBe(true); - expect(supportsSessionRuntime({ - ...directBindings, - dwh: { ...directBindings.dwh, transport: "ssh_tunnel" }, - })).toBe(false); - expect(supportsSessionRuntime({ - ...directBindings, - vector: { ...directBindings.vector, transport: "ssh_tunnel" }, - })).toBe(false); -}); - -test("renders a direct PostgreSQL binding to the legacy harness shape", () => { - const yaml = renderRuntimeConfig(workspace, directBindings, paths, { - workspaceId: "psd-clinical", - workspaceRevision: "a".repeat(40), - }); - const rendered = parse(yaml); +test("renders only the schema-v3 internal Qdrant and Ollama runtime shape", () => { + const rendered = parse(renderRuntimeConfig(workspaceV3, directBindings, paths, { + workspaceId: "psd-clinical", workspaceRevision: "a".repeat(40), + }, {}, semanticRuntime)); expect(rendered).toMatchObject({ runtime_identity: { @@ -156,151 +75,53 @@ test("renders a direct PostgreSQL binding to the legacy harness shape", () => { }, language: "it", database: { - host: "dwh.internal", - port: 5432, - database: "postgres", - schema: "datawarehouse", - user: "thoth_reader", - password_file: "/run/secrets/dwh-password", - ssl_ca_file: "/run/secrets/dwh-ca.pem", - transport: "direct", + host: "dwh.internal", port: 5432, database: "postgres", schema: "datawarehouse", + user: "thoth_reader", password_file: "/run/secrets/dwh-password", + ssl_ca_file: "/run/secrets/dwh-ca.pem", transport: "direct", }, - vector_db: { - host: "vector.internal", - database: "postgres", - schema: "vectors", - password_file: "/run/secrets/vector-password", - ssl_ca_file: "/run/secrets/vector-ca.pem", - }, - embeddings: { - base_url: "http://embedding.internal:11434", - model: "nomic-embed-text-v2-moe", - dim: 768, + dwh: { type: "postgres_direct" }, + resources: { + vector: { engine: "qdrant", base_url: "http://qdrant:6333", collection: "psd-clinical" }, + embeddings: { + provider: "ollama_internal", base_url: "http://embedding:11434", + model: "qwen3-embedding:0.6b", dimensions: 1024, + }, }, paths, }); - expect(yaml).toContain("type: postgres_direct"); - expect(yaml).toContain("schema: datawarehouse"); -}); - -test("refuses to render a v1 descriptor until an explicit migration creates v2", () => { - expect(() => renderRuntimeConfig(legacyWorkspace, directBindings, paths)).toThrow(/migrat/i); -}); - -test("fails closed for v3 runtime rendering and session support", () => { - expect(supportsSessionRuntime(directBindings)).toBe(true); - const rendered = parse(renderRuntimeConfig(workspaceV3, directBindings, paths, undefined, {}, semanticRuntime)); - - expect(rendered.resources).toMatchObject({ - vector: { - engine: "qdrant", - base_url: "http://qdrant:6333", - collection: "psd-clinical", - }, - embeddings: { - provider: "ollama_internal", - base_url: "http://embedding:11434", - model: "qwen3-embedding:0.6b", - dimensions: 1024, - }, - }); + expect(rendered).not.toHaveProperty("vector_db"); expect(rendered).not.toHaveProperty("embeddings"); + expect(rendered).not.toHaveProperty("vector_rest"); }); -test("schema v3 runtime rendering never exposes external semantic endpoints from bindings", () => { +test("renders schema-v3 DWH REST without exposing secret contents", () => { const rendered = parse(renderRuntimeConfig(workspaceV3, { - ...directBindings, - vector: { - transport: "rest_api", - missing: [], - values: { - THT_WS_PSD_CLINICAL_VECTOR_BASE_URL: "https://vector.example.test", - THT_WS_PSD_CLINICAL_VECTOR_API_KEY_FILE: "/run/secrets/vector-api-key", - }, - }, - embedding: { - transport: "rest_api", - missing: [], - values: { - THT_WS_PSD_CLINICAL_EMBEDDING_BASE_URL: "https://embedding.example.test", - }, - }, - }, paths, undefined, {}, semanticRuntime)); - - expect(rendered.resources.vector).toMatchObject({ - engine: "qdrant", - base_url: "http://qdrant:6333", - collection: "psd-clinical", - }); - expect(rendered.resources.embeddings).toMatchObject({ - provider: "ollama_internal", - base_url: "http://embedding:11434", - model: "qwen3-embedding:0.6b", - dimensions: 1024, - }); - expect(rendered).not.toHaveProperty("embeddings"); - expect(JSON.stringify(rendered)).not.toContain("vector.example.test"); - expect(JSON.stringify(rendered)).not.toContain("embedding.example.test"); -}); - -test("omits direct TLS fields when binding validation did not retain a file path", () => { - const dwhValues = { ...directBindings.dwh.values }; - const vectorValues = { ...directBindings.vector.values }; - delete dwhValues.THT_WS_PSD_CLINICAL_DWH_TLS_CA_FILE; - delete vectorValues.THT_WS_PSD_CLINICAL_VECTOR_TLS_CA_FILE; - const yaml = renderRuntimeConfig(workspace, { - ...directBindings, dwh: { - ...directBindings.dwh, - values: dwhValues, - }, - vector: { - ...directBindings.vector, - values: vectorValues, - }, - }, paths); - const rendered = parse(yaml); - - expect(rendered.database).not.toHaveProperty("ssl_ca_file"); - expect(rendered.vector_db).not.toHaveProperty("ssl_ca_file"); -}); - -test("renders REST bindings through the legacy rest sections without secret values", () => { - const yaml = renderRuntimeConfig(workspace, { - ...directBindings, - dwh: { - transport: "rest_api", - missing: [], - values: { + transport: "rest_api", missing: [], values: { THT_WS_PSD_CLINICAL_DWH_BASE_URL: "https://dwh.example.test", THT_WS_PSD_CLINICAL_DWH_API_KEY_FILE: "/run/secrets/dwh-api-key", - THT_WS_PSD_CLINICAL_DWH_TLS_CA_FILE: "/run/secrets/ca.pem", }, }, - vector: { - transport: "rest_api", - missing: [], - values: { - THT_WS_PSD_CLINICAL_VECTOR_BASE_URL: "https://vector.example.test", - THT_WS_PSD_CLINICAL_VECTOR_API_KEY_FILE: "/run/secrets/vector-api-key", - }, - }, - }, paths); - const rendered = parse(yaml); + evidence: { missing: [], values: {} }, + }, paths)); - expect(rendered).toMatchObject({ - database: { transport: "rest", schema: "datawarehouse" }, - rest: { - base_url: "https://dwh.example.test", - api_key_file: "/run/secrets/dwh-api-key", - ssl_ca_file: "/run/secrets/ca.pem", - }, - vector_rest: { - base_url: "https://vector.example.test", - api_key_file: "/run/secrets/vector-api-key", - }, + expect(rendered.rest).toEqual({ + base_url: "https://dwh.example.test", api_key_file: "/run/secrets/dwh-api-key", }); - expect(yaml).not.toContain("\n api_key: "); + expect(rendered.dwh).toMatchObject({ + type: "thoth_rest", database: { database: "postgres", schema: "datawarehouse" }, + }); + expect(JSON.stringify(rendered)).not.toContain("api_key:"); +}); + +test("runtime support is fail-closed for DWH SSH and incomplete Evidence", () => { + expect(supportsSessionRuntime(directBindings)).toBe(true); + expect(supportsSessionRuntime({ + ...directBindings, dwh: { ...directBindings.dwh, transport: "ssh_tunnel" }, + })).toBe(false); + expect(supportsSessionRuntime({ + ...directBindings, evidence: { values: {}, missing: ["EVIDENCE_FILE"] }, + })).toBe(false); }); const evidenceSecretRoots: string[] = []; diff --git a/backend/test/workspaces-bindings.test.ts b/backend/test/workspaces-bindings.test.ts index 5b219915..0d665560 100644 --- a/backend/test/workspaces-bindings.test.ts +++ b/backend/test/workspaces-bindings.test.ts @@ -1,36 +1,14 @@ -import { chmodSync, mkdirSync, mkdtempSync, realpathSync, rmSync, symlinkSync, writeFileSync } from "node:fs"; +import { + chmodSync, mkdirSync, mkdtempSync, realpathSync, rmSync, symlinkSync, writeFileSync, +} from "node:fs"; import { tmpdir } from "node:os"; import { join } from "node:path"; import { afterEach, expect, test } from "vitest"; -import { resolveBinding, resolveEvidenceBinding, resolveRuntimeBindings, supportsSessionRuntime } from "../src/workspaces/bindings.js"; +import { + resolveBinding, resolveEvidenceBinding, resolveRuntimeBindings, supportsSessionRuntime, +} from "../src/workspaces/bindings.js"; import { parseWorkspaceYaml } from "../src/workspaces/schema.js"; -const workspace = parseWorkspaceYaml(`workspace: - schema_version: 2 - id: psd-clinical - name: Policlinico San Donato - language: it -dwh: - engine: postgres - database: postgres - schema: datawarehouse - supported_transports: [postgres_direct, rest_api, ssh_tunnel] -semantic_index: - vector_store: - engine: pgvector - database: postgres - schema: vectors - collection: clinical_documents - dimensions: 768 - distance: cosine - supported_transports: [pgvector_direct, rest_api, ssh_tunnel] - embedding: - provider: ollama_compatible - model: nomic-embed-text-v2-moe - dimensions: 768 -llm_policy: - allowed: [zai/glm-5.2] -`); const workspaceV3 = parseWorkspaceYaml(`workspace: schema_version: 3 id: psd-clinical @@ -40,19 +18,11 @@ dwh: engine: postgres database: postgres schema: datawarehouse - supported_transports: [postgres_direct, rest_api] + supported_transports: [postgres_direct, rest_api, ssh_tunnel] semantic_index: - vector_store: - engine: qdrant - collection: psd-clinical - dimensions: 1024 - distance: cosine - embedding: - provider: ollama_internal - model: qwen3-embedding:0.6b - dimensions: 1024 -llm_policy: - allowed: [zai/glm-5.2] + vector_store: { engine: qdrant, collection: psd-clinical, dimensions: 1024, distance: cosine } + embedding: { provider: ollama_internal, model: qwen3-embedding:0.6b, dimensions: 1024 } +llm_policy: { allowed: [zai/glm-5.2] } `); const temporaryRoots: string[] = []; @@ -70,80 +40,13 @@ function secretPath(name: string): { root: string; path: string } { return { root: secrets, path }; } -test("marks a portable workspace non-activatable when its local REST key file is absent", () => { - const result = resolveBinding(workspace, "DWH", { - THT_WS_PSD_CLINICAL_DWH_TRANSPORT: "rest_api", - THT_WS_PSD_CLINICAL_DWH_BASE_URL: "https://dwh.example.test", - }, ["/run/secrets"]); - - expect(result.missing).toContain("THT_WS_PSD_CLINICAL_DWH_API_KEY_FILE"); -}); - -test("does not require a REST secret file when its declared diagnostic uses auth none", () => { - const unauthenticatedWorkspace = parseWorkspaceYaml(`workspace: - schema_version: 2 - id: psd-clinical - name: Policlinico San Donato - language: it -dwh: - engine: postgres - database: postgres - schema: datawarehouse - supported_transports: [rest_api] -semantic_index: - vector_store: - engine: pgvector - database: postgres - schema: vectors - collection: clinical_documents - dimensions: 768 - distance: cosine - supported_transports: [rest_api] - embedding: - provider: ollama_compatible - model: nomic-embed-text-v2-moe - dimensions: 768 -diagnostics: - dwh_rest: - method: POST - path: /rpc/ping - auth: none - response: { database: database, schema: schema } - vector_rest: - metadata: - method: GET - path: /vector/metadata - auth: none - response: { collection: collection, dimensions: dimensions, distance: distance } - embedding: - method: GET - path: /models - auth: none - response: { model: model, dimensions: dimensions } -llm_policy: - allowed: [zai/glm-5.2] -`); - - const bindings = resolveRuntimeBindings(unauthenticatedWorkspace, { - THT_WS_PSD_CLINICAL_DWH_TRANSPORT: "rest_api", - THT_WS_PSD_CLINICAL_DWH_BASE_URL: "https://dwh.example.test", - THT_WS_PSD_CLINICAL_VECTOR_TRANSPORT: "rest_api", - THT_WS_PSD_CLINICAL_VECTOR_BASE_URL: "https://vector.example.test", - THT_WS_PSD_CLINICAL_EMBEDDING_BASE_URL: "https://embedding.example.test", - }, ["/run/secrets"]); - - expect(bindings.dwh.missing).toEqual([]); - expect(bindings.vector.missing).toEqual([]); - expect(bindings.embedding.missing).toEqual([]); -}); - -test("resolves direct bindings from the stable workspace namespace", () => { +test("resolves schema-v3 direct DWH bindings from the stable namespace", () => { const password = secretPath("dwh-password"); - const result = resolveBinding(workspace, "DWH", { + const result = resolveBinding(workspaceV3, "DWH", { THT_WS_PSD_CLINICAL_DWH_TRANSPORT: "postgres_direct", THT_WS_PSD_CLINICAL_DWH_HOST: "dwh.internal", THT_WS_PSD_CLINICAL_DWH_PORT: "5432", - THT_WS_PSD_CLINICAL_DWH_USER: "thoth_reader", + THT_WS_PSD_CLINICAL_DWH_USER: "reader", THT_WS_PSD_CLINICAL_DWH_PASSWORD_FILE: password.path, }, [password.root]); @@ -158,82 +61,72 @@ test("resolves direct bindings from the stable workspace namespace", () => { }); }); -test("reports only a FILE variable name when a secret path is outside the configured roots", () => { +test("requires schema-v3 REST credentials unless the DWH diagnostic declares auth none", () => { + expect(resolveBinding(workspaceV3, "DWH", { + THT_WS_PSD_CLINICAL_DWH_TRANSPORT: "rest_api", + THT_WS_PSD_CLINICAL_DWH_BASE_URL: "https://dwh.example.test", + }, []).missing).toContain("THT_WS_PSD_CLINICAL_DWH_API_KEY_FILE"); + + const noAuth = parseWorkspaceYaml(`workspace: + schema_version: 3 + id: psd-clinical + name: No auth + language: en +dwh: + engine: postgres + database: postgres + schema: public + supported_transports: [rest_api] +semantic_index: + vector_store: { engine: qdrant, collection: psd-clinical, dimensions: 1024, distance: cosine } + embedding: { provider: ollama_internal, model: qwen3-embedding:0.6b, dimensions: 1024 } +diagnostics: + dwh_rest: + method: GET + path: /health + auth: none + response: { database: database, schema: schema } +llm_policy: { allowed: [zai/glm-5.2] } +`); + expect(resolveBinding(noAuth, "DWH", { + THT_WS_PSD_CLINICAL_DWH_TRANSPORT: "rest_api", + THT_WS_PSD_CLINICAL_DWH_BASE_URL: "https://dwh.example.test", + }, []).missing).toEqual([]); +}); + +test("rejects unsupported transports and secret paths outside configured roots", () => { const outside = secretPath("outside-password"); const allowed = secretPath("allowed-password"); - const result = resolveBinding(workspace, "DWH", { + const directOnly = parseWorkspaceYaml(`workspace: + schema_version: 3 + id: psd-clinical + name: Direct only + language: en +dwh: + engine: postgres + database: postgres + schema: public + supported_transports: [postgres_direct] +semantic_index: + vector_store: { engine: qdrant, collection: psd-clinical, dimensions: 1024, distance: cosine } + embedding: { provider: ollama_internal, model: qwen3-embedding:0.6b, dimensions: 1024 } +llm_policy: { allowed: [zai/glm-5.2] } +`); + expect(resolveBinding(directOnly, "DWH", { + THT_WS_PSD_CLINICAL_DWH_TRANSPORT: "rest_api", + }, []).missing).toContain("THT_WS_PSD_CLINICAL_DWH_TRANSPORT"); + const result = resolveBinding(workspaceV3, "DWH", { THT_WS_PSD_CLINICAL_DWH_TRANSPORT: "postgres_direct", THT_WS_PSD_CLINICAL_DWH_HOST: "dwh.internal", THT_WS_PSD_CLINICAL_DWH_PORT: "5432", - THT_WS_PSD_CLINICAL_DWH_USER: "thoth_reader", + THT_WS_PSD_CLINICAL_DWH_USER: "reader", THT_WS_PSD_CLINICAL_DWH_PASSWORD_FILE: outside.path, }, [allowed.root]); - - expect(result.missing).toEqual(["THT_WS_PSD_CLINICAL_DWH_PASSWORD_FILE"]); - expect(result.missing.join("\n")).not.toContain(outside.path); + expect(result.missing).toContain("THT_WS_PSD_CLINICAL_DWH_PASSWORD_FILE"); + expect(JSON.stringify(result)).not.toContain(outside.path); }); -test("reports an invalid optional secret file instead of silently dropping it", () => { - const password = secretPath("dwh-password"); - const result = resolveBinding(workspace, "DWH", { - THT_WS_PSD_CLINICAL_DWH_TRANSPORT: "postgres_direct", - THT_WS_PSD_CLINICAL_DWH_HOST: "dwh.internal", - THT_WS_PSD_CLINICAL_DWH_PORT: "5432", - THT_WS_PSD_CLINICAL_DWH_USER: "thoth_reader", - THT_WS_PSD_CLINICAL_DWH_PASSWORD_FILE: password.path, - THT_WS_PSD_CLINICAL_DWH_TLS_CA_FILE: "relative-ca.pem", - }, [password.root]); - - expect(result.missing).toContain("THT_WS_PSD_CLINICAL_DWH_TLS_CA_FILE"); - expect(result.values).not.toHaveProperty("THT_WS_PSD_CLINICAL_DWH_TLS_CA_FILE"); -}); - -test("rejects a selected transport that the canonical workspace does not support", () => { - const directOnly = { - ...workspace, - dwh: { ...workspace.dwh, supported_transports: ["postgres_direct"] }, - }; - const result = resolveBinding(directOnly, "DWH", { - THT_WS_PSD_CLINICAL_DWH_TRANSPORT: "rest_api", - }, ["/run/secrets"]); - - expect(result).toMatchObject({ - transport: "rest_api", - missing: ["THT_WS_PSD_CLINICAL_DWH_TRANSPORT"], - }); -}); - -test("never treats a vector reader credential as the optional writer binding", () => { - const readerKey = secretPath("vector-reader-key"); - const writerWorkspace = { - ...workspace, - semantic_index: { ...workspace.semantic_index, vector_writer: {} }, - }; - const resolveWriter = () => resolveBinding(writerWorkspace, "VECTOR_WRITER" as never, { - THT_WS_PSD_CLINICAL_VECTOR_API_KEY_FILE: readerKey.path, - }, [readerKey.root]); - - expect(resolveWriter).not.toThrow(); - expect(resolveWriter()).toMatchObject({ - missing: ["THT_WS_PSD_CLINICAL_VECTOR_WRITER_API_KEY_FILE"], - values: {}, - }); -}); - -test("fails closed for v3 external semantic bindings", () => { - expect(() => resolveBinding(workspaceV3, "VECTOR", {}, ["/run/secrets"])) - .not.toThrow(); - expect(() => resolveBinding(workspaceV3, "EMBEDDING", {}, ["/run/secrets"])) - .not.toThrow(); - expect(() => resolveRuntimeBindings(workspaceV3, { - THT_WS_PSD_CLINICAL_VECTOR_TRANSPORT: "rest_api", - THT_WS_PSD_CLINICAL_VECTOR_BASE_URL: "https://vector.example.test", - THT_WS_PSD_CLINICAL_EMBEDDING_BASE_URL: "https://embedding.example.test", - THT_WS_PSD_CLINICAL_VECTOR_API_KEY_FILE: "/run/secrets/vector-api-key", - }, ["/run/secrets"])).not.toThrow(); -}); - -test("schema v3 ignores external semantic binding variables and reports only DWH requirements", () => { +test("runtime bindings contain only DWH and Evidence roles", () => { const password = secretPath("dwh-password"); const bindings = resolveRuntimeBindings(workspaceV3, { THT_WS_PSD_CLINICAL_DWH_TRANSPORT: "postgres_direct", @@ -241,21 +134,13 @@ test("schema v3 ignores external semantic binding variables and reports only DWH THT_WS_PSD_CLINICAL_DWH_PORT: "5432", THT_WS_PSD_CLINICAL_DWH_USER: "reader", THT_WS_PSD_CLINICAL_DWH_PASSWORD_FILE: password.path, - THT_WS_PSD_CLINICAL_VECTOR_TRANSPORT: "rest_api", - THT_WS_PSD_CLINICAL_VECTOR_BASE_URL: "https://vector.example.test", - THT_WS_PSD_CLINICAL_VECTOR_API_KEY_FILE: "/run/secrets/vector-api-key", - THT_WS_PSD_CLINICAL_EMBEDDING_BASE_URL: "https://embedding.example.test", - THT_WS_PSD_CLINICAL_EMBEDDING_API_KEY_FILE: "/run/secrets/embedding-api-key", + THT_WS_PSD_CLINICAL_VECTOR_BASE_URL: "https://ignored.example.test", }, [password.root]); - + expect(Object.keys(bindings)).toEqual(["dwh", "evidence"]); expect(bindings.dwh.missing).toEqual([]); - expect(bindings.vector.missing).toEqual([]); - expect(bindings.embedding.missing).toEqual([]); - expect(bindings.vector.values).toEqual({}); - expect(bindings.embedding.values).toEqual({}); + expect(supportsSessionRuntime(bindings)).toBe(true); }); - function withEvidence(source: Record) { return parseWorkspaceYaml(`workspace: schema_version: 3 diff --git a/backend/test/workspaces-contracts.test.ts b/backend/test/workspaces-contracts.test.ts index 7cb11a07..fffc4639 100644 --- a/backend/test/workspaces-contracts.test.ts +++ b/backend/test/workspaces-contracts.test.ts @@ -1,46 +1,10 @@ import { expect, test } from "vitest"; -import { existsSync, mkdtempSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs"; +import { mkdtempSync, mkdirSync, rmSync, writeFileSync } from "node:fs"; import { tmpdir } from "node:os"; import { join } from "node:path"; -import { fileURLToPath } from "node:url"; -import { parse } from "yaml"; import { buildInstallationContract, renderWorkspaceDocs } from "../src/workspaces/contracts.js"; import { type CanonicalWorkspace, parseWorkspaceYaml } from "../src/workspaces/schema.js"; -import { renderRuntimeConfig, type RuntimeBindings } from "../src/workspaces/runtime-renderer.js"; -const validWorkspace = parseWorkspaceYaml(`workspace: - schema_version: 2 - id: psd-clinical - name: Policlinico San Donato - language: it -dwh: - engine: postgres - database: postgres - schema: datawarehouse - supported_transports: - - postgres_direct - - rest_api -semantic_index: - vector_store: - engine: pgvector - database: postgres - schema: vectors - collection: clinical_documents - dimensions: 768 - distance: cosine - supported_transports: - - pgvector_direct - - rest_api - embedding: - provider: ollama_compatible - model: nomic-embed-text-v2-moe - dimensions: 768 -llm_policy: - default: zai/glm-5.2 - allowed: - - zai/glm-5.2 - - openai/gpt-5 -`); const workspaceV3 = parseWorkspaceYaml(`workspace: schema_version: 3 id: psd-clinical @@ -50,270 +14,48 @@ dwh: engine: postgres database: postgres schema: datawarehouse - supported_transports: [postgres_direct, rest_api] + supported_transports: [postgres_direct, rest_api, ssh_tunnel] semantic_index: - vector_store: - engine: qdrant - collection: psd-clinical - dimensions: 1024 - distance: cosine - embedding: - provider: ollama_internal - model: qwen3-embedding:0.6b - dimensions: 1024 + vector_store: { engine: qdrant, collection: psd-clinical, dimensions: 1024, distance: cosine } + embedding: { provider: ollama_internal, model: qwen3-embedding:0.6b, dimensions: 1024 } llm_policy: allowed: [zai/glm-5.2] `); -test("generates stable FILE-based secret requirements from an immutable ID", () => { - const contract = buildInstallationContract(validWorkspace); - - expect(contract.variables.map((variable) => variable.name)) - .toContain("THT_WS_PSD_CLINICAL_DWH_PASSWORD_FILE"); - expect(contract.variables.filter((variable) => variable.secret).every((variable) => ( - variable.name.endsWith("_FILE") - ))).toBe(true); - expect(renderWorkspaceDocs(validWorkspace).envExample).not.toContain("secret-value"); -}); - -test("derives variable names from fixed role and suffix metadata", () => { - const contract = buildInstallationContract(validWorkspace); - const password = contract.variables.find((variable) => ( - variable.role === "DWH" && variable.suffix === "PASSWORD_FILE" - )); - - expect(password).toMatchObject({ - name: "THT_WS_PSD_CLINICAL_DWH_PASSWORD_FILE", - role: "DWH", - suffix: "PASSWORD_FILE", - secret: true, - }); -}); - -test("renders English UI headings and workspace-language Italian prose", () => { - const docs = renderWorkspaceDocs(validWorkspace); - - expect(docs.markdown).toContain("# Installation requirements"); - expect(docs.markdown).toContain("Configurazione dell'installazione"); - expect(docs.markdown).not.toContain("## Vector writer"); - expect(docs.envExample).toContain("THT_WS_PSD_CLINICAL_VECTOR_TRANSPORT="); -}); - -test("schema v3 installation contracts expose only DWH bindings and no semantic variables", () => { +test("schema-v3 installation contracts expose only DWH bindings and no semantic variables", () => { const contract = buildInstallationContract(workspaceV3); const names = contract.variables.map((variable) => variable.name); const docs = renderWorkspaceDocs(workspaceV3); + expect(contract.workspaceId).toBe("psd-clinical"); + expect(contract.namespace).toBe("PSD_CLINICAL"); expect(contract.variables.every((variable) => variable.role === "DWH")).toBe(true); - expect(names).toContain("THT_WS_PSD_CLINICAL_DWH_TRANSPORT"); + expect(names).toContain("THT_WS_PSD_CLINICAL_DWH_PASSWORD_FILE"); expect(names.some((name) => /_VECTOR_|_EMBEDDING_/.test(name))).toBe(false); expect(docs.envExample).not.toContain("_VECTOR_"); - expect(docs.envExample).not.toContain("_EMBEDDING_"); + expect(docs.markdown).toContain("Configurazione dell'installazione"); expect(docs.markdown).not.toContain("Vector store"); expect(docs.markdown).not.toContain("Embedding service"); }); -test("renders the vector store identity and creates writer credentials only when declared", () => { - const writerWorkspace = parseWorkspaceYaml(`workspace: - schema_version: 2 - id: psd-clinical - name: Policlinico San Donato - language: it -dwh: - engine: postgres - database: warehouse - schema: datawarehouse - supported_transports: [postgres_direct, rest_api] -semantic_index: - vector_store: - engine: pgvector - database: vector_database - schema: vectors - collection: clinical_documents - dimensions: 768 - distance: cosine - supported_transports: [pgvector_direct, rest_api] - vector_writer: {} - embedding: - provider: ollama_compatible - model: nomic-embed-text-v2-moe - dimensions: 768 -diagnostics: - dwh_rest: - method: POST - path: /rpc/ping - auth: bearer - response: - database: database - schema: schema - vector_rest: - metadata: - method: GET - path: /metadata - auth: bearer - response: - collection: collection - dimensions: dimensions - distance: distance - embedding: - method: GET - path: /models - auth: none - response: - model: model - dimensions: dimensions -llm_policy: - allowed: [zai/glm-5.2] -`); - const bindings: RuntimeBindings = { - dwh: { - transport: "postgres_direct", - missing: [], - values: { - THT_WS_PSD_CLINICAL_DWH_HOST: "dwh.internal", - THT_WS_PSD_CLINICAL_DWH_PORT: "5432", - THT_WS_PSD_CLINICAL_DWH_USER: "reader", - THT_WS_PSD_CLINICAL_DWH_PASSWORD_FILE: "/run/secrets/dwh", - }, - }, - vector: { - transport: "pgvector_direct", - missing: [], - values: { - THT_WS_PSD_CLINICAL_VECTOR_HOST: "vector.internal", - THT_WS_PSD_CLINICAL_VECTOR_PORT: "5432", - THT_WS_PSD_CLINICAL_VECTOR_USER: "vector-reader", - THT_WS_PSD_CLINICAL_VECTOR_PASSWORD_FILE: "/run/secrets/vector-reader", - }, - }, - vectorWriter: { transport: "rest_api", missing: [], values: {} }, - embedding: { - transport: "rest_api", - missing: [], - values: { THT_WS_PSD_CLINICAL_EMBEDDING_BASE_URL: "https://embedding.internal" }, - }, - evidence: { missing: [], values: {} }, - }; - - const writerVariables = buildInstallationContract(writerWorkspace).variables - .filter((variable) => variable.role === "VECTOR_WRITER"); - - expect(writerVariables).toEqual([expect.objectContaining({ - name: "THT_WS_PSD_CLINICAL_VECTOR_WRITER_API_KEY_FILE", - role: "VECTOR_WRITER", - secret: true, - })]); - expect(buildInstallationContract(validWorkspace).variables.some((variable) => ( - variable.role === "VECTOR_WRITER" - ))).toBe(false); - expect(parse(renderRuntimeConfig(writerWorkspace, bindings, { - sessions: "/data/sessions", - artifacts: "/data/artifacts", - indexes: "/data/indexes", - })).vector_db).toMatchObject({ database: "vector_database", schema: "vectors" }); -}); - -test("renders legacy writer secret-file bindings without reintroducing them to the active protocol", () => { - const writerVariable = "THT_WS_PSD_CLINICAL_VECTOR_WRITER_API_KEY_FILE"; - const generated = renderWorkspaceDocs(parseWorkspaceYaml(`workspace: - schema_version: 2 - id: psd-clinical - name: Policlinico San Donato - language: it -dwh: - engine: postgres - database: warehouse - schema: datawarehouse - supported_transports: [postgres_direct] -semantic_index: - vector_store: - engine: pgvector - database: vector_database - schema: vectors - collection: clinical_documents - dimensions: 768 - distance: cosine - supported_transports: [rest_api] - vector_writer: {} - embedding: - provider: ollama_compatible - model: nomic-embed-text-v2-moe - dimensions: 768 -llm_policy: - allowed: [zai/glm-5.2] -`)); - const protocolPath = fileURLToPath(new URL("../../docs/workspace-diagnostic-protocol.md", import.meta.url)); - - expect(generated.markdown).toContain(`\`${writerVariable}\``); - expect(generated.envExample).toContain(`${writerVariable}=`); - expect(existsSync(protocolPath)).toBe(true); - if (existsSync(protocolPath)) { - expect(readFileSync(protocolPath, "utf8")).not.toContain(writerVariable); - expect(readFileSync(protocolPath, "utf8")).toContain("There are no supported `THT_WS__VECTOR_*`"); - } -}); - -function withTransports( - dwhTransport: CanonicalWorkspace["dwh"]["supported_transports"][number], - vectorTransport: CanonicalWorkspace["semantic_index"]["vector_store"]["supported_transports"][number], -): CanonicalWorkspace { - return { - ...validWorkspace, - dwh: { ...validWorkspace.dwh, supported_transports: [dwhTransport] }, - semantic_index: { - ...validWorkspace.semantic_index, - vector_store: { - ...validWorkspace.semantic_index.vector_store, - supported_transports: [vectorTransport], - }, - }, - }; -} - -test("emits only direct connector bindings for direct transports", () => { - const variables = buildInstallationContract(withTransports("postgres_direct", "pgvector_direct")).variables; - - for (const role of ["DWH", "VECTOR"] as const) { - const names = variables.filter((variable) => variable.role === role).map((variable) => variable.name); - expect(names).toContain(`THT_WS_PSD_CLINICAL_${role}_HOST`); - expect(names).toContain(`THT_WS_PSD_CLINICAL_${role}_PASSWORD_FILE`); - expect(names).not.toContain(`THT_WS_PSD_CLINICAL_${role}_BASE_URL`); - expect(names).not.toContain(`THT_WS_PSD_CLINICAL_${role}_API_KEY_FILE`); - expect(names.some((name) => name.includes("_SSH_"))).toBe(false); - } -}); - -test("emits only REST connector bindings for REST transports", () => { - const variables = buildInstallationContract(withTransports("rest_api", "rest_api")).variables; - - for (const role of ["DWH", "VECTOR"] as const) { - const names = variables.filter((variable) => variable.role === role).map((variable) => variable.name); - expect(names).toContain(`THT_WS_PSD_CLINICAL_${role}_BASE_URL`); - expect(names).toContain(`THT_WS_PSD_CLINICAL_${role}_API_KEY_FILE`); - expect(names).not.toContain(`THT_WS_PSD_CLINICAL_${role}_HOST`); - expect(names).not.toContain(`THT_WS_PSD_CLINICAL_${role}_PASSWORD_FILE`); - expect(names.some((name) => name.includes("_SSH_"))).toBe(false); - } -}); - -test("emits SSH bindings only for SSH-tunnel transports", () => { - const variables = buildInstallationContract(withTransports("ssh_tunnel", "ssh_tunnel")).variables; - - for (const role of ["DWH", "VECTOR"] as const) { - const roleVariables = variables.filter((variable) => variable.role === role); - expect(roleVariables.map((variable) => variable.name)) - .toContain(`THT_WS_PSD_CLINICAL_${role}_SSH_PRIVATE_KEY_FILE`); - expect(roleVariables.find((variable) => variable.suffix === "SSH_HOST")?.transports) - .toEqual(["ssh_tunnel"]); - expect(roleVariables.map((variable) => variable.name)) - .not.toContain(`THT_WS_PSD_CLINICAL_${role}_BASE_URL`); - } +test.each([ + ["postgres_direct", "HOST", "BASE_URL"], + ["rest_api", "BASE_URL", "HOST"], + ["ssh_tunnel", "SSH_PRIVATE_KEY_FILE", "BASE_URL"], +] as const)("documents only the DWH fields for %s", (transport, included, excluded) => { + const descriptor = parseWorkspaceYaml(renderWorkspaceWithoutEvidence() + .replace("[postgres_direct]", `[${transport}]`)); + const variables = buildInstallationContract(descriptor).variables; + expect(variables.find(({ suffix }) => suffix === included)?.transports).toEqual([transport]); + expect(variables.some(({ suffix }) => suffix === excluded)).toBe(false); + expect(variables.filter(({ secret }) => secret).every(({ name }) => name.endsWith("_FILE"))) + .toBe(true); }); test("validates public contract and documentation inputs at runtime", () => { const unsafeWorkspace = { - ...validWorkspace, - workspace: { ...validWorkspace.workspace, id: "psd\nclinical" }, + ...workspaceV3, + workspace: { ...workspaceV3.workspace, id: "psd\nclinical" }, } as CanonicalWorkspace; expect(() => buildInstallationContract(unsafeWorkspace)).toThrow(/id/i); diff --git a/backend/test/workspaces-diagnostics.test.ts b/backend/test/workspaces-diagnostics.test.ts index 296df80c..ac455312 100644 --- a/backend/test/workspaces-diagnostics.test.ts +++ b/backend/test/workspaces-diagnostics.test.ts @@ -1,6 +1,5 @@ -import { expect, test, vi } from "vitest"; -import { EventEmitter } from "node:events"; -import { mkdtemp, realpath, rm, writeFile } from "node:fs/promises"; +import { afterEach, expect, test, vi } from "vitest"; +import { mkdtemp, rm, writeFile } from "node:fs/promises"; import { tmpdir } from "node:os"; import { join } from "node:path"; import { @@ -9,159 +8,10 @@ import { createWorkspaceDiagnoser, type DiagnosticAdapters, } from "../src/workspaces/diagnostics.js"; -import { resolveRuntimeBindings } from "../src/workspaces/bindings.js"; -import { renderRuntimeConfig, type RuntimeBindings } from "../src/workspaces/runtime-renderer.js"; +import type { RuntimeBindings } from "../src/workspaces/runtime-renderer.js"; import { parseWorkspaceYaml } from "../src/workspaces/schema.js"; const workspace = parseWorkspaceYaml(`workspace: - schema_version: 2 - id: psd-clinical - name: Policlinico San Donato - language: it -dwh: - engine: postgres - database: warehouse - schema: datawarehouse - timeout_ms: 8000 - supported_transports: [postgres_direct, rest_api, ssh_tunnel] -semantic_index: - vector_store: - engine: pgvector - database: postgres - schema: vectors - collection: clinical_documents - dimensions: 768 - distance: cosine - timeout_ms: 8000 - supported_transports: [pgvector_direct, rest_api, ssh_tunnel] - embedding: - provider: ollama_compatible - model: nomic-embed-text-v2-moe - dimensions: 768 - timeout_ms: 8000 -llm_policy: - allowed: [zai/glm-5.2] -diagnostics: - dwh_rest: - method: POST - path: /rpc/ping - auth: bearer - response: { database: database, schema: schema } - vector_rest: - metadata: - method: GET - path: /vector/metadata - auth: bearer - response: { collection: collection, dimensions: dimensions, distance: distance } - reversible_probe: - method: POST - path: /vector/diagnostic-probe - auth: bearer - response: { operation: operation } -`); - -const writerWorkspace = parseWorkspaceYaml(`workspace: - schema_version: 2 - id: psd-clinical - name: Policlinico San Donato - language: it -dwh: - engine: postgres - database: warehouse - schema: datawarehouse - timeout_ms: 8000 - supported_transports: [postgres_direct, rest_api, ssh_tunnel] -semantic_index: - vector_store: - engine: pgvector - database: postgres - schema: vectors - collection: clinical_documents - dimensions: 768 - distance: cosine - timeout_ms: 8000 - supported_transports: [pgvector_direct, rest_api, ssh_tunnel] - vector_writer: {} - embedding: - provider: ollama_compatible - model: nomic-embed-text-v2-moe - dimensions: 768 - timeout_ms: 8000 -llm_policy: - allowed: [zai/glm-5.2] -diagnostics: - dwh_rest: - method: POST - path: /rpc/ping - auth: bearer - response: { database: database, schema: schema } - vector_rest: - metadata: - method: GET - path: /vector/metadata - auth: bearer - response: { collection: collection, dimensions: dimensions, distance: distance } - reversible_probe: - method: POST - path: /vector/diagnostic-probe - auth: bearer - response: { operation: operation } -`); - -const bindings: RuntimeBindings = { - dwh: { - transport: "postgres_direct", - missing: [], - values: { - THT_WS_PSD_CLINICAL_DWH_HOST: "dwh.example.test", - THT_WS_PSD_CLINICAL_DWH_PORT: "5432", - THT_WS_PSD_CLINICAL_DWH_USER: "reader", - THT_WS_PSD_CLINICAL_DWH_PASSWORD_FILE: "/run/secrets/dwh-password", - THT_WS_PSD_CLINICAL_DWH_TLS_CA_FILE: "/run/secrets/dwh-ca", - }, - }, - vector: { - transport: "pgvector_direct", - missing: [], - values: { - THT_WS_PSD_CLINICAL_VECTOR_HOST: "vector.example.test", - THT_WS_PSD_CLINICAL_VECTOR_PORT: "5432", - THT_WS_PSD_CLINICAL_VECTOR_USER: "vector-reader", - THT_WS_PSD_CLINICAL_VECTOR_PASSWORD_FILE: "/run/secrets/vector-password", - THT_WS_PSD_CLINICAL_VECTOR_TLS_CA_FILE: "/run/secrets/vector-ca", - }, - }, - vectorWriter: { transport: "rest_api", missing: [], values: {} }, - embedding: { - transport: "rest_api", - missing: [], - values: { - THT_WS_PSD_CLINICAL_EMBEDDING_BASE_URL: "https://embedding.example.test", - THT_WS_PSD_CLINICAL_EMBEDDING_API_KEY_FILE: "/run/secrets/embedding-key", - THT_WS_PSD_CLINICAL_EMBEDDING_TLS_CA_FILE: "/run/secrets/embedding-ca", - }, - }, - evidence: { missing: [], values: {} }, -}; - -const writerBindings: RuntimeBindings = { - ...bindings, - vector: { - transport: "rest_api", - missing: [], - values: { - THT_WS_PSD_CLINICAL_VECTOR_BASE_URL: "https://vector.example.test", - THT_WS_PSD_CLINICAL_VECTOR_API_KEY_FILE: "/run/secrets/vector-reader-key", - }, - }, - vectorWriter: { - transport: "rest_api", - missing: [], - values: { THT_WS_PSD_CLINICAL_VECTOR_WRITER_API_KEY_FILE: "/run/secrets/vector-writer-key" }, - }, -}; - -const workspaceV3 = parseWorkspaceYaml(`workspace: schema_version: 3 id: psd-clinical name: Policlinico San Donato @@ -171,7 +21,7 @@ dwh: database: warehouse schema: datawarehouse timeout_ms: 8000 - supported_transports: [postgres_direct, rest_api] + supported_transports: [postgres_direct, rest_api, ssh_tunnel] semantic_index: vector_store: engine: qdrant @@ -186,11 +36,18 @@ llm_policy: allowed: [zai/glm-5.2] `); -const bindingsV3: RuntimeBindings = { - dwh: bindings.dwh, - vector: { transport: "rest_api", missing: [], values: {} }, - vectorWriter: { transport: "rest_api", missing: [], values: {} }, - embedding: { transport: "rest_api", missing: [], values: {} }, +const bindings: RuntimeBindings = { + dwh: { + transport: "postgres_direct", + missing: [], + values: { + THT_WS_PSD_CLINICAL_DWH_HOST: "dwh.example.test", + THT_WS_PSD_CLINICAL_DWH_PORT: "5432", + THT_WS_PSD_CLINICAL_DWH_USER: "reader", + THT_WS_PSD_CLINICAL_DWH_PASSWORD_FILE: "/run/secrets/dwh-password", + THT_WS_PSD_CLINICAL_DWH_TLS_CA_FILE: "/run/secrets/dwh-ca", + }, + }, evidence: { missing: [], values: {} }, }; @@ -202,818 +59,430 @@ function successfulAdapters(overrides: Partial = {}): Diagno authenticated: true, resource: request.resource, })), - withSshTunnel: vi.fn(async (_request, probe) => probe({ host: "127.0.0.1", port: 45678 })), - inspectVector: vi.fn(async () => ({ - collection: "clinical_documents", - dimensions: 768, + inspectQdrant: vi.fn(async (request) => ({ + collection: request.collection, + dimensions: 1024, distance: "cosine", })), - probeEmbedding: vi.fn(async () => ({ available: true, dimensions: 768 })), - writeDiagnosticRecord: vi.fn(async () => undefined), - removeDiagnosticRecord: vi.fn(async () => undefined), + probeEmbedding: vi.fn(async () => ({ available: true, dimensions: 1024 })), ...overrides, }; } function diagnose(adapters = successfulAdapters()) { - return createWorkspaceDiagnoser(adapters, { timeoutMs: 5000 }); + return createWorkspaceDiagnoser(adapters, { timeoutMs: 5_000 }); } -test("reports the missing vector collection dimensions as semantic-index incompatibility", async () => { - const result = await diagnose(successfulAdapters({ - inspectVector: vi.fn(async () => ({ - collection: "clinical_documents", - dimensions: undefined, - distance: "cosine", - })), - }))(workspace, bindings, { writeProbe: false }); - - expect(result.diagnostics).toContainEqual(expect.objectContaining({ - code: "semantic_index_incompatible", - })); - expect(result.activatable).toBe(false); +afterEach(() => { + vi.unstubAllGlobals(); + vi.restoreAllMocks(); }); -test("refuses an SSH tunnel when known-hosts is missing", async () => { - const sshBindingsWithoutKnownHosts: RuntimeBindings = { - ...bindings, - dwh: { - transport: "ssh_tunnel", - missing: ["THT_WS_PSD_CLINICAL_DWH_SSH_KNOWN_HOSTS_FILE"], - values: { - THT_WS_PSD_CLINICAL_DWH_USER: "reader", - THT_WS_PSD_CLINICAL_DWH_PASSWORD_FILE: "/run/secrets/dwh-password", - THT_WS_PSD_CLINICAL_DWH_SSH_HOST: "bastion.example.test", - THT_WS_PSD_CLINICAL_DWH_SSH_PORT: "22", - THT_WS_PSD_CLINICAL_DWH_SSH_USER: "tunnel", - THT_WS_PSD_CLINICAL_DWH_SSH_PRIVATE_KEY_FILE: "/run/secrets/ssh-key", - THT_WS_PSD_CLINICAL_DWH_SSH_TARGET_HOST: "dwh.internal", - THT_WS_PSD_CLINICAL_DWH_SSH_TARGET_PORT: "5432", - }, - }, - }; +test("diagnoses schema-v3 DWH, internal Qdrant, and internal Ollama without semantic bindings", async () => { const adapters = successfulAdapters(); + const result = await diagnose(adapters)(workspace, bindings, { writeProbe: false }); - const result = await diagnose(adapters)(workspace, sshBindingsWithoutKnownHosts, { writeProbe: false }); - - expect(result.activatable).toBe(false); - expect(result.diagnostics[0]).toMatchObject({ - code: "binding_missing", - field: expect.stringContaining("SSH_KNOWN_HOSTS_FILE"), + expect(result).toEqual({ + activatable: true, + diagnostics: [{ + level: "info", code: "binding_ok", + message: "Installation bindings and diagnostics succeeded.", + }], }); - expect(adapters.withSshTunnel).not.toHaveBeenCalled(); -}); - -test("checks direct and REST resolution, TLS, authentication, and resource metadata without exposing failures", async () => { - const adapters = successfulAdapters({ - probeConnector: vi.fn(async (request) => ({ - resolved: true, - tlsVerified: true, - authenticated: true, - resource: request.role === "dwh" - ? { database: "warehouse", schema: "wrong_schema" } - : request.resource, - })), - }); - const restBindings: RuntimeBindings = { - ...bindings, - dwh: { - transport: "rest_api", - missing: [], - values: { - THT_WS_PSD_CLINICAL_DWH_BASE_URL: "https://dwh.example.test", - THT_WS_PSD_CLINICAL_DWH_API_KEY_FILE: "/run/secrets/dwh-api-key", - THT_WS_PSD_CLINICAL_DWH_TLS_CA_FILE: "/run/secrets/dwh-ca", - }, - }, - }; - - const result = await diagnose(adapters)(workspace, restBindings, { writeProbe: false }); - expect(adapters.probeConnector).toHaveBeenCalledWith(expect.objectContaining({ - transport: "rest_api", - timeoutMs: 5000, - tlsCaFile: "/run/secrets/dwh-ca", - credentialFile: "/run/secrets/dwh-api-key", + role: "dwh", transport: "postgres_direct", resource: { database: "warehouse", schema: "datawarehouse" }, })); - expect(result).toMatchObject({ activatable: false }); - expect(JSON.stringify(result)).not.toContain("wrong_schema"); - expect(JSON.stringify(result)).not.toContain("/run/secrets/dwh-api-key"); + expect(adapters.inspectQdrant).toHaveBeenCalledWith(expect.objectContaining({ + baseUrl: "http://qdrant:6333", collection: "psd-clinical", + })); + expect(adapters.probeEmbedding).toHaveBeenCalledWith(expect.objectContaining({ + baseUrl: "http://embedding:11434", model: "qwen3-embedding:0.6b", + })); }); -test("carries auth-none REST bindings from resolver through runtime rendering to diagnostics without a key", async () => { - const unauthenticatedWorkspace = parseWorkspaceYaml(`workspace: - schema_version: 2 +test("reports incompatible internal Qdrant or Ollama metadata", async () => { + const vector = await diagnose(successfulAdapters({ + inspectQdrant: vi.fn(async () => ({ + collection: "psd-clinical", dimensions: 768, distance: "cosine", + })), + }))(workspace, bindings, { writeProbe: false }); + expect(vector.activatable).toBe(false); + expect(vector.diagnostics).toContainEqual(expect.objectContaining({ + code: "semantic_index_incompatible", + })); + + const embedding = await diagnose(successfulAdapters({ + probeEmbedding: vi.fn(async () => ({ available: true, dimensions: 768 })), + }))(workspace, bindings, { writeProbe: false }); + expect(embedding.activatable).toBe(false); + expect(embedding.diagnostics).toContainEqual(expect.objectContaining({ + code: "semantic_index_incompatible", + })); +}); + +test("reports only sanitized DWH and Evidence binding names before network diagnostics", async () => { + const adapters = successfulAdapters(); + const result = await diagnose(adapters)(workspace, { + dwh: { ...bindings.dwh, missing: ["THT_WS_PSD_CLINICAL_DWH_PASSWORD_FILE"] }, + evidence: { + values: {}, missing: ["THT_WS_PSD_CLINICAL_EVIDENCE_SIGNED_URLS_FILE"], + }, + }, { writeProbe: false }); + + expect(result.activatable).toBe(false); + expect(result.diagnostics.map(({ code }) => code)).toEqual(["binding_missing", "binding_missing"]); + expect(result.diagnostics[1]).toMatchObject({ + variable: "THT_WS_PSD_CLINICAL_EVIDENCE_SIGNED_URLS_FILE", + }); + expect(adapters.probeConnector).not.toHaveBeenCalled(); + expect(adapters.inspectQdrant).not.toHaveBeenCalled(); +}); + +test("keeps schema-v3 DWH SSH diagnostic-only and runtime-inactive", async () => { + const adapters = successfulAdapters(); + const result = await diagnose(adapters)(workspace, { + ...bindings, + dwh: { transport: "ssh_tunnel", missing: [], values: {} }, + }, { writeProbe: false }); + expect(result).toEqual({ + activatable: false, + diagnostics: [expect.objectContaining({ code: "workspace_not_activatable" })], + }); + expect(adapters.probeConnector).not.toHaveBeenCalled(); +}); + +test("uses the schema-v3 declared DWH REST diagnostic and auth policy", async () => { + const restWorkspace = parseWorkspaceYaml(`workspace: + schema_version: 3 id: psd-clinical - name: Policlinico San Donato - language: it + name: REST workspace + language: en dwh: engine: postgres database: warehouse schema: datawarehouse supported_transports: [rest_api] semantic_index: - vector_store: - engine: pgvector - database: postgres - schema: vectors - collection: clinical_documents - dimensions: 768 - distance: cosine - supported_transports: [rest_api] - embedding: - provider: ollama_compatible - model: nomic-embed-text-v2-moe - dimensions: 768 + vector_store: { engine: qdrant, collection: psd-clinical, dimensions: 1024, distance: cosine } + embedding: { provider: ollama_internal, model: qwen3-embedding:0.6b, dimensions: 1024 } diagnostics: dwh_rest: method: POST path: /rpc/ping - auth: none + auth: bearer response: { database: database, schema: schema } - vector_rest: - metadata: - method: GET - path: /vector/metadata - auth: none - response: { collection: collection, dimensions: dimensions, distance: distance } - embedding: - method: GET - path: /models - auth: none - response: { model: model, dimensions: dimensions } -llm_policy: - allowed: [zai/glm-5.2] +llm_policy: { allowed: [zai/glm-5.2] } `); - const resolved = resolveRuntimeBindings(unauthenticatedWorkspace, { - THT_WS_PSD_CLINICAL_DWH_TRANSPORT: "rest_api", - THT_WS_PSD_CLINICAL_DWH_BASE_URL: "https://dwh.example.test", - THT_WS_PSD_CLINICAL_VECTOR_TRANSPORT: "rest_api", - THT_WS_PSD_CLINICAL_VECTOR_BASE_URL: "https://vector.example.test", - THT_WS_PSD_CLINICAL_EMBEDDING_BASE_URL: "https://embedding.example.test", - }, ["/run/secrets"]); const adapters = successfulAdapters(); - - const runtime = renderRuntimeConfig(unauthenticatedWorkspace, resolved, { - sessions: "/data/sessions", artifacts: "/data/artifacts", indexes: "/data/indexes", - }); - const result = await diagnose(adapters)(unauthenticatedWorkspace, resolved, { writeProbe: false }); - - expect(runtime).not.toContain("api_key_file"); - expect(result.activatable).toBe(true); - expect(adapters.probeConnector).toHaveBeenCalledWith(expect.objectContaining({ - role: "dwh", credentialFile: undefined, - })); - expect(adapters.probeConnector).toHaveBeenCalledWith(expect.objectContaining({ - role: "vector", credentialFile: undefined, - })); -}); - -test("does not advertise a probe-only SSH tunnel as usable by runtime sessions", async () => { - const adapters = successfulAdapters(); - const sshBindings: RuntimeBindings = { - ...bindings, - dwh: { - transport: "ssh_tunnel", - missing: [], - values: { - THT_WS_PSD_CLINICAL_DWH_USER: "reader", - THT_WS_PSD_CLINICAL_DWH_PASSWORD_FILE: "/run/secrets/dwh-password", - THT_WS_PSD_CLINICAL_DWH_TLS_CA_FILE: "/run/secrets/dwh-ca", - THT_WS_PSD_CLINICAL_DWH_SSH_HOST: "bastion.example.test", - THT_WS_PSD_CLINICAL_DWH_SSH_PORT: "22", - THT_WS_PSD_CLINICAL_DWH_SSH_USER: "tunnel", - THT_WS_PSD_CLINICAL_DWH_SSH_PRIVATE_KEY_FILE: "/run/secrets/ssh-key", - THT_WS_PSD_CLINICAL_DWH_SSH_KNOWN_HOSTS_FILE: "/run/secrets/known-hosts", - THT_WS_PSD_CLINICAL_DWH_SSH_TARGET_HOST: "dwh.internal", - THT_WS_PSD_CLINICAL_DWH_SSH_TARGET_PORT: "5432", - }, - }, - }; - - const result = await diagnose(adapters)(workspace, sshBindings, { writeProbe: false }); - - expect(result.activatable).toBe(false); - expect(result.diagnostics).toContainEqual(expect.objectContaining({ - level: "error", - code: "workspace_not_activatable", - })); - expect(adapters.withSshTunnel).toHaveBeenCalledWith(expect.objectContaining({ - localHost: "127.0.0.1", - localPort: 0, - knownHostsFile: "/run/secrets/known-hosts", - timeoutMs: 5000, - }), expect.any(Function)); - expect(adapters.probeConnector).toHaveBeenCalledWith(expect.objectContaining({ - host: "127.0.0.1", - port: 45678, - })); -}); - -test("retains the SSH target hostname for forwarded PostgreSQL TLS validation", async () => { - const adapters = successfulAdapters(); - const sshBindings: RuntimeBindings = { - ...bindings, - dwh: { - transport: "ssh_tunnel", - missing: [], - values: { - THT_WS_PSD_CLINICAL_DWH_USER: "reader", - THT_WS_PSD_CLINICAL_DWH_PASSWORD_FILE: "/run/secrets/dwh-password", - THT_WS_PSD_CLINICAL_DWH_SSH_HOST: "bastion.example.test", - THT_WS_PSD_CLINICAL_DWH_SSH_PORT: "22", - THT_WS_PSD_CLINICAL_DWH_SSH_USER: "tunnel", - THT_WS_PSD_CLINICAL_DWH_SSH_PRIVATE_KEY_FILE: "/run/secrets/ssh-key", - THT_WS_PSD_CLINICAL_DWH_SSH_KNOWN_HOSTS_FILE: "/run/secrets/known-hosts", - THT_WS_PSD_CLINICAL_DWH_SSH_TARGET_HOST: "dwh.internal", - THT_WS_PSD_CLINICAL_DWH_SSH_TARGET_PORT: "5432", - }, - }, - }; - - await diagnose(adapters)(workspace, sshBindings, { writeProbe: false }); - - expect(adapters.probeConnector).toHaveBeenCalledWith(expect.objectContaining({ - host: "127.0.0.1", - tlsServername: "dwh.internal", - })); -}); - -test("passes the declared vector database and schema to direct diagnostics", async () => { - const adapters = successfulAdapters(); - - await diagnose(adapters)(workspace, bindings, { writeProbe: false }); - - expect(adapters.probeConnector).toHaveBeenCalledWith(expect.objectContaining({ - role: "vector", - resource: { database: "postgres", schema: "vectors", collection: "clinical_documents" }, - })); -}); - -test("uses strict known-host SSH arguments and always closes the temporary tunnel", async () => { - const directory = await mkdtemp(join(tmpdir(), "thothii-diagnostic-")); - const privateKeyFile = join(directory, "ssh-key"); - await writeFile(privateKeyFile, "test-key\n", { mode: 0o600 }); - const close = vi.fn(async () => undefined); - const start = vi.fn(async () => ({ tunnel: { host: "127.0.0.1" as const, port: 45432 }, close })); - - try { - const adapter = createConcreteDiagnosticAdapters({ sshProcess: { start } }); - await adapter.withSshTunnel({ - sshHost: "bastion.example.test", sshPort: 22, sshUser: "tunnel", privateKeyFile, - knownHostsFile: "/run/secrets/known-hosts", targetHost: "vector.internal", targetPort: 5432, - localHost: "127.0.0.1", localPort: 0, timeoutMs: 5000, signal: new AbortController().signal, - }, async () => undefined); - - expect(start).toHaveBeenCalledWith(expect.any(Object), expect.arrayContaining([ - "StrictHostKeyChecking=yes", "UserKnownHostsFile=/run/secrets/known-hosts", "-i", privateKeyFile, - ])); - expect(close).toHaveBeenCalledOnce(); - } finally { - await rm(directory, { recursive: true, force: true }); - } -}); - -test("provides a default bounded SSH factory through injected spawn and loopback allocation", async () => { - const directory = await mkdtemp(join(tmpdir(), "thothii-diagnostic-")); - const privateKeyFile = join(directory, "ssh-key"); - await writeFile(privateKeyFile, "test-key\n", { mode: 0o600 }); - const kill = vi.fn(() => true); - const sshSpawn = vi.fn(() => ({ kill })); - try { - const adapter = createConcreteDiagnosticAdapters({ sshSpawn, reserveLoopbackPort: async () => 45432, sshForwardConfirmed: async () => undefined } as any); - await adapter.withSshTunnel({ sshHost: "bastion.example.test", sshPort: 22, sshUser: "tunnel", privateKeyFile, knownHostsFile: "/run/secrets/known-hosts", targetHost: "dwh.internal", targetPort: 5432, localHost: "127.0.0.1", localPort: 0, timeoutMs: 5000, signal: new AbortController().signal }, async () => undefined); - expect(sshSpawn).toHaveBeenCalledWith(expect.arrayContaining([ - "StrictHostKeyChecking=yes", "UserKnownHostsFile=/run/secrets/known-hosts", "-L", "127.0.0.1:45432:dwh.internal:5432", - ])); - expect(kill).toHaveBeenCalledWith("SIGTERM"); - } finally { - await rm(directory, { recursive: true, force: true }); - } -}); - -test("waits for SSH readiness before probing and includes ExitOnForwardFailure", async () => { - const directory = await mkdtemp(join(tmpdir(), "thothii-diagnostic-")); - const privateKeyFile = join(directory, "ssh-key"); - await writeFile(privateKeyFile, "test-key\n", { mode: 0o600 }); - let releaseReady: (() => void) | undefined; - const ready = new Promise((resolve) => { releaseReady = resolve; }); - const probe = vi.fn(async () => undefined); - const sshSpawn = vi.fn(() => ({ kill: vi.fn(() => true) })); - try { - const adapter = createConcreteDiagnosticAdapters({ sshSpawn, reserveLoopbackPort: async () => 45432, sshForwardConfirmed: async () => await ready } as any); - const running = adapter.withSshTunnel({ sshHost: "bastion.example.test", sshPort: 22, sshUser: "tunnel", privateKeyFile, knownHostsFile: "/run/secrets/known-hosts", targetHost: "dwh.internal", targetPort: 5432, localHost: "127.0.0.1", localPort: 0, timeoutMs: 5000, signal: new AbortController().signal }, probe); - await Promise.resolve(); - expect(probe).not.toHaveBeenCalled(); - releaseReady?.(); - await running; - expect(sshSpawn).toHaveBeenCalledWith(expect.arrayContaining(["ExitOnForwardFailure=yes"])); - expect(probe).toHaveBeenCalledOnce(); - } finally { - await rm(directory, { recursive: true, force: true }); - } -}); - -test("rejects unrelated listener readiness until the SSH child confirms its own forward", async () => { - const directory = await mkdtemp(join(tmpdir(), "thothii-diagnostic-")); - const privateKeyFile = join(directory, "ssh-key"); - await writeFile(privateKeyFile, "test-key\n", { mode: 0o600 }); - const child = Object.assign(new EventEmitter(), { kill: vi.fn(() => true) }); - const probe = vi.fn(async () => undefined); - try { - const adapter = createConcreteDiagnosticAdapters({ sshSpawn: vi.fn(() => child), reserveLoopbackPort: async () => 45432, waitForSshReady: async () => undefined } as any); - await expect(adapter.withSshTunnel({ sshHost: "bastion.example.test", sshPort: 22, sshUser: "tunnel", privateKeyFile, knownHostsFile: "/run/secrets/known-hosts", targetHost: "dwh.internal", targetPort: 5432, localHost: "127.0.0.1", localPort: 0, timeoutMs: 20, signal: new AbortController().signal }, probe)).rejects.toThrow("SSH tunnel readiness failed"); - expect(probe).not.toHaveBeenCalled(); - } finally { - await rm(directory, { recursive: true, force: true }); - } -}); - -test("permits the probe only after this SSH child confirms its forwarded port", async () => { - const directory = await mkdtemp(join(tmpdir(), "thothii-diagnostic-")); - const privateKeyFile = join(directory, "ssh-key"); - await writeFile(privateKeyFile, "test-key\n", { mode: 0o600 }); - const stderr = new EventEmitter(); - const child = Object.assign(new EventEmitter(), { kill: vi.fn(() => true), stderr }); - child.kill.mockImplementation(() => { child.emit("exit", 0); return true; }); - const probe = vi.fn(async () => undefined); - try { - const adapter = createConcreteDiagnosticAdapters({ sshSpawn: vi.fn(() => child), reserveLoopbackPort: async () => 45432 } as any); - const running = adapter.withSshTunnel({ sshHost: "bastion.example.test", sshPort: 22, sshUser: "tunnel", privateKeyFile, knownHostsFile: "/run/secrets/known-hosts", targetHost: "dwh.internal", targetPort: 5432, localHost: "127.0.0.1", localPort: 0, timeoutMs: 500, signal: new AbortController().signal }, probe); - for (let attempt = 0; attempt < 20 && stderr.listenerCount("data") === 0; attempt += 1) { - await new Promise((resolve) => setTimeout(resolve, 1)); - } - expect(stderr.listenerCount("data")).toBeGreaterThan(0); - expect(probe).not.toHaveBeenCalled(); - stderr.emit("data", "debug1: Local forwarding listening on 127.0.0.1 port 45432.\n"); - await running; - expect(probe).toHaveBeenCalledOnce(); - } finally { - await rm(directory, { recursive: true, force: true }); - } -}); - -test("accepts an SSH forward confirmation split across stderr chunks", async () => { - const directory = await mkdtemp(join(tmpdir(), "thothii-diagnostic-")); - const privateKeyFile = join(directory, "ssh-key"); - await writeFile(privateKeyFile, "test-key\n", { mode: 0o600 }); - const stderr = new EventEmitter(); - const child = Object.assign(new EventEmitter(), { kill: vi.fn(() => true), stderr }); - child.kill.mockImplementation(() => { child.emit("exit", 0); return true; }); - const probe = vi.fn(async () => undefined); - try { - const adapter = createConcreteDiagnosticAdapters({ sshSpawn: vi.fn(() => child), reserveLoopbackPort: async () => 45432 } as any); - const running = adapter.withSshTunnel({ sshHost: "bastion.example.test", sshPort: 22, sshUser: "tunnel", privateKeyFile, knownHostsFile: "/run/secrets/known-hosts", targetHost: "dwh.internal", targetPort: 5432, localHost: "127.0.0.1", localPort: 0, timeoutMs: 500, signal: new AbortController().signal }, probe); - for (let attempt = 0; attempt < 20 && stderr.listenerCount("data") === 0; attempt += 1) await new Promise((resolve) => setTimeout(resolve, 1)); - stderr.emit("data", "debug1: Local forwarding listening on 127.0.0.1 "); - stderr.emit("data", "port 45432.\n"); - await running; - expect(probe).toHaveBeenCalledOnce(); - } finally { - await rm(directory, { recursive: true, force: true }); - } -}); - -test("requires a matching embedding model vector and removes its unique write probe", async () => { - const adapters = successfulAdapters(); - - const result = await diagnose(adapters)(writerWorkspace, writerBindings, { writeProbe: true }); - - expect(result.activatable).toBe(true); - expect(adapters.probeEmbedding).toHaveBeenCalledWith(expect.objectContaining({ - model: "nomic-embed-text-v2-moe", - timeoutMs: 5000, - })); - expect(adapters.writeDiagnosticRecord).toHaveBeenCalledWith(expect.objectContaining({ - collection: "clinical_documents", - id: expect.stringMatching(/^diagnostic:/), - dimensions: 768, - })); - expect(adapters.removeDiagnosticRecord).toHaveBeenCalledWith(expect.objectContaining({ - collection: "clinical_documents", - id: expect.stringMatching(/^diagnostic:/), - })); -}); - -test("passes the resolver's distinct vector-writer binding to the diagnoser", async () => { - const directory = await mkdtemp(join(tmpdir(), "thothii-diagnostic-bindings-")); - const readerKey = join(directory, "reader-key"); - const writerKey = join(directory, "writer-key"); - const dwhKey = join(directory, "dwh-key"); - await Promise.all([ - writeFile(readerKey, "reader\n", { mode: 0o600 }), - writeFile(writerKey, "writer\n", { mode: 0o600 }), - writeFile(dwhKey, "dwh\n", { mode: 0o600 }), - ]); - const adapters = successfulAdapters(); - try { - const resolved = resolveRuntimeBindings(writerWorkspace, { - THT_WS_PSD_CLINICAL_DWH_TRANSPORT: "rest_api", + const result = await diagnose(adapters)(restWorkspace, { + dwh: { transport: "rest_api", missing: [], values: { THT_WS_PSD_CLINICAL_DWH_BASE_URL: "https://dwh.example.test", - THT_WS_PSD_CLINICAL_DWH_API_KEY_FILE: dwhKey, - THT_WS_PSD_CLINICAL_VECTOR_TRANSPORT: "rest_api", - THT_WS_PSD_CLINICAL_VECTOR_BASE_URL: "https://vector.example.test", - THT_WS_PSD_CLINICAL_VECTOR_API_KEY_FILE: readerKey, - THT_WS_PSD_CLINICAL_VECTOR_WRITER_API_KEY_FILE: writerKey, - THT_WS_PSD_CLINICAL_EMBEDDING_BASE_URL: "https://embedding.example.test", - }, [directory]); - - await diagnose(adapters)(writerWorkspace, resolved, { writeProbe: true }); - - const canonicalWriterKey = await realpath(writerKey); - expect(resolved.vectorWriter.values).toEqual({ - THT_WS_PSD_CLINICAL_VECTOR_WRITER_API_KEY_FILE: canonicalWriterKey, - }); - expect(adapters.writeDiagnosticRecord).toHaveBeenCalledWith( - expect.objectContaining({ credentialFile: canonicalWriterKey }), - ); - } finally { - await rm(directory, { recursive: true, force: true }); - } -}); - -test("keeps a reader-only workspace activatable without a vector write probe", async () => { - const adapters = successfulAdapters(); - - const result = await diagnose(adapters)(workspace, bindings, { writeProbe: true }); - + THT_WS_PSD_CLINICAL_DWH_API_KEY_FILE: "/run/secrets/dwh-api-key", + } }, + evidence: { missing: [], values: {} }, + }, { writeProbe: false }); expect(result.activatable).toBe(true); - expect(adapters.writeDiagnosticRecord).not.toHaveBeenCalled(); - expect(adapters.removeDiagnosticRecord).not.toHaveBeenCalled(); -}); - -test("does not substitute the reader credential for a declared vector writer", async () => { - const adapters = successfulAdapters(); - - const result = await diagnose(adapters)(writerWorkspace, bindings, { writeProbe: true }); - - expect(result.activatable).toBe(true); - expect(adapters.writeDiagnosticRecord).not.toHaveBeenCalled(); -}); - -test("rejects a writer credential that aliases the reader credential", async () => { - const directory = await mkdtemp(join(tmpdir(), "thothii-diagnostic-")); - const readerKey = join(directory, "reader-key"); - const writerAlias = join(directory, "writer-key"); - await writeFile(readerKey, "same-secret\n", { mode: 0o600 }); - await (await import("node:fs/promises")).symlink(readerKey, writerAlias); - const adapters = successfulAdapters(); - const aliasedBindings: RuntimeBindings = { - ...writerBindings, - vector: { ...writerBindings.vector, values: { ...writerBindings.vector.values, THT_WS_PSD_CLINICAL_VECTOR_API_KEY_FILE: readerKey } }, - vectorWriter: { ...writerBindings.vectorWriter, values: { THT_WS_PSD_CLINICAL_VECTOR_WRITER_API_KEY_FILE: writerAlias } }, - }; - - try { - const result = await diagnose(adapters)(writerWorkspace, aliasedBindings, { writeProbe: true }); - expect(result.activatable).toBe(false); - expect(adapters.writeDiagnosticRecord).not.toHaveBeenCalled(); - } finally { - await rm(directory, { recursive: true, force: true }); - } -}); - -test("uses the declared POST DWH ping endpoint without exposing its local credential", async () => { - const directory = await mkdtemp(join(tmpdir(), "thothii-diagnostic-")); - const credentialFile = join(directory, "dwh-api-key"); - await writeFile(credentialFile, "local-secret\n", { mode: 0o600 }); - const fetchSpy = vi.fn(async () => new Response(JSON.stringify({ database: "warehouse", schema: "datawarehouse" }), { - status: 200, - headers: { "content-type": "application/json" }, + expect(adapters.probeConnector).toHaveBeenCalledWith(expect.objectContaining({ + transport: "rest_api", + baseUrl: "https://dwh.example.test", + credentialFile: "/run/secrets/dwh-api-key", + diagnostic: expect.objectContaining({ path: "/rpc/ping", auth: "bearer" }), })); - vi.stubGlobal("fetch", fetchSpy); - - try { - const result = await createConcreteDiagnosticAdapters().probeConnector({ - role: "dwh", - transport: "rest_api", - baseUrl: "https://dwh.example.test", - credentialFile, - resource: { database: "warehouse", schema: "datawarehouse" }, - diagnostic: { - method: "POST", path: "/rpc/ping", auth: "bearer", - response: { database: "database", schema: "schema" }, - }, - timeoutMs: 5000, - signal: new AbortController().signal, - }); - - expect(fetchSpy).toHaveBeenCalledWith("https://dwh.example.test/rpc/ping", expect.objectContaining({ - method: "POST", - redirect: "error", - })); - expect(result).toMatchObject({ authenticated: true, resource: { database: "warehouse", schema: "datawarehouse" } }); - expect(JSON.stringify(result)).not.toContain("local-secret"); - } finally { - vi.unstubAllGlobals(); - await rm(directory, { recursive: true, force: true }); - } }); -test("requires an authenticated TLS database query before direct diagnostics succeed", async () => { - const directory = await mkdtemp(join(tmpdir(), "thothii-diagnostic-")); - const passwordFile = join(directory, "password"); - const caFile = join(directory, "ca.pem"); - await Promise.all([writeFile(passwordFile, "password\n", { mode: 0o600 }), writeFile(caFile, "test-ca\n")]); - const query = vi.fn(async () => ({ rows: [{ database: "warehouse", schema: "datawarehouse" }] })); - const end = vi.fn(async () => undefined); - const connect = vi.fn(async () => ({ query, end })); - - try { - const result = await createConcreteDiagnosticAdapters({ databaseClient: { connect } } as any).probeConnector({ - role: "dwh", transport: "postgres_direct", host: "127.0.0.1", port: 5432, user: "reader", - credentialFile: passwordFile, tlsCaFile: caFile, - resource: { database: "warehouse", schema: "datawarehouse" }, timeoutMs: 5000, - signal: new AbortController().signal, - }); - - expect(connect).toHaveBeenCalledWith(expect.objectContaining({ database: "warehouse", tlsCaFile: caFile })); - expect(query).toHaveBeenCalledWith(expect.stringContaining("current_database"), []); - expect(end).toHaveBeenCalledOnce(); - expect(result).toMatchObject({ authenticated: true, tlsVerified: true }); - } finally { - await rm(directory, { recursive: true, force: true }); - } -}); - -test("uses system trust for direct and SSH PostgreSQL diagnostics when no CA binding exists", async () => { - const directory = await mkdtemp(join(tmpdir(), "thothii-diagnostic-")); - const passwordFile = join(directory, "password"); - await writeFile(passwordFile, "password\n", { mode: 0o600 }); - const query = vi.fn(async () => ({ rows: [{ database: "warehouse", schema: "datawarehouse" }] })); - const connect = vi.fn(async () => ({ query, end: vi.fn(async () => undefined) })); - const adapter = createConcreteDiagnosticAdapters({ databaseClient: { connect } } as any); - try { - for (const transport of ["postgres_direct", "ssh_tunnel"] as const) { - await expect(adapter.probeConnector({ - role: "dwh", transport, host: "127.0.0.1", port: 5432, user: "reader", credentialFile: passwordFile, - resource: { database: "warehouse", schema: "datawarehouse" }, timeoutMs: 5000, - signal: new AbortController().signal, - })).resolves.toMatchObject({ tlsVerified: true, authenticated: true }); - } - expect(connect).toHaveBeenCalledTimes(2); - expect(connect).toHaveBeenNthCalledWith(1, expect.objectContaining({ tlsCaFile: undefined })); - expect(connect).toHaveBeenNthCalledWith(2, expect.objectContaining({ tlsCaFile: undefined })); - } finally { - await rm(directory, { recursive: true, force: true }); - } -}); - -test("passes the original target hostname to the PostgreSQL TLS client", async () => { - const directory = await mkdtemp(join(tmpdir(), "thothii-diagnostic-")); - const passwordFile = join(directory, "password"); - await writeFile(passwordFile, "password\n", { mode: 0o600 }); - const connect = vi.fn(async () => ({ - query: vi.fn(async () => ({ rows: [{ database: "warehouse", schema: "datawarehouse" }] })), - end: vi.fn(async () => undefined), - })); - try { - await createConcreteDiagnosticAdapters({ databaseClient: { connect } } as any).probeConnector({ - role: "dwh", transport: "ssh_tunnel", host: "127.0.0.1", port: 5432, user: "reader", - credentialFile: passwordFile, tlsServername: "dwh.internal", - resource: { database: "warehouse", schema: "datawarehouse" }, timeoutMs: 5000, - signal: new AbortController().signal, - }); - - expect(connect).toHaveBeenCalledWith(expect.objectContaining({ - host: "127.0.0.1", - tlsServername: "dwh.internal", - })); - } finally { - await rm(directory, { recursive: true, force: true }); - } -}); - -test("selects the vector index containing the declared vector column for direct metadata", async () => { - const directory = await mkdtemp(join(tmpdir(), "thothii-diagnostic-")); - const passwordFile = join(directory, "password"); - const caFile = join(directory, "ca.pem"); - await Promise.all([writeFile(passwordFile, "password\n"), writeFile(caFile, "test-ca\n")]); - const query = vi.fn(async () => ({ rows: [{ dimensions: 768, distance: "cosine" }] })); - const connect = vi.fn(async () => ({ query, end: vi.fn(async () => undefined) })); - try { - const result = await createConcreteDiagnosticAdapters({ databaseClient: { connect } } as any).inspectVector({ - transport: "pgvector_direct", host: "127.0.0.1", port: 5432, user: "reader", credentialFile: passwordFile, tlsCaFile: caFile, - resource: { database: "postgres", schema: "vectors" }, collection: "clinical_documents", timeoutMs: 5000, signal: new AbortController().signal, - }); - expect(query).toHaveBeenCalledWith(expect.stringContaining("a.attnum = ANY(i.indkey)"), ["vectors", "clinical_documents"]); - expect(result).toMatchObject({ dimensions: 768, distance: "cosine" }); - } finally { - await rm(directory, { recursive: true, force: true }); - } -}); - -test("honors a declared unauthenticated REST diagnostic without reading a credential", async () => { - const fetchSpy = vi.fn(async () => new Response(JSON.stringify({ database: "warehouse", schema: "datawarehouse" }), { - status: 200, headers: { "content-type": "application/json" }, - })); - vi.stubGlobal("fetch", fetchSpy); - try { - await expect(createConcreteDiagnosticAdapters().probeConnector({ - role: "dwh", transport: "rest_api", baseUrl: "https://dwh.example.test", - resource: { database: "warehouse", schema: "datawarehouse" }, timeoutMs: 5000, - signal: new AbortController().signal, - diagnostic: { method: "POST", path: "/rpc/ping", auth: "none", response: { database: "database", schema: "schema" } } as any, - })).resolves.toMatchObject({ authenticated: true }); - expect(fetchSpy.mock.calls[0]?.[1]).not.toMatchObject({ headers: expect.objectContaining({ authorization: expect.anything() }) }); - } finally { - vi.unstubAllGlobals(); - } -}); - -test("applies declared auth modes and rejects private CA files across vector REST paths", async () => { - const directory = await mkdtemp(join(tmpdir(), "thothii-diagnostic-")); - const keyFile = join(directory, "api-key"); - const caFile = join(directory, "ca.pem"); - await Promise.all([writeFile(keyFile, "writer-key\n", { mode: 0o600 }), writeFile(caFile, "private-ca\n")]); - const fetchSpy = vi.fn(async () => new Response(JSON.stringify({ collection: "clinical_documents", dimensions: 768, distance: "cosine", model: "embed", operation: "create" }), { status: 200, headers: { "content-type": "application/json" } })); - vi.stubGlobal("fetch", fetchSpy); - const adapter = createConcreteDiagnosticAdapters(); - const signal = new AbortController().signal; - try { - await adapter.inspectVector({ transport: "rest_api", baseUrl: "https://vector.example.test", collection: "clinical_documents", timeoutMs: 1, signal, diagnostic: { method: "GET", path: "/metadata", auth: "none", response: { collection: "collection", dimensions: "dimensions", distance: "distance" } } }); - await adapter.probeEmbedding({ baseUrl: "https://embed.example.test", model: "embed", timeoutMs: 1, signal, credentialFile: keyFile, diagnostic: { method: "POST", path: "/embed", auth: "x-api-key", response: { model: "model", dimensions: "dimensions" } } }); - expect(fetchSpy.mock.calls[0]?.[1]).toMatchObject({ headers: {} }); - expect(fetchSpy.mock.calls[1]?.[1]).toMatchObject({ headers: { "x-api-key": "writer-key" } }); - await expect(adapter.inspectVector({ transport: "rest_api", baseUrl: "https://vector.example.test", credentialFile: keyFile, tlsCaFile: caFile, collection: "clinical_documents", timeoutMs: 1, signal, diagnostic: { method: "GET", path: "/metadata", auth: "bearer", response: { collection: "collection", dimensions: "dimensions", distance: "distance" } } })).rejects.toThrow("vector metadata adapter is unavailable"); - await expect(adapter.probeEmbedding({ baseUrl: "https://embed.example.test", credentialFile: keyFile, tlsCaFile: caFile, model: "embed", timeoutMs: 1, signal, diagnostic: { method: "POST", path: "/embed", auth: "bearer", response: { model: "model", dimensions: "dimensions" } } })).rejects.toThrow("embedding probe failed"); - await expect(adapter.removeDiagnosticRecord({ baseUrl: "https://vector.example.test", credentialFile: keyFile, tlsCaFile: caFile, collection: "clinical_documents", id: "diagnostic:test", dimensions: 768, timeoutMs: 1, signal, diagnostic: { method: "POST", path: "/probe", auth: "bearer", response: { operation: "operation" } } })).rejects.toThrow("vector write adapter is unavailable"); - } finally { - vi.unstubAllGlobals(); - await rm(directory, { recursive: true, force: true }); - } -}); - -test("validates that the reversible writer response confirms each requested operation", async () => { - const directory = await mkdtemp(join(tmpdir(), "thothii-diagnostic-")); - const keyFile = join(directory, "writer-key"); - await writeFile(keyFile, "writer\n", { mode: 0o600 }); - const fetchSpy = vi.fn(async (_url: string, init: RequestInit) => new Response(JSON.stringify({ - operation: JSON.parse(String(init.body)).operation === "create" ? "create" : "not-removed", - }), { status: 200, headers: { "content-type": "application/json" } })); - vi.stubGlobal("fetch", fetchSpy); - const request = { - baseUrl: "https://vector.example.test", credentialFile: keyFile, collection: "clinical_documents", - dimensions: 768, id: "diagnostic:test", timeoutMs: 5000, signal: new AbortController().signal, - diagnostic: { method: "POST" as const, path: "/probe", auth: "bearer" as const, response: { operation: "operation" } }, - }; - try { - const adapter = createConcreteDiagnosticAdapters(); - await expect(adapter.writeDiagnosticRecord(request)).resolves.toBeUndefined(); - await expect(adapter.removeDiagnosticRecord(request)).rejects.toThrow("vector write adapter is unavailable"); - } finally { - vi.unstubAllGlobals(); - await rm(directory, { recursive: true, force: true }); - } -}); - -test("constructs the production diagnoser with the configured timeout and injected adapters", async () => { +test("constructs the production diagnoser with its bounded configured timeout", async () => { const adapters = successfulAdapters(); - - const result = await createProductionWorkspaceDiagnoser(1234, adapters)(workspace, bindings, { + await createProductionWorkspaceDiagnoser(1_234, adapters)(workspace, bindings, { writeProbe: false, }); - - expect(result.activatable).toBe(true); - expect(adapters.probeConnector).toHaveBeenCalledWith(expect.objectContaining({ timeoutMs: 1234 })); - expect(adapters.probeEmbedding).toHaveBeenCalledWith(expect.objectContaining({ timeoutMs: 1234 })); + expect(adapters.probeConnector).toHaveBeenCalledWith(expect.objectContaining({ timeoutMs: 1_234 })); + expect(adapters.inspectQdrant).toHaveBeenCalledWith(expect.objectContaining({ timeoutMs: 1_234 })); }); -test("diagnoses a schema-v3 workspace through internal Qdrant and embedding config without workspace semantic bindings", async () => { +test("concrete DWH direct diagnostics authenticate, verify resource identity, and close", async () => { + const root = await mkdtemp(join(tmpdir(), "thoth-diagnostic-")); + const passwordFile = join(root, "password"); + await writeFile(passwordFile, "password-value"); + const end = vi.fn(async () => undefined); + const connect = vi.fn(async () => ({ + query: vi.fn(async () => ({ rows: [{ database: "warehouse", schema: "datawarehouse" }] })), + end, + })); + try { + const adapter = createConcreteDiagnosticAdapters({ databaseClient: { connect } }); + const result = await adapter.probeConnector({ + role: "dwh", transport: "postgres_direct", host: "127.0.0.1", port: 5432, + user: "reader", credentialFile: passwordFile, + resource: { database: "warehouse", schema: "datawarehouse" }, + timeoutMs: 1_000, signal: new AbortController().signal, + }); + expect(result).toMatchObject({ resolved: true, authenticated: true, tlsVerified: true }); + expect(connect).toHaveBeenCalledWith(expect.objectContaining({ credentialFile: passwordFile })); + expect(end).toHaveBeenCalledOnce(); + } finally { + await rm(root, { recursive: true, force: true }); + } +}); + +test("concrete DWH REST diagnostics honor auth-none without reading credentials", async () => { + const fetchMock = vi.fn(async () => new Response(JSON.stringify({ + database: "warehouse", schema: "datawarehouse", + }), { status: 200 })); + vi.stubGlobal("fetch", fetchMock); + const result = await createConcreteDiagnosticAdapters().probeConnector({ + role: "dwh", transport: "rest_api", baseUrl: "https://dwh.example.test", + resource: { database: "warehouse", schema: "datawarehouse" }, + timeoutMs: 1_000, signal: new AbortController().signal, + diagnostic: { + method: "GET", path: "/health", auth: "none", + response: { database: "database", schema: "schema" }, + }, + }); + expect(result).toMatchObject({ resolved: true, tlsVerified: true, authenticated: true }); + expect(fetchMock).toHaveBeenCalledWith("https://dwh.example.test/health", expect.objectContaining({ + headers: {}, redirect: "error", + })); +}); + +test("concrete internal semantic diagnostics use only Qdrant and Ollama protocols", async () => { + const fetchMock = vi.fn() + .mockResolvedValueOnce(new Response(JSON.stringify({ + result: { config: { params: { vectors: { size: 1024, distance: "Cosine" } } } }, + }), { status: 200 })) + .mockResolvedValueOnce(new Response(JSON.stringify({ embeddings: [Array(1024).fill(0)] }), { + status: 200, + })); + vi.stubGlobal("fetch", fetchMock); + const adapter = createConcreteDiagnosticAdapters(); + await expect(adapter.inspectQdrant({ + baseUrl: "http://qdrant:6333", collection: "psd-clinical", + timeoutMs: 1_000, signal: new AbortController().signal, + })).resolves.toEqual({ collection: "psd-clinical", dimensions: 1024, distance: "cosine" }); + await expect(adapter.probeEmbedding({ + baseUrl: "http://embedding:11434", model: "qwen3-embedding:0.6b", + timeoutMs: 1_000, signal: new AbortController().signal, + })).resolves.toEqual({ available: true, dimensions: 1024 }); + expect(fetchMock.mock.calls[0][0]).toBe("http://qdrant:6333/collections/psd-clinical"); + expect(fetchMock.mock.calls[1][0]).toBe("http://embedding:11434/api/embed"); + expect(JSON.parse(fetchMock.mock.calls[1][1].body)).toEqual({ + model: "qwen3-embedding:0.6b", input: "diagnostic", + }); + expect(Object.keys(adapter).sort()).toEqual(["inspectQdrant", "probeConnector", "probeEmbedding"]); +}); + +test("preserves a configured production timeout above the default up to the global maximum", async () => { + const adapters = successfulAdapters(); + await createProductionWorkspaceDiagnoser(8_000, adapters)(workspace, bindings, { + writeProbe: false, + }); + expect(adapters.probeConnector).toHaveBeenCalledWith(expect.objectContaining({ timeoutMs: 8_000 })); + expect(adapters.inspectQdrant).toHaveBeenCalledWith(expect.objectContaining({ timeoutMs: 8_000 })); + expect(adapters.probeEmbedding).toHaveBeenCalledWith(expect.objectContaining({ timeoutMs: 8_000 })); +}); + +test("bounds descriptor DWH timeout by the configured production timeout", async () => { + const adapters = successfulAdapters(); + await createProductionWorkspaceDiagnoser(10_000, adapters)(workspace, bindings, { + writeProbe: false, + }); + expect(adapters.probeConnector).toHaveBeenCalledWith(expect.objectContaining({ timeoutMs: 8_000 })); + expect(adapters.inspectQdrant).toHaveBeenCalledWith(expect.objectContaining({ timeoutMs: 10_000 })); + expect(adapters.probeEmbedding).toHaveBeenCalledWith(expect.objectContaining({ timeoutMs: 10_000 })); +}); + +test("aborts a timed-out production diagnostic and returns only a sanitized connector code", async () => { + let aborted = false; const adapters = successfulAdapters({ - inspectVector: vi.fn(async () => ({ - collection: "psd-clinical", - dimensions: 1024, - distance: "cosine", + probeConnector: vi.fn((request) => new Promise((_resolve, reject) => { + request.signal.addEventListener("abort", () => { + aborted = true; + reject(new Error("CANARY-TIMEOUT-SECRET")); + }, { once: true }); })), - probeEmbedding: vi.fn(async () => ({ available: true, dimensions: 1024 })), }); - - const result = await createProductionWorkspaceDiagnoser(1234, adapters, { - internalQdrantUrl: "http://qdrant:6333", - internalEmbeddingUrl: "http://embedding:11434", - internalEmbeddingModel: "qwen3-embedding:0.6b", - internalEmbeddingDimensions: 1024, - })(workspaceV3, bindingsV3, { writeProbe: false }); - - expect(result.activatable).toBe(true); - expect(adapters.inspectVector).toHaveBeenCalledWith(expect.objectContaining({ - transport: "rest_api", - baseUrl: "http://qdrant:6333", - collection: "psd-clinical", - dimensions: 1024, - distance: "cosine", - timeoutMs: 1234, - })); - expect(adapters.probeEmbedding).toHaveBeenCalledWith(expect.objectContaining({ - baseUrl: "http://embedding:11434", - model: "qwen3-embedding:0.6b", - timeoutMs: 1234, - })); - expect(adapters.probeConnector).toHaveBeenCalledTimes(1); -}); - -test("fails closed for schema-v3 when internal semantic diagnostics do not match descriptor identity", async () => { - const adapters = successfulAdapters({ - inspectVector: vi.fn(async () => ({ - collection: "wrong-collection", - dimensions: 1024, - distance: "cosine", - })), - probeEmbedding: vi.fn(async () => ({ available: true, dimensions: 1024 })), + const result = await createProductionWorkspaceDiagnoser(5, adapters)(workspace, bindings, { + writeProbe: false, }); - - const result = await createProductionWorkspaceDiagnoser(1234, adapters, { - internalQdrantUrl: "http://qdrant:6333", - internalEmbeddingUrl: "http://embedding:11434", - internalEmbeddingModel: "qwen3-embedding:0.6b", - internalEmbeddingDimensions: 1024, - })(workspaceV3, bindingsV3, { writeProbe: false }); - + expect(aborted).toBe(true); expect(result.activatable).toBe(false); - expect(result.diagnostics).toContainEqual(expect.objectContaining({ - code: "semantic_index_incompatible", - })); + expect(result.diagnostics).toContainEqual(expect.objectContaining({ code: "connector_unavailable" })); + expect(JSON.stringify(result)).not.toContain("CANARY-TIMEOUT-SECRET"); }); -test("retries bounded cleanup after a write-probe removal times out", async () => { - const adapters = successfulAdapters({ - removeDiagnosticRecord: vi.fn(() => new Promise(() => undefined)), - }); - const diagnoseWithShortTimeout = createWorkspaceDiagnoser(adapters, { timeoutMs: 10 }); - - const startedAt = Date.now(); - const result = await diagnoseWithShortTimeout(writerWorkspace, writerBindings, { writeProbe: true }); - - expect(Date.now() - startedAt).toBeLessThan(250); - expect(adapters.writeDiagnosticRecord).toHaveBeenCalledTimes(1); - expect(adapters.removeDiagnosticRecord).toHaveBeenCalledTimes(2); - expect(result).toMatchObject({ activatable: false }); - expect(JSON.stringify(result)).not.toContain("timeout"); -}); - -test("attempts bounded cleanup when a timed-out write may already have created the record", async () => { - const adapters = successfulAdapters({ - writeDiagnosticRecord: vi.fn(() => new Promise(() => undefined)), - }); - const diagnoseWithShortTimeout = createWorkspaceDiagnoser(adapters, { timeoutMs: 10 }); - - const result = await diagnoseWithShortTimeout(writerWorkspace, writerBindings, { writeProbe: true }); - - expect(adapters.writeDiagnosticRecord).toHaveBeenCalledOnce(); - expect(adapters.removeDiagnosticRecord).toHaveBeenCalledOnce(); - expect(result.activatable).toBe(false); -}); - - test.each([ - { - source: { - type: "http", uris: ["https://evidence.example.test/guide.md"], - authentication: "signed_urls_file", - }, - field: "evidence.source.authentication", - variable: "THT_WS_PSD_CLINICAL_EVIDENCE_SIGNED_URLS_FILE", - }, - { - source: { type: "s3", uri: "s3://clinical-evidence/published/", credentials: "static_files" }, - field: "evidence.source.credentials", - variable: "THT_WS_PSD_CLINICAL_EVIDENCE_ACCESS_KEY_FILE", - }, -])("reports sanitized v3 Evidence binding diagnostics for $field", async ({ source, field, variable }) => { - const descriptor = parseWorkspaceYaml(`${renderEvidenceWorkspace()}evidence:\n source: ${JSON.stringify(source)}\n`); - const resolved: RuntimeBindings = { - ...resolveRuntimeBindings(descriptor, { - [variable]: "CANARY-UNSAFE-RELATIVE-PATH", - }, ["/run/secrets"]), - dwh: bindings.dwh, - }; - const result = await createProductionWorkspaceDiagnoser(5_000)(descriptor, resolved, { writeProbe: false }); - - expect(result).toEqual({ - activatable: false, - diagnostics: expect.arrayContaining([expect.objectContaining({ - code: "binding_missing", field, variable, - })]), - }); - expect(JSON.stringify(result)).not.toContain("CANARY-UNSAFE-RELATIVE-PATH"); + ["rejection", () => Promise.reject(new Error("CANARY-CONNECTOR-SECRET"))], + ["unresolved connector", async (request: Parameters[0]) => ({ + resolved: false, tlsVerified: true, authenticated: true, resource: request.resource, + })], + ["wrong resource", async () => ({ + resolved: true, tlsVerified: true, authenticated: true, + resource: { database: "other", schema: "datawarehouse" }, + })], + ["failed TLS", async (request: Parameters[0]) => ({ + resolved: true, tlsVerified: false, authenticated: true, resource: request.resource, + })], + ["failed authentication", async (request: Parameters[0]) => ({ + resolved: true, tlsVerified: true, authenticated: false, resource: request.resource, + })], +] as const)("sanitizes DWH connector %s", async (_label, probeConnector) => { + const result = await diagnose(successfulAdapters({ probeConnector: vi.fn(probeConnector) }))( + workspace, bindings, { writeProbe: false }, + ); + expect(result.activatable).toBe(false); + expect(result.diagnostics).toContainEqual(expect.objectContaining({ code: "connector_unavailable" })); + expect(JSON.stringify(result)).not.toContain("CANARY-CONNECTOR-SECRET"); }); -function renderEvidenceWorkspace(): string { - return `workspace: +test("uses a REST secret only as a header and redacts it from failed diagnostics", async () => { + const root = await mkdtemp(join(tmpdir(), "thoth-rest-diagnostic-")); + const credentialFile = join(root, "api-key"); + const canary = "CANARY-REST-AUTH-SECRET"; + await writeFile(credentialFile, canary); + const restDescriptor = parseWorkspaceYaml(`workspace: schema_version: 3 id: psd-clinical - name: Policlinico San Donato - language: it + name: REST auth + language: en dwh: engine: postgres database: warehouse schema: datawarehouse - supported_transports: [postgres_direct] + supported_transports: [rest_api] semantic_index: vector_store: { engine: qdrant, collection: psd-clinical, dimensions: 1024, distance: cosine } embedding: { provider: ollama_internal, model: qwen3-embedding:0.6b, dimensions: 1024 } +diagnostics: + dwh_rest: + method: GET + path: /health + auth: bearer + response: { database: database, schema: schema } llm_policy: { allowed: [zai/glm-5.2] } -`; -} +`); + const fetchMock = vi.fn() + .mockResolvedValueOnce(new Response("upstream CANARY-REST-AUTH-SECRET", { status: 503 })) + .mockResolvedValueOnce(new Response(JSON.stringify({ + result: { config: { params: { vectors: { size: 1024, distance: "Cosine" } } } }, + }), { status: 200 })) + .mockResolvedValueOnce(new Response(JSON.stringify({ embeddings: [Array(1024).fill(0)] }), { + status: 200, + })); + vi.stubGlobal("fetch", fetchMock); + try { + const result = await createWorkspaceDiagnoser(createConcreteDiagnosticAdapters())( + restDescriptor, + { + dwh: { transport: "rest_api", missing: [], values: { + THT_WS_PSD_CLINICAL_DWH_BASE_URL: "https://dwh.example.test", + THT_WS_PSD_CLINICAL_DWH_API_KEY_FILE: credentialFile, + } }, + evidence: { missing: [], values: {} }, + }, + { writeProbe: false }, + ); + expect(fetchMock.mock.calls[0][1].headers).toEqual({ authorization: `Bearer ${canary}` }); + expect(result.diagnostics).toContainEqual(expect.objectContaining({ code: "connector_unavailable" })); + expect(JSON.stringify(result)).not.toContain(canary); + } finally { + await rm(root, { recursive: true, force: true }); + } +}); + +test.each([ + ["Qdrant non-2xx", [ + new Response("CANARY-QDRANT-BODY", { status: 503 }), + new Response(JSON.stringify({ embeddings: [Array(1024).fill(0)] }), { status: 200 }), + ]], + ["Qdrant malformed", [ + new Response(JSON.stringify({ result: "CANARY-QDRANT-BODY" }), { status: 200 }), + new Response(JSON.stringify({ embeddings: [Array(1024).fill(0)] }), { status: 200 }), + ]], + ["Ollama non-2xx", [ + new Response(JSON.stringify({ result: { config: { params: { vectors: { + size: 1024, distance: "Cosine", + } } } } }), { status: 200 }), + new Response("CANARY-OLLAMA-BODY", { status: 503 }), + ]], + ["Ollama malformed", [ + new Response(JSON.stringify({ result: { config: { params: { vectors: { + size: 1024, distance: "Cosine", + } } } } }), { status: 200 }), + new Response(JSON.stringify({ embeddings: "CANARY-OLLAMA-BODY" }), { status: 200 }), + ]], +] as const)("sanitizes %s failures", async (_label, responses) => { + const fetchMock = vi.fn(); + for (const response of responses) fetchMock.mockResolvedValueOnce(response); + vi.stubGlobal("fetch", fetchMock); + const adapters = createConcreteDiagnosticAdapters({ + directProtocol: { + probe: async (request) => ({ + resolved: true, tlsVerified: true, authenticated: true, resource: request.resource, + }), + }, + }); + const result = await createWorkspaceDiagnoser(adapters)(workspace, bindings, { writeProbe: false }); + expect(result.activatable).toBe(false); + expect(result.diagnostics).toContainEqual(expect.objectContaining({ code: "connector_unavailable" })); + expect(JSON.stringify(result)).not.toMatch(/CANARY-(QDRANT|OLLAMA)-BODY/); +}); + +test("closes the concrete PostgreSQL diagnostic client when resource verification fails", async () => { + const root = await mkdtemp(join(tmpdir(), "thoth-diagnostic-close-")); + const passwordFile = join(root, "password"); + await writeFile(passwordFile, "CANARY-DATABASE-SECRET"); + const end = vi.fn(async () => undefined); + const connect = vi.fn(async () => ({ + query: vi.fn(async () => ({ rows: [{ database: "wrong", schema: "datawarehouse" }] })), + end, + })); + try { + const adapter = createConcreteDiagnosticAdapters({ databaseClient: { connect } }); + await expect(adapter.probeConnector({ + role: "dwh", transport: "postgres_direct", host: "127.0.0.1", port: 5432, + user: "reader", credentialFile: passwordFile, + resource: { database: "warehouse", schema: "datawarehouse" }, + timeoutMs: 1_000, signal: new AbortController().signal, + })).rejects.toThrow("direct probe failed"); + expect(end).toHaveBeenCalledOnce(); + } finally { + await rm(root, { recursive: true, force: true }); + } +}); + +test("returns observed normalized Qdrant distance for semantic mismatch classification", async () => { + vi.stubGlobal("fetch", vi.fn(async () => new Response(JSON.stringify({ + result: { config: { params: { vectors: { size: 1024, distance: "Euclid" } } } }, + }), { status: 200 }))); + await expect(createConcreteDiagnosticAdapters().inspectQdrant({ + baseUrl: "http://qdrant:6333", collection: "psd-clinical", + timeoutMs: 1_000, signal: new AbortController().signal, + })).resolves.toEqual({ collection: "psd-clinical", dimensions: 1024, distance: "euclid" }); +}); + + +test("classifies an observed non-cosine Qdrant distance as semantic incompatibility", async () => { + const fetchMock = vi.fn() + .mockResolvedValueOnce(new Response(JSON.stringify({ + result: { config: { params: { vectors: { size: 1024, distance: "Euclid" } } } }, + }), { status: 200 })) + .mockResolvedValueOnce(new Response(JSON.stringify({ embeddings: [Array(1024).fill(0)] }), { + status: 200, + })); + vi.stubGlobal("fetch", fetchMock); + const adapters = createConcreteDiagnosticAdapters({ + directProtocol: { + probe: async (request) => ({ + resolved: true, tlsVerified: true, authenticated: true, resource: request.resource, + }), + }, + }); + const result = await createWorkspaceDiagnoser(adapters)(workspace, bindings, { writeProbe: false }); + expect(result.activatable).toBe(false); + expect(result.diagnostics).toContainEqual(expect.objectContaining({ + code: "semantic_index_incompatible", + })); + expect(result.diagnostics).not.toContainEqual(expect.objectContaining({ + code: "connector_unavailable", + })); +}); diff --git a/backend/test/workspaces-runtime-v3-boundaries.test.ts b/backend/test/workspaces-runtime-v3-boundaries.test.ts new file mode 100644 index 00000000..022f9015 --- /dev/null +++ b/backend/test/workspaces-runtime-v3-boundaries.test.ts @@ -0,0 +1,70 @@ +import { expect, test, vi } from "vitest"; +import { buildInstallationContract } from "../src/workspaces/contracts.js"; +import { + createProductionWorkspaceDiagnoser, + createWorkspaceDiagnoser, + type DiagnosticAdapters, +} from "../src/workspaces/diagnostics.js"; +import { + resolveBinding, + resolveEvidenceBinding, + resolveRuntimeBindings, +} from "../src/workspaces/bindings.js"; +import { renderRuntimeConfig, type RuntimeBindings } from "../src/workspaces/runtime-renderer.js"; + +const unsupportedWorkspace = { + workspace: { schema_version: 2, id: "legacy-workspace", name: "Legacy", language: "en" }, + dwh: { + engine: "postgres", database: "warehouse", schema: "public", + supported_transports: ["postgres_direct"], + }, + semantic_index: { + vector_store: { + engine: "pgvector", database: "warehouse", schema: "vectors", + collection: "documents", dimensions: 768, distance: "cosine", + supported_transports: ["pgvector_direct"], + }, + embedding: { provider: "ollama_compatible", model: "legacy", dimensions: 768 }, + }, + llm_policy: { allowed: ["zai/glm-5.2"] }, +}; + +const bindings: RuntimeBindings = { + dwh: { transport: "postgres_direct", values: {}, missing: [] }, + evidence: { values: {}, missing: [] }, +}; + +const adapters: DiagnosticAdapters = { + probeConnector: vi.fn(), + inspectQdrant: vi.fn(), + probeEmbedding: vi.fn(), +}; + +test("renderer rejects callers that bypass the schema-v3 type contract", () => { + expect(() => renderRuntimeConfig(unsupportedWorkspace as never, bindings, { + sessions: "/data/sessions", artifacts: "/data/artifacts", indexes: "/data/indexes", + })).toThrow("Runtime renderer supports only workspace schema version 3"); +}); + +test("installation contract rejects callers that bypass the schema-v3 type contract", () => { + expect(() => buildInstallationContract(unsupportedWorkspace as never)) + .toThrow("Installation contract supports only workspace schema version 3"); +}); + +test("binding entry points reject callers that bypass the schema-v3 type contract", () => { + expect(() => resolveBinding(unsupportedWorkspace as never, "DWH", {}, [])) + .toThrow("Workspace bindings support only workspace schema version 3"); + expect(() => resolveEvidenceBinding(unsupportedWorkspace as never, {}, [])) + .toThrow("Workspace bindings support only workspace schema version 3"); + expect(() => resolveRuntimeBindings(unsupportedWorkspace as never, {}, [])) + .toThrow("Workspace bindings support only workspace schema version 3"); +}); + +test("diagnoser factories reject callers that bypass the schema-v3 type contract", async () => { + await expect(createWorkspaceDiagnoser(adapters)(unsupportedWorkspace as never, bindings, { + writeProbe: false, + })).rejects.toThrow("Workspace diagnoser supports only workspace schema version 3"); + await expect(createProductionWorkspaceDiagnoser(5_000, adapters)( + unsupportedWorkspace as never, bindings, { writeProbe: false }, + )).rejects.toThrow("Workspace diagnoser supports only workspace schema version 3"); +}); diff --git a/backend/test/workspaces-schema.test.ts b/backend/test/workspaces-schema.test.ts index 66ce210a..5db97411 100644 --- a/backend/test/workspaces-schema.test.ts +++ b/backend/test/workspaces-schema.test.ts @@ -548,39 +548,3 @@ test.each([ type: "filesystem", uri: "workspace-content/psd-clinical/evidence", }, { ...explicitPolicy, [field]: value }), new RegExp(field, "i")); }); - -test("rejects evidence on strict schema v1 and v2 descriptors", () => { - for (const schemaVersion of [1, 2]) { - const yaml = validYaml - .replace("schema_version: 3", `schema_version: ${schemaVersion}`) - .replace(`semantic_index: - vector_store: - engine: qdrant - collection: psd-clinical - dimensions: 1024 - distance: cosine - embedding: - provider: ollama_internal - model: qwen3-embedding:0.6b - dimensions: 1024`, `semantic_index: - vector_store: - engine: pgvector - database: postgres - schema: vectors - collection: documents - dimensions: 1024 - distance: cosine - supported_transports: - - pgvector_direct - embedding: - provider: ollama_compatible - model: evidence-test - dimensions: 1024`) - + `evidence: - source: - type: filesystem - uri: workspace-content/psd-clinical/evidence -`; - expect(() => parseWorkspaceYaml(yaml)).toThrow(/evidence|unrecognized/i); - } -});