Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
100 changes: 97 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 @@ -122,3 +122,97 @@ 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. This candidate creates no PAC file or CONNECT entry proxy.

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.

### 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
Comment thread
coderabbitai[bot] marked this conversation as resolved.
**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.

### 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.
11 changes: 11 additions & 0 deletions scripts/test-layout/layout.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,17 @@
"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-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",
"desktop-app-server-shim.test.ts": "clients",
"desktop-app-server-shim-launcher.test.ts": "clients",
"desktop-chatgpt-config.test.ts": "clients",
Expand Down
4 changes: 2 additions & 2 deletions skills/ocx/references/01_management_surface.md
Original file line number Diff line number Diff line change
Expand Up @@ -532,13 +532,13 @@ Each of these writes. Check the flags column before running one unattended.

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

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 link issue`

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