From d47f3fdc1982d8ffe08b06c2d290b1297a69c68b Mon Sep 17 00:00:00 2001 From: Gustavo Madeira Santana Date: Sun, 5 Apr 2026 21:38:11 -0400 Subject: [PATCH] Matrix: scope dispatcher compatibility fallback --- .../image-generation-core/src/runtime.ts | 6 +-- extensions/matrix/runtime-api.ts | 2 - extensions/matrix/src/matrix/sdk/transport.ts | 43 ++++++++++++++--- extensions/matrix/src/runtime-api.ts | 2 - .../video-generation-core/src/runtime.ts | 6 +-- scripts/lib/plugin-sdk-entrypoints.json | 2 + src/infra/net/fetch-guard.ssrf.test.ts | 35 +------------- src/infra/net/fetch-guard.ts | 47 +------------------ src/infra/net/pinned-dispatcher-compat.ts | 36 -------------- src/plugin-sdk/image-generation-core.ts | 5 ++ src/plugin-sdk/ssrf-runtime.ts | 4 -- src/plugin-sdk/video-generation-core.ts | 5 ++ .../provider-family-plugin-tests.test.ts | 1 + .../contracts/runtime-seams.contract.test.ts | 2 +- 14 files changed, 58 insertions(+), 138 deletions(-) delete mode 100644 src/infra/net/pinned-dispatcher-compat.ts diff --git a/extensions/image-generation-core/src/runtime.ts b/extensions/image-generation-core/src/runtime.ts index 0ad5edceb40..8636f29775e 100644 --- a/extensions/image-generation-core/src/runtime.ts +++ b/extensions/image-generation-core/src/runtime.ts @@ -1,15 +1,13 @@ import { buildNoCapabilityModelConfiguredMessage, - resolveCapabilityModelCandidates, - throwCapabilityGenerationFailure, -} from "../../../src/media-generation/runtime-shared.js"; -import { createSubsystemLogger, describeFailoverError, getImageGenerationProvider, isFailoverError, listImageGenerationProviders, parseImageGenerationModelRef, + resolveCapabilityModelCandidates, + throwCapabilityGenerationFailure, type AuthProfileStore, type FallbackAttempt, type GeneratedImageAsset, diff --git a/extensions/matrix/runtime-api.ts b/extensions/matrix/runtime-api.ts index 3a451f4ee4e..594a280bc81 100644 --- a/extensions/matrix/runtime-api.ts +++ b/extensions/matrix/runtime-api.ts @@ -12,10 +12,8 @@ export * from "./src/storage-paths.js"; export { ensureMatrixSdkInstalled, isMatrixSdkAvailable } from "./src/matrix/deps.js"; export { assertHttpUrlTargetsPrivateNetwork, - canBypassPinnedDispatcherForCompatibility, closeDispatcher, createPinnedDispatcher, - isPinnedDispatcherRuntimeCompatibilityError, resolvePinnedHostnameWithPolicy, ssrfPolicyFromDangerouslyAllowPrivateNetwork, ssrfPolicyFromAllowPrivateNetwork, diff --git a/extensions/matrix/src/matrix/sdk/transport.ts b/extensions/matrix/src/matrix/sdk/transport.ts index 3558e982b3a..1959d98f84a 100644 --- a/extensions/matrix/src/matrix/sdk/transport.ts +++ b/extensions/matrix/src/matrix/sdk/transport.ts @@ -1,10 +1,8 @@ import type { PinnedDispatcherPolicy } from "openclaw/plugin-sdk/infra-runtime"; import { buildTimeoutAbortSignal, - canBypassPinnedDispatcherForCompatibility, closeDispatcher, createPinnedDispatcher, - isPinnedDispatcherRuntimeCompatibilityError, resolvePinnedHostnameWithPolicy, type SsrFPolicy, } from "../../runtime-api.js"; @@ -86,10 +84,43 @@ function buildBufferedResponse(params: { return response; } +type ErrorWithCause = { + code?: unknown; + message?: unknown; + cause?: unknown; +}; + +function* iterateErrorCauseChain(error: unknown): Generator { + const seen = new Set(); + let current: unknown = error; + while (current && typeof current === "object" && !seen.has(current)) { + seen.add(current); + yield current as ErrorWithCause; + current = (current as ErrorWithCause).cause; + } +} + +function canBypassPinnedDispatcherForCompatibility(policy?: PinnedDispatcherPolicy): boolean { + return !policy || policy.mode === "direct"; +} + +function isPinnedDispatcherRuntimeCompatibilityError(error: unknown): boolean { + for (const candidate of iterateErrorCauseChain(error)) { + const message = typeof candidate.message === "string" ? candidate.message : ""; + if ( + candidate.code === "UND_ERR_INVALID_ARG" && + message.toLowerCase().includes("onrequeststart") + ) { + return true; + } + } + return false; +} + async function fetchWithPinnedDispatcherCompatibilityRetry(params: { url: string; init: RequestInit & { dispatcher?: unknown }; - canBypassPinnedDispatcher: boolean; + dispatcherPolicy?: PinnedDispatcherPolicy; dispatcher: ReturnType | undefined; }): Promise { try { @@ -97,7 +128,7 @@ async function fetchWithPinnedDispatcherCompatibilityRetry(params: { } catch (error) { if ( !params.dispatcher || - !params.canBypassPinnedDispatcher || + !canBypassPinnedDispatcherForCompatibility(params.dispatcherPolicy) || !isPinnedDispatcherRuntimeCompatibilityError(error) ) { throw error; @@ -136,9 +167,7 @@ async function fetchWithMatrixGuardedRedirects(params: { dispatcher = createPinnedDispatcher(pinned, params.dispatcherPolicy, params.ssrfPolicy); const response = await fetchWithPinnedDispatcherCompatibilityRetry({ url: currentUrl.toString(), - canBypassPinnedDispatcher: canBypassPinnedDispatcherForCompatibility( - params.dispatcherPolicy, - ), + dispatcherPolicy: params.dispatcherPolicy, dispatcher, init: { ...params.init, diff --git a/extensions/matrix/src/runtime-api.ts b/extensions/matrix/src/runtime-api.ts index 62f33a2ec9e..9d2b0be676a 100644 --- a/extensions/matrix/src/runtime-api.ts +++ b/extensions/matrix/src/runtime-api.ts @@ -61,10 +61,8 @@ export { export type { RuntimeEnv } from "openclaw/plugin-sdk/runtime"; export { assertHttpUrlTargetsPrivateNetwork, - canBypassPinnedDispatcherForCompatibility, closeDispatcher, createPinnedDispatcher, - isPinnedDispatcherRuntimeCompatibilityError, isPrivateOrLoopbackHost, resolvePinnedHostnameWithPolicy, ssrfPolicyFromDangerouslyAllowPrivateNetwork, diff --git a/extensions/video-generation-core/src/runtime.ts b/extensions/video-generation-core/src/runtime.ts index 7efc716be34..0f5b06976d0 100644 --- a/extensions/video-generation-core/src/runtime.ts +++ b/extensions/video-generation-core/src/runtime.ts @@ -1,15 +1,13 @@ import { buildNoCapabilityModelConfiguredMessage, - resolveCapabilityModelCandidates, - throwCapabilityGenerationFailure, -} from "../../../src/media-generation/runtime-shared.js"; -import { createSubsystemLogger, describeFailoverError, getVideoGenerationProvider, isFailoverError, listVideoGenerationProviders, parseVideoGenerationModelRef, + resolveCapabilityModelCandidates, + throwCapabilityGenerationFailure, type AuthProfileStore, type FallbackAttempt, type GeneratedVideoAsset, diff --git a/scripts/lib/plugin-sdk-entrypoints.json b/scripts/lib/plugin-sdk-entrypoints.json index a2d319dad79..50aee3035a1 100644 --- a/scripts/lib/plugin-sdk-entrypoints.json +++ b/scripts/lib/plugin-sdk-entrypoints.json @@ -169,6 +169,8 @@ "memory-core-host-runtime-files", "memory-lancedb", "msteams", + "music-generation", + "music-generation-core", "models-provider-runtime", "skill-commands-runtime", "native-command-registry", diff --git a/src/infra/net/fetch-guard.ssrf.test.ts b/src/infra/net/fetch-guard.ssrf.test.ts index 6f2d56560d9..ecb075c7010 100644 --- a/src/infra/net/fetch-guard.ssrf.test.ts +++ b/src/infra/net/fetch-guard.ssrf.test.ts @@ -317,7 +317,7 @@ describe("fetchWithSsrFGuard hardening", () => { } }); - it("retries without the direct pinned dispatcher when the runtime rejects that dispatcher shape", async () => { + it("fails closed when the runtime rejects the pinned dispatcher shape", async () => { const fetchImpl = vi.fn(async (_input: RequestInfo | URL, init?: RequestInit) => { const requestInit = init as RequestInit & { dispatcher?: unknown }; if (requestInit.dispatcher) { @@ -326,42 +326,11 @@ describe("fetchWithSsrFGuard hardening", () => { return okResponse(); }); - const result = await fetchWithSsrFGuard({ - url: "https://public.example/resource", - fetchImpl, - lookupFn: createPublicLookup(), - }); - - expect(fetchImpl).toHaveBeenCalledTimes(2); - expect( - (fetchImpl.mock.calls[0]?.[1] as RequestInit & { dispatcher?: unknown })?.dispatcher, - ).toBeDefined(); - expect( - (fetchImpl.mock.calls[1]?.[1] as RequestInit & { dispatcher?: unknown })?.dispatcher, - ).toBeUndefined(); - await result.release(); - }); - - it("does not bypass proxy routing when proxy dispatchers fail the same way", async () => { - const fetchImpl = vi.fn(async () => { - throw createPinnedDispatcherCompatibilityError(); - }); - const lookupFn = vi.fn(async (hostname: string) => [ - { - address: hostname === "proxy.example" ? "93.184.216.35" : "93.184.216.34", - family: 4, - }, - ]) as unknown as LookupFn; - await expect( fetchWithSsrFGuard({ url: "https://public.example/resource", fetchImpl, - lookupFn, - dispatcherPolicy: { - mode: "explicit-proxy", - proxyUrl: "http://proxy.example:7890", - }, + lookupFn: createPublicLookup(), }), ).rejects.toThrow("fetch failed"); expect(fetchImpl).toHaveBeenCalledTimes(1); diff --git a/src/infra/net/fetch-guard.ts b/src/infra/net/fetch-guard.ts index 4a0cb40936f..876742dcdec 100644 --- a/src/infra/net/fetch-guard.ts +++ b/src/infra/net/fetch-guard.ts @@ -1,10 +1,6 @@ import type { Dispatcher } from "undici"; import { logWarn } from "../../logger.js"; import { buildTimeoutAbortSignal } from "../../utils/fetch-timeout.js"; -import { - canBypassPinnedDispatcherForCompatibility, - isPinnedDispatcherRuntimeCompatibilityError, -} from "./pinned-dispatcher-compat.ts"; import { hasProxyEnvConfigured } from "./proxy-env.js"; import { retainSafeHeadersForCrossOriginRedirect as retainSafeRedirectHeaders } from "./redirect-headers.js"; import { @@ -190,29 +186,6 @@ function retainSafeHeadersForCrossOriginRedirect(init?: RequestInit): RequestIni return { ...init, headers: retainSafeRedirectHeaders(init.headers) }; } -async function fetchWithPinnedDispatcherCompatibilityRetry(params: { - url: string; - init: DispatcherAwareRequestInit; - dispatcher: Dispatcher | null; - fetchImpl: FetchLike; - canBypassPinnedDispatcher: boolean; -}): Promise { - try { - return await params.fetchImpl(params.url, params.init); - } catch (error) { - if ( - !params.dispatcher || - !params.canBypassPinnedDispatcher || - !isPinnedDispatcherRuntimeCompatibilityError(error) - ) { - throw error; - } - await closeDispatcher(params.dispatcher); - const { dispatcher: _dispatcher, ...retryInit } = params.init; - return await params.fetchImpl(params.url, retryInit); - } -} - function dropBodyHeaders(headers?: HeadersInit): HeadersInit | undefined { if (!headers) { return headers; @@ -349,24 +322,8 @@ export async function fetchWithSsrFGuard(params: GuardedFetchOptions): Promise { - const seen = new Set(); - let current: unknown = error; - while (current && typeof current === "object" && !seen.has(current)) { - seen.add(current); - yield current as ErrorWithCause; - current = (current as ErrorWithCause).cause; - } -} - -export function canBypassPinnedDispatcherForCompatibility( - policy?: PinnedDispatcherPolicy, -): boolean { - return !policy || policy.mode === "direct"; -} - -export function isPinnedDispatcherRuntimeCompatibilityError(error: unknown): boolean { - for (const candidate of iterateErrorCauseChain(error)) { - const message = typeof candidate.message === "string" ? candidate.message : ""; - if ( - candidate.code === "UND_ERR_INVALID_ARG" && - message.toLowerCase().includes("onrequeststart") - ) { - return true; - } - } - return false; -} diff --git a/src/plugin-sdk/image-generation-core.ts b/src/plugin-sdk/image-generation-core.ts index 65670f92408..ae2f094bd38 100644 --- a/src/plugin-sdk/image-generation-core.ts +++ b/src/plugin-sdk/image-generation-core.ts @@ -15,6 +15,11 @@ export type { export type { OpenClawConfig } from "../config/config.js"; export { describeFailoverError, isFailoverError } from "../agents/failover-error.js"; +export { + buildNoCapabilityModelConfiguredMessage, + resolveCapabilityModelCandidates, + throwCapabilityGenerationFailure, +} from "../media-generation/runtime-shared.js"; export { resolveAgentModelFallbackValues, resolveAgentModelPrimaryValue, diff --git a/src/plugin-sdk/ssrf-runtime.ts b/src/plugin-sdk/ssrf-runtime.ts index ca33d629fd2..9f214c899a0 100644 --- a/src/plugin-sdk/ssrf-runtime.ts +++ b/src/plugin-sdk/ssrf-runtime.ts @@ -10,10 +10,6 @@ export { type LookupFn, type SsrFPolicy, } from "../infra/net/ssrf.js"; -export { - canBypassPinnedDispatcherForCompatibility, - isPinnedDispatcherRuntimeCompatibilityError, -} from "../infra/net/pinned-dispatcher-compat.ts"; export { formatErrorMessage } from "../infra/errors.js"; export { fetchWithSsrFGuard } from "../infra/net/fetch-guard.js"; export { diff --git a/src/plugin-sdk/video-generation-core.ts b/src/plugin-sdk/video-generation-core.ts index 6d815f631b8..b7fa0383ece 100644 --- a/src/plugin-sdk/video-generation-core.ts +++ b/src/plugin-sdk/video-generation-core.ts @@ -16,6 +16,11 @@ export type { export type { OpenClawConfig } from "../config/config.js"; export { describeFailoverError, isFailoverError } from "../agents/failover-error.js"; +export { + buildNoCapabilityModelConfiguredMessage, + resolveCapabilityModelCandidates, + throwCapabilityGenerationFailure, +} from "../media-generation/runtime-shared.js"; export { resolveAgentModelFallbackValues, resolveAgentModelPrimaryValue, diff --git a/src/plugins/contracts/provider-family-plugin-tests.test.ts b/src/plugins/contracts/provider-family-plugin-tests.test.ts index ae103cacc4d..a11ef11e22d 100644 --- a/src/plugins/contracts/provider-family-plugin-tests.test.ts +++ b/src/plugins/contracts/provider-family-plugin-tests.test.ts @@ -41,6 +41,7 @@ const EXPECTED_SHARED_FAMILY_CONTRACTS: Record { }; const lookupFn = vi.fn( - async () => ({ address: "93.184.216.34", family: 4 }) as const, + async () => [{ address: "93.184.216.34", family: 4 }] as const, ) as unknown as NonNullable[0]["lookupFn"]>; const result = await fetchWithSsrFGuard({