Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 10 additions & 3 deletions src/lib/destination-policy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -130,13 +130,20 @@ function registryAllowsPrivateNetwork(name: string): boolean {
return getProviderRegistryEntry(name)?.allowPrivateNetworkByDefault === true;
}

/** True when the provider config or registry default admits private/loopback destinations. */
export function providerAllowsPrivateNetwork(
name: string,
provider: Pick<OcxProviderConfig, "allowPrivateNetwork">,
): boolean {
return provider.allowPrivateNetwork === true || registryAllowsPrivateNetwork(name);
}

export function providerDestinationConfigError(name: string, provider: Pick<OcxProviderConfig, "baseUrl" | "allowPrivateNetwork">): string | null {
const assessment = assessDestination(provider.baseUrl);
if (!assessment) return null;
if (assessment.kind === "public" || assessment.kind === "hostname") return null;
if (assessment.kind === "metadata") return "baseUrl targets a blocked metadata endpoint";
if (registryAllowsPrivateNetwork(name)) return null;
if (provider.allowPrivateNetwork === true) return null;
if (providerAllowsPrivateNetwork(name, provider)) return null;
return `baseUrl points to a ${assessment.detail}; set allowPrivateNetwork:true only for intentionally local/self-hosted providers`;
}

Expand Down Expand Up @@ -174,7 +181,7 @@ export async function providerDestinationResolvedError(
if (!hostname || isIP(hostname) !== 0 || hostname === "localhost" || hostname.endsWith(".localhost")) {
return null; // literals and localhost are fully handled by the sync path
}
if (registryAllowsPrivateNetwork(name) || provider.allowPrivateNetwork === true) return null;
if (providerAllowsPrivateNetwork(name, provider)) return null;
let addresses: { address: string }[];
try {
addresses = await lookup(hostname, { all: true, verbatim: true });
Expand Down
7 changes: 5 additions & 2 deletions src/lib/provider-outbound.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import type { OcxProviderConfig } from "../types";
import {
assessUrlDestination,
DestinationDnsResolutionError,
providerAllowsPrivateNetwork,
providerDestinationConfigError,
resolvePublicAddresses,
} from "./destination-policy";
Expand Down Expand Up @@ -116,7 +117,8 @@ export async function providerOutboundGet(
if (assessment?.kind === "metadata" || assessment?.kind === "link-local" || assessment?.kind === "unspecified") {
throw new ProviderOutboundPolicyError(`provider URL targets ${assessment.detail}`);
}
if (!provider.allowPrivateNetwork) {
const allowPrivate = providerAllowsPrivateNetwork(name, provider);
if (!allowPrivate) {
const destinationError = providerDestinationConfigError(name, {
baseUrl: url,
allowPrivateNetwork: false,
Expand All @@ -129,11 +131,12 @@ export async function providerOutboundGet(
const proxyConfigured = configuredProxyFor();
const resolveAddresses = dependencies.resolveAddresses ?? resolvePublicAddresses;
const pinnedGet = dependencies.pinnedGet ?? pinnedHttpGet;
const allowPrivate = providerAllowsPrivateNetwork(name, provider);
let resolved: Awaited<ReturnType<typeof resolvePublicAddresses>>;
try {
resolved = await resolveAddresses(url, {
context: "provider URL",
allowPrivateNetwork: provider.allowPrivateNetwork,
allowPrivateNetwork: allowPrivate,
});
} catch (error) {
const dnsResolutionFailed = error instanceof DestinationDnsResolutionError
Expand Down
30 changes: 30 additions & 0 deletions tests/provider-outbound.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,36 @@ describe("provider outbound GET transport", () => {
expect(captured.address).toBeUndefined();
});

test("built-in ollama admits loopback discovery without an explicit allowPrivateNetwork flag (#758)", async () => {
for (const key of proxyKeys) delete process.env[key];
const { providerOutboundGet } = await import("../src/lib/provider-outbound");
let sawAllowPrivate: boolean | undefined;
const dependencies: ProviderOutboundDependencies = {
resolveAddresses: mock(async (_url, options) => {
sawAllowPrivate = typeof options === "object" && options?.allowPrivateNetwork === true;
return {
hostname: "127.0.0.1",
addresses: [{ address: "127.0.0.1", family: 4 }],
privateNetwork: true,
};
}),
pinnedGet: mock(async () => new Response('{"data":[{"id":"llama"}]}', {
status: 200,
headers: { "content-type": "application/json" },
})),
};

const response = await providerOutboundGet(
"ollama",
{ baseUrl: "http://127.0.0.1:11434/v1" },
"http://127.0.0.1:11434/v1/models",
{},
dependencies,
);
expect(sawAllowPrivate).toBe(true);
expect(await response.json()).toEqual({ data: [{ id: "llama" }] });
});

test("direct redirects return the same credential-safe final-URL guidance", async () => {
for (const key of proxyKeys) delete process.env[key];
const redirectTarget = new URL("https://final.example/v1/models?token=secret#fragment");
Expand Down
Loading