From a379ac056261d57c8000808e99d38a3ff6023128 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Fri, 1 May 2026 08:26:29 +0100 Subject: [PATCH] fix: guard plugin HTTP calls in CI --- extensions/kilocode/provider-models.ts | 84 ++++++++++--------- .../voice-call/src/providers/twilio/api.ts | 42 ++++++---- .../gateway-cli/run.option-collisions.test.ts | 13 ++- 3 files changed, 83 insertions(+), 56 deletions(-) diff --git a/extensions/kilocode/provider-models.ts b/extensions/kilocode/provider-models.ts index eb8453cf85a..7d42b18726b 100644 --- a/extensions/kilocode/provider-models.ts +++ b/extensions/kilocode/provider-models.ts @@ -1,5 +1,6 @@ import type { ModelDefinitionConfig } from "openclaw/plugin-sdk/provider-model-shared"; import { createSubsystemLogger } from "openclaw/plugin-sdk/runtime-env"; +import { fetchWithSsrFGuard } from "openclaw/plugin-sdk/ssrf-runtime"; import { normalizeLowercaseStringOrEmpty } from "openclaw/plugin-sdk/text-runtime"; const log = createSubsystemLogger("kilocode-models"); @@ -135,49 +136,56 @@ export async function discoverKilocodeModels(): Promise } try { - const response = await fetch(KILOCODE_MODELS_URL, { - headers: { Accept: "application/json" }, - signal: AbortSignal.timeout(DISCOVERY_TIMEOUT_MS), + const { response, release } = await fetchWithSsrFGuard({ + url: KILOCODE_MODELS_URL, + init: { + headers: { Accept: "application/json" }, + }, + timeoutMs: DISCOVERY_TIMEOUT_MS, + auditContext: "kilocode.model_discovery", }); - - if (!response.ok) { - log.warn(`Failed to discover models: HTTP ${response.status}, using static catalog`); - return buildStaticCatalog(); - } - - const data = (await response.json()) as GatewayModelsResponse; - if (!Array.isArray(data.data) || data.data.length === 0) { - log.warn("No models found from gateway API, using static catalog"); - return buildStaticCatalog(); - } - - const models: ModelDefinitionConfig[] = []; - const discoveredIds = new Set(); - - for (const entry of data.data) { - if (!entry || typeof entry !== "object") { - continue; + try { + if (!response.ok) { + log.warn(`Failed to discover models: HTTP ${response.status}, using static catalog`); + return buildStaticCatalog(); } - const id = typeof entry.id === "string" ? entry.id.trim() : ""; - if (!id || discoveredIds.has(id)) { - continue; - } - try { - models.push(toModelDefinition(entry)); - discoveredIds.add(id); - } catch (e) { - log.warn(`Skipping malformed model entry "${id}": ${String(e)}`); - } - } - const staticModels = buildStaticCatalog(); - for (const staticModel of staticModels) { - if (!discoveredIds.has(staticModel.id)) { - models.unshift(staticModel); + const data = (await response.json()) as GatewayModelsResponse; + if (!Array.isArray(data.data) || data.data.length === 0) { + log.warn("No models found from gateway API, using static catalog"); + return buildStaticCatalog(); } - } - return models.length > 0 ? models : buildStaticCatalog(); + const models: ModelDefinitionConfig[] = []; + const discoveredIds = new Set(); + + for (const entry of data.data) { + if (!entry || typeof entry !== "object") { + continue; + } + const id = typeof entry.id === "string" ? entry.id.trim() : ""; + if (!id || discoveredIds.has(id)) { + continue; + } + try { + models.push(toModelDefinition(entry)); + discoveredIds.add(id); + } catch (e) { + log.warn(`Skipping malformed model entry "${id}": ${String(e)}`); + } + } + + const staticModels = buildStaticCatalog(); + for (const staticModel of staticModels) { + if (!discoveredIds.has(staticModel.id)) { + models.unshift(staticModel); + } + } + + return models.length > 0 ? models : buildStaticCatalog(); + } finally { + await release(); + } } catch (error) { log.warn(`Discovery failed: ${String(error)}, using static catalog`); return buildStaticCatalog(); diff --git a/extensions/voice-call/src/providers/twilio/api.ts b/extensions/voice-call/src/providers/twilio/api.ts index 24b17aac622..e2a93fd938c 100644 --- a/extensions/voice-call/src/providers/twilio/api.ts +++ b/extensions/voice-call/src/providers/twilio/api.ts @@ -1,8 +1,12 @@ +import { fetchWithSsrFGuard } from "openclaw/plugin-sdk/ssrf-runtime"; + type ParsedTwilioApiError = { code?: number; message?: string; }; +const TWILIO_API_TIMEOUT_MS = 30_000; + function parseTwilioApiError(text: string): ParsedTwilioApiError { try { const parsed: unknown = JSON.parse(text); @@ -57,23 +61,31 @@ export async function twilioApiRequest(params: { return acc; }, new URLSearchParams()); - const response = await fetch(`${params.baseUrl}${params.endpoint}`, { - method: "POST", - headers: { - Authorization: `Basic ${Buffer.from(`${params.accountSid}:${params.authToken}`).toString("base64")}`, - "Content-Type": "application/x-www-form-urlencoded", + const { response, release } = await fetchWithSsrFGuard({ + url: `${params.baseUrl}${params.endpoint}`, + init: { + method: "POST", + headers: { + Authorization: `Basic ${Buffer.from(`${params.accountSid}:${params.authToken}`).toString("base64")}`, + "Content-Type": "application/x-www-form-urlencoded", + }, + body: bodyParams, }, - body: bodyParams, + timeoutMs: TWILIO_API_TIMEOUT_MS, + auditContext: "voice-call.twilio_api", }); - - if (!response.ok) { - if (params.allowNotFound && response.status === 404) { - return undefined as T; + try { + if (!response.ok) { + if (params.allowNotFound && response.status === 404) { + return undefined as T; + } + const errorText = await response.text(); + throw new TwilioApiError(response.status, errorText); } - const errorText = await response.text(); - throw new TwilioApiError(response.status, errorText); - } - const text = await response.text(); - return text ? (JSON.parse(text) as T) : (undefined as T); + const text = await response.text(); + return text ? (JSON.parse(text) as T) : (undefined as T); + } finally { + await release(); + } } diff --git a/src/cli/gateway-cli/run.option-collisions.test.ts b/src/cli/gateway-cli/run.option-collisions.test.ts index 171900b3529..65ce27817e4 100644 --- a/src/cli/gateway-cli/run.option-collisions.test.ts +++ b/src/cli/gateway-cli/run.option-collisions.test.ts @@ -1,6 +1,8 @@ import path from "node:path"; import { Command } from "commander"; import { beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; +import { SUPERVISOR_HINT_ENV_VARS } from "../../infra/supervisor-markers.js"; +import { withEnvAsync } from "../../test-utils/env.js"; import { withTempSecretFiles } from "../../test-utils/secret-file-fixture.js"; import { createCliRuntimeCapture } from "../test-runtime-capture.js"; @@ -42,6 +44,9 @@ const writeDiagnosticStabilityBundleForFailureSync = vi.fn((_reason: string, _er const controlUiState = vi.hoisted(() => ({ root: "/tmp/openclaw-control-ui" as string | null, })); +const withoutSupervisorEnv = Object.fromEntries( + SUPERVISOR_HINT_ENV_VARS.map((key) => [key, undefined]), +) as Record; const { runtimeErrors, defaultRuntime, resetRuntimeCapture } = createCliRuntimeCapture(); @@ -317,9 +322,11 @@ describe("gateway run option collisions", () => { }); startGatewayServer.mockRejectedValueOnce(err); - await expect(runGatewayCli(["gateway", "run", "--allow-unconfigured"])).rejects.toThrow( - "__exit__:0", - ); + await withEnvAsync(withoutSupervisorEnv, async () => { + await expect(runGatewayCli(["gateway", "run", "--allow-unconfigured"])).rejects.toThrow( + "__exit__:0", + ); + }); expect(writeDiagnosticStabilityBundleForFailureSync).not.toHaveBeenCalled(); });