From 71c57ea647fbc376d1207f11d851c09504c9c02d Mon Sep 17 00:00:00 2001 From: bitkyc08-arch Date: Tue, 25 Aug 2026 10:37:05 +0900 Subject: [PATCH 1/4] release: v2.32.1 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index f73ed2d0e5..063ecfe73e 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@bitkyc08/opencodex", - "version": "2.32.0", + "version": "2.32.1", "description": "Universal provider proxy for OpenAI Codex & Claude Code — use any LLM with Codex CLI/App/SDK and Claude Code", "type": "module", "main": "./bin/package-main.mjs", From ec51e42d745d2645bcb22cb67855fa053ba1778e Mon Sep 17 00:00:00 2001 From: bitkyc08-arch Date: Tue, 25 Aug 2026 20:25:22 +0900 Subject: [PATCH 2/4] release: v2.33.0 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 063ecfe73e..6f8499ffbf 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@bitkyc08/opencodex", - "version": "2.32.1", + "version": "2.33.0", "description": "Universal provider proxy for OpenAI Codex & Claude Code — use any LLM with Codex CLI/App/SDK and Claude Code", "type": "module", "main": "./bin/package-main.mjs", From aaa9eaf37058965373dc42d1ca344e987950b6b6 Mon Sep 17 00:00:00 2001 From: JUN Date: Wed, 2 Sep 2026 18:43:29 +0900 Subject: [PATCH 3/4] fix(release): pass the bump job's permissions through the reusable-workflow call (#3262) Both v2.40.0 release dispatches (33615174183 preview, 33615177849 main) died at startup_failure: a workflow_call cannot grant its callee more than the calling job holds, and dev-version-bump.yml's job declares contents+pull- requests write. #3129 wired the call but never dispatched a release, so this is its first live run. The caller job now declares exactly the callee's two permissions; no other job in release.yml gains anything. Co-authored-by: jun (cherry picked from commit 7ce0ba51834740d7b4d5ec4793f6572d84624409) --- .github/workflows/release.yml | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 458bb67e0a..261aece1d1 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -67,6 +67,14 @@ jobs: bump-dev-version: needs: publish if: ${{ inputs.dry-run != true }} + # A reusable-workflow CALL cannot grant the callee more than the calling job holds, + # and GitHub refuses the whole run at startup when the called workflow's own job + # declares permissions the caller did not pass down ("startup_failure", runs + # 33615174183 / 33615177849 — the first dispatches since #3129 wired this call). + # The callee's job declares exactly these two; nothing else in this file gains them. + permissions: + contents: write + pull-requests: write uses: ./.github/workflows/dev-version-bump.yml with: released-version: v${{ inputs.version }} From f03a17d16197efc0446a58337272f697fb3dd400 Mon Sep 17 00:00:00 2001 From: luvs01 Date: Thu, 3 Sep 2026 15:13:21 +0900 Subject: [PATCH 4/4] fix(proxy): preserve Windows per-scheme proxy scope --- .../docs/reference/configuration/server.md | 2 +- src/config.ts | 10 +++++-- src/lib/windows-system-proxy.ts | 27 ++++++++++------- src/types/config.ts | 6 ++-- tests/proxy-env.test.ts | 30 +++++++++++++++---- 5 files changed, 53 insertions(+), 22 deletions(-) diff --git a/docs-site/src/content/docs/reference/configuration/server.md b/docs-site/src/content/docs/reference/configuration/server.md index 1a82eb82cd..0016e24180 100644 --- a/docs-site/src/content/docs/reference/configuration/server.md +++ b/docs-site/src/content/docs/reference/configuration/server.md @@ -12,7 +12,7 @@ runs helper features around provider requests. | --- | --- | --- | --- | | `port` | `number` | `10100` | Proxy listen port. | | `hostname?` | `string` | `"127.0.0.1"` | Bind address. Non-loopback binds require `OPENCODEX_API_AUTH_TOKEN`. | -| `proxy?` | `string` | — | Outbound HTTP(S) proxy URL, `${ENV_VAR}`, or `"auto"`. Applied to `HTTP_PROXY` / `HTTPS_PROXY` only when those variables are unset; loopback remains in `NO_PROXY`. `"auto"` reads the Windows system proxy (WinINET `ProxyEnable`/`ProxyServer`, `https=` then `http=` entry) once at process start and logs the host it chose. On other platforms, or when the system proxy is off, SOCKS-only, or unreadable, it uses direct egress and says so. PAC/WPAD and live proxy changes are not followed; restart the service after changing the system proxy. | +| `proxy?` | `string` | — | Outbound HTTP(S) proxy URL, `${ENV_VAR}`, or `"auto"`. Applied to `HTTP_PROXY` / `HTTPS_PROXY` only when those variables are unset; loopback remains in `NO_PROXY`. `"auto"` reads the Windows system proxy (WinINET `ProxyEnable`/`ProxyServer`) once at process start, preserves distinct `http=` and `https=` entries, and logs the hosts it chose. A bare `ProxyServer` value applies to both schemes. On other platforms, or when the system proxy is off, SOCKS-only, or unreadable, it uses direct egress and says so. PAC/WPAD and live proxy changes are not followed; restart the service after changing the system proxy. | | `noProxy?` | `string \| string[]` | — | Hosts that bypass `proxy`, merged with inherited `NO_PROXY` and loopback entries. A string may use comma-separated `NO_PROXY` syntax or `${ENV_VAR}`. | | `emptyCompletionRetry?` | `boolean` | `false` | Opt in to one identical Responses retry when a turn has no text or tool call, including a stream that ends before a terminal event. The retry may be billable. `OCX_EMPTY_COMPLETION_RETRY=0` disables it without changing config; combo and routed-compaction turns remain excluded. | | `stallTimeoutSec?` | `number` | `300` | Seconds without upstream data before `response.incomplete`. Minimum 1. | diff --git a/src/config.ts b/src/config.ts index 0641f772eb..966ac59554 100644 --- a/src/config.ts +++ b/src/config.ts @@ -3628,8 +3628,14 @@ export function applyProxyEnvWith( } else { const found = readWindowsSystemProxy(auto.reader, auto.platform); if (found.kind === "proxy") { - console.log(`[opencodex] proxy "auto": using Windows system proxy ${describeProxyForLog(found.url)}`); - proxy = found.url; + const origins = [ + found.httpUrl && `HTTP ${describeProxyForLog(found.httpUrl)}`, + found.httpsUrl && `HTTPS ${describeProxyForLog(found.httpsUrl)}`, + ].filter(Boolean).join(", "); + console.log(`[opencodex] proxy "auto": using Windows system proxy ${origins}`); + if (found.httpUrl) process.env.HTTP_PROXY = found.httpUrl; + if (found.httpsUrl) process.env.HTTPS_PROXY = found.httpsUrl; + proxy = undefined; } else { const reason = found.kind === "unsupported" ? "only Windows system proxy discovery is supported; using direct egress on this OS" diff --git a/src/lib/windows-system-proxy.ts b/src/lib/windows-system-proxy.ts index 1316698b23..d347921c6b 100644 --- a/src/lib/windows-system-proxy.ts +++ b/src/lib/windows-system-proxy.ts @@ -16,7 +16,7 @@ import { decodeWindowsTextBytes } from "./windows-text"; const INTERNET_SETTINGS_KEY = "HKCU\\Software\\Microsoft\\Windows\\CurrentVersion\\Internet Settings"; export type WindowsSystemProxyResult = - | { kind: "proxy"; url: string } + | { kind: "proxy"; httpUrl?: string; httpsUrl?: string } | { kind: "disabled" } | { kind: "socks-only" } | { kind: "unsupported" } @@ -63,35 +63,40 @@ export function readWindowsProxyRegistry(): WindowsProxyRegistryValues | null { /** * `ProxyServer` is either a bare `host:port` (applies to every scheme) or a semicolon list of - * `scheme=host:port` entries. Prefer the https entry, then http; a SOCKS-only value cannot be - * mirrored into HTTP_PROXY/HTTPS_PROXY. + * `scheme=host:port` entries. Bare values apply to both HTTP and HTTPS destinations; per-scheme + * values retain their WinINET scope. A SOCKS-only value cannot be mirrored into HTTP(S)_PROXY. */ -export function parseWindowsProxyServer(value: string): { kind: "proxy"; url: string } | { kind: "socks-only" } | { kind: "disabled" } { +export function parseWindowsProxyServer(value: string): Extract { const trimmed = value.trim(); if (!trimmed) return { kind: "disabled" }; - if (!trimmed.includes("=")) return normalize(trimmed); + if (!trimmed.includes("=")) { + const url = normalize(trimmed); + return url ? { kind: "proxy", httpUrl: url, httpsUrl: url } : { kind: "disabled" }; + } const entries = new Map(); for (const part of trimmed.split(";")) { const eq = part.indexOf("="); if (eq <= 0) continue; entries.set(part.slice(0, eq).trim().toLowerCase(), part.slice(eq + 1).trim()); } - const candidate = entries.get("https") || entries.get("http"); - if (candidate) return normalize(candidate); + const httpUrl = normalize(entries.get("http") ?? ""); + const httpsUrl = normalize(entries.get("https") ?? ""); + if (httpUrl || httpsUrl) return { kind: "proxy", ...(httpUrl && { httpUrl }), ...(httpsUrl && { httpsUrl }) }; if (entries.has("socks")) return { kind: "socks-only" }; return { kind: "disabled" }; } -function normalize(hostPort: string): { kind: "proxy"; url: string } | { kind: "disabled" } { +function normalize(hostPort: string): string | undefined { + if (!hostPort) return undefined; const withScheme = /^[a-z][a-z0-9+.-]*:\/\//i.test(hostPort) ? hostPort : `http://${hostPort}`; try { const url = new URL(withScheme); - if (!url.hostname || (url.protocol !== "http:" && url.protocol !== "https:")) return { kind: "disabled" }; + if (!url.hostname || (url.protocol !== "http:" && url.protocol !== "https:")) return undefined; // Keep userinfo: a credentialed proxy is valid in HTTP_PROXY. Only the log strips it. const auth = url.username ? `${url.username}${url.password ? `:${url.password}` : ""}@` : ""; - return { kind: "proxy", url: `${url.protocol}//${auth}${url.host}` }; + return `${url.protocol}//${auth}${url.host}`; } catch { - return { kind: "disabled" }; + return undefined; } } diff --git a/src/types/config.ts b/src/types/config.ts index 9bead2ad10..86bd86b962 100644 --- a/src/types/config.ts +++ b/src/types/config.ts @@ -599,9 +599,9 @@ export interface OcxConfig { * "${HTTPS_PROXY}"-style env reference). Mirrored into HTTP_PROXY/HTTPS_PROXY at startup when * those are unset — Bun's fetch honors them for all outbound calls; localhost is excluded. * The literal `"auto"` reads the Windows WinINET static proxy (`ProxyEnable`/`ProxyServer`) - * once at process start; on other platforms, or when the system proxy is off, SOCKS-only, - * or unreadable, it degrades to direct egress with one log line (#1525). PAC/WPAD and live - * changes are not followed. + * once at process start, preserving separate HTTP and HTTPS entries; on other platforms, or + * when the system proxy is off, SOCKS-only, or unreadable, it degrades to direct egress with + * one log line (#1525). PAC/WPAD and live changes are not followed. */ proxy?: string; /** diff --git a/tests/proxy-env.test.ts b/tests/proxy-env.test.ts index 1439013162..f4842fc3fd 100644 --- a/tests/proxy-env.test.ts +++ b/tests/proxy-env.test.ts @@ -165,16 +165,17 @@ describe("applyProxyEnv with proxy: \"auto\" (#1525)", () => { } test("parses bare, per-scheme, and socks-only ProxyServer values", () => { - expect(parseWindowsProxyServer("127.0.0.1:7890")).toEqual({ kind: "proxy", url: "http://127.0.0.1:7890" }); - expect(parseWindowsProxyServer("http=10.0.0.5:3128;https=10.0.0.6:3129;ftp=x:1")).toEqual({ kind: "proxy", url: "http://10.0.0.6:3129" }); - expect(parseWindowsProxyServer("http=10.0.0.5:3128")).toEqual({ kind: "proxy", url: "http://10.0.0.5:3128" }); + expect(parseWindowsProxyServer("127.0.0.1:7890")).toEqual({ kind: "proxy", httpUrl: "http://127.0.0.1:7890", httpsUrl: "http://127.0.0.1:7890" }); + expect(parseWindowsProxyServer("http=10.0.0.5:3128;https=10.0.0.6:3129;ftp=x:1")).toEqual({ kind: "proxy", httpUrl: "http://10.0.0.5:3128", httpsUrl: "http://10.0.0.6:3129" }); + expect(parseWindowsProxyServer("http=10.0.0.5:3128")).toEqual({ kind: "proxy", httpUrl: "http://10.0.0.5:3128" }); + expect(parseWindowsProxyServer("https=10.0.0.6:3129")).toEqual({ kind: "proxy", httpsUrl: "http://10.0.0.6:3129" }); expect(parseWindowsProxyServer("socks=127.0.0.1:1080")).toEqual({ kind: "socks-only" }); expect(parseWindowsProxyServer("")).toEqual({ kind: "disabled" }); }); test("readWindowsSystemProxy honors ProxyEnable and platform", () => { const on = () => ({ proxyEnable: "0x1", proxyServer: "127.0.0.1:7893" }); - expect(readWindowsSystemProxy(on, "win32")).toEqual({ kind: "proxy", url: "http://127.0.0.1:7893" }); + expect(readWindowsSystemProxy(on, "win32")).toEqual({ kind: "proxy", httpUrl: "http://127.0.0.1:7893", httpsUrl: "http://127.0.0.1:7893" }); expect(readWindowsSystemProxy(() => ({ proxyEnable: "0x0", proxyServer: "127.0.0.1:7893" }), "win32")).toEqual({ kind: "disabled" }); expect(readWindowsSystemProxy(() => null, "win32")).toEqual({ kind: "unreadable" }); expect(readWindowsSystemProxy(on, "darwin")).toEqual({ kind: "unsupported" }); @@ -192,6 +193,26 @@ describe("applyProxyEnv with proxy: \"auto\" (#1525)", () => { expect(lines.join("\n")).not.toContain("secret-pass-91"); }); + test("auto preserves per-scheme Windows proxy scope", () => { + const lines = capture(() => applyProxyEnvWith(configWithProxy("auto"), { + platform: "win32", + reader: () => ({ proxyEnable: "0x1", proxyServer: "http=proxy-a:8080;https=user:secret-pass-92@proxy-b:8443" }), + })); + expect(process.env.HTTP_PROXY).toBe("http://proxy-a:8080"); + expect(process.env.HTTPS_PROXY).toBe("http://user:secret-pass-92@proxy-b:8443"); + expect(lines.join("\n")).toContain("HTTP http://proxy-a:8080"); + expect(lines.join("\n")).toContain("HTTPS http://proxy-b:8443"); + expect(lines.join("\n")).not.toContain("secret-pass-92"); + + delete process.env.HTTP_PROXY; delete process.env.HTTPS_PROXY; + capture(() => applyProxyEnvWith(configWithProxy("auto"), { + platform: "win32", + reader: () => ({ proxyEnable: "0x1", proxyServer: "https=proxy-b:8443" }), + })); + expect(process.env.HTTP_PROXY).toBeUndefined(); + expect(process.env.HTTPS_PROXY).toBe("http://proxy-b:8443"); + }); + test("auto never leaks the literal into HTTP_PROXY when discovery yields nothing", () => { for (const [platform, reader] of [ ["darwin", () => ({ proxyEnable: "0x1", proxyServer: "127.0.0.1:1" })], @@ -217,4 +238,3 @@ describe("applyProxyEnv with proxy: \"auto\" (#1525)", () => { expect(process.env.HTTP_PROXY).toBeUndefined(); }); }); -