fix(comfy): allow private service hostnames (#99065)

* fix(comfy): allow private service hostnames

* fix(comfy): scope private hostname trust to origin

* test(comfy): use fetch-compatible response bodies

---------

Co-authored-by: joshavant <830519+joshavant@users.noreply.github.com>
This commit is contained in:
Jacob Tomlinson
2026-07-08 21:11:08 -05:00
committed by GitHub
co-authored by joshavant
parent 0307deacfa
commit 9c86529e44
4 changed files with 572 additions and 61 deletions
+5 -1
View File
@@ -206,7 +206,11 @@ Comfy supports shared top-level connection settings plus per-capability workflow
| `mode` | `"local"` or `"cloud"` | Connection mode. Defaults to `"local"`. |
| `baseUrl` | string | Defaults to `http://127.0.0.1:8188` for local or `https://cloud.comfy.org` for cloud. |
| `apiKey` | string | Optional inline key, alternative to `COMFY_API_KEY` / `COMFY_CLOUD_API_KEY` env vars. |
| `allowPrivateNetwork` | boolean | Allow a private/LAN `baseUrl` in cloud mode. |
| `allowPrivateNetwork` | boolean | Allow a private/LAN `baseUrl` in cloud mode or a local private-DNS FQDN. |
<Note>
In `local` mode, loopback/private IP literals and single-label service names such as `http://comfyui:8188` work without `allowPrivateNetwork`. Public-looking private-DNS FQDNs such as `https://comfy.local.example.com` require `allowPrivateNetwork: true`. Private-origin trust stays scoped to the configured scheme, hostname, and port; local redirects cannot leave the configured hostname, while cloud redirects to public CDNs are checked with the default SSRF policy.
</Note>
### Per-capability keys
@@ -1,5 +1,7 @@
// Comfy tests cover image generation provider plugin behavior.
import type { LookupAddress } from "node:dns";
import { MAX_TIMER_TIMEOUT_MS } from "openclaw/plugin-sdk/number-runtime";
import { fetchWithSsrFGuard } from "openclaw/plugin-sdk/ssrf-runtime";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import {
setComfyFetchGuardForTesting,
@@ -21,12 +23,28 @@ type FetchGuardRequest = {
url?: unknown;
auditContext?: unknown;
timeoutMs?: unknown;
policy?: unknown;
init?: {
method?: unknown;
headers?: HeadersInit;
body?: BodyInit | null;
};
};
type RealGuardParams = Parameters<typeof fetchWithSsrFGuard>[0];
type RealGuardFetchImpl = NonNullable<RealGuardParams["fetchImpl"]>;
type RealGuardLookupFn = NonNullable<RealGuardParams["lookupFn"]>;
type RealGuardHarness = {
fetchUrls: string[];
guardCalls: RealGuardParams[];
};
type RealComfyFetchOptions = {
dns: Record<string, string>;
promptId?: string;
redirectLocation?: string;
body?: Buffer;
contentType?: string;
};
function fetchRequest(call: number): FetchGuardRequest {
const request = fetchWithSsrFGuardMock.mock.calls[call - 1]?.[0] as FetchGuardRequest | undefined;
@@ -40,6 +58,167 @@ function parseJsonBody(call: number): Record<string, unknown> {
return parseComfyJsonBody(fetchWithSsrFGuardMock, call);
}
function mockLocalImageResponses(promptId = "local-prompt-1") {
fetchWithSsrFGuardMock
.mockResolvedValueOnce({
response: new Response(JSON.stringify({ prompt_id: promptId }), {
status: 200,
headers: { "content-type": "application/json" },
}),
release: vi.fn(async () => {}),
})
.mockResolvedValueOnce({
response: new Response(
JSON.stringify({
[promptId]: {
outputs: {
"9": {
images: [{ filename: "generated.png", subfolder: "", type: "output" }],
},
},
},
}),
{
status: 200,
headers: { "content-type": "application/json" },
},
),
release: vi.fn(async () => {}),
})
.mockResolvedValueOnce({
response: new Response(Buffer.from("png-data"), {
status: 200,
headers: { "content-type": "image/png" },
}),
release: vi.fn(async () => {}),
});
}
const COMFY_SERVICE_HOST_LOCAL_POLICY = {
allowedOrigins: ["http://comfyui:8188"],
hostnameAllowlist: ["comfyui"],
};
const COMFY_SERVICE_HOST_EXPLICIT_PRIVATE_NETWORK_POLICY = {
allowedOrigins: ["http://comfyui:8188"],
};
const COMFY_PUBLIC_LOCAL_HOST_POLICY = {
hostnameAllowlist: ["images.example.com"],
};
function testWorkflowConfig(config: Record<string, unknown> = {}) {
return {
workflow: {
"6": { inputs: { text: "" } },
"9": { inputs: {} },
},
promptNodeId: "6",
outputNodeId: "9",
...config,
};
}
function toFetchUrl(input: RequestInfo | URL): string {
if (typeof input === "string") {
return input;
}
if (input instanceof URL) {
return input.toString();
}
return input.url;
}
function jsonResponse(body: unknown): Response {
return new Response(JSON.stringify(body), {
status: 200,
headers: { "content-type": "application/json" },
});
}
function generatedHistory(promptId: string) {
return {
[promptId]: {
outputs: {
"9": {
images: [{ filename: "generated.png", subfolder: "", type: "output" }],
},
},
},
};
}
function createLookupFn(dns: Record<string, string>): RealGuardLookupFn {
return (async (hostname: string, options?: unknown) => {
const normalized = hostname.toLowerCase().replace(/\.+$/u, "");
const address = dns[normalized] ?? "93.184.216.34";
const record: LookupAddress = {
address,
family: address.includes(":") ? 6 : 4,
};
if (
typeof options === "object" &&
options !== null &&
(options as { all?: unknown }).all === true
) {
return [record];
}
return record;
}) as RealGuardLookupFn;
}
function installRealComfyFetchGuard(options: RealComfyFetchOptions): RealGuardHarness {
const promptId = options.promptId ?? "real-guard-prompt-1";
const body = options.body ?? Buffer.from("png-data");
const contentType = options.contentType ?? "image/png";
const fetchUrls: string[] = [];
const guardCalls: RealGuardParams[] = [];
const lookupFn = createLookupFn(options.dns);
const fetchImpl: RealGuardFetchImpl = async (input) => {
const url = toFetchUrl(input);
fetchUrls.push(url);
const parsed = new URL(url);
if (parsed.pathname.endsWith("/prompt")) {
return jsonResponse({ prompt_id: promptId });
}
if (parsed.pathname === `/api/job/${promptId}/status`) {
return jsonResponse({ status: "completed" });
}
if (
parsed.pathname === `/history/${promptId}` ||
parsed.pathname === `/api/history_v2/${promptId}`
) {
return jsonResponse(generatedHistory(promptId));
}
if (parsed.pathname === "/view" || parsed.pathname === "/api/view") {
if (options.redirectLocation) {
return new Response(null, {
status: 302,
headers: { location: options.redirectLocation },
});
}
return new Response(new Uint8Array(body), {
status: 200,
headers: { "content-type": contentType },
});
}
return new Response(new Uint8Array(body), {
status: 200,
headers: { "content-type": contentType },
});
};
setComfyFetchGuardForTesting(async (params) => {
guardCalls.push(params);
return await fetchWithSsrFGuard({
...params,
fetchImpl,
lookupFn,
});
});
return { fetchUrls, guardCalls };
}
describe("comfy image-generation provider", () => {
beforeEach(() => {
vi.clearAllMocks();
@@ -204,6 +383,352 @@ describe("comfy image-generation provider", () => {
});
});
it("honors local private-network access for service-discovery hostnames", async () => {
setComfyFetchGuardForTesting(fetchWithSsrFGuardMock);
mockLocalImageResponses("compose-prompt-1");
const provider = buildComfyImageGenerationProvider();
await provider.generateImage({
provider: "comfy",
model: "workflow",
prompt: "draw a lobster",
cfg: buildComfyConfig({
baseUrl: "http://comfyui:8188",
workflow: {
"6": { inputs: { text: "" } },
"9": { inputs: {} },
},
promptNodeId: "6",
outputNodeId: "9",
}),
});
const submitRequest = fetchRequest(1);
expect(submitRequest.url).toBe("http://comfyui:8188/prompt");
expect(submitRequest.policy).toEqual(COMFY_SERVICE_HOST_LOCAL_POLICY);
expect(fetchRequest(2).policy).toEqual(COMFY_SERVICE_HOST_LOCAL_POLICY);
expect(fetchRequest(3).policy).toEqual(COMFY_SERVICE_HOST_LOCAL_POLICY);
});
it("keeps local public-looking hostnames strict without explicit private-network access", async () => {
setComfyFetchGuardForTesting(fetchWithSsrFGuardMock);
mockLocalImageResponses("public-host-prompt-1");
const provider = buildComfyImageGenerationProvider();
await provider.generateImage({
provider: "comfy",
model: "workflow",
prompt: "draw a lobster",
cfg: buildComfyConfig({
baseUrl: "http://images.example.com:8188",
workflow: {
"6": { inputs: { text: "" } },
"9": { inputs: {} },
},
promptNodeId: "6",
outputNodeId: "9",
}),
});
expect(fetchRequest(1).url).toBe("http://images.example.com:8188/prompt");
expect(fetchRequest(1).policy).toEqual(COMFY_PUBLIC_LOCAL_HOST_POLICY);
});
it("keeps cloud service-discovery hostnames strict without explicit private-network access", async () => {
setComfyFetchGuardForTesting(fetchWithSsrFGuardMock);
mockComfyCloudJobResponses(fetchWithSsrFGuardMock, {
body: Buffer.from("cloud-data"),
contentType: "image/png",
filename: "cloud.png",
outputKind: "images",
promptId: "strict-cloud-job-1",
redirectLocation: "https://cdn.example.com/cloud.png",
});
const provider = buildComfyImageGenerationProvider();
await provider.generateImage({
provider: "comfy",
model: "workflow",
prompt: "cloud workflow prompt",
cfg: buildComfyConfig({
mode: "cloud",
apiKey: "comfy-test-key",
baseUrl: "http://comfyui:8188",
workflow: {
"6": { inputs: { text: "" } },
"9": { inputs: {} },
},
promptNodeId: "6",
outputNodeId: "9",
}),
});
expect(fetchRequest(1).url).toBe("http://comfyui:8188/api/prompt");
expect(fetchRequest(1).policy).toBeUndefined();
});
it("honors explicit cloud private-network access for service-discovery hostnames", async () => {
setComfyFetchGuardForTesting(fetchWithSsrFGuardMock);
mockComfyCloudJobResponses(fetchWithSsrFGuardMock, {
body: Buffer.from("cloud-data"),
contentType: "image/png",
filename: "cloud.png",
outputKind: "images",
promptId: "private-cloud-job-1",
redirectLocation: "https://cdn.example.com/cloud.png",
});
const provider = buildComfyImageGenerationProvider();
await provider.generateImage({
provider: "comfy",
model: "workflow",
prompt: "cloud workflow prompt",
cfg: buildComfyConfig({
mode: "cloud",
apiKey: "comfy-test-key",
baseUrl: "http://comfyui:8188",
allowPrivateNetwork: true,
workflow: {
"6": { inputs: { text: "" } },
"9": { inputs: {} },
},
promptNodeId: "6",
outputNodeId: "9",
}),
});
expect(fetchRequest(1).url).toBe("http://comfyui:8188/api/prompt");
expect(fetchRequest(1).policy).toEqual(COMFY_SERVICE_HOST_EXPLICIT_PRIVATE_NETWORK_POLICY);
expect(fetchRequest(2).policy).toEqual(COMFY_SERVICE_HOST_EXPLICIT_PRIVATE_NETWORK_POLICY);
expect(fetchRequest(3).policy).toEqual(COMFY_SERVICE_HOST_EXPLICIT_PRIVATE_NETWORK_POLICY);
expect(fetchRequest(4).policy).toEqual(COMFY_SERVICE_HOST_EXPLICIT_PRIVATE_NETWORK_POLICY);
expect(fetchWithSsrFGuardMock).toHaveBeenCalledTimes(4);
});
it("allows local single-label hostnames that resolve to RFC1918 addresses", async () => {
const harness = installRealComfyFetchGuard({
dns: { comfyui: "10.0.0.25" },
});
const provider = buildComfyImageGenerationProvider();
const result = await provider.generateImage({
provider: "comfy",
model: "workflow",
prompt: "draw a lobster",
cfg: buildComfyConfig(
testWorkflowConfig({
baseUrl: "http://comfyui:8188",
}),
),
});
expect(harness.fetchUrls).toContain("http://comfyui:8188/prompt");
expect(result.images[0]?.buffer).toEqual(Buffer.from("png-data"));
});
it("blocks local public-looking FQDNs resolving private without explicit opt-in", async () => {
const harness = installRealComfyFetchGuard({
dns: { "images.example.com": "10.0.0.25" },
});
const provider = buildComfyImageGenerationProvider();
await expect(
provider.generateImage({
provider: "comfy",
model: "workflow",
prompt: "draw a lobster",
cfg: buildComfyConfig(
testWorkflowConfig({
baseUrl: "http://images.example.com:8188",
}),
),
}),
).rejects.toThrow("Blocked: resolves to private/internal/special-use IP address");
expect(harness.fetchUrls).toEqual([]);
});
it("allows local private-DNS FQDNs with explicit opt-in", async () => {
const harness = installRealComfyFetchGuard({
dns: { "comfy.private.example.com": "10.0.0.25" },
});
const provider = buildComfyImageGenerationProvider();
const result = await provider.generateImage({
provider: "comfy",
model: "workflow",
prompt: "draw a lobster",
cfg: buildComfyConfig(
testWorkflowConfig({
baseUrl: "http://comfy.private.example.com:8188",
allowPrivateNetwork: true,
}),
),
});
expect(harness.fetchUrls).toContain("http://comfy.private.example.com:8188/prompt");
expect(result.images[0]?.buffer).toEqual(Buffer.from("png-data"));
});
it("blocks explicit private-DNS FQDNs resolving to metadata addresses", async () => {
const harness = installRealComfyFetchGuard({
dns: { "comfy.private.example.com": "169.254.169.254" },
});
const provider = buildComfyImageGenerationProvider();
await expect(
provider.generateImage({
provider: "comfy",
model: "workflow",
prompt: "draw a lobster",
cfg: buildComfyConfig(
testWorkflowConfig({
baseUrl: "http://comfy.private.example.com:8188",
allowPrivateNetwork: true,
}),
),
}),
).rejects.toThrow("Blocked: resolves to private/internal/special-use IP address");
expect(harness.fetchUrls).toEqual([]);
});
it.each([
[
"subdomain",
"http://assets.comfyui:8188/generated.png",
{ comfyui: "10.0.0.25", "assets.comfyui": "10.0.0.26" },
],
[
"different hostname",
"http://other-comfy:8188/generated.png",
{ comfyui: "10.0.0.25", "other-comfy": "10.0.0.26" },
],
[
"same hostname private alternate port",
"http://comfyui:8288/generated.png",
{ comfyui: "10.0.0.25" },
],
[
"public CDN hostname",
"https://cdn.example.com/generated.png",
{ comfyui: "10.0.0.25", "cdn.example.com": "93.184.216.34" },
],
])("blocks local output redirects to %s", async (_label, redirectLocation, dns) => {
const harness = installRealComfyFetchGuard({
dns,
redirectLocation,
});
const provider = buildComfyImageGenerationProvider();
await expect(
provider.generateImage({
provider: "comfy",
model: "workflow",
prompt: "draw a lobster",
cfg: buildComfyConfig(
testWorkflowConfig({
baseUrl: "http://comfyui:8188",
}),
),
}),
).rejects.toThrow("Blocked");
expect(harness.fetchUrls).not.toContain(redirectLocation);
});
it("blocks local public FQDN redirects to other public hosts", async () => {
const redirectLocation = "https://cdn.example.com/generated.png";
const harness = installRealComfyFetchGuard({
dns: {
"comfy.example.com": "93.184.216.34",
"cdn.example.com": "93.184.216.35",
},
redirectLocation,
});
const provider = buildComfyImageGenerationProvider();
await expect(
provider.generateImage({
provider: "comfy",
model: "workflow",
prompt: "draw a lobster",
cfg: buildComfyConfig(
testWorkflowConfig({
baseUrl: "https://comfy.example.com",
}),
),
}),
).rejects.toThrow("Blocked hostname (not in allowlist)");
expect(harness.fetchUrls).not.toContain(redirectLocation);
});
it("allows explicit private cloud origins redirecting to public CDNs", async () => {
const harness = installRealComfyFetchGuard({
dns: {
"private-comfy.example.com": "10.0.0.25",
"cdn.example.com": "93.184.216.34",
},
redirectLocation: "https://cdn.example.com/generated.png",
body: Buffer.from("cdn-data"),
});
const provider = buildComfyImageGenerationProvider();
const result = await provider.generateImage({
provider: "comfy",
model: "workflow",
prompt: "cloud workflow prompt",
cfg: buildComfyConfig(
testWorkflowConfig({
mode: "cloud",
apiKey: "comfy-test-key",
baseUrl: "https://private-comfy.example.com",
allowPrivateNetwork: true,
}),
),
});
expect(harness.fetchUrls).toContain("https://cdn.example.com/generated.png");
expect(harness.guardCalls).toHaveLength(4);
expect(result.images[0]?.buffer).toEqual(Buffer.from("cdn-data"));
});
it.each([
[
"private DNS destination",
"http://other-private.example.com/generated.png",
{
"private-comfy.example.com": "10.0.0.25",
"other-private.example.com": "10.0.0.26",
},
],
[
"metadata destination",
"http://169.254.169.254/latest/meta-data",
{ "private-comfy.example.com": "10.0.0.25" },
],
])("blocks explicit private cloud redirects to %s", async (_label, redirectLocation, dns) => {
const harness = installRealComfyFetchGuard({
dns,
redirectLocation,
});
const provider = buildComfyImageGenerationProvider();
await expect(
provider.generateImage({
provider: "comfy",
model: "workflow",
prompt: "cloud workflow prompt",
cfg: buildComfyConfig(
testWorkflowConfig({
mode: "cloud",
apiKey: "comfy-test-key",
baseUrl: "https://private-comfy.example.com",
allowPrivateNetwork: true,
}),
),
}),
).rejects.toThrow("Blocked");
expect(harness.fetchUrls).not.toContain(redirectLocation);
});
it("caps oversized local workflow timeouts", async () => {
setComfyFetchGuardForTesting(fetchWithSsrFGuardMock);
const nowSpy = vi.spyOn(Date, "now");
@@ -481,9 +1006,7 @@ describe("comfy image-generation provider", () => {
"https://cloud.comfy.org/api/view?filename=cloud.png&subfolder=&type=output",
);
expect(viewRequest.auditContext).toBe("comfy-image-download");
const cdnRequest = fetchRequest(5);
expect(cdnRequest.url).toBe("https://cdn.example.com/cloud.png");
expect(cdnRequest.auditContext).toBe("comfy-image-download");
expect(fetchWithSsrFGuardMock).toHaveBeenCalledTimes(4);
expect(result.metadata).toEqual({
promptId: "cloud-job-1",
outputNodeIds: ["9"],
+1 -9
View File
@@ -17,7 +17,7 @@ type ComfyCloudJobResponseOptions = {
filename: string;
outputKind: "gifs" | "images";
promptId: string;
redirectLocation: string;
redirectLocation?: string;
};
export function buildComfyConfig(config: Record<string, unknown>): OpenClawConfig {
@@ -79,14 +79,6 @@ export function mockComfyCloudJobResponses(
},
}),
)
.mockResolvedValueOnce(
fetchGuardResponse(
new Response(null, {
status: 302,
headers: { location: options.redirectLocation },
}),
),
)
.mockResolvedValueOnce(
fetchGuardResponse(
new Response(options.body, {
+40 -48
View File
@@ -21,11 +21,10 @@ import {
resolveSecretInputString,
} from "openclaw/plugin-sdk/secret-input-runtime";
import {
buildHostnameAllowlistPolicyFromSuffixAllowlist,
fetchWithSsrFGuard,
isPrivateOrLoopbackHost,
mergeSsrFPolicies,
ssrfPolicyFromDangerouslyAllowPrivateNetwork,
ssrfPolicyFromHttpBaseUrlAllowedOrigin,
type SsrFPolicy,
} from "openclaw/plugin-sdk/ssrf-runtime";
import {
@@ -280,6 +279,8 @@ function setWorkflowInput(params: {
function resolveComfyNetworkPolicy(params: {
baseUrl: string;
allowPrivateNetwork: boolean;
explicitAllowPrivateNetwork: boolean;
mode: ComfyMode;
}): ComfyNetworkPolicy {
let parsed: URL;
try {
@@ -289,17 +290,45 @@ function resolveComfyNetworkPolicy(params: {
}
const hostname = normalizeOptionalLowercaseString(parsed.hostname) ?? "";
if (!hostname || !params.allowPrivateNetwork || !isPrivateOrLoopbackHost(hostname)) {
if (!hostname) {
return {};
}
const localHostnamePolicy: SsrFPolicy | undefined =
params.mode === "local" ? { hostnameAllowlist: [hostname] } : undefined;
const hostnameOnlyPolicy = localHostnamePolicy ? { apiPolicy: localHostnamePolicy } : {};
if (!params.allowPrivateNetwork) {
return hostnameOnlyPolicy;
}
// Local mode auto-trusts loopback/IP targets and Compose-style single-label
// service names; public-looking FQDNs require the operator's explicit
// allowPrivateNetwork opt-in.
if (!params.explicitAllowPrivateNetwork && params.mode !== "local") {
return {};
}
if (
!params.explicitAllowPrivateNetwork &&
params.mode === "local" &&
!isPrivateOrLoopbackHost(hostname) &&
!isSingleLabelServiceHostname(hostname)
) {
return hostnameOnlyPolicy;
}
const originPolicy = ssrfPolicyFromHttpBaseUrlAllowedOrigin(params.baseUrl);
if (!originPolicy) {
return hostnameOnlyPolicy;
}
const hostnamePolicy = buildHostnameAllowlistPolicyFromSuffixAllowlist([hostname]);
const privateNetworkPolicy = ssrfPolicyFromDangerouslyAllowPrivateNetwork(true);
return {
apiPolicy: mergeSsrFPolicies(hostnamePolicy, privateNetworkPolicy),
apiPolicy:
params.mode === "local" ? mergeSsrFPolicies(originPolicy, localHostnamePolicy) : originPolicy,
};
}
function isSingleLabelServiceHostname(hostname: string): boolean {
return /^[a-z0-9_](?:[a-z0-9_-]{0,61}[a-z0-9_])?$/u.test(hostname);
}
async function readJsonResponse<T>(params: {
url: string;
init?: RequestInit;
@@ -561,7 +590,6 @@ async function downloadOutputFile(params: {
init: {
method: "GET",
headers: params.headers,
...(params.mode === "cloud" ? { redirect: "manual" } : {}),
},
timeoutMs: params.timeoutMs,
policy: params.policy,
@@ -570,45 +598,6 @@ async function downloadOutputFile(params: {
});
try {
if (
params.mode === "cloud" &&
[301, 302, 303, 307, 308].includes(firstResponse.response.status)
) {
const redirectUrl = normalizeOptionalString(firstResponse.response.headers.get("location"));
if (!redirectUrl) {
throw new Error("Comfy cloud output redirect missing location header");
}
const redirected = await comfyFetchGuard({
url: redirectUrl,
init: {
method: "GET",
},
timeoutMs: params.timeoutMs,
dispatcherPolicy: params.dispatcherPolicy,
auditContext,
});
try {
await assertOkOrThrowHttpError(redirected.response, "Comfy output download failed");
const mimeType =
normalizeOptionalString(redirected.response.headers.get("content-type")) ||
"application/octet-stream";
return {
buffer: await readResponseWithLimit(redirected.response, params.maxBytes, {
chunkTimeoutMs: params.timeoutMs,
onOverflow: ({ maxBytes }) =>
new Error(`Comfy ${params.capability} output download exceeds ${maxBytes} bytes`),
onIdleTimeout: ({ chunkTimeoutMs }) =>
new Error(
`Comfy ${params.capability} output download stalled after ${chunkTimeoutMs}ms`,
),
}),
mimeType,
};
} finally {
await redirected.release();
}
}
await assertOkOrThrowHttpError(firstResponse.response, "Comfy output download failed");
const mimeType =
normalizeOptionalString(firstResponse.response.headers.get("content-type")) ||
@@ -720,13 +709,14 @@ export async function runComfyWorkflow(params: {
throw new Error("Comfy Cloud API key missing");
}
const explicitAllowPrivateNetwork =
readConfigBoolean(capabilityConfig, "allowPrivateNetwork") === true;
const { baseUrl, allowPrivateNetwork, headers, dispatcherPolicy } =
resolveProviderHttpRequestConfig({
baseUrl: normalizeOptionalString(capabilityConfig.baseUrl),
defaultBaseUrl:
mode === "cloud" ? DEFAULT_COMFY_CLOUD_BASE_URL : DEFAULT_COMFY_LOCAL_BASE_URL,
allowPrivateNetwork:
mode === "local" || readConfigBoolean(capabilityConfig, "allowPrivateNetwork") === true,
allowPrivateNetwork: mode === "local" || explicitAllowPrivateNetwork,
defaultHeaders:
mode === "cloud"
? {
@@ -746,6 +736,8 @@ export async function runComfyWorkflow(params: {
const networkPolicy = resolveComfyNetworkPolicy({
baseUrl: normalizedBaseUrl,
allowPrivateNetwork,
explicitAllowPrivateNetwork,
mode,
});
if (params.inputImage) {