Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
d35592b
merge dev into main for the v2.32.1 release
lidge-jun Aug 25, 2026
71c57ea
release: v2.32.1
lidge-jun Aug 25, 2026
d560ac6
merge dev into main for the v2.33.0 release
lidge-jun Aug 25, 2026
08ada6f
Merge pull request #2553 from lidge-jun/codex/promote-main-2330
lidge-jun Aug 25, 2026
ec51e42
release: v2.33.0
lidge-jun Aug 25, 2026
e25b653
merge dev into main for the v2.34.0 release
lidge-jun Aug 27, 2026
80fff9a
Merge pull request #2760 from lidge-jun/codex/promote-main-2340
lidge-jun Aug 27, 2026
fc4de77
Merge pull request #2826 from lidge-jun/codex/promote-main-2350
lidge-jun Aug 28, 2026
c7d8407
Merge pull request #3002 from lidge-jun/codex/promote-main-2360
lidge-jun Aug 30, 2026
54e2274
Merge pull request #3037 from lidge-jun/codex/promote-main-2370
lidge-jun Aug 31, 2026
2c4dca1
merge dev into the promotion branch for v2.38.0
lidge-jun Aug 31, 2026
a34e8b7
merge dev into the promotion branch for v2.38.0 (picks up the ReDoS fix)
lidge-jun Aug 31, 2026
ebb4d55
Merge pull request #3073 from lidge-jun/codex/promote-main-2380
lidge-jun Aug 31, 2026
682112e
Merge remote-tracking branch 'origin/dev' into codex/promote-main-2390
lidge-jun Sep 1, 2026
af6113a
merge dev into main for the v2.39.0 release
lidge-jun Sep 1, 2026
847f4f1
merge dev into main for the v2.40.0 release
Sep 2, 2026
ac78647
Merge pull request #3261 from lidge-jun/codex/promote-main-2400
lidge-jun Sep 2, 2026
aaa9eaf
fix(release): pass the bump job's permissions through the reusable-wo…
lidge-jun Sep 2, 2026
35ff3a4
Merge pull request #3263 from lidge-jun/codex/promote-main-2400-relfix
lidge-jun Sep 2, 2026
f03a17d
fix(proxy): preserve Windows per-scheme proxy scope
luvs01 Sep 3, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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 }}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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. |
Expand Down
10 changes: 8 additions & 2 deletions src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
27 changes: 16 additions & 11 deletions src/lib/windows-system-proxy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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" }
Expand Down Expand Up @@ -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<WindowsSystemProxyResult, { kind: "proxy" | "socks-only" | "disabled" }> {
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<string, string>();
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;
}
}

Expand Down
6 changes: 3 additions & 3 deletions src/types/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
/**
Expand Down
30 changes: 25 additions & 5 deletions tests/proxy-env.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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" });
Expand All @@ -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" })],
Expand All @@ -217,4 +238,3 @@ describe("applyProxyEnv with proxy: \"auto\" (#1525)", () => {
expect(process.env.HTTP_PROXY).toBeUndefined();
});
});

Loading