Skip to content

Commit 15efb16

Browse files
hclsysclaude
andcommitted
fix(image-gen): propagate ssrfPolicy into provider HTTP calls (#79716)
Image-generation providers called fetchWithSsrFGuard without inheriting the agent-level ssrfPolicy (tools.web.fetch.ssrfPolicy), so allowRfc2544BenchmarkRange had no effect on provider requests even when explicitly configured. Clash TUN / Surge fake-IP setups (198.18.0.0/15) were blocked despite the flag being true. Changes: - ImageGenerationRequest: add optional ssrfPolicy field - GenerateImageParams: add optional ssrfPolicy field - runtime.ts: thread ssrfPolicy into provider.generateImage() - image-generate-tool.ts: pass remoteMediaSsrfPolicy to generateImage() - shared.ts resolveGuardedPostRequestOptions: accept full ssrfPolicy and merge with allowPrivateNetwork instead of always truncating to {allowPrivateNetwork:true} - postJsonRequest / postMultipartRequest: expose ssrfPolicy param - openai-compatible-image-provider.ts: forward req.ssrfPolicy - extensions/openai/image-generation-provider.ts: forward req.ssrfPolicy in both JSON and multipart request paths Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
1 parent 17f0356 commit 15efb16

8 files changed

Lines changed: 46 additions & 1 deletion

File tree

extensions/openai/image-generation-provider.test.ts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -364,6 +364,25 @@ describe("openai image generation provider", () => {
364364
expect(result.images).toHaveLength(1);
365365
});
366366

367+
it("propagates req.ssrfPolicy to postJsonRequest so allowRfc2544BenchmarkRange reaches the HTTP guard", async () => {
368+
mockGeneratedPngResponse();
369+
370+
const provider = buildOpenAIImageGenerationProvider();
371+
await provider.generateImage({
372+
provider: "openai",
373+
model: "gpt-image-2",
374+
prompt: "test",
375+
cfg: {},
376+
ssrfPolicy: { allowRfc2544BenchmarkRange: true },
377+
});
378+
379+
expect(postJsonRequestMock).toHaveBeenCalledWith(
380+
expect.objectContaining({
381+
ssrfPolicy: { allowRfc2544BenchmarkRange: true },
382+
}),
383+
);
384+
});
385+
367386
it("forwards generation count and custom size overrides", async () => {
368387
mockGeneratedPngResponse();
369388

extensions/openai/image-generation-provider.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -842,6 +842,7 @@ export function buildOpenAIImageGenerationProvider(): ImageGenerationProvider {
842842
timeoutMs,
843843
fetchFn: fetch,
844844
allowPrivateNetwork,
845+
ssrfPolicy: req.ssrfPolicy,
845846
dispatcherPolicy,
846847
});
847848
})()
@@ -864,6 +865,7 @@ export function buildOpenAIImageGenerationProvider(): ImageGenerationProvider {
864865
timeoutMs,
865866
fetchFn: fetch,
866867
allowPrivateNetwork,
868+
ssrfPolicy: req.ssrfPolicy,
867869
dispatcherPolicy,
868870
});
869871
})();

src/agents/tools/image-generate-tool.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -742,6 +742,7 @@ export function createImageGenerateTool(options?: {
742742
inputImages,
743743
timeoutMs,
744744
providerOptions,
745+
ssrfPolicy: remoteMediaSsrfPolicy,
745746
});
746747
const ignoredOverrides = result.ignoredOverrides ?? [];
747748
const displayProvider = sanitizeInlineDirectiveText(result.provider);

