fix: guard plugin HTTP calls in CI

This commit is contained in:
Peter Steinberger
2026-05-01 08:26:29 +01:00
parent d0dac324c6
commit a379ac0562
3 changed files with 83 additions and 56 deletions

View File

@@ -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<ModelDefinitionConfig[]>
}
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<string>();
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<string>();
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();

View File

@@ -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<T = unknown>(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();
}
}

View File

@@ -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<string, string | undefined>;
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();
});