From ff795d1c91770d738a82d9babee347837a64ba32 Mon Sep 17 00:00:00 2001 From: mptyl Date: Mon, 3 Aug 2026 22:52:29 +0200 Subject: [PATCH] feat: diagnose workspace connector bindings --- backend/src/config.ts | 12 + backend/src/workspaces/diagnostics.ts | 408 ++++++++++++++++++++ backend/test/workspaces-diagnostics.test.ts | 235 +++++++++++ docker/core.Dockerfile | 2 +- 4 files changed, 656 insertions(+), 1 deletion(-) create mode 100644 backend/src/workspaces/diagnostics.ts create mode 100644 backend/test/workspaces-diagnostics.test.ts diff --git a/backend/src/config.ts b/backend/src/config.ts index 84b8e006..2d207ef1 100644 --- a/backend/src/config.ts +++ b/backend/src/config.ts @@ -23,9 +23,12 @@ export interface AppConfig { * pay the probe; the local dev launcher (run-stack.sh) opts in via THT_DWH_PRECHECK. */ dwhPrecheck: boolean; + workspaceDiagnosticTimeoutMs: number; workspaceRegistry: WorkspaceRegistryConfig; } +export const MAX_WORKSPACE_DIAGNOSTIC_TIMEOUT_MS = 10_000; + function requiredRegistryValue(value: string, label: string): string { if (value.length === 0 || value.trim() !== value || value.includes("\0")) { throw new Error(`workspace registry ${label} configuration is invalid`); @@ -77,6 +80,14 @@ function positiveImportLimit(value: string | undefined, fallback: number): numbe return limit; } +function diagnosticTimeout(value: string | undefined): number { + const timeout = Number(value ?? 5_000); + if (!Number.isSafeInteger(timeout) || timeout < 1 || timeout > MAX_WORKSPACE_DIAGNOSTIC_TIMEOUT_MS) { + throw new Error("workspace diagnostic timeout configuration is invalid"); + } + return timeout; +} + export function loadConfig(env: Record): AppConfig { const authMode = env.AUTH_MODE ?? "none"; if (!(["none", "mock", "upstream"] as const).includes(authMode as AppConfig["authMode"])) { @@ -192,6 +203,7 @@ export function loadConfig(env: Record): AppConfig { secretFiles, modelApiKeyFile, dwhPrecheck: env.THT_DWH_PRECHECK === "true" || env.THT_DWH_PRECHECK === "1", + workspaceDiagnosticTimeoutMs: diagnosticTimeout(env.THT_WORKSPACE_DIAGNOSTIC_TIMEOUT_MS), workspaceRegistry, }; } diff --git a/backend/src/workspaces/diagnostics.ts b/backend/src/workspaces/diagnostics.ts new file mode 100644 index 00000000..a650306e --- /dev/null +++ b/backend/src/workspaces/diagnostics.ts @@ -0,0 +1,408 @@ +import { randomUUID } from "node:crypto"; +import { MAX_WORKSPACE_DIAGNOSTIC_TIMEOUT_MS } from "../config.js"; +import { buildInstallationContract } from "./contracts.js"; +import type { RuntimeBindings } from "./runtime-renderer.js"; +import { validateCanonicalWorkspace, type CanonicalWorkspace } from "./schema.js"; +import type { WorkspaceErrorCode } from "./types.js"; + +export interface Diagnostic { + level: "error" | "warning" | "info"; + code: WorkspaceErrorCode | "binding_ok"; + field?: string; + message: string; +} + +export interface WorkspaceDiagnostics { + activatable: boolean; + diagnostics: Diagnostic[]; +} + +type ConnectorRole = "dwh" | "vector"; + +interface DiagnosticResource { + database?: string; + schema?: string; + collection?: string; +} + +export interface ConnectorDiagnosticRequest { + role: ConnectorRole; + transport: "postgres_direct" | "pgvector_direct" | "rest_api" | "ssh_tunnel"; + host?: string; + port?: number; + baseUrl?: string; + user?: string; + credentialFile: string; + tlsCaFile?: string; + resource: DiagnosticResource; + timeoutMs: number; + signal: AbortSignal; +} + +export interface ConnectorDiagnosticResult { + resolved: boolean; + tlsVerified: boolean; + authenticated: boolean; + 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; + timeoutMs: number; + signal: AbortSignal; +} + +export interface LoopbackTunnel { + host: "127.0.0.1"; + port: number; +} + +export interface VectorDiagnosticRequest { + collection: string; + timeoutMs: number; + signal: AbortSignal; +} + +export interface VectorDiagnosticResult { + collection?: string; + dimensions?: number; + distance?: "cosine" | "l2" | "inner_product"; +} + +export interface EmbeddingDiagnosticRequest { + baseUrl: string; + credentialFile?: string; + tlsCaFile?: string; + model: string; + timeoutMs: number; + signal: AbortSignal; +} + +export interface EmbeddingDiagnosticResult { + available: boolean; + dimensions?: number; +} + +export interface WriteDiagnosticRecordRequest { + collection: string; + id: string; + dimensions: number; + timeoutMs: number; + signal: AbortSignal; +} + +/** + * 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. + */ +export interface DiagnosticAdapters { + probeConnector(request: ConnectorDiagnosticRequest): Promise; + withSshTunnel( + request: SshTunnelRequest, + probe: (tunnel: LoopbackTunnel) => Promise, + ): Promise; + inspectVector(request: VectorDiagnosticRequest): Promise; + probeEmbedding(request: EmbeddingDiagnosticRequest): Promise; + writeDiagnosticRecord(request: WriteDiagnosticRecordRequest): Promise; + removeDiagnosticRecord(request: WriteDiagnosticRecordRequest): Promise; +} + +export const DEFAULT_WORKSPACE_DIAGNOSTIC_TIMEOUT_MS = 5_000; + +function unavailableAdapters(): DiagnosticAdapters { + const unavailable = async (): Promise => { + throw new Error("diagnostic adapter unavailable"); + }; + return { + probeConnector: unavailable, + withSshTunnel: unavailable, + inspectVector: unavailable, + probeEmbedding: unavailable, + writeDiagnosticRecord: unavailable, + removeDiagnosticRecord: unavailable, + }; +} + +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); +} + +async function withTimeout(timeoutMs: number, operation: (signal: AbortSignal) => Promise): Promise { + const controller = new AbortController(); + let timer: NodeJS.Timeout | undefined; + try { + return await new Promise((resolve, reject) => { + timer = setTimeout(() => { + controller.abort(); + reject(new Error("diagnostic timed out")); + }, timeoutMs); + void operation(controller.signal).then(resolve, reject); + }); + } finally { + if (timer !== undefined) clearTimeout(timer); + controller.abort(); + } +} + +function sameResource(expected: DiagnosticResource, actual: DiagnosticResource): boolean { + 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 diagnosticError(code: WorkspaceErrorCode, field?: string): Diagnostic { + return { + level: "error", + code, + ...(field ? { field } : {}), + message: code === "binding_missing" + ? "Installation binding is missing or invalid." + : code === "semantic_index_incompatible" + ? "Semantic index metadata is incompatible with this workspace." + : "Connector diagnostic failed.", + }; +} + +function bindingName( + workspace: CanonicalWorkspace, + role: "DWH" | "VECTOR" | "EMBEDDING", + suffix: string, +): string { + const entry = buildInstallationContract(workspace).variables.find((variable) => ( + variable.role === role && variable.suffix === suffix + )); + if (!entry) throw new Error(`workspace contract is missing ${role}_${suffix}`); + return entry.name; +} + +function numericBinding(binding: Record, name: string): number | undefined { + const value = Number(binding[name]); + return Number.isInteger(value) && value > 0 && value <= 65_535 ? value : undefined; +} + +function diagnosticsForMissingBindings( + workspace: CanonicalWorkspace, + 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: CanonicalWorkspace, + 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 } + : { collection: workspace.semantic_index.vector_store.collection }; + const field = (suffix: string) => bindingName(workspace, contractRole, suffix); + const credentialFile = values[field(binding.transport === "rest_api" ? "API_KEY_FILE" : "PASSWORD_FILE")]; + if (credentialFile === undefined) return undefined; + + if (binding.transport === "rest_api") { + const baseUrl = values[field("BASE_URL")]; + if (baseUrl === undefined) return undefined; + return { + role, + transport: "rest_api", + baseUrl, + credentialFile, + tlsCaFile: values[field("TLS_CA_FILE")], + resource, + timeoutMs, + signal: new AbortController().signal, + }; + } + + 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 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: CanonicalWorkspace, + 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")], + resource: role === "dwh" + ? { database: workspace.dwh.database, schema: workspace.dwh.schema } + : { collection: workspace.semantic_index.vector_store.collection }, + timeoutMs, + signal, + }; +} + +export function createWorkspaceDiagnoser( + adapters: DiagnosticAdapters, + options: { timeoutMs?: number } = {}, +) { + const fallbackTimeout = boundedTimeout(options.timeoutMs, DEFAULT_WORKSPACE_DIAGNOSTIC_TIMEOUT_MS); + + return async function diagnoseWorkspace( + workspace: CanonicalWorkspace, + bindings: RuntimeBindings, + options: { writeProbe: boolean }, + ): Promise { + const canonical = validateCanonicalWorkspace(workspace); + 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); + + 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 }, + (tunnel) => adapters.probeConnector(tunnelProbeRequest( + canonical, role, bindings, timeoutMs, tunnel, signal, + )), + )) + : await withTimeout(timeoutMs, (signal) => adapters.probeConnector({ ...request, signal })); + const resource = role === "dwh" + ? { database: canonical.dwh.database, schema: canonical.dwh.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 vector = await withTimeout(vectorTimeout, (signal) => adapters.inspectVector({ + collection: canonical.semantic_index.vector_store.collection, + 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, + })); + 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 && !diagnostics.some((diagnostic) => diagnostic.level === "error")) { + 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, + }; + try { + await withTimeout(vectorTimeout, (signal) => adapters.writeDiagnosticRecord({ ...request, signal })); + await withTimeout(vectorTimeout, (signal) => adapters.removeDiagnosticRecord({ ...request, signal })); + } catch { + diagnostics.push(diagnosticError("connector_unavailable")); + } + } + + return { + activatable: !diagnostics.some((diagnostic) => diagnostic.level === "error"), + diagnostics, + }; + }; +} + +export const diagnoseWorkspace = createWorkspaceDiagnoser(unavailableAdapters()); diff --git a/backend/test/workspaces-diagnostics.test.ts b/backend/test/workspaces-diagnostics.test.ts new file mode 100644 index 00000000..27900d48 --- /dev/null +++ b/backend/test/workspaces-diagnostics.test.ts @@ -0,0 +1,235 @@ +import { expect, test, vi } from "vitest"; +import { + createWorkspaceDiagnoser, + type DiagnosticAdapters, +} from "../src/workspaces/diagnostics.js"; +import type { RuntimeBindings } from "../src/workspaces/runtime-renderer.js"; +import { parseWorkspaceYaml } from "../src/workspaces/schema.js"; + +const workspace = parseWorkspaceYaml(`workspace: + schema_version: 1 + 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 + 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] +`); + +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", + }, + }, + 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", + }, + }, +}; + +function successfulAdapters(overrides: Partial = {}): DiagnosticAdapters { + return { + probeConnector: vi.fn(async (request) => ({ + resolved: true, + tlsVerified: true, + 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, + distance: "cosine", + })), + probeEmbedding: vi.fn(async () => ({ available: true, dimensions: 768 })), + writeDiagnosticRecord: vi.fn(async () => undefined), + removeDiagnosticRecord: vi.fn(async () => undefined), + ...overrides, + }; +} + +function diagnose(adapters = successfulAdapters()) { + return createWorkspaceDiagnoser(adapters, { timeoutMs: 5000 }); +} + +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); +}); + +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", + }, + }, + }; + const adapters = successfulAdapters(); + + 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(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", + 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"); +}); + +test("uses a loopback-only SSH tunnel for the bounded connector probe", 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(true); + 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("requires a matching embedding model vector and removes its unique write probe", async () => { + const adapters = successfulAdapters(); + + const result = await diagnose(adapters)(workspace, bindings, { 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:/), + })); +}); diff --git a/docker/core.Dockerfile b/docker/core.Dockerfile index 7eb8be45..697d1a63 100644 --- a/docker/core.Dockerfile +++ b/docker/core.Dockerfile @@ -17,7 +17,7 @@ ARG PI_VERSION # Runtime tools RUN apt-get update && apt-get install -y --no-install-recommends \ - curl ca-certificates ripgrep fd-find tini \ + curl ca-certificates ripgrep fd-find tini git openssh-client \ && rm -rf /var/lib/apt/lists/* \ && ln -s /usr/bin/fdfind /usr/local/bin/fd