import { constants, realpathSync, statSync, accessSync } from "node:fs"; import { isAbsolute, relative } from "node:path"; import { buildInstallationContract, type InstallationRole, 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; missing: string[]; } export interface RuntimeBindings { dwh: ResolvedBinding; vector: ResolvedBinding; vectorWriter: ResolvedBinding; embedding: ResolvedBinding; evidence: ResolvedEvidenceBinding; } export interface ResolvedBinding { transport: DwhTransport | VectorTransport; 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 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 isInside(path: string, root: string): boolean { const pathRelative = relative(root, path); return pathRelative !== "" && !pathRelative.startsWith("..") && !isAbsolute(pathRelative); } function safeSecretFilePath(path: string, secretRoots: readonly string[]): string | undefined { if (!isAbsolute(path)) return undefined; try { const resolvedPath = realpathSync(path); const resolvedRoots = secretRoots.map((root) => realpathSync(root)); if (!resolvedRoots.some((root) => isInside(resolvedPath, root))) return undefined; if (!statSync(resolvedPath).isFile()) return undefined; accessSync(resolvedPath, constants.R_OK); return resolvedPath; } catch { return undefined; } } function requiredSuffixes( workspace: WorkspaceDescriptor, role: Exclude, transport: DwhTransport | VectorTransport, ): 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" ? required.filter((suffix) => suffix !== "API_KEY_FILE") : required; } /** * Resolve only installation-local values. Secret files remain file paths: their contents are * deliberately left for the harness secret-file loader, so bindings cannot leak credentials. */ export function resolveBinding( workspace: WorkspaceDescriptor, role: Exclude, env: NodeJS.ProcessEnv, secretRoots: readonly string[], ): ResolvedBinding { 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 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))) { missing.push(transportVariable.name); } const required = new Set(requiredSuffixes(descriptor, role, selectedTransport)); const values: Record = {}; for (const variable of variables) { if (variable.suffix === "TRANSPORT") continue; if (variable.transports && !variable.transports.includes(selectedTransport as never)) continue; const value = env[variable.name]; const present = value !== undefined && value.trim() !== ""; const safePath = variable.secret && present ? safeSecretFilePath(value, secretRoots) : undefined; const safe = !variable.secret || safePath !== undefined; if ((required.has(variable.suffix) && !present) || (present && !safe)) { missing.push(variable.name); } if (present && safe) values[variable.name] = variable.secret ? safePath! : value; } return { transport: selectedTransport, values, missing }; } /** Resolve descriptor-selected Evidence credentials without reading any secret file contents. */ export function resolveEvidenceBinding( workspace: WorkspaceDescriptor, env: NodeJS.ProcessEnv, secretRoots: readonly string[], ): ResolvedEvidenceBinding { 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 required = new Set( source?.type === "http" ? ["SIGNED_URLS_FILE"] : source?.type === "s3" ? ["ACCESS_KEY_FILE", "SECRET_KEY_FILE"] : [], ); const values: Record = {}; const missing: string[] = []; for (const variable of variables) { const value = env[variable.name]; const present = value !== undefined && value.trim() !== ""; const safePath = present ? safeSecretFilePath(value, secretRoots) : undefined; if ((required.has(variable.suffix) && !present) || (present && safePath === undefined)) { missing.push(variable.name); } if (safePath !== undefined) values[variable.name] = safePath; } return { values, missing }; } /** Resolve all runtime roles together so optional writer credentials cannot be smuggled into reader bindings. */ export function resolveRuntimeBindings( workspace: WorkspaceDescriptor, env: NodeJS.ProcessEnv, secretRoots: readonly string[], ): RuntimeBindings { 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. */ export function supportsSessionRuntime(bindings: RuntimeBindings): boolean { return bindings.dwh.transport !== "ssh_tunnel" && bindings.vector.transport !== "ssh_tunnel" && bindings.evidence.missing.length === 0; }