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, validateCanonicalWorkspace, type DwhTransport, type VectorTransport, type WorkspaceDescriptor, } from "./schema.js"; export interface RuntimeBindings { dwh: ResolvedBinding; vector: ResolvedBinding; vectorWriter: ResolvedBinding; embedding: ResolvedBinding; } 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 isSafeSecretFile(path: string, secretRoots: readonly string[]): boolean { if (!isAbsolute(path)) return false; try { const resolvedPath = realpathSync(path); const resolvedRoots = secretRoots.map((root) => realpathSync(root)); if (!resolvedRoots.some((root) => isInside(resolvedPath, root))) return false; if (!statSync(resolvedPath).isFile()) return false; accessSync(resolvedPath, constants.R_OK); return true; } catch { return false; } } function requiredSuffixes( workspace: WorkspaceDescriptor, role: InstallationRole, 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.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: InstallationRole, env: NodeJS.ProcessEnv, secretRoots: readonly string[], ): ResolvedBinding { const canonical = validateCanonicalWorkspace(workspace); const contract = buildInstallationContract(canonical); const variables = contract.variables.filter((variable) => variable.role === role); const transportVariable = variables.find((variable) => variable.suffix === "TRANSPORT"); const supported = role === "DWH" ? canonical.dwh.supported_transports : role === "VECTOR" ? canonical.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(canonical, 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 safe = !variable.secret || (present && isSafeSecretFile(value, secretRoots)); if ((required.has(variable.suffix) && !present) || (present && !safe)) { missing.push(variable.name); } if (present && safe) values[variable.name] = value; } return { transport: selectedTransport, 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 { return { dwh: resolveBinding(workspace, "DWH", env, secretRoots), vector: resolveBinding(workspace, "VECTOR", env, secretRoots), vectorWriter: resolveBinding(workspace, "VECTOR_WRITER", env, secretRoots), embedding: resolveBinding(workspace, "EMBEDDING", env, secretRoots), }; }