Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
59 commits
Select commit Hold shift + click to select a range
4e0e8f4
feat(chatgpt): add experimental macOS app-server quota-gate shim
lidge-jun Oct 1, 2026
98194c3
feat(chatgpt): carry the local-CA send-unblock intercept (split from …
lidge-jun Oct 1, 2026
51a92cf
docs(structure): keep runtime.md within its 600-line budget
lidge-jun Oct 1, 2026
f89bffc
perf(chatgpt): load the send-unblock intercept only when it is enabled
lidge-jun Oct 1, 2026
9b45354
feat: ChatGPT desktop send-unblock intercept (opt-in)
lcxhh521 Sep 24, 2026
83ce896
feat: launch watcher for ChatGPT desktop send-unblock
lcxhh521 Sep 24, 2026
350e53f
fix(chatgpt-unblock): working launch path, WebSocket relay and review…
lcxhh521 Sep 26, 2026
8f9aac8
fix(chatgpt-unblock): cap fragmented WS messages by bytes and honor h…
lcxhh521 Sep 26, 2026
b29b7f3
fix(chatgpt-unblock): keep an error listener on the WS tunnel until a…
lcxhh521 Sep 26, 2026
5ba579d
feat(chatgpt-unblock): PAC-fallback mode so traffic survives opencode…
lcxhh521 Sep 26, 2026
dc16332
fix(chatgpt-unblock): review fixes for the PAC fallback
lcxhh521 Sep 26, 2026
5985b44
fix(chatgpt-unblock): backpressure-safe splice and a working head dea…
lcxhh521 Sep 26, 2026
f2cc6ab
fix(chatgpt-unblock): honour a system PAC and release listeners on fa…
lcxhh521 Sep 26, 2026
4083723
docs(chatgpt-desktop): document the PAC fallback mode
lcxhh521 Sep 26, 2026
72c0b63
fix(chatgpt-unblock): restore undoes either launch switch
lcxhh521 Sep 26, 2026
978238d
fix(chatgpt-unblock): RFC 1929 auth for the SOCKS5 WebSocket dial
lcxhh521 Sep 27, 2026
b7f9fdf
fix(chatgpt-unblock): reject a malformed chatgptDesktop block at the …
lcxhh521 Sep 29, 2026
2e68809
feat(chatgpt-unblock): route the bundled app-server's account reads t…
lcxhh521 Sep 29, 2026
4a9cd5b
fix(chatgpt-unblock): also drop a plain-quota rate_limit_reached_type…
lcxhh521 Sep 29, 2026
effadd3
fix(chatgpt-unblock): pass the PAC to the app inline as a data: URL
lcxhh521 Sep 29, 2026
5e548a8
fix(chatgpt-unblock): address review findings on the app-server path
lcxhh521 Sep 29, 2026
8c9ddbf
fix(chatgpt-unblock): recognise the injected chatgpt_base_url by valu…
lcxhh521 Sep 29, 2026
65afaf1
fix(chatgpt-unblock): serve the app-server route over HTTPS and make …
lcxhh521 Sep 29, 2026
13f7910
feat(chatgpt-unblock): open the quota gate the app-server reports ove…
lcxhh521 Sep 29, 2026
001bb6a
fix(chatgpt-unblock): exec the real app-server so the app keeps trust…
lcxhh521 Sep 29, 2026
c6ced22
test(chatgpt-unblock): cover the 5-hour-only exhaustion reported in #…
lcxhh521 Sep 29, 2026
e7fa5d6
fix(chatgpt-unblock): scope the gate rewrites and clean up on launche…
lcxhh521 Sep 29, 2026
dc27cb2
fix(chatgpt-unblock): keep the rate-limit flags closed beside a non-q…
lcxhh521 Sep 29, 2026
710be45
fix(chatgpt-unblock): look for non-quota reasons across the whole pay…
lcxhh521 Sep 29, 2026
875d36e
fix(chatgpt-unblock): find the app from its own terminal, and refuse …
lcxhh521 Oct 1, 2026
819f440
fix(chatgpt-unblock): fail open on a filter that does not load, and k…
lcxhh521 Oct 1, 2026
248f966
docs(chatgpt-unblock): say what the unreadable-PAC route does, and th…
lcxhh521 Oct 1, 2026
33f271c
feat(chatgpt): route an app that opened before opencodex
lcxhh521 Oct 2, 2026
eb6ab8a
docs(chatgpt): document the readiness trigger and the full explicit-l…
lcxhh521 Oct 2, 2026
71bce16
Merge dev into the ready-marker watcher branch
lcxhh521 Oct 2, 2026
c74fa3c
fix(chatgpt): count the web snapshot's used_percent as plain-quota ev…
lcxhh521 Oct 2, 2026
4d80aaa
fix(chatgpt): report a failed relaunch and a held lock from the launc…
lcxhh521 Oct 3, 2026
b185b4e
Merge dev into the ready-marker watcher branch
lcxhh521 Oct 3, 2026
9c6dc70
test(chatgpt): prove the launch script drops an inherited CODEX_CLI_PATH
lcxhh521 Oct 3, 2026
755161b
Merge dev into the ready-marker watcher branch
lcxhh521 Oct 3, 2026
3360443
Merge dev into the ready-marker watcher branch
lcxhh521 Oct 4, 2026
5d6a968
Merge the ready-marker watcher branch and build PAC fallback on it
lcxhh521 Oct 4, 2026
242ca57
Merge dev into feat/chatgpt-unblock-ready-watcher
lcxhh521 Oct 4, 2026
e02c901
Merge feat/chatgpt-unblock-ready-watcher into the PAC-fallback branch
lcxhh521 Oct 4, 2026
b67f603
Merge dev into feat/chatgpt-unblock-ready-watcher
lcxhh521 Oct 5, 2026
41008f8
Merge feat/chatgpt-unblock-ready-watcher into the PAC-fallback branch
lcxhh521 Oct 5, 2026
06797ec
Merge latest dev into PAC fallback branch
lcxhh521 Oct 15, 2026
45ed3fd
fix(chatgpt-unblock): address watcher and proxy review findings
lcxhh521 Oct 15, 2026
61575aa
fix(chatgpt-unblock): clarify PAC dependencies and restore guard
lcxhh521 Oct 15, 2026
cc98957
fix(chatgpt-unblock): prepare PAC arguments before quitting the app
lcxhh521 Oct 15, 2026
651b28c
fix(chatgpt-unblock): bind watcher actions and authenticate readiness
lcxhh521 Oct 16, 2026
d5fa5ce
fix(chatgpt): preserve configured proxy routes and sync dev
lcxhh521 Oct 16, 2026
acef451
fix(chatgpt-unblock): gate readiness and complete socket teardown
lcxhh521 Oct 16, 2026
05098ef
fix(chatgpt-unblock): bound system PAC loading
lcxhh521 Oct 16, 2026
6b9f39b
fix(chatgpt-unblock): bound SSE rewrite line buffering
lcxhh521 Oct 16, 2026
f4edc36
Merge dev c03eafd into ChatGPT PAC fallback safety branch
lcxhh521 Oct 16, 2026
f0026eb
test(chatgpt): bound loopback port-pair retries
lcxhh521 Oct 16, 2026
d596984
Merge current dev into ChatGPT PAC fallback
lcxhh521 Oct 16, 2026
36a6319
fix(chatgpt): enforce captured PAC policy for runtime upstreams
lcxhh521 Oct 16, 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
6 changes: 6 additions & 0 deletions bun.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

199 changes: 196 additions & 3 deletions docs-site/src/content/docs/guides/chatgpt-desktop.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
---
title: ChatGPT Desktop app-server shim (experimental)
description: An opt-in macOS experiment that rewrites plain-quota gate fields on the bundled app-server stdout pipe.
title: ChatGPT Desktop integrations (experimental)
description: Opt-in macOS app-server shim and local-CA TLS intercept experiments for plain-quota send gates.
---

This experiment is **macOS only and off by default**. It filters the bundled ChatGPT
Expand Down Expand Up @@ -91,7 +91,7 @@ folders and trusted sticky folders retain their normal permissions behavior.
These are ownership, POSIX-permission, and signature checks; native ACL and
volume ownership-policy behavior has not been verified.

This integration installs no certificate, network listener, PAC, or background
The app-server shim alone installs no certificate, network listener, PAC, or background
watcher. It does not log the app's messages or environment. Status reports whether
the running ChatGPT bundle process carries the expected launcher override.

Expand Down Expand Up @@ -129,3 +129,196 @@ This standalone shim does not rewrite conversation metadata or route model calls
Other app gates or upstream refusals can still prevent sending. Evidence reported
on an exhausted Plus account also used an intercept, so it does not establish
that this shim alone resolves every desktop send lock.

## Local-CA TLS intercept (experimental candidate)

The separate `chatgptDesktop.unblockSend` experiment terminates TLS for the
`chatgpt.com` apex host on loopback, relays the account's cookies and credentials,
and rewrites known quota send gates in conversation metadata and usage responses.
It is **off by default**, macOS only, and independent of `appServerShim`:

```json
{
"chatgptDesktop": {
"unblockSend": true,
"port": 10300
}
}
```

`port` is optional; its default is the running proxy's public port plus 200
(`10100` → `10300`). A derived port outside the TCP range requires an explicit
free port. Client-role processes do not start the intercept. A bind or certificate
failure warns without stopping the proxy's other services.

Start OpenCodex with this config, then run `ocx chatgpt status`. The listener
creates or reuses the local authority shared with the Claude intercept. **You
must trust this CA yourself** in the macOS login keychain before launching the
intercepted app. Status prints the exact command; with the default config path:

```bash
security add-trusted-cert -r trustRoot -p ssl -k "$HOME/Library/Keychains/login.keychain-db" "$HOME/.opencodex/claude-intercept/ca.pem"
ocx chatgpt launch
ocx chatgpt status
```

Use the certificate path reported by status if your OpenCodex home differs. The
CLI prints the trust command and never runs it. Trusting a local CA changes the
login keychain's TLS trust: anyone controlling its private key can issue trusted
certificates. The listener sees the decrypted account traffic, including cookies,
authorization headers and message content that it relays. Protect the config
directory and CA key. The relay does not log request bodies or credentials.
`restore` removes launch overrides; it does **not** remove CA trust or delete the
shared authority. Remove trust manually through Keychain Access when you no
longer need it, accounting for other integrations using the same authority.

Status checks the current CA's validity, exact presence in the login keychain, and
usable user-domain SSL trust settings. An explicit deny or unrelated/scoped policy
does not count as trust; malformed or ambiguous settings report `unknown`. This
is a configuration check, not proof that a particular app build accepts the
intercepted certificate or routes all its traffic through Chromium.

Launch restarts ChatGPT with
`--host-resolver-rules=MAP chatgpt.com 127.0.0.1:<port>`. Explicit system HTTP/SOCKS
proxies get an apex-host bypass while other hosts retain the proxy without a synthetic
direct fallback; TUN/direct networking needs only the resolver rule. An existing system
PAC cannot be combined with that bypass, so it may prevent the intercept from
seeing traffic. With `pacFallback` (below) the app is launched through a generated
PAC instead of the resolver rule.

If both flags are true, `ocx chatgpt launch` applies the existing app-server shim
and the intercept together. The shim alone still works without a running proxy.
The intercept requires a listener authenticated by the published local CA and the `chatgpt.com` hostname,
then confirmed by its service identity. Missing or invalid CA files leave the app untouched; these
checks do not install keychain trust. When OpenCodex
stops, an app still carrying the resolver rule cannot reach `chatgpt.com`; run
`ocx chatgpt restore` to relaunch with native networking, or use PAC fallback.

### PAC fallback: preserve your network policy when OpenCodex stops

PAC fallback launches the app with a generated PAC script instead of the resolver
rule, so the app falls back on its own. The script is passed inline (a `data:` URL)
because the app ignores a `file://` PAC and an `http://` one would need OpenCodex
running to be fetched:

```json
{ "chatgptDesktop": { "unblockSend": true, "pacFallback": true } }
```

`pacFallback` only takes effect together with `unblockSend`. Config diagnostics
warn when PAC fallback is enabled on its own; the saved flags are left unchanged.
OpenCodex normally also listens on the listener port plus one (`10301` by default),
or port `65534` when the listener uses `65535`. This CONNECT entry splices onto the
TLS listener. OpenCodex rewrites `chatgpt-unblock.pac` in its home
directory at every start, before the `chatgpt-unblock.ready` marker. Before restarting an
app, the launcher prepares the PAC argument; if the file cannot be read and encoded,
is empty, or is not a regular file, it leaves the running app untouched. The PAC sends
`chatgpt.com` to that entry first, and every other host the way the system routes it:

The listener does not report ready or forward traffic until this startup completes.
Stopping it rejects new requests and closes active policy-routed HTTP streams, CONNECT and WebSocket relays.
Stale PAC/readiness files alone never authorize a watcher restart.

| Setup | Other hosts, and `chatgpt.com` while OpenCodex is stopped |
|---|---|
| No proxy, or VPN in TUN mode | Direct. |
| VPN in system-proxy mode | The configured system proxy chain, without adding direct fallback. |
| PAC file | The system PAC, embedded in the generated file. |

The generated PAC is intended to let the app use the captured route without a
restart when OpenCodex stops; the send unblock pauses until OpenCodex is back.
Loopback tests verify socket cleanup, process-crash port release and restart, but
actual macOS app cached-PAC recovery still needs acceptance testing.
If every configured proxy is unavailable,
requests fail instead of silently connecting directly. Embedded system PACs retain
Chromium’s non-mandatory PAC behavior: invalid results or execution failures can
still fall back directly. Do not use that mode when a fail-closed system-PAC policy is required. The route is captured when
OpenCodex starts: after changing the VPN mode, restart OpenCodex and run
`ocx chatgpt launch`. PAC downloads must finish within 3 seconds; downloads and local file
reads are capped at 4 MiB. If a system PAC is set but cannot be read at that moment, or
is too large to pass to the app (the PAC travels inside one launch argument,
limited to 512 KiB once encoded), the optional intercept does not start. An unreadable
system proxy configuration or automatic proxy discovery also prevents PAC-mode startup; fix it and restart OpenCodex. After turning `pacFallback` on
or off, restart OpenCodex, run `ocx chatgpt launch`, and run
`ocx chatgpt install-watcher` again if you use the watcher. `restore` undoes a PAC
launch the same way as a resolver-rule one. Resolver-rule launches likewise reject unreadable
system proxy settings before quitting the app and never add a direct fallback to an explicit
proxy. `restore` remains available even when proxy settings cannot be read.

### Existing proxy policy while the intercept is running

For both resolver and PAC launch modes, OpenCodex now reads the system proxy configuration once
at startup. A configured system PAC is evaluated inside an isolated, runtime-owned JavaScript
worker for both HTTP and WebSocket upstream requests. A static system proxy chain is also kept
strict. No separate background service is installed. If neither system policy exists, the existing
OpenCodex outbound proxy selection remains in use.

Only an explicit `DIRECT` PAC result permits direct upstream access. Supported proxy results are
`PROXY`, `HTTPS` and `SOCKS5`; malformed or unsupported results fail the request. Failover to the
next listed route is limited to failure before establishing its TCP connection. Proxy refusal,
TLS/certificate errors and responses do not retry or replay a chat POST. Upstream TLS verification
remains enabled.

The evaluator deliberately supports a restricted PAC subset. Classic domain matching, `*`/`?`
globs, weekday ranges and IPv4 DNS helpers are supported. `myIpAddress`, date/time ranges, IPv6
extension helpers, extended glob syntax, dynamic/async code, and `catch`/`finally` recovery syntax
are unsupported. Keywords appearing only in comments or strings are allowed. Exception recovery
is excluded because the engine cannot reliably report a caught allocation failure. DNS lookup
errors also stop routing, including ambiguous negative answers, instead of permitting a rule to
mistake an error for a direct route.

These restrictions can reject a PAC that works in your browser. The worker has bounded memory,
execution time, DNS work and request queue size. An unsupported operation, invalid route, runtime
error, cancellation or exceeded limit closes that policy until OpenCodex restarts; it never switches
to an environment proxy or an implicit direct route. HTTPS and secure WebSocket PAC matching uses
the scheme and host, with path, query and credentials removed.

This protection applies while traffic passes through the active OpenCodex listener. The browser's
embedded PAC after OpenCodex exits still has the non-mandatory behavior described above; native
app certificate acceptance and cached-PAC stop/crash recovery require separate macOS acceptance.

### Intercept rewrite boundary

Only `/backend-api/conversation/init`, `/backend-api/conversation` and
`/backend-api/f/conversation` (including child paths), plus the exact
`/backend-api/wham/usage` and `/backend-api/wham/usage/stream` paths are rewritten.
Conversation metadata loses known quota `send` / `tpp_send` blocks and exhausted
send progress entries. Unknown and subscription/policy/workspace reasons remain;
status reports preserved reasons. Usage rewriting reuses the shim's gate helpers,
keeping workspace, credit and spend-control gates and usage display intact. Other
HTTP responses pass through; WebSocket upgrades, voice and dictation relay
without rewriting through direct, HTTP CONNECT or shared SOCKS5 transport.

### Optional intercept launch watcher

```bash
ocx chatgpt install-watcher --yes
ocx chatgpt uninstall-watcher
ocx chatgpt restore
```

Installation without `--yes` asks in an interactive terminal. The launchd agent
watches the app's Electron `SingletonLock` and the `chatgpt-unblock.ready` marker. In watch
mode, it restarts an app launched without intercept switches only while the listener answers as
OpenCodex and the app is no more than five minutes old; a missing or unparseable process age
counts as fresh, and explicit `ocx chatgpt launch` is not age-limited. This can interrupt
startup work; it does nothing while the listener is unavailable. It manages
**intercept launches only**; use explicit launch for the app-server shim. A loaded
watcher must be uninstalled before `restore`, so it cannot put the switches back.
The watcher records the absolute bundle path discovered at installation. Before each restart,
it rechecks the bundle identifier, ownership, parent permissions and OpenAI signature, then quits
and opens that same path. If the bundle or OpenCodex runtime moved, reinstall the watcher; a failed
check leaves the running app untouched.
In PAC-fallback mode the watcher also verifies a complete CONNECT and CA-authenticated identity
exchange through the entry, and launches with the PAC switch instead of the resolver rule. A foreign
listener or an entry that only accepts TCP is not considered ready.

### Evidence and decision limits

[#6196](https://github.com/lidge-jun/opencodex/issues/6196) reported zero established
listener connections over about 20 hours on current Desktop builds: the bundled
app-server performs the gate reads and may bypass Chromium's resolver rule.
The later exhausted-Plus-account report used the shim, intercept and restart
together and does not isolate the intercept's effectiveness. This candidate
conflicts with the maintainer's provider-aware admission design, which rejects
local CA installation and quota-data rewriting; maintainers may close it.
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,9 @@
"@bufbuild/protobuf": "^2.14.0",
"@modelcontextprotocol/sdk": "^1.30.0",
"@napi-rs/keyring": "1.3.0",
"acorn": "8.17.0",
"bun": "1.4.2",
"quickjs-wasi": "3.6.2",
"zod": "4.4.3"
},
"devDependencies": {
Expand Down
1 change: 1 addition & 0 deletions scripts/build-standalone.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ const WORKER_ENTRIES: Record<string, string> = {
"restore-worker": join(repoRoot, "src", "storage", "restore-worker.ts"),
"history-worker": join(repoRoot, "src", "codex", "history-worker.ts"),
"request-history-rebuild-worker": join(repoRoot, "src", "routing", "history", "rebuild-worker.ts"),
"pac-worker": join(repoRoot, "src", "chatgpt", "desktop-unblock", "pac-worker.ts"),
};
const GEN_FILE = join(repoRoot, "src", "generated", "worker-bundles.gen.ts");
const GEN_PLACEHOLDER = `// Generated by scripts/build-standalone.ts — do not edit by hand.\n// Placeholder for source checkouts; standalone builds overwrite this file\n// with pre-bundled worker sources before compiling.\nexport const WORKER_BUNDLES: Record<string, string> = {};\n`;
Expand Down
21 changes: 21 additions & 0 deletions scripts/test-layout/layout.json
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,27 @@
"main-account-external-usage.test.ts": "codex-integration",
"jev-decision-model-config.test.ts": "routing",
"cli-combo-partial-update.test.ts": "cli",
"desktop-unblock-ws-frame.test.ts": "clients",
"desktop-unblock-watcher-install.test.ts": "clients",
"desktop-unblock-lifecycle.test.ts": "clients",
"desktop-unblock-readiness-auth.test.ts": "clients",
"desktop-unblock-bundle-preflight.test.ts": "clients",
"desktop-unblock-ws-relay.test.ts": "clients",
"desktop-unblock-ws-upstream.test.ts": "clients",
"desktop-unblock-runtime.test.ts": "clients",
"desktop-unblock-pac.test.ts": "clients",
"desktop-unblock-entry-proxy.test.ts": "clients",
"desktop-unblock-pac-runtime.test.ts": "clients",
"desktop-unblock-pac-policy.test.ts": "clients",
"desktop-unblock-policy-runtime.test.ts": "clients",
"desktop-unblock-policy-transport.test.ts": "clients",
"desktop-unblock-pac-packaging.test.ts": "clients",
"desktop-unblock-listener.test.ts": "clients",
"desktop-unblock-launch-script.test.ts": "clients",
"desktop-unblock-config-boundary.test.ts": "clients",
"desktop-unblock-ca-trust.test.ts": "clients",
"desktop-rewrite.test.ts": "clients",
"socks5-handshake.test.ts": "lib",
"owner-registry-acl.test.ts": "config",
"minimax-combo-image-input.test.ts": "providers",
"web-search-forced-declaration.test.ts": "web-search",
Expand Down
4 changes: 2 additions & 2 deletions skills/ocx/references/01_surface_lifecycle.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,13 +28,13 @@ Drives no management route.

### `ocx chatgpt`

Experimental ChatGPT app-server shim: launch, restore or status (macOS only).
Experimental ChatGPT shim/intercept: launch, restore, status and watcher management (macOS only).

State-changing: yes. JSON mode: `none`.

Drives no management route.

- Default off; launch requires chatgptDesktop.appServerShim: true. Restore removes the generated launcher.
- Default off; launch requires chatgptDesktop.appServerShim or unblockSend. Intercept needs the running proxy and manual CA trust. Watcher manages intercept launches only; restore removes the shim launcher.

### `ocx status`

Expand Down
2 changes: 1 addition & 1 deletion src/chatgpt/app-server-shim/gate-rewrite.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
/** Plain-quota gate rewriting; usage display and non-quota restrictions are preserved. */
const PLAIN_QUOTA_REACHED_TYPE = "rate_limit_reached";

function isRecord(value: unknown): value is Record<string, unknown> {
export function isRecord(value: unknown): value is Record<string, unknown> {
return typeof value === "object" && value !== null && !Array.isArray(value);
}

Expand Down
Loading
Loading