diff --git a/src/lib/skills/webFetchExecution.ts b/src/lib/skills/webFetchExecution.ts index d0cce51bb6..e2b4188c19 100644 --- a/src/lib/skills/webFetchExecution.ts +++ b/src/lib/skills/webFetchExecution.ts @@ -61,11 +61,30 @@ function resolvePinnedBackend(input: ExecuteWebFetchInput): WebFetchProviderId | return backend ? FETCH_BACKEND_TO_PROVIDER[backend] : undefined; } +export function normalizeWebFetchCredentials(value: unknown): WebFetchCredentials | null { + if (!value || typeof value !== "object") return null; + const credentials = value as Record; + if (credentials.allRateLimited === true || credentials.allExpired === true) return null; + + const providerSpecificData = + credentials.providerSpecificData && + typeof credentials.providerSpecificData === "object" && + !Array.isArray(credentials.providerSpecificData) + ? (credentials.providerSpecificData as Record) + : undefined; + + return { + ...(typeof credentials.apiKey === "string" && { apiKey: credentials.apiKey }), + ...(typeof credentials.baseUrl === "string" && { baseUrl: credentials.baseUrl }), + ...(providerSpecificData && { providerSpecificData }), + }; +} + async function resolveCredentials( providerId: WebFetchProviderId ): Promise { try { - return (await getProviderCredentialsWithQuotaPreflight(providerId)) ?? null; + return normalizeWebFetchCredentials(await getProviderCredentialsWithQuotaPreflight(providerId)); } catch { return null; } diff --git a/tests/unit/web-fetch-execution-credentials.test.ts b/tests/unit/web-fetch-execution-credentials.test.ts new file mode 100644 index 0000000000..8a004a14eb --- /dev/null +++ b/tests/unit/web-fetch-execution-credentials.test.ts @@ -0,0 +1,28 @@ +import test from "node:test"; +import assert from "node:assert/strict"; + +const { normalizeWebFetchCredentials } = await import("../../src/lib/skills/webFetchExecution.ts"); + +test("web-fetch skills reject unavailable credential sentinels", () => { + assert.equal( + normalizeWebFetchCredentials({ allRateLimited: true, retryAfter: "tomorrow" }), + null + ); + assert.equal(normalizeWebFetchCredentials({ allExpired: true, expiredCount: 2 }), null); +}); + +test("web-fetch skills expose only the credential fields used by fetch executors", () => { + assert.deepEqual( + normalizeWebFetchCredentials({ + apiKey: "secret", + baseUrl: "https://fetch.example.test", + providerSpecificData: { region: "test" }, + accessToken: "must-not-leak-through", + }), + { + apiKey: "secret", + baseUrl: "https://fetch.example.test", + providerSpecificData: { region: "test" }, + } + ); +});