src/image-generation/openai-compatible-image-provider.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -234,6 +234,7 @@ export function createOpenAiCompatibleImageGenerationProvider(
234234
timeoutMs,
235235
fetchFn: fetch,
236236
allowPrivateNetwork: resolvedAllowPrivateNetwork,
237+
ssrfPolicy: req.ssrfPolicy,
237238
dispatcherPolicy,
238239
})
239240
: postJsonRequest({
@@ -247,6 +248,7 @@ export function createOpenAiCompatibleImageGenerationProvider(
247248
timeoutMs,
248249
fetchFn: fetch,
249250
allowPrivateNetwork: resolvedAllowPrivateNetwork,
251+
ssrfPolicy: req.ssrfPolicy,
250252
dispatcherPolicy,
251253
});
252254

src/image-generation/runtime-types.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import type { AuthProfileStore } from "../agents/auth-profiles/types.js";
22
import type { FallbackAttempt } from "../agents/model-fallback.types.js";
33
import type { OpenClawConfig } from "../config/types.openclaw.js";
4+
import type { SsrFPolicy } from "../infra/net/ssrf.js";
45
import type {
56
GeneratedImageAsset,
67
ImageGenerationBackground,
@@ -32,6 +33,8 @@ export type GenerateImageParams = {
3233
/** Optional per-request provider timeout in milliseconds. */
3334
timeoutMs?: number;
3435
providerOptions?: ImageGenerationProviderOptions;
36+
/** SSRF policy to propagate into image-generation provider HTTP calls. */
37+
ssrfPolicy?: SsrFPolicy;
3538
};
3639

3740
export type GenerateImageRuntimeResult = {

src/image-generation/runtime.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,7 @@ export async function generateImage(
118118
inputImages: params.inputImages,
119119
...(timeoutMs !== undefined ? { timeoutMs } : {}),
120120
providerOptions: params.providerOptions,
121+
ssrfPolicy: params.ssrfPolicy,
121122
});
122123
if (!Array.isArray(result.images) || result.images.length === 0) {
123124
throw new Error("Image generation provider returned no images.");

src/image-generation/types.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import type { AuthProfileStore } from "../agents/auth-profiles/types.js";
22
import type { OpenClawConfig } from "../config/types.openclaw.js";
3+
import type { SsrFPolicy } from "../infra/net/ssrf.js";
34
import type { MediaNormalizationEntry } from "../media-generation/normalization.types.js";
45

56
export type GeneratedImageAsset = {
@@ -75,6 +76,7 @@ export type ImageGenerationRequest = {
7576
background?: ImageGenerationBackground;
7677
inputImages?: ImageGenerationSourceImage[];
7778
providerOptions?: ImageGenerationProviderOptions;
79+
ssrfPolicy?: SsrFPolicy;
7880
};
7981

8082
export type ImageGenerationResult = {

src/media-understanding/shared.ts

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -324,21 +324,32 @@ type GuardedPostRequestOptions = NonNullable<Parameters<typeof fetchWithTimeoutG
324324
function resolveGuardedPostRequestOptions(params: {
325325
pinDns?: boolean;
326326
allowPrivateNetwork?: boolean;
327+
ssrfPolicy?: SsrFPolicy;
327328
dispatcherPolicy?: PinnedDispatcherPolicy;
328329
auditContext?: string;
329330
mode?: GuardedFetchMode;
330331
}): GuardedPostRequestOptions | undefined {
331332
if (
332333
!params.allowPrivateNetwork &&
334+
!params.ssrfPolicy &&
333335
!params.dispatcherPolicy &&
334336
params.pinDns === undefined &&
335337
!params.auditContext &&
336338
params.mode === undefined
337339
) {
338340
return undefined;
339341
}
342+
// Merge allowPrivateNetwork into ssrfPolicy when both are provided so callers
343+
// that use the boolean shorthand still get the flag set.
344+
const mergedSsrfPolicy: SsrFPolicy | undefined = params.ssrfPolicy
345+
? params.allowPrivateNetwork
346+
? { ...params.ssrfPolicy, allowPrivateNetwork: true }
347+
: params.ssrfPolicy
348+
: params.allowPrivateNetwork
349+
? { allowPrivateNetwork: true }
350+
: undefined;
340351
return {
341-
...(params.allowPrivateNetwork ? { ssrfPolicy: { allowPrivateNetwork: true } } : {}),
352+
...(mergedSsrfPolicy ? { ssrfPolicy: mergedSsrfPolicy } : {}),
342353
...(params.pinDns !== undefined ? { pinDns: params.pinDns } : {}),
343354
...(params.dispatcherPolicy ? { dispatcherPolicy: params.dispatcherPolicy } : {}),
344355
...(params.auditContext ? { auditContext: params.auditContext } : {}),
@@ -384,6 +395,8 @@ export async function postJsonRequest(params: {
384395
fetchFn: typeof fetch;
385396
pinDns?: boolean;
386397
allowPrivateNetwork?: boolean;
398+
/** Full SSRF policy. When provided, merged with allowPrivateNetwork. */
399+
ssrfPolicy?: SsrFPolicy;
387400
dispatcherPolicy?: PinnedDispatcherPolicy;
388401
auditContext?: string;
389402
/**
@@ -414,6 +427,8 @@ export async function postMultipartRequest(params: {
414427
fetchFn: typeof fetch;
415428
pinDns?: boolean;
416429
allowPrivateNetwork?: boolean;
430+
/** Full SSRF policy. When provided, merged with allowPrivateNetwork. */
431+
ssrfPolicy?: SsrFPolicy;
417432
dispatcherPolicy?: PinnedDispatcherPolicy;
418433
auditContext?: string;
419434
/**

0 commit comments

Comments
 (0)