Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
50 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
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
141 changes: 138 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,138 @@ 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.

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 with a 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 OpenCodex's identity-confirmed listener. 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: keep the app working 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:

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

When OpenCodex stops, the app keeps working on that route without a restart; only
the send unblock pauses until OpenCodex is back. The route is captured when
OpenCodex starts: after changing the VPN mode, restart OpenCodex and run
`ocx chatgpt launch`. 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), other hosts follow the system proxy, if there is
one, then go direct, and OpenCodex prints a warning. 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.

### 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.
In PAC-fallback mode the watcher also waits for the CONNECT entry and launches
with the PAC switch instead of the resolver rule.

### 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.
14 changes: 14 additions & 0 deletions scripts/test-layout/layout.json
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,20 @@
"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-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-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 @@ -12,15 +12,15 @@ Declared capabilities: 12.

### `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.

Drives no management route.

JSON mode: `none`.

- 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
51 changes: 51 additions & 0 deletions src/chatgpt/desktop-unblock/ca-trust.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import { X509Certificate } from "node:crypto";
import { existsSync, mkdtempSync, readFileSync, rmSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { defaultSecurityRunner, loginKeychainPath, type SecurityRunner } from "../../claude/intercept/picker-trust";

/**
* Whether macOS trusts the intercept CA the ChatGPT listener's leaf is issued from.
*
* Without that trust every request the app sends to chatgpt.com fails certificate
* verification, which the app does not report: account, usage and settings pages just stay
* empty. `ocx chatgpt status` surfaces this state so the cause is visible. Trust is matched by
* the CA's SHA-1 fingerprint in the user's exported trust settings, so a different or
* regenerated certificate with the same name never counts.
*/

export type ChatgptCaTrust = "trusted" | "untrusted" | "missing" | "unknown" | "unsupported";

export function certificateSha1(pem: string): string {
return new X509Certificate(pem).fingerprint.replace(/:/g, "").toUpperCase();
}

export async function inspectChatgptCaTrust(
caPath: string,
run: SecurityRunner = defaultSecurityRunner,
platform: NodeJS.Platform = process.platform,
): Promise<ChatgptCaTrust> {
if (platform !== "darwin") return "unsupported";
if (!existsSync(caPath)) return "missing";
let dir: string | undefined;
try {
const sha1 = certificateSha1(readFileSync(caPath, "utf8"));
dir = mkdtempSync(join(tmpdir(), "ocx-chatgpt-trust-"));
const file = join(dir, "trust-settings.plist");
const exported = await run(["trust-settings-export", file]);
if (exported.code !== 0) {
// A user domain with no trust settings at all cannot be exported; that is plain "untrusted".
return /no trust settings/i.test(`${exported.stdout}${exported.stderr}`) ? "untrusted" : "unknown";
}
return readFileSync(file, "utf8").includes(`<key>${sha1}</key>`) ? "trusted" : "untrusted";
} catch { // no-excuse-ok: catch -- unreadable certificate or trust settings are no evidence of trust.
return "unknown";
} finally {
if (dir) rmSync(dir, { recursive: true, force: true });
}
}

/** The command that restores trust; it prompts for the login password, so only the user runs it. */
export function chatgptCaTrustCommand(caPath: string): string {
return `security add-trusted-cert -r trustRoot -p ssl -k "${loginKeychainPath()}" "${caPath}"`;
}
Loading
Loading