From cb6f5a7b1f0c7c5ec172f0b0e0e4acbd05368748 Mon Sep 17 00:00:00 2001 From: yansigit Date: Fri, 18 Sep 2026 13:45:29 -0600 Subject: [PATCH 01/14] feat(transport): add opt-in provider TLS profile boundary --- scripts/test-layout/layout.json | 2 + src/config/schema/config-schema.ts | 9 +++ src/config/schema/leaf-validators.ts | 1 + src/lib/provider-runtime-fetch.ts | 23 ++++++ src/lib/provider-tls-profile.ts | 80 +++++++++++++++++++ src/server/auth-cors.ts | 1 + src/server/responses/fetch-helpers.ts | 4 +- src/types/provider.ts | 2 + tests/fixtures/test-layout-expected.json | 2 + .../providers/provider-runtime-fetch.test.ts | 10 +++ tests/providers/provider-tls-profile.test.ts | 38 +++++++++ 11 files changed, 171 insertions(+), 1 deletion(-) create mode 100644 src/lib/provider-runtime-fetch.ts create mode 100644 src/lib/provider-tls-profile.ts create mode 100644 tests/providers/provider-runtime-fetch.test.ts create mode 100644 tests/providers/provider-tls-profile.test.ts diff --git a/scripts/test-layout/layout.json b/scripts/test-layout/layout.json index b4d27b9425e..042697135c5 100644 --- a/scripts/test-layout/layout.json +++ b/scripts/test-layout/layout.json @@ -1324,6 +1324,8 @@ "provider-model-discovery-contract.test.ts": "providers", "provider-outbound-private-network.test.ts": "providers", "provider-outbound.test.ts": "providers", + "provider-runtime-fetch.test.ts": "providers", + "provider-tls-profile.test.ts": "providers", "provider-payload.test.ts": "gui", "provider-quota-label-sanitize.test.ts": "providers", "provider-quota-observed-marker.test.ts": "providers", diff --git a/src/config/schema/config-schema.ts b/src/config/schema/config-schema.ts index 6febc06293f..e9841f30c46 100644 --- a/src/config/schema/config-schema.ts +++ b/src/config/schema/config-schema.ts @@ -57,6 +57,7 @@ import { COMBO_NAMESPACE, comboConfigIssues } from "../../combos/types"; import { routingProfileIssues } from "../../routing/profile"; import { POLICY_NAMESPACE } from "../../routing/profile-namespace"; import { providerDestinationConfigError } from "../../lib/destination-policy"; +import { providerTlsProfileConfigError } from "../../lib/provider-tls-profile"; import { redactSecretString } from "../../lib/redact"; import { openRouterRoutingConfigError } from "../../providers/openrouter-routing"; import { vercelGatewayRoutingConfigError } from "../../providers/vercel-gateway-routing"; @@ -470,6 +471,14 @@ export const configSchema = z.object({ }); } } + const tlsProfileError = providerTlsProfileConfigError(name, provider); + if (tlsProfileError) { + ctx.addIssue({ + code: "custom", + path: ["providers", redactSecretString(name), "tlsProfile"], + message: tlsProfileError, + }); + } const headersError = providerHeadersConfigError((provider as { headers?: unknown }).headers); if (headersError) { ctx.addIssue({ diff --git a/src/config/schema/leaf-validators.ts b/src/config/schema/leaf-validators.ts index bce878456d4..654c1217a32 100644 --- a/src/config/schema/leaf-validators.ts +++ b/src/config/schema/leaf-validators.ts @@ -294,6 +294,7 @@ export const providerConfigSchema = z.object({ autoReviewModelOverrides: autoReviewModelOverridesSchema.optional(), adapter: z.string().min(1), baseUrl: z.string().min(1), + tlsProfile: z.literal("antigravity-browser").optional(), alias: z.string().optional(), modelAliases: z.record(z.string(), z.string()).optional(), modelDisplayNames: modelDisplayNamesSchema.optional(), diff --git a/src/lib/provider-runtime-fetch.ts b/src/lib/provider-runtime-fetch.ts new file mode 100644 index 00000000000..1bdf9bdb4a7 --- /dev/null +++ b/src/lib/provider-runtime-fetch.ts @@ -0,0 +1,23 @@ +import type { OcxProviderConfig } from "../types"; + +export const RUNTIME_PROVIDER_FETCH = Symbol("opencodex.provider.runtime-fetch"); + +export interface RuntimeProviderFetch { + providerName: string; + origins: readonly string[]; + fetch: typeof globalThis.fetch; +} + +/** Return only an explicitly injected executor matching the provider and exact destination origin. */ +export function runtimeProviderFetch( + provider: OcxProviderConfig, + providerName: string | undefined, +): typeof globalThis.fetch | undefined { + const runtime = (provider as OcxProviderConfig & { [RUNTIME_PROVIDER_FETCH]?: RuntimeProviderFetch })[RUNTIME_PROVIDER_FETCH]; + if (!runtime || runtime.providerName !== providerName) return undefined; + try { + return runtime.origins.includes(new URL(provider.baseUrl).origin) ? runtime.fetch : undefined; + } catch { + return undefined; + } +} diff --git a/src/lib/provider-tls-profile.ts b/src/lib/provider-tls-profile.ts new file mode 100644 index 00000000000..9ae51b01a05 --- /dev/null +++ b/src/lib/provider-tls-profile.ts @@ -0,0 +1,80 @@ +import type { OcxProviderConfig } from "../types"; +import { redactSecretString } from "./redact"; +import { runtimeProviderFetch } from "./provider-runtime-fetch"; + +export type ProviderTlsProfile = "antigravity-browser"; +export type ProviderTlsProfileStatus = "disabled" | "active" | "fallback"; +export const ANTIGRAVITY_TLS_HOSTS = new Set(["daily-cloudcode-pa.googleapis.com", "cloudcode-pa.googleapis.com"]); +type TlsRuntime = { fetch(input: string | URL | Request, init?: RequestInit): Promise }; +let status = new Map(); +let runtime: TlsRuntime | undefined; + +export function isCanonicalAntigravityUrl(input: string | URL): boolean { + try { + const url = new URL(input); + return url.protocol === "https:" && (url.port === "" || url.port === "443") + && !url.username && !url.password && ANTIGRAVITY_TLS_HOSTS.has(url.hostname.toLowerCase()); + } catch { + return false; + } +} + +export function providerTlsProfileConfigError( + providerName: string, + provider: Pick, +): string | null { + if (provider.tlsProfile === undefined) return null; + if (provider.tlsProfile !== "antigravity-browser") return "tlsProfile must be antigravity-browser"; + if (providerName !== "google-antigravity" || provider.adapter !== "google" || provider.authMode !== "oauth" + || provider.googleMode !== "cloud-code-assist" || !isCanonicalAntigravityUrl(provider.baseUrl)) { + return "tlsProfile antigravity-browser requires the canonical Google Antigravity OAuth destination"; + } + return null; +} + +export function getProviderTlsProfileStatus(name: string): ProviderTlsProfileStatus { + return status.get(name) ?? "disabled"; +} + +export function resetProviderTlsProfileForTests(): void { + status = new Map(); + runtime = undefined; +} + +export function setProviderTlsRuntimeForTest(next: TlsRuntime | undefined): void { + runtime = next; +} + +export function providerTlsFetch( + name: string, + provider: Pick, + fallback: typeof globalThis.fetch, +): typeof globalThis.fetch { + if (provider.tlsProfile === undefined) { + status.set(name, "disabled"); + return fallback; + } + if (providerTlsProfileConfigError(name, provider)) { + status.set(name, "fallback"); + return (async () => { throw new Error("invalid provider TLS profile"); }) as unknown as typeof globalThis.fetch; + } + return (async (input, init) => { + const destination = typeof input === "string" || input instanceof URL ? input : input.url; + if (!isCanonicalAntigravityUrl(destination)) throw new Error("provider TLS profile refused noncanonical destination"); + try { + const configured = runtimeProviderFetch(provider as OcxProviderConfig, name); + const mod = runtime ?? (await import("wreq-js") as unknown as TlsRuntime); + const response = await (configured ?? mod.fetch)(input, { + ...init, + redirect: "manual", + browser: "chrome_142", + os: "windows", + } as RequestInit & { browser: string; os: string }); + status.set(name, "active"); + return response; + } catch (error) { + status.set(name, "fallback"); + throw new Error(redactSecretString(error instanceof Error ? error.message : "provider TLS transport failed")); + } + }) as typeof globalThis.fetch; +} diff --git a/src/server/auth-cors.ts b/src/server/auth-cors.ts index e53662b844e..8899e8a101c 100644 --- a/src/server/auth-cors.ts +++ b/src/server/auth-cors.ts @@ -1110,6 +1110,7 @@ const PROVIDER_CONFIG_FIELD_POLICY = { desktopExecutor: "redacted", unsafeAllowNativeLocalExec: "editor", nativeLocalExec: "editor", + tlsProfile: "editor", } as const satisfies Record; type ProviderFieldWithPolicy = { diff --git a/src/server/responses/fetch-helpers.ts b/src/server/responses/fetch-helpers.ts index c6cfd2525e5..b4bbe5af817 100644 --- a/src/server/responses/fetch-helpers.ts +++ b/src/server/responses/fetch-helpers.ts @@ -15,6 +15,7 @@ import { type ProviderRequestSlot, } from "../../providers/request-pacing"; import { withUpstreamHttpVersion } from "../../lib/upstream-http-version"; +import { providerTlsFetch } from "../../lib/provider-tls-profile"; import type { CodexWsQuotaObserver } from "./codex-ws-metadata"; import { configuredOutboundFetch } from "../../lib/proxy-env"; import { isLoopbackUrl, rewriteUpstream } from "../../plugins/upstream-hooks"; @@ -252,6 +253,7 @@ export function providerFetch( { preconnect: globalThis.fetch.preconnect?.bind(globalThis.fetch) }, ) as typeof globalThis.fetch); const base = customExecutor ?? configuredFetch; + const transport = options.providerName ? providerTlsFetch(options.providerName, provider, base) : base; const preconnect = (...args: Parameters): void => { base.preconnect?.(...args); }; @@ -266,7 +268,7 @@ export function providerFetch( // that decided for itself has already marked the init and this pass defers to that decision. const dispatch = markEgressTransparentExecutor(Object.assign( (input: Parameters[0], init?: RequestInit) => - sendWithConnectionPolicy(base, input, init, egressBinding), + sendWithConnectionPolicy(transport, input, init, egressBinding), { preconnect }, ) as typeof globalThis.fetch); const httpFetch = Object.assign( diff --git a/src/types/provider.ts b/src/types/provider.ts index e2fda139323..5b0ae797980 100644 --- a/src/types/provider.ts +++ b/src/types/provider.ts @@ -274,6 +274,8 @@ export interface ModelCapabilities { } export interface OcxProviderConfig { + /** Optional browser-compatible outbound TLS profile; disabled by default. */ + tlsProfile?: "antigravity-browser"; /** Optional short provider namespace used only at request/catalog presentation time. */ alias?: string; /** Native model id -> short, slash-free request alias. */ diff --git a/tests/fixtures/test-layout-expected.json b/tests/fixtures/test-layout-expected.json index 83170e64b6b..b60da597540 100644 --- a/tests/fixtures/test-layout-expected.json +++ b/tests/fixtures/test-layout-expected.json @@ -1335,6 +1335,8 @@ "provider-model-discovery-contract.test.ts": "providers", "provider-outbound-private-network.test.ts": "providers", "provider-outbound.test.ts": "providers", + "provider-runtime-fetch.test.ts": "providers", + "provider-tls-profile.test.ts": "providers", "provider-payload.test.ts": "gui", "provider-quota-label-sanitize.test.ts": "providers", "provider-quota-observed-marker.test.ts": "providers", diff --git a/tests/providers/provider-runtime-fetch.test.ts b/tests/providers/provider-runtime-fetch.test.ts new file mode 100644 index 00000000000..2fb545137ef --- /dev/null +++ b/tests/providers/provider-runtime-fetch.test.ts @@ -0,0 +1,10 @@ +import { expect, test } from "bun:test"; +import { RUNTIME_PROVIDER_FETCH, runtimeProviderFetch } from "../../src/lib/provider-runtime-fetch"; + +test("runtime provider fetch is scoped to provider and exact origin", () => { + const fetcher = async () => new Response("ok"); + const provider = { adapter: "openai-chat", baseUrl: "https://example.com", [RUNTIME_PROVIDER_FETCH]: { providerName: "p", origins: ["https://example.com"], fetch: fetcher } } as any; + expect(runtimeProviderFetch(provider, "p")).toBe(fetcher); + expect(runtimeProviderFetch(provider, "other")).toBeUndefined(); + expect(runtimeProviderFetch({ ...provider, baseUrl: "https://example.net" }, "p")).toBeUndefined(); +}); diff --git a/tests/providers/provider-tls-profile.test.ts b/tests/providers/provider-tls-profile.test.ts new file mode 100644 index 00000000000..0b3ba764d1c --- /dev/null +++ b/tests/providers/provider-tls-profile.test.ts @@ -0,0 +1,38 @@ +import { afterEach, describe, expect, test } from "bun:test"; +import { isCanonicalAntigravityUrl, providerTlsProfileConfigError, providerTlsFetch, resetProviderTlsProfileForTests, setProviderTlsRuntimeForTest } from "../../src/lib/provider-tls-profile"; + +afterEach(() => resetProviderTlsProfileForTests()); + +describe("provider TLS profile", () => { + test("accepts only canonical Antigravity HTTPS origins", () => { + expect(isCanonicalAntigravityUrl("https://cloudcode-pa.googleapis.com")).toBe(true); + expect(isCanonicalAntigravityUrl("https://cloudcode-pa.googleapis.com:443")).toBe(true); + expect(isCanonicalAntigravityUrl("http://cloudcode-pa.googleapis.com")).toBe(false); + expect(isCanonicalAntigravityUrl("https://evil.example")).toBe(false); + }); + + test("rejects malformed profiles before fallback dispatch", async () => { + const provider = { adapter: "openai-chat", authMode: "key", googleMode: undefined, baseUrl: "https://evil.example", tlsProfile: "antigravity-browser" as const }; + expect(providerTlsProfileConfigError("evil", provider)).toBeString(); + const fetcher = providerTlsFetch("evil", provider, async () => new Response("sent")); + await expect(fetcher("https://evil.example")).rejects.toThrow("invalid provider TLS profile"); + }); + + test("uses manual redirects, browser profile, and caller abort signal", async () => { + let seen: RequestInit | undefined; + setProviderTlsRuntimeForTest({ fetch: async (_input, init) => { seen = init; return new Response("ok"); } }); + const signal = new AbortController().signal; + const provider = { adapter: "google", authMode: "oauth", googleMode: "cloud-code-assist", baseUrl: "https://cloudcode-pa.googleapis.com", tlsProfile: "antigravity-browser" as const }; + await providerTlsFetch("google-antigravity", provider, fetch)("https://cloudcode-pa.googleapis.com/v1", { signal }); + expect(seen?.redirect).toBe("manual"); + expect(seen?.signal).toBe(signal); + expect((seen as any)?.browser).toBe("chrome_142"); + }); + + test("redacts credential text from transport errors", async () => { + setProviderTlsRuntimeForTest({ fetch: async () => { throw new Error("Authorization: Bearer super-secret"); } }); + const provider = { adapter: "google", authMode: "oauth", googleMode: "cloud-code-assist", baseUrl: "https://cloudcode-pa.googleapis.com", tlsProfile: "antigravity-browser" as const }; + await expect(providerTlsFetch("google-antigravity", provider, fetch)("https://cloudcode-pa.googleapis.com/v1")).rejects.toThrow("[REDACTED]"); + await expect(providerTlsFetch("google-antigravity", provider, fetch)("https://cloudcode-pa.googleapis.com/v1")).rejects.not.toThrow("super-secret"); + }); +}); From 9a6fbae1a92966cf9d78f8559c4d3de730f47c27 Mon Sep 17 00:00:00 2001 From: SB Yoon <44089734+yansigit@users.noreply.github.com> Date: Sat, 5 Sep 2026 18:49:24 -0600 Subject: [PATCH 02/14] fix(transport): preserve TLS aborts and proxy routing --- src/lib/provider-tls-profile.ts | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/src/lib/provider-tls-profile.ts b/src/lib/provider-tls-profile.ts index 9ae51b01a05..7259c02b698 100644 --- a/src/lib/provider-tls-profile.ts +++ b/src/lib/provider-tls-profile.ts @@ -1,6 +1,7 @@ import type { OcxProviderConfig } from "../types"; import { redactSecretString } from "./redact"; import { runtimeProviderFetch } from "./provider-runtime-fetch"; +import { resolveProxyRoute } from "./proxy-env"; export type ProviderTlsProfile = "antigravity-browser"; export type ProviderTlsProfileStatus = "disabled" | "active" | "fallback"; @@ -45,6 +46,15 @@ export function setProviderTlsRuntimeForTest(next: TlsRuntime | undefined): void runtime = next; } +function preserveTransportError(error: unknown): Error { + const message = redactSecretString(error instanceof Error ? error.message : "provider TLS transport failed"); + const name = error instanceof Error ? error.name : "Error"; + if (name === "AbortError" || name === "TimeoutError") return new DOMException(message, name); + const wrapped = new Error(message); + wrapped.name = name; + return wrapped; +} + export function providerTlsFetch( name: string, provider: Pick, @@ -64,17 +74,20 @@ export function providerTlsFetch( try { const configured = runtimeProviderFetch(provider as OcxProviderConfig, name); const mod = runtime ?? (await import("wreq-js") as unknown as TlsRuntime); + const proxyRoute = resolveProxyRoute(new URL(destination)); + if (proxyRoute.kind === "fallback") throw new Error("provider TLS profile cannot preserve configured proxy semantics"); const response = await (configured ?? mod.fetch)(input, { ...init, redirect: "manual", browser: "chrome_142", os: "windows", + ...(proxyRoute.kind === "proxy" ? { proxy: proxyRoute.proxy } : {}), } as RequestInit & { browser: string; os: string }); status.set(name, "active"); return response; } catch (error) { status.set(name, "fallback"); - throw new Error(redactSecretString(error instanceof Error ? error.message : "provider TLS transport failed")); + throw preserveTransportError(error); } }) as typeof globalThis.fetch; } From 2f18f8ff83054c4063758b27858341260a066fa0 Mon Sep 17 00:00:00 2001 From: SB Yoon <44089734+yansigit@users.noreply.github.com> Date: Sat, 5 Sep 2026 18:51:26 -0600 Subject: [PATCH 03/14] fix(transport): label failed TLS profile state accurately --- src/lib/provider-tls-profile.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/lib/provider-tls-profile.ts b/src/lib/provider-tls-profile.ts index 7259c02b698..953226b47b1 100644 --- a/src/lib/provider-tls-profile.ts +++ b/src/lib/provider-tls-profile.ts @@ -4,7 +4,7 @@ import { runtimeProviderFetch } from "./provider-runtime-fetch"; import { resolveProxyRoute } from "./proxy-env"; export type ProviderTlsProfile = "antigravity-browser"; -export type ProviderTlsProfileStatus = "disabled" | "active" | "fallback"; +export type ProviderTlsProfileStatus = "disabled" | "active" | "failed"; export const ANTIGRAVITY_TLS_HOSTS = new Set(["daily-cloudcode-pa.googleapis.com", "cloudcode-pa.googleapis.com"]); type TlsRuntime = { fetch(input: string | URL | Request, init?: RequestInit): Promise }; let status = new Map(); @@ -65,7 +65,7 @@ export function providerTlsFetch( return fallback; } if (providerTlsProfileConfigError(name, provider)) { - status.set(name, "fallback"); + status.set(name, "failed"); return (async () => { throw new Error("invalid provider TLS profile"); }) as unknown as typeof globalThis.fetch; } return (async (input, init) => { @@ -86,7 +86,7 @@ export function providerTlsFetch( status.set(name, "active"); return response; } catch (error) { - status.set(name, "fallback"); + status.set(name, "failed"); throw preserveTransportError(error); } }) as typeof globalThis.fetch; From 2b4ca58f84a0226b1b8e1624d4f32038fa4c88e2 Mon Sep 17 00:00:00 2001 From: SB Yoon <44089734+yansigit@users.noreply.github.com> Date: Sat, 5 Sep 2026 19:42:07 -0600 Subject: [PATCH 04/14] test(transport): admit TLS profile fetch boundary --- tests/responses/responses-fetch-helpers-boundary.test.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/responses/responses-fetch-helpers-boundary.test.ts b/tests/responses/responses-fetch-helpers-boundary.test.ts index 455c3d0d671..1f59e4ef4b0 100644 --- a/tests/responses/responses-fetch-helpers-boundary.test.ts +++ b/tests/responses/responses-fetch-helpers-boundary.test.ts @@ -46,6 +46,7 @@ describe("Responses fetch-helper import boundary", () => { test("loads only transport-owned runtime dependencies", () => { expect(expectRuntimeImportBoundary(readFileSync(helperPath, "utf8"))).toEqual([ "../../lib/provider-egress", + "../../lib/provider-tls-profile", "../../lib/proxy-env", "../../lib/redact", "../../lib/upstream-http-version", From 32103944c68c6819e9bbf0422611868ff830edc9 Mon Sep 17 00:00:00 2001 From: SB Yoon <44089734+yansigit@users.noreply.github.com> Date: Sat, 5 Sep 2026 21:02:44 -0600 Subject: [PATCH 05/14] test(transport): cover TLS proxy fail-closed status --- tests/providers/provider-tls-profile.test.ts | 174 +++++++++++++++++-- 1 file changed, 160 insertions(+), 14 deletions(-) diff --git a/tests/providers/provider-tls-profile.test.ts b/tests/providers/provider-tls-profile.test.ts index 0b3ba764d1c..fe3f95eb958 100644 --- a/tests/providers/provider-tls-profile.test.ts +++ b/tests/providers/provider-tls-profile.test.ts @@ -1,38 +1,184 @@ import { afterEach, describe, expect, test } from "bun:test"; -import { isCanonicalAntigravityUrl, providerTlsProfileConfigError, providerTlsFetch, resetProviderTlsProfileForTests, setProviderTlsRuntimeForTest } from "../../src/lib/provider-tls-profile"; +import { + getProviderTlsProfileStatus, + isCanonicalAntigravityUrl, + providerTlsProfileConfigError, + providerTlsFetch, + resetProviderTlsProfileForTests, + setProviderTlsRuntimeForTest, +} from "../../src/lib/provider-tls-profile"; afterEach(() => resetProviderTlsProfileForTests()); describe("provider TLS profile", () => { test("accepts only canonical Antigravity HTTPS origins", () => { - expect(isCanonicalAntigravityUrl("https://cloudcode-pa.googleapis.com")).toBe(true); - expect(isCanonicalAntigravityUrl("https://cloudcode-pa.googleapis.com:443")).toBe(true); - expect(isCanonicalAntigravityUrl("http://cloudcode-pa.googleapis.com")).toBe(false); + expect( + isCanonicalAntigravityUrl("https://cloudcode-pa.googleapis.com"), + ).toBe(true); + expect( + isCanonicalAntigravityUrl("https://cloudcode-pa.googleapis.com:443"), + ).toBe(true); + expect( + isCanonicalAntigravityUrl("http://cloudcode-pa.googleapis.com"), + ).toBe(false); expect(isCanonicalAntigravityUrl("https://evil.example")).toBe(false); }); test("rejects malformed profiles before fallback dispatch", async () => { - const provider = { adapter: "openai-chat", authMode: "key", googleMode: undefined, baseUrl: "https://evil.example", tlsProfile: "antigravity-browser" as const }; + const provider = { + adapter: "openai-chat", + authMode: "key", + googleMode: undefined, + baseUrl: "https://evil.example", + tlsProfile: "antigravity-browser" as const, + }; expect(providerTlsProfileConfigError("evil", provider)).toBeString(); - const fetcher = providerTlsFetch("evil", provider, async () => new Response("sent")); - await expect(fetcher("https://evil.example")).rejects.toThrow("invalid provider TLS profile"); + const fetcher = providerTlsFetch( + "evil", + provider, + async () => new Response("sent"), + ); + await expect(fetcher("https://evil.example")).rejects.toThrow( + "invalid provider TLS profile", + ); }); test("uses manual redirects, browser profile, and caller abort signal", async () => { let seen: RequestInit | undefined; - setProviderTlsRuntimeForTest({ fetch: async (_input, init) => { seen = init; return new Response("ok"); } }); + setProviderTlsRuntimeForTest({ + fetch: async (_input, init) => { + seen = init; + return new Response("ok"); + }, + }); const signal = new AbortController().signal; - const provider = { adapter: "google", authMode: "oauth", googleMode: "cloud-code-assist", baseUrl: "https://cloudcode-pa.googleapis.com", tlsProfile: "antigravity-browser" as const }; - await providerTlsFetch("google-antigravity", provider, fetch)("https://cloudcode-pa.googleapis.com/v1", { signal }); + const provider = { + adapter: "google", + authMode: "oauth", + googleMode: "cloud-code-assist", + baseUrl: "https://cloudcode-pa.googleapis.com", + tlsProfile: "antigravity-browser" as const, + }; + await providerTlsFetch( + "google-antigravity", + provider, + fetch, + )("https://cloudcode-pa.googleapis.com/v1", { signal }); expect(seen?.redirect).toBe("manual"); expect(seen?.signal).toBe(signal); expect((seen as any)?.browser).toBe("chrome_142"); + expect(getProviderTlsProfileStatus("google-antigravity")).toBe("active"); + }); + + test("passes supported proxy semantics to the TLS transport", async () => { + const previous = { + HTTPS_PROXY: process.env.HTTPS_PROXY, + https_proxy: process.env.https_proxy, + NO_PROXY: process.env.NO_PROXY, + no_proxy: process.env.no_proxy, + }; + let seen: RequestInit | undefined; + try { + process.env.HTTPS_PROXY = "http://127.0.0.1:9191"; + delete process.env.https_proxy; + delete process.env.NO_PROXY; + delete process.env.no_proxy; + setProviderTlsRuntimeForTest({ + fetch: async (_input, init) => { + seen = init; + return new Response("ok"); + }, + }); + const provider = { + adapter: "google", + authMode: "oauth", + googleMode: "cloud-code-assist", + baseUrl: "https://cloudcode-pa.googleapis.com", + tlsProfile: "antigravity-browser" as const, + }; + await providerTlsFetch( + "google-antigravity", + provider, + fetch, + )("https://cloudcode-pa.googleapis.com/v1"); + expect((seen as RequestInit & { proxy?: string }).proxy).toBe( + "http://127.0.0.1:9191", + ); + expect(getProviderTlsProfileStatus("google-antigravity")).toBe("active"); + } finally { + for (const [key, value] of Object.entries(previous)) { + if (value === undefined) delete process.env[key]; + else process.env[key] = value; + } + } + }); + + test("fails closed when configured proxy semantics cannot be preserved", async () => { + const previous = { + HTTPS_PROXY: process.env.HTTPS_PROXY, + https_proxy: process.env.https_proxy, + NO_PROXY: process.env.NO_PROXY, + no_proxy: process.env.no_proxy, + }; + let called = false; + try { + process.env.HTTPS_PROXY = "socks5://127.0.0.1:9191"; + delete process.env.https_proxy; + delete process.env.NO_PROXY; + delete process.env.no_proxy; + setProviderTlsRuntimeForTest({ + fetch: async () => { + called = true; + return new Response("unexpected"); + }, + }); + const provider = { + adapter: "google", + authMode: "oauth", + googleMode: "cloud-code-assist", + baseUrl: "https://cloudcode-pa.googleapis.com", + tlsProfile: "antigravity-browser" as const, + }; + const fetcher = providerTlsFetch("google-antigravity", provider, fetch); + await expect( + fetcher("https://cloudcode-pa.googleapis.com/v1"), + ).rejects.toThrow("cannot preserve configured proxy semantics"); + expect(called).toBe(false); + expect(getProviderTlsProfileStatus("google-antigravity")).toBe("failed"); + } finally { + for (const [key, value] of Object.entries(previous)) { + if (value === undefined) delete process.env[key]; + else process.env[key] = value; + } + } }); test("redacts credential text from transport errors", async () => { - setProviderTlsRuntimeForTest({ fetch: async () => { throw new Error("Authorization: Bearer super-secret"); } }); - const provider = { adapter: "google", authMode: "oauth", googleMode: "cloud-code-assist", baseUrl: "https://cloudcode-pa.googleapis.com", tlsProfile: "antigravity-browser" as const }; - await expect(providerTlsFetch("google-antigravity", provider, fetch)("https://cloudcode-pa.googleapis.com/v1")).rejects.toThrow("[REDACTED]"); - await expect(providerTlsFetch("google-antigravity", provider, fetch)("https://cloudcode-pa.googleapis.com/v1")).rejects.not.toThrow("super-secret"); + setProviderTlsRuntimeForTest({ + fetch: async () => { + throw new Error("Authorization: Bearer super-secret"); + }, + }); + const provider = { + adapter: "google", + authMode: "oauth", + googleMode: "cloud-code-assist", + baseUrl: "https://cloudcode-pa.googleapis.com", + tlsProfile: "antigravity-browser" as const, + }; + await expect( + providerTlsFetch( + "google-antigravity", + provider, + fetch, + )("https://cloudcode-pa.googleapis.com/v1"), + ).rejects.toThrow("[REDACTED]"); + await expect( + providerTlsFetch( + "google-antigravity", + provider, + fetch, + )("https://cloudcode-pa.googleapis.com/v1"), + ).rejects.not.toThrow("super-secret"); }); }); From 5b048c3edb3520594de90acdc034f2c5b4f693ed Mon Sep 17 00:00:00 2001 From: SB Yoon <44089734+yansigit@users.noreply.github.com> Date: Sat, 5 Sep 2026 21:07:26 -0600 Subject: [PATCH 06/14] test(transport): isolate TLS proxy route cases --- src/lib/provider-tls-profile.ts | 83 ++++++++++--- tests/providers/provider-tls-profile.test.ts | 121 +++++++------------ 2 files changed, 109 insertions(+), 95 deletions(-) diff --git a/src/lib/provider-tls-profile.ts b/src/lib/provider-tls-profile.ts index 953226b47b1..80d315066dd 100644 --- a/src/lib/provider-tls-profile.ts +++ b/src/lib/provider-tls-profile.ts @@ -5,16 +5,27 @@ import { resolveProxyRoute } from "./proxy-env"; export type ProviderTlsProfile = "antigravity-browser"; export type ProviderTlsProfileStatus = "disabled" | "active" | "failed"; -export const ANTIGRAVITY_TLS_HOSTS = new Set(["daily-cloudcode-pa.googleapis.com", "cloudcode-pa.googleapis.com"]); -type TlsRuntime = { fetch(input: string | URL | Request, init?: RequestInit): Promise }; +export const ANTIGRAVITY_TLS_HOSTS = new Set([ + "daily-cloudcode-pa.googleapis.com", + "cloudcode-pa.googleapis.com", +]); +type TlsRuntime = { + fetch(input: string | URL | Request, init?: RequestInit): Promise; + resolveProxyRoute?: typeof resolveProxyRoute; +}; let status = new Map(); let runtime: TlsRuntime | undefined; export function isCanonicalAntigravityUrl(input: string | URL): boolean { try { const url = new URL(input); - return url.protocol === "https:" && (url.port === "" || url.port === "443") - && !url.username && !url.password && ANTIGRAVITY_TLS_HOSTS.has(url.hostname.toLowerCase()); + return ( + url.protocol === "https:" && + (url.port === "" || url.port === "443") && + !url.username && + !url.password && + ANTIGRAVITY_TLS_HOSTS.has(url.hostname.toLowerCase()) + ); } catch { return false; } @@ -22,18 +33,29 @@ export function isCanonicalAntigravityUrl(input: string | URL): boolean { export function providerTlsProfileConfigError( providerName: string, - provider: Pick, + provider: Pick< + OcxProviderConfig, + "adapter" | "authMode" | "googleMode" | "baseUrl" | "tlsProfile" + >, ): string | null { if (provider.tlsProfile === undefined) return null; - if (provider.tlsProfile !== "antigravity-browser") return "tlsProfile must be antigravity-browser"; - if (providerName !== "google-antigravity" || provider.adapter !== "google" || provider.authMode !== "oauth" - || provider.googleMode !== "cloud-code-assist" || !isCanonicalAntigravityUrl(provider.baseUrl)) { + if (provider.tlsProfile !== "antigravity-browser") + return "tlsProfile must be antigravity-browser"; + if ( + providerName !== "google-antigravity" || + provider.adapter !== "google" || + provider.authMode !== "oauth" || + provider.googleMode !== "cloud-code-assist" || + !isCanonicalAntigravityUrl(provider.baseUrl) + ) { return "tlsProfile antigravity-browser requires the canonical Google Antigravity OAuth destination"; } return null; } -export function getProviderTlsProfileStatus(name: string): ProviderTlsProfileStatus { +export function getProviderTlsProfileStatus( + name: string, +): ProviderTlsProfileStatus { return status.get(name) ?? "disabled"; } @@ -42,14 +64,19 @@ export function resetProviderTlsProfileForTests(): void { runtime = undefined; } -export function setProviderTlsRuntimeForTest(next: TlsRuntime | undefined): void { +export function setProviderTlsRuntimeForTest( + next: TlsRuntime | undefined, +): void { runtime = next; } function preserveTransportError(error: unknown): Error { - const message = redactSecretString(error instanceof Error ? error.message : "provider TLS transport failed"); + const message = redactSecretString( + error instanceof Error ? error.message : "provider TLS transport failed", + ); const name = error instanceof Error ? error.name : "Error"; - if (name === "AbortError" || name === "TimeoutError") return new DOMException(message, name); + if (name === "AbortError" || name === "TimeoutError") + return new DOMException(message, name); const wrapped = new Error(message); wrapped.name = name; return wrapped; @@ -57,7 +84,10 @@ function preserveTransportError(error: unknown): Error { export function providerTlsFetch( name: string, - provider: Pick, + provider: Pick< + OcxProviderConfig, + "adapter" | "authMode" | "googleMode" | "baseUrl" | "tlsProfile" + >, fallback: typeof globalThis.fetch, ): typeof globalThis.fetch { if (provider.tlsProfile === undefined) { @@ -66,16 +96,29 @@ export function providerTlsFetch( } if (providerTlsProfileConfigError(name, provider)) { status.set(name, "failed"); - return (async () => { throw new Error("invalid provider TLS profile"); }) as unknown as typeof globalThis.fetch; + return (async () => { + throw new Error("invalid provider TLS profile"); + }) as unknown as typeof globalThis.fetch; } return (async (input, init) => { - const destination = typeof input === "string" || input instanceof URL ? input : input.url; - if (!isCanonicalAntigravityUrl(destination)) throw new Error("provider TLS profile refused noncanonical destination"); + const destination = + typeof input === "string" || input instanceof URL ? input : input.url; + if (!isCanonicalAntigravityUrl(destination)) + throw new Error("provider TLS profile refused noncanonical destination"); try { - const configured = runtimeProviderFetch(provider as OcxProviderConfig, name); - const mod = runtime ?? (await import("wreq-js") as unknown as TlsRuntime); - const proxyRoute = resolveProxyRoute(new URL(destination)); - if (proxyRoute.kind === "fallback") throw new Error("provider TLS profile cannot preserve configured proxy semantics"); + const configured = runtimeProviderFetch( + provider as OcxProviderConfig, + name, + ); + const mod = + runtime ?? ((await import("wreq-js")) as unknown as TlsRuntime); + const proxyRoute = (runtime?.resolveProxyRoute ?? resolveProxyRoute)( + new URL(destination), + ); + if (proxyRoute.kind === "fallback") + throw new Error( + "provider TLS profile cannot preserve configured proxy semantics", + ); const response = await (configured ?? mod.fetch)(input, { ...init, redirect: "manual", diff --git a/tests/providers/provider-tls-profile.test.ts b/tests/providers/provider-tls-profile.test.ts index fe3f95eb958..b6c73f0e81d 100644 --- a/tests/providers/provider-tls-profile.test.ts +++ b/tests/providers/provider-tls-profile.test.ts @@ -71,86 +71,57 @@ describe("provider TLS profile", () => { }); test("passes supported proxy semantics to the TLS transport", async () => { - const previous = { - HTTPS_PROXY: process.env.HTTPS_PROXY, - https_proxy: process.env.https_proxy, - NO_PROXY: process.env.NO_PROXY, - no_proxy: process.env.no_proxy, - }; let seen: RequestInit | undefined; - try { - process.env.HTTPS_PROXY = "http://127.0.0.1:9191"; - delete process.env.https_proxy; - delete process.env.NO_PROXY; - delete process.env.no_proxy; - setProviderTlsRuntimeForTest({ - fetch: async (_input, init) => { - seen = init; - return new Response("ok"); - }, - }); - const provider = { - adapter: "google", - authMode: "oauth", - googleMode: "cloud-code-assist", - baseUrl: "https://cloudcode-pa.googleapis.com", - tlsProfile: "antigravity-browser" as const, - }; - await providerTlsFetch( - "google-antigravity", - provider, - fetch, - )("https://cloudcode-pa.googleapis.com/v1"); - expect((seen as RequestInit & { proxy?: string }).proxy).toBe( - "http://127.0.0.1:9191", - ); - expect(getProviderTlsProfileStatus("google-antigravity")).toBe("active"); - } finally { - for (const [key, value] of Object.entries(previous)) { - if (value === undefined) delete process.env[key]; - else process.env[key] = value; - } - } + setProviderTlsRuntimeForTest({ + fetch: async (_input, init) => { + seen = init; + return new Response("ok"); + }, + resolveProxyRoute: () => ({ + kind: "proxy", + proxy: "http://127.0.0.1:9191", + }), + }); + const provider = { + adapter: "google", + authMode: "oauth", + googleMode: "cloud-code-assist", + baseUrl: "https://cloudcode-pa.googleapis.com", + tlsProfile: "antigravity-browser" as const, + }; + await providerTlsFetch( + "google-antigravity", + provider, + fetch, + )("https://cloudcode-pa.googleapis.com/v1"); + expect((seen as RequestInit & { proxy?: string }).proxy).toBe( + "http://127.0.0.1:9191", + ); + expect(getProviderTlsProfileStatus("google-antigravity")).toBe("active"); }); test("fails closed when configured proxy semantics cannot be preserved", async () => { - const previous = { - HTTPS_PROXY: process.env.HTTPS_PROXY, - https_proxy: process.env.https_proxy, - NO_PROXY: process.env.NO_PROXY, - no_proxy: process.env.no_proxy, - }; let called = false; - try { - process.env.HTTPS_PROXY = "socks5://127.0.0.1:9191"; - delete process.env.https_proxy; - delete process.env.NO_PROXY; - delete process.env.no_proxy; - setProviderTlsRuntimeForTest({ - fetch: async () => { - called = true; - return new Response("unexpected"); - }, - }); - const provider = { - adapter: "google", - authMode: "oauth", - googleMode: "cloud-code-assist", - baseUrl: "https://cloudcode-pa.googleapis.com", - tlsProfile: "antigravity-browser" as const, - }; - const fetcher = providerTlsFetch("google-antigravity", provider, fetch); - await expect( - fetcher("https://cloudcode-pa.googleapis.com/v1"), - ).rejects.toThrow("cannot preserve configured proxy semantics"); - expect(called).toBe(false); - expect(getProviderTlsProfileStatus("google-antigravity")).toBe("failed"); - } finally { - for (const [key, value] of Object.entries(previous)) { - if (value === undefined) delete process.env[key]; - else process.env[key] = value; - } - } + setProviderTlsRuntimeForTest({ + fetch: async () => { + called = true; + return new Response("unexpected"); + }, + resolveProxyRoute: () => ({ kind: "fallback" }), + }); + const provider = { + adapter: "google", + authMode: "oauth", + googleMode: "cloud-code-assist", + baseUrl: "https://cloudcode-pa.googleapis.com", + tlsProfile: "antigravity-browser" as const, + }; + const fetcher = providerTlsFetch("google-antigravity", provider, fetch); + await expect( + fetcher("https://cloudcode-pa.googleapis.com/v1"), + ).rejects.toThrow("cannot preserve configured proxy semantics"); + expect(called).toBe(false); + expect(getProviderTlsProfileStatus("google-antigravity")).toBe("failed"); }); test("redacts credential text from transport errors", async () => { From 37d855723d42afd9839a03a3c34491ae4fa5ab83 Mon Sep 17 00:00:00 2001 From: SB Yoon <44089734+yansigit@users.noreply.github.com> Date: Sat, 5 Sep 2026 21:31:47 -0600 Subject: [PATCH 07/14] docs(transport): document Antigravity TLS profile Co-authored-by: SB Yoon <44089734+yansigit@users.noreply.github.com> --- docs-site/src/content/docs/reference/configuration/providers.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs-site/src/content/docs/reference/configuration/providers.md b/docs-site/src/content/docs/reference/configuration/providers.md index 33371d1104f..e407f85be0c 100644 --- a/docs-site/src/content/docs/reference/configuration/providers.md +++ b/docs-site/src/content/docs/reference/configuration/providers.md @@ -212,6 +212,7 @@ Providers can expose a built-in shorthand, such as `agy` for `google-antigravity | `noProxy?` | `string \| string[]` | Destinations this provider reaches directly, using `NO_PROXY` host-pattern syntax. A match bypasses both this provider's own proxy and an inherited global proxy. | | `requestPacing?` | `{ enabled, requestsPerMinute?, minIntervalMs?, maxConcurrentRequests?, models? }` | Optional client-side outbound request-start pacing, separate from upstream usage, billing, and rate-limit indicators. RPM is converted to an even interval; `minIntervalMs` may impose a longer interval. `maxConcurrentRequests` is a positive integer cap on in-flight requests. A provider or model rule may use the concurrency cap alone; provider limits apply across all models, while `models` entries use exact upstream model IDs (for example `nvidia/llama-3.1-nemotron-ultra-253b-v1`) and can only add delay or narrow concurrency. Queue waits do not consume the upstream response-header timeout. HTTP and explicit adapter `fetchResponse`/`runTurn` dispatches are covered. A concurrency-capped canonical Responses WebSocket turn uses HTTP/SSE so its lease can be released when the response body completes, errors, or is cancelled. For `runTurn` adapters, including Cursor, the cap counts active turns rather than physical sends: RunSSE and BidiAppend may overlap within one turn, while another turn waits. Follow-up sends still obey start intervals. | | `upstreamHttpVersion?` | `"auto" \| "http1.1" \| "h1" \| "http2" \| "h2"` | Pin the HTTP version used for upstream requests to this provider. Defaults to `auto`, which lets Bun negotiate. An explicit pin requires an HTTPS target and fails locally when it cannot be honored. Set `http1.1` when a provider's HTTP/2 SSE stream stalls instead of delivering events — the symptom is a long-running streaming request that produces nothing and eventually times out. For Cursor, `http1.1`/`h1` selects its `RunSSE` + `BidiAppend` compatibility transport for inference and also pins live model discovery. Management `POST`/`PATCH` accept `null` to clear it back to `auto`. | +| `tlsProfile?` | `"antigravity-browser"` | Opt-in browser-compatible TLS transport for the canonical `google-antigravity` OAuth provider. It is accepted only with the Google adapter, Cloud Code Assist mode, and Google's canonical HTTPS Antigravity hosts. Redirects remain manual, configured proxy routing must be preserved or the request fails closed, and omission keeps the normal Bun transport without loading the optional TLS dependency. | | `responsesPath?` | `string` | Relative resource path for key-auth `openai-responses` requests. It must start with `/` and contain no scheme, query, or fragment. | | `chatCompletionsPath?` | `string` | Relative resource path for `openai-chat` requests, the mirror of `responsesPath` and subject to the same shape rules. Needed when one upstream serves Chat Completions and Responses under different prefixes: a per-model wire override changes the adapter and leaves `baseUrl` alone, so without this an opted-in Chat request would be sent to the Responses base. Z.AI is the shipped example. | | `allowEncryptedV2AgentTasks?` | `boolean` | Disabled by default. Trust a direct key-auth `openai-responses` provider to consume or relay opaque encrypted V2 sub-agent tasks unchanged. Eligible routes skip `agentTaskRecovery`; all other routes keep the existing recovery or fail-closed behavior. OpenCodex does not decrypt, translate, or recover tasks sent through this opt-in. | From 3bcf64df4e1d8efae1cb2119dafea1b7f229f4d6 Mon Sep 17 00:00:00 2001 From: SB Yoon <44089734+yansigit@users.noreply.github.com> Date: Wed, 16 Sep 2026 22:24:17 -0600 Subject: [PATCH 08/14] fix(transport): preserve abort reason identity on cancellation rejections --- src/lib/provider-tls-profile.ts | 3 ++ tests/providers/provider-tls-profile.test.ts | 31 ++++++++++++++++++++ 2 files changed, 34 insertions(+) diff --git a/src/lib/provider-tls-profile.ts b/src/lib/provider-tls-profile.ts index 80d315066dd..d657648f4ca 100644 --- a/src/lib/provider-tls-profile.ts +++ b/src/lib/provider-tls-profile.ts @@ -130,6 +130,9 @@ export function providerTlsFetch( return response; } catch (error) { status.set(name, "failed"); + if (init?.signal?.aborted && error === init.signal.reason) { + throw error; + } throw preserveTransportError(error); } }) as typeof globalThis.fetch; diff --git a/tests/providers/provider-tls-profile.test.ts b/tests/providers/provider-tls-profile.test.ts index b6c73f0e81d..e4c298b554e 100644 --- a/tests/providers/provider-tls-profile.test.ts +++ b/tests/providers/provider-tls-profile.test.ts @@ -152,4 +152,35 @@ describe("provider TLS profile", () => { )("https://cloudcode-pa.googleapis.com/v1"), ).rejects.not.toThrow("super-secret"); }); + test("preserves exact abort reason identity when transport rejects with active signal reason", async () => { + const customReason = new Error("caller-owned cancel"); + const controller = new AbortController(); + controller.abort(customReason); + + setProviderTlsRuntimeForTest({ + fetch: async () => { + throw customReason; + }, + }); + + const provider = { + adapter: "google", + authMode: "oauth", + googleMode: "cloud-code-assist", + baseUrl: "https://cloudcode-pa.googleapis.com", + tlsProfile: "antigravity-browser" as const, + }; + + const fetcher = providerTlsFetch("google-antigravity", provider, fetch); + let caught: unknown; + try { + await fetcher("https://cloudcode-pa.googleapis.com/v1", { + signal: controller.signal, + }); + } catch (err) { + caught = err; + } + expect(caught).toBe(customReason); + expect(getProviderTlsProfileStatus("google-antigravity")).toBe("failed"); + }); }); From d8bb74b7fb313159d681066af7222b44c2d82ac1 Mon Sep 17 00:00:00 2001 From: JUN Date: Thu, 1 Oct 2026 15:30:51 +0900 Subject: [PATCH 09/14] fix(transport): carry Antigravity TLS profile onto provider egress Route the opt-in TLS transport through the per-provider egress decision made at the physical send: a decided HTTP(S)/SOCKS5(H) route is passed to wreq-js, an inherited route is resolved from the environment, and a direct route is refused while any proxy variable is set because wreq-js reads them itself. Mark the transport egress-transparent, skip Bun preconnect while the profile owns the handshake, report pending/active/failed on /api/providers, and document the user-risk nature of the profile. Co-authored-by: SB Yoon <44089734+yansigit@users.noreply.github.com> --- .../docs/reference/configuration/providers.md | 2 +- src/lib/provider-tls-profile.ts | 92 +++++++++++++--- src/providers/model-rename-fields.ts | 1 + src/server/management/provider-routes.ts | 4 + src/server/responses/fetch-helpers.ts | 5 +- structure/transports/inventory.md | 1 + tests/providers/provider-tls-profile.test.ts | 101 ++++++++++++++++++ 7 files changed, 186 insertions(+), 20 deletions(-) diff --git a/docs-site/src/content/docs/reference/configuration/providers.md b/docs-site/src/content/docs/reference/configuration/providers.md index e407f85be0c..814af137fba 100644 --- a/docs-site/src/content/docs/reference/configuration/providers.md +++ b/docs-site/src/content/docs/reference/configuration/providers.md @@ -212,7 +212,7 @@ Providers can expose a built-in shorthand, such as `agy` for `google-antigravity | `noProxy?` | `string \| string[]` | Destinations this provider reaches directly, using `NO_PROXY` host-pattern syntax. A match bypasses both this provider's own proxy and an inherited global proxy. | | `requestPacing?` | `{ enabled, requestsPerMinute?, minIntervalMs?, maxConcurrentRequests?, models? }` | Optional client-side outbound request-start pacing, separate from upstream usage, billing, and rate-limit indicators. RPM is converted to an even interval; `minIntervalMs` may impose a longer interval. `maxConcurrentRequests` is a positive integer cap on in-flight requests. A provider or model rule may use the concurrency cap alone; provider limits apply across all models, while `models` entries use exact upstream model IDs (for example `nvidia/llama-3.1-nemotron-ultra-253b-v1`) and can only add delay or narrow concurrency. Queue waits do not consume the upstream response-header timeout. HTTP and explicit adapter `fetchResponse`/`runTurn` dispatches are covered. A concurrency-capped canonical Responses WebSocket turn uses HTTP/SSE so its lease can be released when the response body completes, errors, or is cancelled. For `runTurn` adapters, including Cursor, the cap counts active turns rather than physical sends: RunSSE and BidiAppend may overlap within one turn, while another turn waits. Follow-up sends still obey start intervals. | | `upstreamHttpVersion?` | `"auto" \| "http1.1" \| "h1" \| "http2" \| "h2"` | Pin the HTTP version used for upstream requests to this provider. Defaults to `auto`, which lets Bun negotiate. An explicit pin requires an HTTPS target and fails locally when it cannot be honored. Set `http1.1` when a provider's HTTP/2 SSE stream stalls instead of delivering events — the symptom is a long-running streaming request that produces nothing and eventually times out. For Cursor, `http1.1`/`h1` selects its `RunSSE` + `BidiAppend` compatibility transport for inference and also pins live model discovery. Management `POST`/`PATCH` accept `null` to clear it back to `auto`. | -| `tlsProfile?` | `"antigravity-browser"` | Opt-in browser-compatible TLS transport for the canonical `google-antigravity` OAuth provider. It is accepted only with the Google adapter, Cloud Code Assist mode, and Google's canonical HTTPS Antigravity hosts. Redirects remain manual, configured proxy routing must be preserved or the request fails closed, and omission keeps the normal Bun transport without loading the optional TLS dependency. | +| `tlsProfile?` | `"antigravity-browser"` | **Use at your own risk.** Opt-in browser-like TLS handshake (through the optional `wreq-js` dependency) for the canonical `google-antigravity` OAuth provider. It changes only how the connection looks on the wire; it is not an official Google client, and it does not change what Google's terms allow. Google can still detect, rate-limit, suspend, or ban the account you signed in with, and you alone carry that risk. It is off by default and accepted only with the Google adapter, Cloud Code Assist mode, and Google's canonical HTTPS Antigravity hosts. Redirects stay manual. The provider's own `proxy`/`noProxy` route is carried by the TLS transport; a route it cannot keep fails the request instead of leaving by another path, which includes any direct route while `HTTP_PROXY`, `HTTPS_PROXY`, or `ALL_PROXY` is set. `GET /api/providers` reports the profile state as `pending`, `active`, or `failed`. Omitting the field keeps the normal Bun transport and never loads the dependency. | | `responsesPath?` | `string` | Relative resource path for key-auth `openai-responses` requests. It must start with `/` and contain no scheme, query, or fragment. | | `chatCompletionsPath?` | `string` | Relative resource path for `openai-chat` requests, the mirror of `responsesPath` and subject to the same shape rules. Needed when one upstream serves Chat Completions and Responses under different prefixes: a per-model wire override changes the adapter and leaves `baseUrl` alone, so without this an opted-in Chat request would be sent to the Responses base. Z.AI is the shipped example. | | `allowEncryptedV2AgentTasks?` | `boolean` | Disabled by default. Trust a direct key-auth `openai-responses` provider to consume or relay opaque encrypted V2 sub-agent tasks unchanged. Eligible routes skip `agentTaskRecovery`; all other routes keep the existing recovery or fail-closed behavior. OpenCodex does not decrypt, translate, or recover tasks sent through this opt-in. | diff --git a/src/lib/provider-tls-profile.ts b/src/lib/provider-tls-profile.ts index d657648f4ca..6202fe2c0b5 100644 --- a/src/lib/provider-tls-profile.ts +++ b/src/lib/provider-tls-profile.ts @@ -1,10 +1,15 @@ import type { OcxProviderConfig } from "../types"; import { redactSecretString } from "./redact"; import { runtimeProviderFetch } from "./provider-runtime-fetch"; -import { resolveProxyRoute } from "./proxy-env"; +import { markEgressTransparentExecutor } from "./provider-egress"; +import { outboundProxyConfigured, resolveProxyRoute, type ProxyEnvMap } from "./proxy-env"; export type ProviderTlsProfile = "antigravity-browser"; -export type ProviderTlsProfileStatus = "disabled" | "active" | "failed"; +/** + * `pending` means the profile is configured and valid but no request has used it yet; the + * dashboard must not report a configured profile as `disabled` before its first send. + */ +export type ProviderTlsProfileStatus = "disabled" | "pending" | "active" | "failed"; export const ANTIGRAVITY_TLS_HOSTS = new Set([ "daily-cloudcode-pa.googleapis.com", "cloudcode-pa.googleapis.com", @@ -12,6 +17,7 @@ export const ANTIGRAVITY_TLS_HOSTS = new Set([ type TlsRuntime = { fetch(input: string | URL | Request, init?: RequestInit): Promise; resolveProxyRoute?: typeof resolveProxyRoute; + env?: ProxyEnvMap; }; let status = new Map(); let runtime: TlsRuntime | undefined; @@ -55,8 +61,12 @@ export function providerTlsProfileConfigError( export function getProviderTlsProfileStatus( name: string, + configured?: boolean, ): ProviderTlsProfileStatus { - return status.get(name) ?? "disabled"; + const recorded = status.get(name); + if (configured === undefined) return recorded ?? "disabled"; + if (!configured) return "disabled"; + return recorded === undefined || recorded === "disabled" ? "pending" : recorded; } export function resetProviderTlsProfileForTests(): void { @@ -82,6 +92,53 @@ function preserveTransportError(error: unknown): Error { return wrapped; } +/** Proxy schemes the native TLS transport can carry for a route decided elsewhere. */ +const TLS_PROXY_PROTOCOLS = new Set(["http:", "https:", "socks5:", "socks5h:"]); + +function requireDirect(env: ProxyEnvMap): Record { + // The native transport reads HTTP_PROXY/HTTPS_PROXY/ALL_PROXY itself whenever no proxy option + // is given, and it has no per-request "direct" switch. A direct route is therefore only + // honoured when no proxy variable exists at all; otherwise omitting the option would send the + // credential through the environment proxy the operator routed this request away from. + if (outboundProxyConfigured(env)) { + throw new Error("provider TLS profile cannot force a direct connection while a proxy environment variable is set"); + } + return {}; +} + +/** + * The proxy option expressing this send's route on the native transport, or a refusal. + * + * `sendWithConnectionPolicy` resolves the per-provider egress (`providers..proxy` / + * `noProxy`) and passes it as `init.proxy`: a URL, `false` for direct, or absent when the + * provider inherits the global environment route. + */ +function tlsProxyOption(init: RequestInit | undefined, destination: string | URL): { proxy?: string } { + const env = runtime?.env ?? process.env; + const decided = init !== undefined && Object.hasOwn(init, "proxy") + ? (init as RequestInit & { proxy?: unknown }).proxy + : undefined; + if (typeof decided === "string") { + let protocol: string; + try { + protocol = new URL(decided).protocol; + } catch { + throw new Error("provider TLS profile cannot preserve configured proxy semantics"); + } + if (!TLS_PROXY_PROTOCOLS.has(protocol)) { + throw new Error("provider TLS profile cannot preserve configured proxy semantics"); + } + return { proxy: decided }; + } + if (decided === false) return requireDirect(env); + const route = (runtime?.resolveProxyRoute ?? resolveProxyRoute)(new URL(destination), env); + if (route.kind === "fallback") { + throw new Error("provider TLS profile cannot preserve configured proxy semantics"); + } + if (route.kind === "proxy") return { proxy: route.proxy }; + return requireDirect(env); +} + export function providerTlsFetch( name: string, provider: Pick< @@ -100,31 +157,32 @@ export function providerTlsFetch( throw new Error("invalid provider TLS profile"); }) as unknown as typeof globalThis.fetch; } - return (async (input, init) => { + // Transparent to provider egress: the route decided at the physical send arrives as + // `init.proxy` and is either carried by the native transport or refused below. + return markEgressTransparentExecutor((async (input, init) => { const destination = typeof input === "string" || input instanceof URL ? input : input.url; - if (!isCanonicalAntigravityUrl(destination)) + if (!isCanonicalAntigravityUrl(destination)) { + status.set(name, "failed"); throw new Error("provider TLS profile refused noncanonical destination"); + } try { const configured = runtimeProviderFetch( provider as OcxProviderConfig, name, ); + const proxyOption = tlsProxyOption(init, destination); const mod = - runtime ?? ((await import("wreq-js")) as unknown as TlsRuntime); - const proxyRoute = (runtime?.resolveProxyRoute ?? resolveProxyRoute)( - new URL(destination), - ); - if (proxyRoute.kind === "fallback") - throw new Error( - "provider TLS profile cannot preserve configured proxy semantics", - ); - const response = await (configured ?? mod.fetch)(input, { - ...init, + configured === undefined + ? runtime ?? ((await import("wreq-js")) as unknown as TlsRuntime) + : undefined; + const { proxy: _decidedRoute, ...rest } = (init ?? {}) as RequestInit & { proxy?: unknown }; + const response = await (configured ?? mod!.fetch)(input, { + ...rest, redirect: "manual", browser: "chrome_142", os: "windows", - ...(proxyRoute.kind === "proxy" ? { proxy: proxyRoute.proxy } : {}), + ...proxyOption, } as RequestInit & { browser: string; os: string }); status.set(name, "active"); return response; @@ -135,5 +193,5 @@ export function providerTlsFetch( } throw preserveTransportError(error); } - }) as typeof globalThis.fetch; + }) as typeof globalThis.fetch); } diff --git a/src/providers/model-rename-fields.ts b/src/providers/model-rename-fields.ts index a12fbf88083..191165b8e74 100644 --- a/src/providers/model-rename-fields.ts +++ b/src/providers/model-rename-fields.ts @@ -142,6 +142,7 @@ export const PROVIDER_MODEL_RENAME_ROLES = { desktopExecutor: "none", unsafeAllowNativeLocalExec: "none", nativeLocalExec: "none", + tlsProfile: "none", } as const satisfies Record; function fieldsWithRole(role: ModelRenameRole): string[] { diff --git a/src/server/management/provider-routes.ts b/src/server/management/provider-routes.ts index 39fd7a5371e..94a9e40bd61 100644 --- a/src/server/management/provider-routes.ts +++ b/src/server/management/provider-routes.ts @@ -137,6 +137,7 @@ import { isPlainRecord, parseDebugLogQuery, tokPerSecondResult, unavailableCostR import type { MetricUnavailableReason, TokPerSecondResult, CostEstimateReason, CostResult, MetricSource } from "./shared"; import type { ManagementContext } from "./context"; import { readManagementJsonBody, rethrowManagementBodyTooLarge } from "./body"; +import { getProviderTlsProfileStatus } from "../../lib/provider-tls-profile"; type ProviderPatchApplication = | { error: string } @@ -963,6 +964,9 @@ export async function handleProviderRoutes(ctx: ManagementContext): Promise): void => { - base.preconnect?.(...args); + // A TLS profile owns the handshake; a Bun preconnect would open a differently fingerprinted one. + if (transport === base) base.preconnect?.(...args); }; // Rebuilt dispatches must use the same physical-send boundary as ordinary HTTP sends. // Return the original 3xx so the response owner retains its retry/health/relay contract. @@ -283,7 +284,7 @@ export function providerFetch( // the override may rebuild against a different host and select a different transport, and // refusing on this destination would reject a request whose real route is fine. if (options.dispatchOverride) egressFor(input); - else providerEgressSendInit(egressBinding, base, input); + else providerEgressSendInit(egressBinding, transport, input); // The hook inspects the outgoing headers and refuses the send by throwing; it is not a // mutator, and the copy it receives is deliberately not threaded onward. `Connection` // is decided inside `dispatch`, which runs after this, so the fresh-connection policy diff --git a/structure/transports/inventory.md b/structure/transports/inventory.md index 15674c7b081..c96e8bb4ab8 100644 --- a/structure/transports/inventory.md +++ b/structure/transports/inventory.md @@ -161,6 +161,7 @@ by transports that can preserve that request-local decision: | API-key validation probes in `src/oauth/key-providers.ts` | Not honoured | `validateApiKey` receives a `KeyLoginProvider` derived preset, which carries no egress fields, and its caller builds the real provider record afterwards. | | Responses WebSocket upstream in `src/server/responses/ws-upstream.ts` | Not directly | The WebSocket dial selects its proxy from the process environment. An explicit provider route therefore serves that provider's turns over HTTP/SSE instead and emits one warning per provider per process. | | Caller-supplied `provider.fetch` executor | Not honoured | The caller owns that executor's transport. An explicit provider route is refused instead of being ignored. | +| Opt-in Antigravity TLS profile in `src/lib/provider-tls-profile.ts` (`providers.google-antigravity.tlsProfile`) | Honoured or refused | Selected inside `providerFetch` only for the canonical Antigravity OAuth provider and destination, and marked egress-transparent. A decided HTTP(S) or SOCKS5(H) route is passed to the native `wreq-js` transport; an inherited route is resolved from the environment. The native transport reads proxy variables itself and has no per-request direct switch, so a direct route (`"direct"`, `noProxy`, or a global `NO_PROXY` match) is refused while any outbound proxy variable is set. | | Cursor's default HTTP/2 transport in `src/adapters/cursor/live-transport.ts` | Not honoured | The native HTTP/2 dial does not consume the provider route. | | Coding-agent subprocess providers in `src/adapters/coding-agent/turn.ts` | Not honoured | Their scoped child environment omits proxy variables, so a provider route is not projected into the subprocess. | | Compatibility Lab pinned sender in `src/lib/lab-live-pinned-sender.ts` | Not honoured | The sender uses the approved pinned address and does not resolve a provider route. | diff --git a/tests/providers/provider-tls-profile.test.ts b/tests/providers/provider-tls-profile.test.ts index e4c298b554e..985d8d52aed 100644 --- a/tests/providers/provider-tls-profile.test.ts +++ b/tests/providers/provider-tls-profile.test.ts @@ -7,6 +7,9 @@ import { resetProviderTlsProfileForTests, setProviderTlsRuntimeForTest, } from "../../src/lib/provider-tls-profile"; +import { isEgressTransparentExecutor } from "../../src/lib/provider-egress"; +import { providerFetch } from "../../src/server/responses/fetch-helpers"; +import type { OcxProviderConfig } from "../../src/types"; afterEach(() => resetProviderTlsProfileForTests()); @@ -46,6 +49,7 @@ describe("provider TLS profile", () => { test("uses manual redirects, browser profile, and caller abort signal", async () => { let seen: RequestInit | undefined; setProviderTlsRuntimeForTest({ + env: {}, fetch: async (_input, init) => { seen = init; return new Response("ok"); @@ -73,6 +77,7 @@ describe("provider TLS profile", () => { test("passes supported proxy semantics to the TLS transport", async () => { let seen: RequestInit | undefined; setProviderTlsRuntimeForTest({ + env: {}, fetch: async (_input, init) => { seen = init; return new Response("ok"); @@ -103,6 +108,7 @@ describe("provider TLS profile", () => { test("fails closed when configured proxy semantics cannot be preserved", async () => { let called = false; setProviderTlsRuntimeForTest({ + env: {}, fetch: async () => { called = true; return new Response("unexpected"); @@ -126,6 +132,7 @@ describe("provider TLS profile", () => { test("redacts credential text from transport errors", async () => { setProviderTlsRuntimeForTest({ + env: {}, fetch: async () => { throw new Error("Authorization: Bearer super-secret"); }, @@ -158,6 +165,7 @@ describe("provider TLS profile", () => { controller.abort(customReason); setProviderTlsRuntimeForTest({ + env: {}, fetch: async () => { throw customReason; }, @@ -183,4 +191,97 @@ describe("provider TLS profile", () => { expect(caught).toBe(customReason); expect(getProviderTlsProfileStatus("google-antigravity")).toBe("failed"); }); + + const canonical = { + adapter: "google", + authMode: "oauth", + googleMode: "cloud-code-assist", + baseUrl: "https://cloudcode-pa.googleapis.com", + tlsProfile: "antigravity-browser" as const, + }; + + function captureRuntime(env: Record) { + const seen: { init?: RequestInit & { proxy?: unknown }; calls: number } = { calls: 0 }; + setProviderTlsRuntimeForTest({ + env, + fetch: async (_input, init) => { + seen.calls += 1; + seen.init = init; + return new Response("ok"); + }, + }); + return seen; + } + + test("carries a per-provider proxy route decided at the physical send", async () => { + const seen = captureRuntime({ HTTPS_PROXY: "http://global.invalid:1" }); + const fetcher = providerTlsFetch("google-antigravity", canonical, fetch); + await fetcher("https://cloudcode-pa.googleapis.com/v1", { proxy: "socks5://127.0.0.1:1080" } as RequestInit); + expect(seen.init?.proxy).toBe("socks5://127.0.0.1:1080"); + }); + + test("honours a direct route only when no proxy environment exists", async () => { + const clean = captureRuntime({}); + await providerTlsFetch("google-antigravity", canonical, fetch)( + "https://cloudcode-pa.googleapis.com/v1", { proxy: false } as RequestInit); + expect(clean.calls).toBe(1); + expect(clean.init !== undefined && Object.hasOwn(clean.init, "proxy")).toBe(false); + + const proxied = captureRuntime({ HTTP_PROXY: "http://global.invalid:1" }); + await expect(providerTlsFetch("google-antigravity", canonical, fetch)( + "https://cloudcode-pa.googleapis.com/v1", { proxy: false } as RequestInit)) + .rejects.toThrow("cannot force a direct connection"); + expect(proxied.calls).toBe(0); + expect(getProviderTlsProfileStatus("google-antigravity", true)).toBe("failed"); + }); + + test("refuses a NO_PROXY bypass the native transport would not honour", async () => { + const seen = captureRuntime({ HTTPS_PROXY: "http://global.invalid:1", NO_PROXY: "cloudcode-pa.googleapis.com" }); + await expect(providerTlsFetch("google-antigravity", canonical, fetch)("https://cloudcode-pa.googleapis.com/v1")) + .rejects.toThrow("cannot force a direct connection"); + expect(seen.calls).toBe(0); + }); + + test("refuses a decided proxy scheme the native transport cannot carry", async () => { + const seen = captureRuntime({}); + await expect(providerTlsFetch("google-antigravity", canonical, fetch)( + "https://cloudcode-pa.googleapis.com/v1", { proxy: "ftp://127.0.0.1:21" } as RequestInit)) + .rejects.toThrow("cannot preserve configured proxy semantics"); + expect(seen.calls).toBe(0); + }); + + test("is transparent to provider egress and reports pending before its first send", () => { + const fetcher = providerTlsFetch("google-antigravity", canonical, fetch); + expect(isEgressTransparentExecutor(fetcher)).toBe(true); + expect(getProviderTlsProfileStatus("google-antigravity", true)).toBe("pending"); + expect(getProviderTlsProfileStatus("google-antigravity", false)).toBe("disabled"); + }); + + test("providerFetch routes the profile through the per-provider egress decision", async () => { + const seen = captureRuntime({}); + const provider = { ...canonical, proxy: "http://provider-proxy.invalid:3128" } as unknown as OcxProviderConfig; + const response = await providerFetch(provider, undefined, { providerName: "google-antigravity" })( + "https://cloudcode-pa.googleapis.com/v1internal:streamGenerateContent", + { method: "POST", body: "{}" }, + ); + expect(await response.text()).toBe("ok"); + expect(seen.calls).toBe(1); + expect(seen.init?.proxy).toBe("http://provider-proxy.invalid:3128/"); + expect(seen.init?.redirect).toBe("manual"); + expect(getProviderTlsProfileStatus("google-antigravity", true)).toBe("active"); + }); + + test("providerFetch leaves providers without the profile on their own executor", async () => { + const seen = captureRuntime({}); + let baseCalls = 0; + const provider = { + adapter: "google", + baseUrl: "https://generativelanguage.googleapis.com", + fetch: async () => { baseCalls += 1; return new Response("base"); }, + } as unknown as OcxProviderConfig; + await providerFetch(provider, undefined, { providerName: "gemini" })("https://generativelanguage.googleapis.com/v1beta/models"); + expect(baseCalls).toBe(1); + expect(seen.calls).toBe(0); + expect(getProviderTlsProfileStatus("gemini")).toBe("disabled"); + }); }); From 22e745cdbd5d87f0c30e2fd4c43ee0c473458fc4 Mon Sep 17 00:00:00 2001 From: JUN Date: Thu, 1 Oct 2026 15:58:18 +0900 Subject: [PATCH 10/14] fix(transport): keep provider route diagnostics under the file-size cap Co-authored-by: SB Yoon <44089734+yansigit@users.noreply.github.com> --- src/lib/provider-tls-profile.ts | 9 +++++++++ src/server/management/provider-routes.ts | 9 +++------ tests/providers/provider-tls-profile.test.ts | 5 +++++ 3 files changed, 17 insertions(+), 6 deletions(-) diff --git a/src/lib/provider-tls-profile.ts b/src/lib/provider-tls-profile.ts index 6202fe2c0b5..1c3c9baf739 100644 --- a/src/lib/provider-tls-profile.ts +++ b/src/lib/provider-tls-profile.ts @@ -69,6 +69,15 @@ export function getProviderTlsProfileStatus( return recorded === undefined || recorded === "disabled" ? "pending" : recorded; } +/** The `/api/providers` fragment for a configured profile; empty when the provider has none. */ +export function providerTlsProfileDiagnostic( + name: string, + provider: Pick, +): { tlsProfile?: { profile: ProviderTlsProfile; status: ProviderTlsProfileStatus } } { + if (provider.tlsProfile === undefined) return {}; + return { tlsProfile: { profile: provider.tlsProfile, status: getProviderTlsProfileStatus(name, true) } }; +} + export function resetProviderTlsProfileForTests(): void { status = new Map(); runtime = undefined; diff --git a/src/server/management/provider-routes.ts b/src/server/management/provider-routes.ts index 94a9e40bd61..02de78ce206 100644 --- a/src/server/management/provider-routes.ts +++ b/src/server/management/provider-routes.ts @@ -137,7 +137,7 @@ import { isPlainRecord, parseDebugLogQuery, tokPerSecondResult, unavailableCostR import type { MetricUnavailableReason, TokPerSecondResult, CostEstimateReason, CostResult, MetricSource } from "./shared"; import type { ManagementContext } from "./context"; import { readManagementJsonBody, rethrowManagementBodyTooLarge } from "./body"; -import { getProviderTlsProfileStatus } from "../../lib/provider-tls-profile"; +import { providerTlsProfileDiagnostic } from "../../lib/provider-tls-profile"; type ProviderPatchApplication = | { error: string } @@ -931,8 +931,7 @@ export async function handleProviderRoutes(ctx: ManagementContext): Promise ({ - name, adapter: p.adapter, baseUrl: publicProviderBaseUrl(p.baseUrl), defaultModel: p.defaultModel, - hasApiKey: !!p.apiKey, + name, adapter: p.adapter, baseUrl: publicProviderBaseUrl(p.baseUrl), defaultModel: p.defaultModel, hasApiKey: !!p.apiKey, // Presence only (#959 review): header names and values never leave the process. hasHeaders: !!p.headers && Object.keys(p.headers).length > 0, allowPrivateNetwork: p.allowPrivateNetwork === true, @@ -964,9 +963,7 @@ export async function handleProviderRoutes(ctx: ManagementContext): Promise { expect(isEgressTransparentExecutor(fetcher)).toBe(true); expect(getProviderTlsProfileStatus("google-antigravity", true)).toBe("pending"); expect(getProviderTlsProfileStatus("google-antigravity", false)).toBe("disabled"); + expect(providerTlsProfileDiagnostic("google-antigravity", canonical)).toEqual({ + tlsProfile: { profile: "antigravity-browser", status: "pending" }, + }); + expect(providerTlsProfileDiagnostic("gemini", {})).toEqual({}); }); test("providerFetch routes the profile through the per-provider egress decision", async () => { From 8bf7e61fd7410a80b19bbc3e5e4091c4e0b7c320 Mon Sep 17 00:00:00 2001 From: JUN Date: Thu, 1 Oct 2026 16:21:32 +0900 Subject: [PATCH 11/14] fix(transport): carry inherited SOCKS routes and gate tlsProfile at write time The TLS profile refused every Antigravity send when the only global proxy was ALL_PROXY=socks5:// or socks5h://, because resolveProxyRoute() classifies a SOCKS URL as fallback. Carry that inherited route to wreq-js when no HTTPS-specific variable outranks it, matching socks5ProxyFromEnv() on the ordinary outbound path. providerManagementConfigError() now runs providerTlsProfileConfigError(), so POST, PUT, reload and both PATCH passes refuse a row that keeps tlsProfile after authMode, baseUrl, adapter or the provider name leave eligibility, instead of persisting a row the config schema later rejects as fatal. Co-authored-by: SB Yoon <44089734+yansigit@users.noreply.github.com> --- src/lib/provider-tls-profile.ts | 14 +++++++++- src/server/auth-cors.ts | 6 +++++ tests/providers/provider-tls-profile.test.ts | 28 ++++++++++++++++++++ 3 files changed, 47 insertions(+), 1 deletion(-) diff --git a/src/lib/provider-tls-profile.ts b/src/lib/provider-tls-profile.ts index 1c3c9baf739..df98fc18f69 100644 --- a/src/lib/provider-tls-profile.ts +++ b/src/lib/provider-tls-profile.ts @@ -2,7 +2,13 @@ import type { OcxProviderConfig } from "../types"; import { redactSecretString } from "./redact"; import { runtimeProviderFetch } from "./provider-runtime-fetch"; import { markEgressTransparentExecutor } from "./provider-egress"; -import { outboundProxyConfigured, resolveProxyRoute, type ProxyEnvMap } from "./proxy-env"; +import { + outboundProxyConfigured, + proxyEnvPresent, + resolveProxyRoute, + socks5ProxyFromEnv, + type ProxyEnvMap, +} from "./proxy-env"; export type ProviderTlsProfile = "antigravity-browser"; /** @@ -142,6 +148,12 @@ function tlsProxyOption(init: RequestInit | undefined, destination: string | URL if (decided === false) return requireDirect(env); const route = (runtime?.resolveProxyRoute ?? resolveProxyRoute)(new URL(destination), env); if (route.kind === "fallback") { + // `resolveProxyRoute` only classifies HTTP(S) proxies; an inherited ALL_PROXY of socks5:// + // or socks5h:// is the route the ordinary outbound path takes through socks5ProxyFromEnv(). + // Carry that same route when no HTTPS-specific variable outranks it, instead of refusing + // every Antigravity send for an operator whose only global proxy is SOCKS. + const socks = proxyEnvPresent("HTTPS_PROXY", env) ? undefined : socks5ProxyFromEnv(env); + if (socks) return { proxy: socks.trim() }; throw new Error("provider TLS profile cannot preserve configured proxy semantics"); } if (route.kind === "proxy") return { proxy: route.proxy }; diff --git a/src/server/auth-cors.ts b/src/server/auth-cors.ts index 8899e8a101c..f552913d686 100644 --- a/src/server/auth-cors.ts +++ b/src/server/auth-cors.ts @@ -32,6 +32,7 @@ import { } from "../config/provider-validation"; import { providerDestinationConfigError } from "../lib/destination-policy"; import { providerEgressConfigError } from "../lib/provider-egress"; +import { providerTlsProfileConfigError } from "../lib/provider-tls-profile"; import { redactSecretString } from "../lib/redact"; import { DECLARABLE_HOSTED_TOOL_TYPES } from "../responses/hosted-tool-policy"; import { effectiveGoogleMode, getProviderRegistryEntry, providerCodexAccountMode, providerMatchesRegistryTransport, registryEntryForProviderDestination } from "../providers/registry"; @@ -780,6 +781,11 @@ export function providerManagementConfigError( return `provider ${name} must not include codexAccountMode`; } const typed = provider as unknown as OcxProviderConfig; + // Every write path (POST, PUT, reload, both PATCH passes) funnels through here, so a PATCH + // that changes authMode/baseUrl/adapter under a retained tlsProfile is refused before it + // persists a row the config schema would later reject as document-fatal. + const tlsProfileError = providerTlsProfileConfigError(name, typed); + if (tlsProfileError) return `provider ${JSON.stringify(redactSecretString(name))} ${tlsProfileError}`; const baseUrlError = providerBaseUrlConfigError(typed.baseUrl); if (baseUrlError) return `provider ${name} ${baseUrlError}`; if (effectiveGoogleMode(name, typed) === "vertex" && typed.location !== undefined) { diff --git a/tests/providers/provider-tls-profile.test.ts b/tests/providers/provider-tls-profile.test.ts index 5bbaf298e48..4a285682e8b 100644 --- a/tests/providers/provider-tls-profile.test.ts +++ b/tests/providers/provider-tls-profile.test.ts @@ -9,6 +9,7 @@ import { setProviderTlsRuntimeForTest, } from "../../src/lib/provider-tls-profile"; import { isEgressTransparentExecutor } from "../../src/lib/provider-egress"; +import { providerManagementConfigError } from "../../src/server/auth-cors"; import { providerFetch } from "../../src/server/responses/fetch-helpers"; import type { OcxProviderConfig } from "../../src/types"; @@ -251,6 +252,33 @@ describe("provider TLS profile", () => { expect(seen.calls).toBe(0); }); + test("carries an inherited SOCKS5 ALL_PROXY route like the ordinary outbound path", async () => { + for (const socks of ["socks5://127.0.0.1:1080", "socks5h://127.0.0.1:1080"]) { + const seen = captureRuntime({ ALL_PROXY: socks }); + await providerTlsFetch("google-antigravity", canonical, fetch)("https://cloudcode-pa.googleapis.com/v1"); + expect(seen.calls).toBe(1); + expect(seen.init?.proxy).toBe(socks); + } + }); + + test("still refuses an inherited route when an HTTPS proxy variable outranks the SOCKS fallback", async () => { + const seen = captureRuntime({ HTTPS_PROXY: "ftp://global.invalid:21", ALL_PROXY: "socks5://127.0.0.1:1080" }); + await expect(providerTlsFetch("google-antigravity", canonical, fetch)("https://cloudcode-pa.googleapis.com/v1")) + .rejects.toThrow("cannot preserve configured proxy semantics"); + expect(seen.calls).toBe(0); + }); + + test("the management write boundary refuses a row that keeps tlsProfile after leaving eligibility", () => { + const keyAuth = providerManagementConfigError("google-antigravity", { ...canonical, authMode: "key" }); + expect(keyAuth).toContain("tlsProfile antigravity-browser requires"); + const moved = providerManagementConfigError("google-antigravity", { ...canonical, baseUrl: "https://example.com" }); + expect(moved).toContain("tlsProfile antigravity-browser requires"); + const renamed = providerManagementConfigError("antigravity-copy", canonical); + expect(renamed).toContain("tlsProfile antigravity-browser requires"); + const eligible = providerManagementConfigError("google-antigravity", canonical); + expect(eligible ?? "").not.toContain("tlsProfile"); + }); + test("is transparent to provider egress and reports pending before its first send", () => { const fetcher = providerTlsFetch("google-antigravity", canonical, fetch); expect(isEgressTransparentExecutor(fetcher)).toBe(true); From a3d8874707153529e56c7aa2e1e965ee6d339a46 Mon Sep 17 00:00:00 2001 From: JUN Date: Thu, 1 Oct 2026 22:49:58 +0900 Subject: [PATCH 12/14] fix(transport): strip proxy userinfo from TLS profile errors and ignore caller cancels in status From the independent security review of #6359: a proxy URL with user:pass@ could reach logs through a native transport error, and a client cancellation marked the profile failed. Also documents that the TLS profile owns the send over a caller-supplied provider.fetch. --- src/lib/provider-tls-profile.ts | 7 ++++-- structure/transports/inventory.md | 2 +- tests/providers/provider-tls-profile.test.ts | 26 +++++++++++++++++++- 3 files changed, 31 insertions(+), 4 deletions(-) diff --git a/src/lib/provider-tls-profile.ts b/src/lib/provider-tls-profile.ts index df98fc18f69..2ac5931d2a5 100644 --- a/src/lib/provider-tls-profile.ts +++ b/src/lib/provider-tls-profile.ts @@ -96,9 +96,11 @@ export function setProviderTlsRuntimeForTest( } function preserveTransportError(error: unknown): Error { + // A configured proxy URL can carry user:pass@, and the native transport may echo it. The + // shared redactor does not mask URL userinfo, so strip it here before the message travels. const message = redactSecretString( error instanceof Error ? error.message : "provider TLS transport failed", - ); + ).replace(/\/\/[^/@\s]+@/g, "//@"); const name = error instanceof Error ? error.name : "Error"; if (name === "AbortError" || name === "TimeoutError") return new DOMException(message, name); @@ -208,10 +210,11 @@ export function providerTlsFetch( status.set(name, "active"); return response; } catch (error) { - status.set(name, "failed"); if (init?.signal?.aborted && error === init.signal.reason) { + // A caller cancellation says nothing about the profile's health. throw error; } + status.set(name, "failed"); throw preserveTransportError(error); } }) as typeof globalThis.fetch); diff --git a/structure/transports/inventory.md b/structure/transports/inventory.md index c96e8bb4ab8..5dd9b0e4bcd 100644 --- a/structure/transports/inventory.md +++ b/structure/transports/inventory.md @@ -160,7 +160,7 @@ by transports that can preserve that request-local decision: | OAuth-backed quota probes in `src/providers/quota/vendor-probes-oauth.ts` and `src/providers/quota/devin.ts` | Not honoured | `fetchXaiQuota`, `fetchAnthropicQuota`, `fetchCursorQuota`, `fetchDevinQuota` and their neighbours receive a provider name and a token rather than a provider config. | | API-key validation probes in `src/oauth/key-providers.ts` | Not honoured | `validateApiKey` receives a `KeyLoginProvider` derived preset, which carries no egress fields, and its caller builds the real provider record afterwards. | | Responses WebSocket upstream in `src/server/responses/ws-upstream.ts` | Not directly | The WebSocket dial selects its proxy from the process environment. An explicit provider route therefore serves that provider's turns over HTTP/SSE instead and emits one warning per provider per process. | -| Caller-supplied `provider.fetch` executor | Not honoured | The caller owns that executor's transport. An explicit provider route is refused instead of being ignored. | +| Caller-supplied `provider.fetch` executor | Not honoured | The caller owns that executor's transport. An explicit provider route is refused instead of being ignored. When the Antigravity TLS profile is enabled on the same provider, the profile owns the physical send and the caller-supplied executor is not used. | | Opt-in Antigravity TLS profile in `src/lib/provider-tls-profile.ts` (`providers.google-antigravity.tlsProfile`) | Honoured or refused | Selected inside `providerFetch` only for the canonical Antigravity OAuth provider and destination, and marked egress-transparent. A decided HTTP(S) or SOCKS5(H) route is passed to the native `wreq-js` transport; an inherited route is resolved from the environment. The native transport reads proxy variables itself and has no per-request direct switch, so a direct route (`"direct"`, `noProxy`, or a global `NO_PROXY` match) is refused while any outbound proxy variable is set. | | Cursor's default HTTP/2 transport in `src/adapters/cursor/live-transport.ts` | Not honoured | The native HTTP/2 dial does not consume the provider route. | | Coding-agent subprocess providers in `src/adapters/coding-agent/turn.ts` | Not honoured | Their scoped child environment omits proxy variables, so a provider route is not projected into the subprocess. | diff --git a/tests/providers/provider-tls-profile.test.ts b/tests/providers/provider-tls-profile.test.ts index 4a285682e8b..0df2615e9aa 100644 --- a/tests/providers/provider-tls-profile.test.ts +++ b/tests/providers/provider-tls-profile.test.ts @@ -191,7 +191,31 @@ describe("provider TLS profile", () => { caught = err; } expect(caught).toBe(customReason); - expect(getProviderTlsProfileStatus("google-antigravity")).toBe("failed"); + expect(getProviderTlsProfileStatus("google-antigravity")).not.toBe("failed"); + }); + + test("strips proxy userinfo from transport errors", async () => { + setProviderTlsRuntimeForTest({ + env: {}, + fetch: async () => { + throw new Error("proxy connect failed: http://alice:hunter2@proxy.example:8080"); + }, + }); + const provider = { + adapter: "google", + authMode: "oauth", + googleMode: "cloud-code-assist", + baseUrl: "https://cloudcode-pa.googleapis.com", + tlsProfile: "antigravity-browser" as const, + }; + let caught: unknown; + try { + await providerTlsFetch("google-antigravity", provider, fetch)("https://cloudcode-pa.googleapis.com/v1"); + } catch (err) { + caught = err; + } + expect(String((caught as Error).message)).not.toContain("hunter2"); + expect(String((caught as Error).message)).toContain("//@proxy.example:8080"); }); const canonical = { From c163f1442effb33a185cfe4ce588a5168ad7dc3e Mon Sep 17 00:00:00 2001 From: JUN Date: Thu, 1 Oct 2026 22:50:10 +0900 Subject: [PATCH 13/14] test(transport): use a loopback proxy host in the userinfo fixture --- tests/providers/provider-tls-profile.test.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/providers/provider-tls-profile.test.ts b/tests/providers/provider-tls-profile.test.ts index 0df2615e9aa..d5beb5918ad 100644 --- a/tests/providers/provider-tls-profile.test.ts +++ b/tests/providers/provider-tls-profile.test.ts @@ -198,7 +198,7 @@ describe("provider TLS profile", () => { setProviderTlsRuntimeForTest({ env: {}, fetch: async () => { - throw new Error("proxy connect failed: http://alice:hunter2@proxy.example:8080"); + throw new Error("proxy connect failed: http://alice:hunter2@127.0.0.1:8080"); }, }); const provider = { @@ -215,7 +215,7 @@ describe("provider TLS profile", () => { caught = err; } expect(String((caught as Error).message)).not.toContain("hunter2"); - expect(String((caught as Error).message)).toContain("//@proxy.example:8080"); + expect(String((caught as Error).message)).toContain("//@127.0.0.1:8080"); }); const canonical = { From 5bd44661e2b665712d7e6aec42a87337e726380f Mon Sep 17 00:00:00 2001 From: JUN Date: Fri, 2 Oct 2026 01:16:55 +0900 Subject: [PATCH 14/14] fix(carry): keep provider-routes.ts under the 2,000-line ceiling after the dev merge dev (Zed #6362) and this branch each left the file at 1,999 lines; their union reached 2,000 and would fail the file-size ratchet on dev. Joins the TLS diagnostic spread onto the discovery line; no behavior change. --- src/server/management/provider-routes.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/server/management/provider-routes.ts b/src/server/management/provider-routes.ts index 5f12ceb6ca9..b0372cb3a27 100644 --- a/src/server/management/provider-routes.ts +++ b/src/server/management/provider-routes.ts @@ -962,8 +962,7 @@ export async function handleProviderRoutes(ctx: ManagementContext): Promise