-
Notifications
You must be signed in to change notification settings - Fork 1.2k
feat: ChatGPT desktop send-unblock intercept (opt-in) #5733
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: dev
Are you sure you want to change the base?
Changes from all commits
cf762b1
678517f
4a5a749
62dfc6c
809a06b
6667e34
e0e3234
dc1feaf
9f577ad
0df2ae4
dc3dd5b
1e6497b
f86c975
93704b4
230cb19
75f3895
84b48d6
7fd141f
3224168
49812c9
798611e
d7de528
1017867
491ffcf
f2ed110
b064b8f
c779ae7
bd85a6c
3959e6d
0748cf5
5c2d634
53c784c
06ec553
e609ada
f58fab8
116c2ac
07b48da
8b031f8
bcdf559
15d0e8b
b0900e5
3970601
6ccfe7e
bba6322
9ef2aaf
3d53e5f
63fb304
665ef82
eda8754
d2ec06b
f9e3515
7ef13dd
6f71931
09d1f04
9a60256
8ad5ca0
9e9b1d3
80b1f52
947bae9
3bef206
f7f890f
a5aaed6
544ebee
954b99d
d24ff57
c71474e
9a27e86
ad18c27
62849df
a433d39
2f3f736
3a3de88
2d4d7a2
cf456e8
c1af925
c155cc7
69207f1
8d7e24e
9d4e45a
3608119
adb39cb
95c4875
4d37c35
b06e54c
90ff8aa
641b05a
aa05b3e
8e532c5
f8d5fd9
9f7397e
4d633cb
02044b2
704857f
7bdd1b2
58c15b8
f978400
1cc89cf
e4a8539
44de45d
6fe4cd0
134c92a
7c625fc
e45692f
9bc8acb
b730157
5a7c48c
891674c
5aa92e9
9314146
b007d33
b1ae178
f1b2180
2f82167
3c663e4
fca5134
96b1406
839909a
4cb43cb
8db8c3d
57d604d
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,182 @@ | ||
| import { execFileSync } from "node:child_process"; | ||
| import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs"; | ||
| import { homedir } from "node:os"; | ||
| import { join } from "node:path"; | ||
| import { getConfigDir } from "../../config/paths"; | ||
| import { CHATGPT_INTERCEPT_HOST } from "./listener"; | ||
| import { chatgptUnblockResolverRule } from "./runtime"; | ||
|
|
||
| /** | ||
| * Launch integration for the ChatGPT desktop send-unblock intercept. | ||
| * | ||
| * The Chromium resolver rule only applies when the app is launched with it, so a normal | ||
| * Dock/Spotlight start reaches the real chatgpt.com and the composer locks again. This module | ||
| * installs a launchd agent that watches the app's Electron `SingletonLock` -- written on every | ||
| * launch -- and, exactly once per launch, restarts the app with the resolver rule if it was | ||
| * started without one. There is no resident polling process: launchd wakes the script on the | ||
| * lock event and the script exits after one check. | ||
| * | ||
| * The watcher only acts when the opencodex intercept listener is actually listening, so with | ||
| * the feature off the app is left completely native. | ||
| */ | ||
|
|
||
| export const CHATGPT_APP_PATH = "/Applications/ChatGPT.app"; | ||
| /** The desktop app is `openai-codex-electron` internally: its Electron userData dir is `Codex`. */ | ||
| export const CHATGPT_SINGLETON_LOCK_PATH = "Library/Application Support/Codex/SingletonLock"; | ||
| export const CHATGPT_UNBLOCK_WATCHER_LABEL = "com.opencodex.chatgpt-unblock-watcher"; | ||
|
|
||
| function expandHome(path: string): string { | ||
| return path.startsWith("~") ? join(homedir(), path.slice(1)) : path; | ||
| } | ||
|
|
||
| export interface ChatgptUnblockWatcherPaths { | ||
| scriptPath: string; | ||
| plistPath: string; | ||
| errPath: string; | ||
| lockPath: string; | ||
| } | ||
|
|
||
| export function chatgptUnblockWatcherPaths(configDir?: string): ChatgptUnblockWatcherPaths { | ||
| const dir = configDir ?? getConfigDir(); | ||
| return { | ||
| scriptPath: join(dir, "chatgpt-unblock-watcher.sh"), | ||
| plistPath: expandHome(`~/Library/LaunchAgents/${CHATGPT_UNBLOCK_WATCHER_LABEL}.plist`), | ||
| errPath: join(dir, "chatgpt-unblock-watcher.err"), | ||
| lockPath: expandHome(`~/${CHATGPT_SINGLETON_LOCK_PATH}`), | ||
| }; | ||
| } | ||
|
|
||
| /** The one-shot launchd script: restart the app with the rule if this launch lacked it. */ | ||
| export function buildChatgptUnblockWatcherScript(port: number): string { | ||
| const rule = chatgptUnblockResolverRule(port); | ||
| return `#!/bin/bash | ||
| # opencodex ChatGPT send-unblock launch watcher (one-shot, launchd-triggered). | ||
| # Fires when the ChatGPT desktop app creates its Electron SingletonLock (i.e. on every | ||
| # launch). If the app was started WITHOUT the host-resolver rule that points ${CHATGPT_INTERCEPT_HOST} at | ||
| # the opencodex TLS listener (normal Dock/Spotlight launch), it is restarted once with the | ||
| # rule. Correctly-launched instances and an absent intercept are left alone. | ||
|
|
||
| PORT=${port} | ||
| RULE='${rule}' | ||
| LOG="$HOME/.opencodex/chatgpt-unblock-watcher.log" | ||
|
|
||
| log() { echo "$(date '+%F %T') $*" >> "$LOG"; } | ||
|
|
||
| # Intercept must be listening; otherwise leave the app alone. | ||
| if ! lsof -nP -iTCP:"$PORT" -sTCP:LISTEN >/dev/null 2>&1; then | ||
| exit 0 | ||
| fi | ||
| # App running? | ||
| if ! pgrep -f "ChatGPT.app/Contents/MacOS/ChatGPT" >/dev/null 2>&1; then | ||
| exit 0 | ||
| fi | ||
| # Already launched with the rule? | ||
| if pgrep -f "MacOS/ChatGPT $RULE" >/dev/null 2>&1; then | ||
| exit 0 | ||
| fi | ||
| log "unflagged ChatGPT detected; restarting with resolver rule" | ||
| osascript -e 'quit app "ChatGPT"' >/dev/null 2>&1 | ||
| sleep 3 | ||
| open -a ChatGPT --args "$RULE" | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win Use one switch-bearing resolver argument for launch and detection. Both launch paths pass a bare
🧰 Tools🪛 ast-grep (0.45.3)[warning] Importing child_process exposes a command-execution surface; ensure any command/argument built from input is validated, and prefer execFile/spawn with an argument array over exec. (detect-child-process-typescript) 📍 Affects 2 files
🤖 Prompt for AI Agents |
||
| log "relaunched with rule" | ||
| `; | ||
| } | ||
|
|
||
| /** One-shot launchd agent: wake on the app's SingletonLock event, run the script, exit. */ | ||
| export function buildChatgptUnblockWatcherPlist(scriptPath: string, watchPath: string, errPath: string): string { | ||
| return `<?xml version="1.0" encoding="UTF-8"?> | ||
| <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> | ||
| <plist version="1.0"> | ||
| <dict> | ||
| <key>Label</key> | ||
| <string>${CHATGPT_UNBLOCK_WATCHER_LABEL}</string> | ||
| <key>ProgramArguments</key> | ||
| <array> | ||
| <string>/bin/bash</string> | ||
| <string>${scriptPath}</string> | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win Escape path values in the generated plist. A valid 🧰 Tools🪛 ast-grep (0.45.3)[warning] Importing child_process exposes a command-execution surface; ensure any command/argument built from input is validated, and prefer execFile/spawn with an argument array over exec. (detect-child-process-typescript) 🤖 Prompt for AI Agents |
||
| </array> | ||
| <key>WatchPaths</key> | ||
| <array> | ||
| <string>${watchPath}</string> | ||
| </array> | ||
| <key>StandardErrorPath</key> | ||
| <string>${errPath}</string> | ||
| </dict> | ||
| </plist> | ||
| `; | ||
| } | ||
|
|
||
| function sh(command: string, args: string[]): { ok: boolean; output: string } { | ||
| try { | ||
| const output = execFileSync(command, args, { encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] }); | ||
| return { ok: true, output }; | ||
| } catch (error) { | ||
| const err = error as { status?: number; stdout?: string; stderr?: string }; | ||
| return { ok: false, output: `${err.stdout ?? ""}${err.stderr ?? ""}`.trim() }; | ||
| } | ||
| } | ||
|
|
||
| export interface InstallChatgptUnblockWatcherOptions { | ||
| port: number; | ||
| configDir?: string; | ||
| /** Test seam: skip the macOS / app-presence guards. */ | ||
| assumeSupported?: boolean; | ||
| } | ||
|
|
||
| /** Install the launch watcher: write script + agent plist and load it with launchd. */ | ||
| export function installChatgptUnblockWatcher(options: InstallChatgptUnblockWatcherOptions): void { | ||
| if (process.platform !== "darwin" && !options.assumeSupported) { | ||
| throw new Error("the ChatGPT launch watcher is only supported on macOS"); | ||
| } | ||
| if (!options.assumeSupported && !existsSync(CHATGPT_APP_PATH)) { | ||
| throw new Error(`${CHATGPT_APP_PATH} not found; install the ChatGPT desktop app first`); | ||
| } | ||
| const paths = chatgptUnblockWatcherPaths(options.configDir); | ||
| mkdirSync(expandHome("~/Library/LaunchAgents"), { recursive: true }); | ||
| writeFileSync(paths.scriptPath, buildChatgptUnblockWatcherScript(options.port), { mode: 0o700 }); | ||
| writeFileSync(paths.plistPath, buildChatgptUnblockWatcherPlist(paths.scriptPath, paths.lockPath, paths.errPath)); | ||
| // Idempotent load: boot out any previous generation first. | ||
| sh("launchctl", ["bootout", `gui/${process.getuid?.() ?? 0}/${CHATGPT_UNBLOCK_WATCHER_LABEL}`]); | ||
| sh("launchctl", ["bootstrap", `gui/${process.getuid?.() ?? 0}`, paths.plistPath]); | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win Propagate launchd operation failures. Both operations discard the result returned by
🧰 Tools🪛 ast-grep (0.45.3)[warning] Importing child_process exposes a command-execution surface; ensure any command/argument built from input is validated, and prefer execFile/spawn with an argument array over exec. (detect-child-process-typescript) 📍 Affects 1 file
🤖 Prompt for AI Agents |
||
| } | ||
|
|
||
| /** Remove the launch watcher: unload the agent and delete its files. */ | ||
| export function uninstallChatgptUnblockWatcher(configDir?: string): void { | ||
| const paths = chatgptUnblockWatcherPaths(configDir); | ||
| sh("launchctl", ["bootout", `gui/${process.getuid?.() ?? 0}/${CHATGPT_UNBLOCK_WATCHER_LABEL}`]); | ||
| for (const path of [paths.plistPath, paths.scriptPath]) { | ||
| try { | ||
| rmSync(path); | ||
| } catch { | ||
| /* already gone */ | ||
| } | ||
| } | ||
| } | ||
|
|
||
| export interface ChatgptUnblockWatcherStatus { | ||
| scriptInstalled: boolean; | ||
| plistInstalled: boolean; | ||
| agentLoaded: boolean; | ||
| scriptUpToDate: boolean; | ||
| plistUpToDate: boolean; | ||
| } | ||
|
|
||
| export function chatgptUnblockWatcherStatus(port: number, configDir?: string): ChatgptUnblockWatcherStatus { | ||
| const paths = chatgptUnblockWatcherPaths(configDir); | ||
| const scriptInstalled = existsSync(paths.scriptPath); | ||
| const plistInstalled = existsSync(paths.plistPath); | ||
| const agentLoaded = sh("launchctl", ["print", `gui/${process.getuid?.() ?? 0}/${CHATGPT_UNBLOCK_WATCHER_LABEL}`]).ok; | ||
| const scriptUpToDate = scriptInstalled | ||
| && readFileSync(paths.scriptPath, "utf8") === buildChatgptUnblockWatcherScript(port); | ||
| const plistUpToDate = plistInstalled | ||
| && readFileSync(paths.plistPath, "utf8") === buildChatgptUnblockWatcherPlist(paths.scriptPath, paths.lockPath, paths.errPath); | ||
| return { scriptInstalled, plistInstalled, agentLoaded, scriptUpToDate, plistUpToDate }; | ||
| } | ||
|
|
||
| /** Launch the ChatGPT desktop app with the resolver rule (macOS). */ | ||
| export function launchChatgptWithRule(port: number): void { | ||
| if (process.platform !== "darwin") { | ||
| throw new Error("launching the ChatGPT desktop app is only supported on macOS"); | ||
| } | ||
| execFileSync("open", ["-a", "ChatGPT", "--args", chatgptUnblockResolverRule(port)], { stdio: "ignore" }); | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,140 @@ | ||
| import type { Server } from "bun"; | ||
| import type { PemKeyPair } from "../../claude/intercept/local-ca"; | ||
| import { forwardHeadersForUpstream } from "../../claude/intercept/listener"; | ||
| import { stripSendBlocksFromJson, stripSendBlocksFromSseLine } from "./rewrite"; | ||
|
|
||
| /** | ||
| * TLS listener for the ChatGPT desktop send-unblock intercept. | ||
| * | ||
| * Launched with `--host-resolver-rules="MAP chatgpt.com 127.0.0.1:<port>"`, the desktop app | ||
| * dialls this listener believing it reached chatgpt.com. Requests are relayed verbatim to the | ||
| * real upstream with the caller's own auth headers; responses pass through untouched except | ||
| * that conversation payloads lose their client-side send-lock entries. Nothing is logged and | ||
| * no credential is persisted -- the listener is a pipe, not a store. | ||
| * | ||
| * Only the exact host `chatgpt.com` is ever presented here. Subdomains (`ab.chatgpt.com`, | ||
| * `codex-cloud-backend.chatgpt.com`) and `auth.openai.com` are not mapped by the launcher, so | ||
| * login, telemetry and cloud sessions stay native. | ||
| */ | ||
|
|
||
| export const CHATGPT_UNBLOCK_UPSTREAM = "https://chatgpt.com"; | ||
| export const CHATGPT_INTERCEPT_HOST = "chatgpt.com"; | ||
|
|
||
| // fetch() transparently decodes the body, so the encoding headers would describe bytes the | ||
| // client never sees. | ||
| const RESPONSE_STRIP_HEADERS = new Set([ | ||
| "connection", "keep-alive", "transfer-encoding", "content-encoding", "content-length", | ||
| ]); | ||
|
|
||
| export interface ChatgptUnblockListenerOptions { | ||
| leaf: PemKeyPair; | ||
| upstreamBase?: string; | ||
| idleTimeout?: number; | ||
| fetchImpl?: typeof fetch; | ||
| /** Test seam: bind a fixed port instead of an ephemeral one. */ | ||
| port?: number; | ||
| } | ||
|
|
||
| function responseHeaders(source: Response): Headers { | ||
| const headers = new Headers(); | ||
| source.headers.forEach((value, name) => { | ||
| if (!RESPONSE_STRIP_HEADERS.has(name.toLowerCase())) headers.append(name, value); | ||
| }); | ||
| return headers; | ||
| } | ||
|
|
||
| /** | ||
| * Line-oriented SSE rewriter. Complete lines are checked one at a time so an untouched stream | ||
| * keeps its exact chunking and line endings; only `data:` lines whose JSON loses an entry are | ||
| * re-serialized. | ||
| */ | ||
| export function sseRewriteStream(debug?: (line: string, rewritten: string | null) => void): TransformStream<Uint8Array, Uint8Array> { | ||
| const decoder = new TextDecoder(); | ||
| const encoder = new TextEncoder(); | ||
| let pending = ""; | ||
| return new TransformStream<Uint8Array, Uint8Array>({ | ||
| transform(chunk, controller) { | ||
| pending += decoder.decode(chunk, { stream: true }); | ||
| let index: number; | ||
| while ((index = pending.indexOf("\n")) !== -1) { | ||
| const line = pending.slice(0, index); | ||
| pending = pending.slice(index + 1); | ||
| const rewritten = stripSendBlocksFromSseLine(line); | ||
| debug?.(line, rewritten); | ||
| controller.enqueue(encoder.encode(`${rewritten ?? line}\n`)); | ||
| } | ||
| }, | ||
| flush(controller) { | ||
| if (pending.length === 0) return; | ||
| const rewritten = stripSendBlocksFromSseLine(pending); | ||
| debug?.(pending, rewritten); | ||
| controller.enqueue(encoder.encode(rewritten ?? pending)); | ||
| pending = ""; | ||
| }, | ||
| }); | ||
| } | ||
|
Comment on lines
+51
to
+75
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win Add regression tests for This file adds a new request and response relay. The file already has a test seam: both functions are exported, and the relay accepts
When you write the tests, collect all chunks into one buffer and split on Put the tests in The path instructions say: "A behavior change in src/ should come with a focused regression test near the existing tests for that subsystem." The chunking advice comes from a learning: "Tests should accumulate/decode all chunks into a single buffer and split on the frame delimiter." Also applies to: 85-126 🤖 Prompt for AI AgentsSources: Path instructions, Learnings |
||
|
|
||
| function isJsonContentType(contentType: string): boolean { | ||
| return contentType.includes("application/json") || contentType.endsWith("+json"); | ||
| } | ||
|
|
||
| function isEventStreamContentType(contentType: string): boolean { | ||
| return contentType.includes("text/event-stream"); | ||
| } | ||
|
|
||
| export async function relayWithSendUnblock( | ||
| req: Request, | ||
| upstreamBase: string, | ||
| fetchImpl: typeof fetch = fetch, | ||
| ): Promise<Response> { | ||
| const url = new URL(req.url); | ||
| const target = `${upstreamBase.replace(/\/$/, "")}${url.pathname}${url.search}`; | ||
| const hasBody = req.method !== "GET" && req.method !== "HEAD"; | ||
| let upstream: Response; | ||
| try { | ||
| upstream = await fetchImpl(target, { | ||
| method: req.method, | ||
| headers: forwardHeadersForUpstream(req.headers), | ||
| body: hasBody ? req.body : undefined, | ||
| signal: req.signal, | ||
| redirect: "manual", | ||
| // @ts-expect-error -- streaming request bodies require half duplex under the fetch spec. | ||
| duplex: "half", | ||
| }); | ||
| } catch (error) { | ||
| return Response.json( | ||
| { error: { message: `chatgpt unblock relay failed: ${error instanceof Error ? error.message : String(error)}` } }, | ||
| { status: 502 }, | ||
| ); | ||
| } | ||
| const headers = responseHeaders(upstream); | ||
| const contentType = upstream.headers.get("content-type") ?? ""; | ||
| if (isJsonContentType(contentType)) { | ||
| let text: string; | ||
| try { | ||
| text = await upstream.text(); | ||
| } catch { | ||
| return new Response(JSON.stringify({ error: { message: "chatgpt unblock upstream read failed" } }), { status: 502, headers }); | ||
| } | ||
| const rewritten = stripSendBlocksFromJson(text); | ||
|
Comment on lines
+110
to
+119
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [P1] Restrict rewriting to an eligible conversation/provider and known response surfaces. The URL is used to construct the upstream target, but the decision here checks only content type; the SSE branch is equally broad. Offline reproduction with the unchanged relay function and injected fetch: request Please preserve unrelated/unknown/native-route responses and upstream authorization errors unchanged. Use the existing OpenCodex routing authority to establish eligibility and narrowly identify the actual composer readiness surface. If a global account snapshot cannot safely distinguish mixed native/third-party conversations, do not infer that the entire account is allowed from this opt-in flag; retain the explicit native-queue fallback instead. Add negative tests for unrelated paths and absent/unsupported provider eligibility. |
||
| return new Response(rewritten ?? text, { status: upstream.status, statusText: upstream.statusText, headers }); | ||
| } | ||
|
Comment on lines
+112
to
+121
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win 🔎 Supported by static analysis🏁 Script executed: #!/bin/bash
# Test: Look for any existing handling of null-body statuses in the relays.
rg -nP 'new Response\(' -C2 src/claude/intercept src/chatgpt
rg -nP '\b(204|304)\b' src/claude/intercept src/chatgptRepository: lidge-jun/opencodex Length of output: 1800 🏁 Script executed: #!/bin/bash
printf '%s\n' '--- listener outline ---'
ast-grep outline src/chatgpt/desktop-unblock/listener.ts
printf '%s\n' '--- relay and request forwarding ---'
cat -n src/chatgpt/desktop-unblock/listener.ts | sed -n '1,145p'
printf '%s\n' '--- callers ---'
rg -n -C3 'relayWithSendUnblock|forwardHeadersForUpstream|startChatgptUnblockListener' src/chatgpt src/structure structure 2>/dev/null || trueRepository: lidge-jun/opencodex Length of output: 11722 🏁 Script executed: #!/bin/bash
cat -n src/claude/intercept/listener.ts | sed -n '1,105p'
rg -n -C4 'If-None-Match|If-Modified-Since|forwardHeadersForUpstream' src testsRepository: lidge-jun/opencodex Length of output: 10701 Handle null-body statuses before response rewriting. If the upstream returns 🐛 Proposed fix const headers = responseHeaders(upstream);
+ const nullBodyStatus = [204, 205, 304].includes(upstream.status);
+ if (nullBodyStatus || req.method === "HEAD") {
+ return new Response(null, { status: upstream.status, statusText: upstream.statusText, headers });
+ }
const contentType = upstream.headers.get("content-type") ?? "";🤖 Prompt for AI Agents |
||
| if (isEventStreamContentType(contentType) && upstream.body) { | ||
| return new Response(upstream.body.pipeThrough(sseRewriteStream()), { status: upstream.status, statusText: upstream.statusText, headers }); | ||
| } | ||
| return new Response(upstream.body, { status: upstream.status, statusText: upstream.statusText, headers }); | ||
| } | ||
|
|
||
| /** Bind the intercept TLS listener on an ephemeral loopback port. */ | ||
| export function startChatgptUnblockListener<T = undefined>(options: ChatgptUnblockListenerOptions): Server<T> { | ||
| const upstreamBase = options.upstreamBase ?? CHATGPT_UNBLOCK_UPSTREAM; | ||
| return Bun.serve<T>({ | ||
| port: options.port ?? 0, | ||
| hostname: "127.0.0.1", | ||
| tls: { cert: options.leaf.certPem, key: options.leaf.keyPem }, | ||
| idleTimeout: options.idleTimeout ?? 255, | ||
| async fetch(req) { | ||
| return relayWithSendUnblock(req, upstreamBase, options.fetchImpl); | ||
| }, | ||
| }); | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🩺 Stability & Availability | 🟠 Major | 🏗️ Heavy lift
Distinguish the OpenCodex listener from another process on the port. If another process occupies the selected port, the OpenCodex listener can fail to bind while both checks still report success. The watcher can then restart ChatGPT toward the wrong service.
src/chatgpt/desktop-unblock/launch-watcher.ts#L66-L66: confirm listener ownership or identity before changing ChatGPT's route.src/cli/chatgpt-command.ts#L52-L52: use that identity check instead of reporting any listening process as the intercept.🧰 Tools
🪛 ast-grep (0.45.3)
[warning] Importing child_process exposes a command-execution surface; ensure any command/argument built from input is validated, and prefer execFile/spawn with an argument array over exec.
Context: import { execFileSync } from "node:child_process";
Note: [CWE-78] Improper Neutralization of Special Elements used in an OS Command ('OS Command Injection').
(detect-child-process-typescript)
📍 Affects 2 files
src/chatgpt/desktop-unblock/launch-watcher.ts#L66-L66(this comment)src/cli/chatgpt-command.ts#L52-L52🤖 Prompt for AI Agents