diff --git a/src/lib/actions/sandbox/doctor.ts b/src/lib/actions/sandbox/doctor.ts index 731c7b90a9..c0787a1c47 100644 --- a/src/lib/actions/sandbox/doctor.ts +++ b/src/lib/actions/sandbox/doctor.ts @@ -5,6 +5,7 @@ import { spawnSync } from "node:child_process"; import fs from "node:fs"; import path from "node:path"; +import { buildValidatedCurlCommandArgs } from "../../adapters/http/curl-args"; import { stripAnsi } from "../../adapters/openshell/client"; import { resolveOpenshell } from "../../adapters/openshell/resolve"; import { captureOpenshell } from "../../adapters/openshell/runtime"; @@ -320,7 +321,7 @@ function ollamaDoctorCheck(currentProvider: string): DoctorCheck { const endpoint = `http://127.0.0.1:${OLLAMA_PORT}/api/tags`; const result = captureHostCommand( "curl", - ["-sS", "--connect-timeout", "2", "--max-time", "4", endpoint], + buildValidatedCurlCommandArgs(["-sS", "--connect-timeout", "2", "--max-time", "4", endpoint]), 6000, ); const required = currentProvider === "ollama-local"; diff --git a/src/lib/adapters/http/curl-args.ts b/src/lib/adapters/http/curl-args.ts new file mode 100644 index 0000000000..a43e80220d --- /dev/null +++ b/src/lib/adapters/http/curl-args.ts @@ -0,0 +1,207 @@ +// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +import path from "node:path"; + +import { ROOT } from "../../state/paths"; + +export interface CurlProbeArgOptions { + cwd?: string; + trustedConfigFiles?: readonly string[]; +} + +const CURL_CONFIG_OPTIONS = new Set(["--config", "-K"]); +const CURL_OPTIONS_THAT_READ_FILES = new Set([ + "--cookie", + "-b", + "--netrc-file", + "--upload-file", + "-T", + "--cert", + "--key", + "--proxy-cert", + "--proxy-key", +]); +const CURL_OPTIONS_THAT_READ_IMPLICIT_FILES = new Set(["--netrc", "--netrc-optional"]); +const CURL_DATA_OPTIONS = new Set([ + "--data", + "--data-raw", + "--data-binary", + "--data-ascii", + "--data-urlencode", + "--json", + "--form", + "-d", + "-F", +]); +const CURL_HEADER_OPTIONS = new Set(["--header", "--proxy-header", "-H"]); +const CURL_SAFE_FLAG_OPTIONS = new Set([ + "-s", + "-S", + "-sS", + "-sf", + "-f", + "-L", + "-sfL", + "--fail", + "--silent", + "--show-error", + "--location", + "--compressed", + "--get", +]); +const CURL_SAFE_VALUE_OPTIONS = new Set(["--connect-timeout", "--max-time", "-X", "--request"]); +const CURL_FORBIDDEN_MULTI_TRANSFER_OPTIONS = new Set(["--next"]); +const CURL_SHORT_OPTIONS_WITH_VALUES = new Set(["-K", "-b", "-T", "-d", "-F", "-H", "-X"]); + +function normalizeHttpProbeUrl(rawUrl: unknown): string { + if (typeof rawUrl !== "string" || rawUrl.trim() === "") { + throw new Error("curl probe URL is required"); + } + const url = new URL(rawUrl); + if (url.protocol !== "http:" && url.protocol !== "https:") { + throw new Error(`curl probe URL must use http or https: ${url.protocol}`); + } + if (url.username || url.password) { + throw new Error("curl probe URL must not embed credentials"); + } + return url.toString(); +} + +function splitCurlOptionArg(arg: string): { option: string; inlineValue?: string } { + if (arg.startsWith("--")) { + const [option, inlineValue] = arg.includes("=") + ? arg.split(/=(.*)/s, 2) + : [arg, undefined]; + return { option, inlineValue }; + } + for (const option of CURL_SHORT_OPTIONS_WITH_VALUES) { + if (arg.startsWith(option) && arg.length > option.length) { + return { option, inlineValue: arg.slice(option.length) }; + } + } + return { option: arg }; +} + +function curlValueReadsFromFile(option: string, value: string): boolean { + if ((value.startsWith("@") && value !== "@-") || /(^|=)@[^-]/.test(value)) return true; + if (option === "--data-urlencode" && /^[^=]+@[^-]/.test(value)) return true; + if ((option === "--form" || option === "-F") && /(^|=)<[^-]/.test(value)) return true; + return false; +} + +function curlHeaderValueReadsFromFile(value: string): boolean { + return value.startsWith("@") && value !== "@-"; +} + +function getCurlOptionValue( + args: string[], + index: number, + option: string, + inlineValue: string | undefined, +): string { + if (inlineValue !== undefined) return inlineValue; + const value = args[index + 1]; + if (value === undefined) throw new Error(`curl probe option requires a value: ${option}`); + return value; +} + +function normalizeCurlConfigPath(value: string, opts: CurlProbeArgOptions): string { + if (value.trim() === "") throw new Error("curl probe config path is required"); + if (value.includes("\0")) throw new Error("curl probe config path must not contain NUL bytes"); + return path.resolve(opts.cwd ?? ROOT, value); +} + +function isTrustedCurlConfigPath(value: string, opts: CurlProbeArgOptions): boolean { + if (!opts.trustedConfigFiles?.length) return false; + const candidate = normalizeCurlConfigPath(value, opts); + return opts.trustedConfigFiles + .map((trustedPath) => normalizeCurlConfigPath(trustedPath, opts)) + .includes(candidate); +} + +export function validateCurlProbeArgs( + argv: string[], + opts: CurlProbeArgOptions = {}, +): { args: string[]; url: string } { + const args = [...argv]; + const url = normalizeHttpProbeUrl(args.pop()); + for (let index = 0; index < args.length; index += 1) { + const arg = args[index]; + const { option, inlineValue } = splitCurlOptionArg(arg); + if (CURL_FORBIDDEN_MULTI_TRANSFER_OPTIONS.has(option)) { + throw new Error(`curl probe option is not allowed because it creates multiple transfers: ${option}`); + } + if (CURL_OPTIONS_THAT_READ_IMPLICIT_FILES.has(option)) { + throw new Error(`curl probe option is not allowed because it reads local files: ${option}`); + } + if (CURL_OPTIONS_THAT_READ_FILES.has(option)) { + getCurlOptionValue(args, index, option, inlineValue); + if (inlineValue === undefined) index += 1; + throw new Error(`curl probe option is not allowed because it reads local files: ${option}`); + } + if (CURL_CONFIG_OPTIONS.has(option)) { + const value = getCurlOptionValue(args, index, option, inlineValue); + if (!isTrustedCurlConfigPath(value, opts)) { + throw new Error(`curl probe config file is not trusted: ${option}`); + } + if (inlineValue === undefined) index += 1; + continue; + } + if (CURL_HEADER_OPTIONS.has(option)) { + const value = getCurlOptionValue(args, index, option, inlineValue); + if (curlHeaderValueReadsFromFile(value)) { + throw new Error(`curl probe option must not read headers from a file: ${option}`); + } + if (inlineValue === undefined) index += 1; + continue; + } + if (arg === "--url" || arg.startsWith("--url=")) { + throw new Error("curl probe URLs must be passed as the final argv entry"); + } + if (CURL_DATA_OPTIONS.has(option)) { + const value = getCurlOptionValue(args, index, option, inlineValue); + if (curlValueReadsFromFile(option, value)) { + throw new Error(`curl probe option must not read request data from a file: ${option}`); + } + if (inlineValue === undefined) index += 1; + continue; + } + if (CURL_SAFE_VALUE_OPTIONS.has(option)) { + getCurlOptionValue(args, index, option, inlineValue); + if (inlineValue === undefined) index += 1; + continue; + } + if (CURL_SAFE_FLAG_OPTIONS.has(option)) { + continue; + } + if (!arg.startsWith("-")) { + throw new Error("curl probe received unexpected positional argument before URL"); + } + throw new Error(`curl probe option is not allowed: ${option}`); + } + return { args, url }; +} + +export function buildValidatedCurlCommandArgs( + argv: string[], + opts: CurlProbeArgOptions = {}, +): string[] { + const { args, url } = validateCurlProbeArgs(argv, opts); + return [...args, url]; +} + +export type CurlProbeMode = "json" | "chat-stream" | "event-stream"; + +export function buildCurlProbeSpawnArgs( + args: string[], + url: string, + bodyFile: string, + mode: CurlProbeMode, +): string[] { + const outputArgs = + mode === "json" ? ["-o", bodyFile, "-w", "%{http_code}"] : ["-N", "-o", bodyFile]; + const statusArgs = mode === "chat-stream" ? ["-w", "%{http_code}"] : []; + // lgtm[js/file-access-to-http] URL/argv are validated; file-backed config paths must be explicitly trusted. + return [...args, ...outputArgs, ...statusArgs, url]; +} diff --git a/src/lib/adapters/http/probe.ts b/src/lib/adapters/http/probe.ts index 1c2d57dd0c..03f92b0e47 100644 --- a/src/lib/adapters/http/probe.ts +++ b/src/lib/adapters/http/probe.ts @@ -11,6 +11,7 @@ import os from "node:os"; import path from "node:path"; import { isErrnoException } from "../../core/errno"; import { compactText } from "../../core/url-utils"; +import { buildCurlProbeSpawnArgs, validateCurlProbeArgs } from "./curl-args"; import type { ProbeResult } from "../../onboard/types"; import { ROOT } from "../../state/paths"; import { addTraceEvent, withTraceSpan } from "../../trace"; @@ -128,180 +129,6 @@ function sanitizeCurlUrl(value: string): string { } } -const CURL_CONFIG_OPTIONS = new Set(["--config", "-K"]); -const CURL_OPTIONS_THAT_READ_FILES = new Set([ - "--cookie", - "-b", - "--netrc-file", - "--upload-file", - "-T", - "--cert", - "--key", - "--proxy-cert", - "--proxy-key", -]); -const CURL_OPTIONS_THAT_READ_IMPLICIT_FILES = new Set(["--netrc", "--netrc-optional"]); -const CURL_DATA_OPTIONS = new Set([ - "--data", - "--data-raw", - "--data-binary", - "--data-ascii", - "--data-urlencode", - "--json", - "--form", - "-d", - "-F", -]); -const CURL_HEADER_OPTIONS = new Set(["--header", "--proxy-header", "-H"]); -const CURL_SAFE_FLAG_OPTIONS = new Set(["-s", "-S", "-sS", "-sf", "--compressed", "--get"]); -const CURL_SAFE_VALUE_OPTIONS = new Set(["--connect-timeout", "--max-time", "-X", "--request"]); -const CURL_FORBIDDEN_MULTI_TRANSFER_OPTIONS = new Set(["--next"]); -const CURL_SHORT_OPTIONS_WITH_VALUES = new Set(["-K", "-b", "-T", "-d", "-F", "-H", "-X"]); - -function normalizeHttpProbeUrl(rawUrl: unknown): string { - if (typeof rawUrl !== "string" || rawUrl.trim() === "") { - throw new Error("curl probe URL is required"); - } - const url = new URL(rawUrl); - if (url.protocol !== "http:" && url.protocol !== "https:") { - throw new Error(`curl probe URL must use http or https: ${url.protocol}`); - } - if (url.username || url.password) { - throw new Error("curl probe URL must not embed credentials"); - } - return url.toString(); -} - -function splitCurlOptionArg(arg: string): { option: string; inlineValue?: string } { - if (arg.startsWith("--")) { - const [option, inlineValue] = arg.includes("=") - ? arg.split(/=(.*)/s, 2) - : [arg, undefined]; - return { option, inlineValue }; - } - for (const option of CURL_SHORT_OPTIONS_WITH_VALUES) { - if (arg.startsWith(option) && arg.length > option.length) { - return { option, inlineValue: arg.slice(option.length) }; - } - } - return { option: arg }; -} - -function curlValueReadsFromFile(option: string, value: string): boolean { - if ((value.startsWith("@") && value !== "@-") || /(^|=)@[^-]/.test(value)) return true; - if (option === "--data-urlencode" && /^[^=]+@[^-]/.test(value)) return true; - if ((option === "--form" || option === "-F") && /(^|=)<[^-]/.test(value)) return true; - return false; -} - -function curlHeaderValueReadsFromFile(value: string): boolean { - return value.startsWith("@") && value !== "@-"; -} - -function getCurlOptionValue( - args: string[], - index: number, - option: string, - inlineValue: string | undefined, -): string { - if (inlineValue !== undefined) return inlineValue; - const value = args[index + 1]; - if (value === undefined) throw new Error(`curl probe option requires a value: ${option}`); - return value; -} - -function normalizeCurlConfigPath(value: string, opts: CurlProbeOptions): string { - if (value.trim() === "") throw new Error("curl probe config path is required"); - if (value.includes("\0")) throw new Error("curl probe config path must not contain NUL bytes"); - return path.resolve(opts.cwd ?? ROOT, value); -} - -function isTrustedCurlConfigPath(value: string, opts: CurlProbeOptions): boolean { - if (!opts.trustedConfigFiles?.length) return false; - const candidate = normalizeCurlConfigPath(value, opts); - return opts.trustedConfigFiles - .map((trustedPath) => normalizeCurlConfigPath(trustedPath, opts)) - .includes(candidate); -} - -function validateCurlProbeArgs( - argv: string[], - opts: CurlProbeOptions = {}, -): { args: string[]; url: string } { - const args = [...argv]; - const url = normalizeHttpProbeUrl(args.pop()); - for (let index = 0; index < args.length; index += 1) { - const arg = args[index]; - const { option, inlineValue } = splitCurlOptionArg(arg); - if (CURL_FORBIDDEN_MULTI_TRANSFER_OPTIONS.has(option)) { - throw new Error(`curl probe option is not allowed because it creates multiple transfers: ${option}`); - } - if (CURL_OPTIONS_THAT_READ_IMPLICIT_FILES.has(option)) { - throw new Error(`curl probe option is not allowed because it reads local files: ${option}`); - } - if (CURL_OPTIONS_THAT_READ_FILES.has(option)) { - getCurlOptionValue(args, index, option, inlineValue); - if (inlineValue === undefined) index += 1; - throw new Error(`curl probe option is not allowed because it reads local files: ${option}`); - } - if (CURL_CONFIG_OPTIONS.has(option)) { - const value = getCurlOptionValue(args, index, option, inlineValue); - if (!isTrustedCurlConfigPath(value, opts)) { - throw new Error(`curl probe config file is not trusted: ${option}`); - } - if (inlineValue === undefined) index += 1; - continue; - } - if (CURL_HEADER_OPTIONS.has(option)) { - const value = getCurlOptionValue(args, index, option, inlineValue); - if (curlHeaderValueReadsFromFile(value)) { - throw new Error(`curl probe option must not read headers from a file: ${option}`); - } - if (inlineValue === undefined) index += 1; - continue; - } - if (arg === "--url" || arg.startsWith("--url=")) { - throw new Error("curl probe URLs must be passed as the final argv entry"); - } - if (CURL_DATA_OPTIONS.has(option)) { - const value = getCurlOptionValue(args, index, option, inlineValue); - if (curlValueReadsFromFile(option, value)) { - throw new Error(`curl probe option must not read request data from a file: ${option}`); - } - if (inlineValue === undefined) index += 1; - continue; - } - if (CURL_SAFE_VALUE_OPTIONS.has(option)) { - getCurlOptionValue(args, index, option, inlineValue); - if (inlineValue === undefined) index += 1; - continue; - } - if (CURL_SAFE_FLAG_OPTIONS.has(option)) { - continue; - } - if (!arg.startsWith("-")) { - throw new Error("curl probe received unexpected positional argument before URL"); - } - throw new Error(`curl probe option is not allowed: ${option}`); - } - return { args, url }; -} - -type CurlProbeMode = "json" | "chat-stream" | "event-stream"; - -function buildCurlProbeSpawnArgs( - args: string[], - url: string, - bodyFile: string, - mode: CurlProbeMode, -): string[] { - const outputArgs = - mode === "json" ? ["-o", bodyFile, "-w", "%{http_code}"] : ["-N", "-o", bodyFile]; - const statusArgs = mode === "chat-stream" ? ["-w", "%{http_code}"] : []; - // lgtm[js/file-access-to-http] URL/argv are validated; file-backed config paths must be explicitly trusted. - return [...args, ...outputArgs, ...statusArgs, url]; -} - function getCurlProbeTraceAttributes(argv: string[], opts: CurlProbeOptions): Record { const url = argv.at(-1) || ""; const methodIndex = argv.findIndex((arg) => arg === "-X" || arg === "--request"); diff --git a/src/lib/agent/onboard.ts b/src/lib/agent/onboard.ts index 5fca1d1be3..d4092910d1 100644 --- a/src/lib/agent/onboard.ts +++ b/src/lib/agent/onboard.ts @@ -10,6 +10,7 @@ import os from "os"; import path from "path"; import { dockerBuild, dockerImageInspect } from "../adapters/docker"; +import { buildValidatedCurlCommandArgs } from "../adapters/http/curl-args"; import { getAgentBranding } from "../cli/branding"; import type { JsonObject as LooseObject } from "../core/json-types"; import { sleepSeconds } from "../core/wait"; @@ -428,7 +429,15 @@ export async function handleAgentSetup( const probe = agent.healthProbe; if (probe?.url) { const result = runCaptureOpenshell( - ["sandbox", "exec", "-n", sandboxName, "--", "curl", "-sf", "--max-time", "3", probe.url], + [ + "sandbox", + "exec", + "-n", + sandboxName, + "--", + "curl", + ...buildValidatedCurlCommandArgs(["-sf", "--max-time", "3", probe.url]), + ], { ignoreError: true }, ); if (isHealthProbeOk(result)) { @@ -468,7 +477,15 @@ export async function handleAgentSetup( let healthy = false; for (let i = 0; i < maxAttempts; i++) { const result = runCaptureOpenshell( - ["sandbox", "exec", "-n", sandboxName, "--", "curl", "-sf", "--max-time", "3", probe.url], + [ + "sandbox", + "exec", + "-n", + sandboxName, + "--", + "curl", + ...buildValidatedCurlCommandArgs(["-sf", "--max-time", "3", probe.url]), + ], { ignoreError: true }, ); if (isHealthProbeOk(result)) { diff --git a/src/lib/core/wait.ts b/src/lib/core/wait.ts index b789d368fc..9a48f002a1 100644 --- a/src/lib/core/wait.ts +++ b/src/lib/core/wait.ts @@ -5,6 +5,7 @@ * Synchronous waiting primitives for CLI commands. */ +import { buildValidatedCurlCommandArgs } from "../adapters/http/curl-args"; import { withLocalNoProxy } from "../subprocess-env.js"; /** @@ -79,7 +80,7 @@ export function waitForHttp(url: string, timeoutSeconds = 5): boolean { try { const result = spawnSync( "curl", - ["-sf", "--connect-timeout", "1", "--max-time", "1", url], + buildValidatedCurlCommandArgs(["-sf", "--connect-timeout", "1", "--max-time", "1", url]), { stdio: "ignore", env }, ); return result.status === 0; diff --git a/src/lib/inference/local.ts b/src/lib/inference/local.ts index 23de02773b..4f1dc8de55 100644 --- a/src/lib/inference/local.ts +++ b/src/lib/inference/local.ts @@ -10,6 +10,7 @@ import fs from "node:fs"; import os from "node:os"; import nodePath from "node:path"; import type { CurlProbeResult } from "../adapters/http/probe"; +import { buildValidatedCurlCommandArgs } from "../adapters/http/curl-args"; import { runCurlProbe } from "../adapters/http/probe"; import type { CaptureResult } from "../runner"; import { buildSubprocessEnv } from "../subprocess-env"; @@ -354,7 +355,7 @@ export function getLocalProviderHealthEndpoint(provider: string): string | null export function getLocalProviderHealthCheck(provider: string): string[] | null { const endpoint = getLocalProviderHealthEndpoint(provider); - return endpoint ? ["curl", "-sf", endpoint] : null; + return endpoint ? ["curl", ...buildValidatedCurlCommandArgs(["-sf", endpoint])] : null; } export function getLocalProviderLabel(provider: string): string | null { @@ -807,12 +808,14 @@ export function getOllamaModelOptions(runCaptureImpl?: RunCaptureFn): string[] { const tagsOutput = capture( [ "curl", - "-sf", - "--connect-timeout", - "3", - "--max-time", - "5", - `http://${host}:${OLLAMA_PORT}/api/tags`, + ...buildValidatedCurlCommandArgs([ + "-sf", + "--connect-timeout", + "3", + "--max-time", + "5", + `http://${host}:${OLLAMA_PORT}/api/tags`, + ]), ], { ignoreError: true }, ); @@ -946,12 +949,23 @@ export function getOllamaProbeCommand( options: { num_predict: 16 }, }); const host = getResolvedOllamaHost(); + const endpoint = `http://${host}:${OLLAMA_PORT}/api/generate`; + buildValidatedCurlCommandArgs([ + "-sS", + "--max-time", + String(timeoutSeconds), + "-H", + "Content-Type: application/json", + "-d", + payload, + endpoint, + ]); return [ "curl", "-sS", "--max-time", String(timeoutSeconds), - `http://${host}:${OLLAMA_PORT}/api/generate`, + endpoint, "-H", "Content-Type: application/json", "-d", diff --git a/src/lib/inference/nim.ts b/src/lib/inference/nim.ts index 91e6bfa2da..b99c2d4be4 100644 --- a/src/lib/inference/nim.ts +++ b/src/lib/inference/nim.ts @@ -21,6 +21,7 @@ const { const { sleepSeconds } = require("../core/wait"); const nimImages = require("../../../bin/lib/nim-images.json"); +import { buildValidatedCurlCommandArgs } from "../adapters/http/curl-args"; import { VLLM_PORT } from "../core/ports"; import { isSafeModelId } from "../validation"; import { @@ -382,12 +383,14 @@ export function getServedModelId(port = VLLM_PORT): string | null { const out = runCapture( [ "curl", - "-sf", - "--connect-timeout", - "5", - "--max-time", - "5", - `http://127.0.0.1:${Number(port)}/v1/models`, + ...buildValidatedCurlCommandArgs([ + "-sf", + "--connect-timeout", + "5", + "--max-time", + "5", + `http://127.0.0.1:${Number(port)}/v1/models`, + ]), ], { ignoreError: true }, ); @@ -894,12 +897,14 @@ export function waitForNimHealth( const result = runCapture( [ "curl", - "-sf", - "--connect-timeout", - "5", - "--max-time", - "5", - `http://127.0.0.1:${hostPort}/v1/models`, + ...buildValidatedCurlCommandArgs([ + "-sf", + "--connect-timeout", + "5", + "--max-time", + "5", + `http://127.0.0.1:${hostPort}/v1/models`, + ]), ], { ignoreError: true }, ); @@ -981,12 +986,14 @@ export function nimStatusByName(name: string, port?: number): NimStatus { const health = runCapture( [ "curl", - "-sf", - "--connect-timeout", - "5", - "--max-time", - "5", - `http://127.0.0.1:${resolvedHostPort}/v1/models`, + ...buildValidatedCurlCommandArgs([ + "-sf", + "--connect-timeout", + "5", + "--max-time", + "5", + `http://127.0.0.1:${resolvedHostPort}/v1/models`, + ]), ], { ignoreError: true, timeout: NIM_STATUS_PROBE_TIMEOUT_MS + 1000 }, ); diff --git a/src/lib/inference/ollama-runtime-context.ts b/src/lib/inference/ollama-runtime-context.ts index 27ecc1595b..36bffcc7c4 100644 --- a/src/lib/inference/ollama-runtime-context.ts +++ b/src/lib/inference/ollama-runtime-context.ts @@ -9,6 +9,7 @@ * of re-implementing parsing or process-env state handling. */ +import { buildValidatedCurlCommandArgs } from "../adapters/http/curl-args"; import { OLLAMA_PORT } from "../core/ports"; const { runCapture } = require("../runner"); @@ -117,12 +118,14 @@ export function probeOllamaRuntimeModelStatus( const output = capture( [ "curl", - "-sf", - "--connect-timeout", - "3", - "--max-time", - "5", - `http://${host}:${OLLAMA_PORT}/api/ps`, + ...buildValidatedCurlCommandArgs([ + "-sf", + "--connect-timeout", + "3", + "--max-time", + "5", + `http://${host}:${OLLAMA_PORT}/api/ps`, + ]), ], { ignoreError: true }, ); diff --git a/src/lib/inference/ollama-version.ts b/src/lib/inference/ollama-version.ts index 675b8f8624..da15e81c7a 100644 --- a/src/lib/inference/ollama-version.ts +++ b/src/lib/inference/ollama-version.ts @@ -8,6 +8,7 @@ */ const { runCapture } = require("../runner"); +import { buildValidatedCurlCommandArgs } from "../adapters/http/curl-args"; import { OLLAMA_PORT } from "../core/ports"; export type OllamaVersionRunCapture = ( @@ -49,12 +50,14 @@ export function getRunningOllamaDaemonVersion( const out = capture( [ "curl", - "-sf", - "--connect-timeout", - "2", - "--max-time", - "5", - endpoint, + ...buildValidatedCurlCommandArgs([ + "-sf", + "--connect-timeout", + "2", + "--max-time", + "5", + endpoint, + ]), ], { ignoreError: true }, ); diff --git a/src/lib/inference/ollama/model-size.ts b/src/lib/inference/ollama/model-size.ts index 2b4c1d1bc2..a3d9bb9c39 100644 --- a/src/lib/inference/ollama/model-size.ts +++ b/src/lib/inference/ollama/model-size.ts @@ -1,6 +1,7 @@ // SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. // SPDX-License-Identifier: Apache-2.0 +import { buildValidatedCurlCommandArgs } from "../../adapters/http/curl-args"; import { runCapture } from "../../runner"; import { OLLAMA_DOWNLOAD_SIZE_FALLBACK_BYTES } from "../ollama-model-registry"; @@ -43,12 +44,14 @@ export function probeRegistrySize(model: string, capture: CaptureFn = runCapture const body = capture( [ "curl", - "-sfL", - "--max-time", - String(PROBE_TIMEOUT_SECONDS), - "-H", - MANIFEST_ACCEPT_HEADER, - url, + ...buildValidatedCurlCommandArgs([ + "-sfL", + "--max-time", + String(PROBE_TIMEOUT_SECONDS), + "-H", + MANIFEST_ACCEPT_HEADER, + url, + ]), ], { ignoreError: true }, ); diff --git a/src/lib/inference/vllm.ts b/src/lib/inference/vllm.ts index 4b4f68ca67..09855364f2 100644 --- a/src/lib/inference/vllm.ts +++ b/src/lib/inference/vllm.ts @@ -10,6 +10,7 @@ import { dockerPullWithProgressWatchdog, dockerSpawn, } from "../adapters/docker"; +import { buildValidatedCurlCommandArgs } from "../adapters/http/curl-args"; import { VLLM_PORT } from "../core/ports"; import { runCapture, runShell } from "../runner"; import { getGpuIndicesByName } from "./nim"; @@ -371,7 +372,17 @@ function vllmModelsEndpoint(): string { function vllmEndpointReady(): boolean { const response = runCapture( - ["curl", "-sf", "--connect-timeout", "2", "--max-time", "5", vllmModelsEndpoint()], + [ + "curl", + ...buildValidatedCurlCommandArgs([ + "-sf", + "--connect-timeout", + "2", + "--max-time", + "5", + vllmModelsEndpoint(), + ]), + ], { ignoreError: true }, ).trim(); if (!response) return false;