Skip to content
Merged
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
7 changes: 7 additions & 0 deletions .changeset/desktop-startup-passthrough.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
"grok-bot-cli": patch
---

Pass Desktop startup configuration, sign-out, and CLI help/version requests through to the real Codex CLI instead of the interactive daemon bridge.

Allow fallback when the bridge reaches EOF without consuming input or committing output. Existing shim users must re-run `gbot codex desktop-shim install` after upgrading, then fully quit and relaunch ChatGPT.
4 changes: 3 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -225,10 +225,12 @@ gbot codex desktop-shim uninstall # removes wrapper/bridge/LaunchAgent; Desktop

Install copies the scripts out of the package into durable `~/.codex/bin/` paths, so uninstalling the npm package never leaves Desktop pointing into a deleted checkout. The wrapper exports install-time `CODEX_HOME` and derives the socket from it at runtime (`CODEX_APP_SERVER_SOCK`), so custom `CODEX_HOME` layouts work; `gbot` itself resolves the socket the same way (`CODEX_APP_SERVER_SOCK` wins, else `CODEX_HOME`), and `status` reports the effective path and which variable won.

Fail-open runs only before any stdin byte is consumed and no daemon payload was committed to stdout: the wrapper preflights the daemon socket (3 s deadline) and runs the bridge as a child — never `exec` — falling through to the real standalone `codex` for every non-`app-server` spawn, every preflight failure, and every pre-session bridge failure (exit 1), always on pristine stdio+stdout. The bridge's connect + WebSocket-upgrade handshake runs under one absolute monotonic deadline (`CODEX_BRIDGE_CONNECT_TIMEOUT`, default 10 s) requiring `HTTP/1.1 101` plus a matching `Sec-WebSocket-Accept` (a `200` fails even with a valid hash), and a first-response deadline (`CODEX_BRIDGE_FIRST_MESSAGE_TIMEOUT`, default 30 s) bounds connect-to-matching-daemon-response covering the first RPC — an unrelated notification or foreign response never satisfies it, only the matching response/error (or timeout) clears it — preflight (3 s) + upgrade (10 s) + first RPC (30 s) stays far below a daemon-lock hang, and preflight and upgrade leave stdin untouched. Reads after a successful init block with no timeout so healthy idle sessions survive silence; every mid-session socket send and stdout write runs under its own write budget (`CODEX_BRIDGE_IO_TIMEOUT`, default 30 s), so a wedged or non-reading peer cannot hang Desktop. The commit point is byte-precise on input OR output, committed before the stdout write: the bridge counts every stdin byte read (buffered readahead included, even an unforwarded blank line) and marks stdout used before writing any forwarded daemon payload via a bounded write, so the fallback only ever runs on truly pristine stdio+stdout. Buffered EOF-tail data flushes before the WS Close. A mid-session bridge failure (exit 2+) makes the wrapper exit promptly so Desktop reconnects; the fallback never runs on half-consumed stdin or dirty stdout. The wrapper never starts the daemon on the spawn path (a wedged daemon lock must not block Desktop) — upkeep belongs to the LaunchAgent login script and install, which share one `CODEX_HOME` for the GUI domain and the daemon they start; if the socket is absent, Desktop simply runs stock Codex until the daemon is started.
Fail-open runs only before any stdin byte is consumed and no daemon payload was committed to stdout: the wrapper preflights the daemon socket (3 s deadline) and runs the bridge as a child — never `exec` — passing one-shot startup configuration, sign-out, help, version, and non-`app-server` requests to the real standalone `codex`, and falling back on every preflight failure or pre-session bridge failure (exit 1), always on pristine stdio+stdout. The bridge's connect + WebSocket-upgrade handshake runs under one absolute monotonic deadline (`CODEX_BRIDGE_CONNECT_TIMEOUT`, default 10 s) requiring `HTTP/1.1 101` plus a matching `Sec-WebSocket-Accept` (a `200` fails even with a valid hash), and a first-response deadline (`CODEX_BRIDGE_FIRST_MESSAGE_TIMEOUT`, default 30 s) bounds connect-to-matching-daemon-response covering the first RPC — an unrelated notification or foreign response never satisfies it, only the matching response/error (or timeout) clears it — preflight (3 s) + upgrade (10 s) + first RPC (30 s) stays far below a daemon-lock hang, and preflight and upgrade leave stdin untouched. Reads after a successful init block with no timeout so healthy idle sessions survive silence; every mid-session socket send and stdout write runs under its own write budget (`CODEX_BRIDGE_IO_TIMEOUT`, default 30 s), so a wedged or non-reading peer cannot hang Desktop. The commit point is byte-precise on input OR output, committed before the stdout write: the bridge counts every stdin byte read (buffered readahead included, even an unforwarded blank line) and marks stdout used before writing any forwarded daemon payload via a bounded write, so the fallback only ever runs on truly pristine stdio+stdout. Buffered EOF-tail data flushes before the WS Close. A mid-session bridge failure (exit 2+) makes the wrapper exit promptly so Desktop reconnects; the fallback never runs on half-consumed stdin or dirty stdout. The wrapper never starts the daemon on the spawn path (a wedged daemon lock must not block Desktop) — upkeep belongs to the LaunchAgent login script and install, which share one `CODEX_HOME` for the GUI domain and the daemon they start; if the socket is absent, Desktop simply runs stock Codex until the daemon is started.

Residual risks, stated honestly: requests without both matching thread and turn IDs remain unanswered; a Codex client must handle those requests. A daemon that speaks framing-valid but semantically unexpected JSON-RPC (unknown methods, id-less responses) is treated as transport; pins are to app-server schema 0.158.0. The bridge trusts the local control socket; a malicious local daemon could hold the session up to the stated budgets, not past them.

After upgrading `gbot`, existing shim users must re-run `gbot codex desktop-shim install` and fully quit and relaunch ChatGPT to refresh the installed scripts. EOF without any consumed input or committed output falls back to the real CLI instead of counting as a served session.

Current shim limitation: Desktop's spawn-time app-tools MCP `-c` overrides are not forwarded to the already-running managed daemon. No restoration path or app-tools parity has been demonstrated here; this is not a claim of permanent protocol impossibility. Fully quit and relaunch ChatGPT.app after install (or login) so it inherits `CODEX_CLI_PATH`. `status` reads the macOS GUI-domain value via `launchctl getenv` (what Desktop actually inherits) alongside the calling shell's value. LaunchAgent persistence is macOS-first; elsewhere install still writes the wrapper and bridge but leaves `CODEX_CLI_PATH` for you to export. `~/.codex/bin` holds scripts only — there is no extra revert note to clean up; revert is `gbot codex desktop-shim uninstall` plus this section.

**ChatGPT Desktop over CDP (scaffold).** Prefer the Desktop UI surface via Chrome DevTools Protocol when you need more than the app-server socket. CDP is **local-only** (`127.0.0.1`); there is no remote transport. Relaunch the macOS app with a debug port (Electron fuses block Node inspect, not Chromium's flag):
Expand Down
2 changes: 1 addition & 1 deletion artifact/agent-bundle.compile-evidence.json

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion artifact/agent-bundle.manifest.json

Large diffs are not rendered by default.

20 changes: 16 additions & 4 deletions artifact/bin/gbot-flight.mjs

Large diffs are not rendered by default.

20 changes: 16 additions & 4 deletions artifact/bin/gbot.mjs

Large diffs are not rendered by default.

18 changes: 15 additions & 3 deletions artifact/mcp/mcp-grok-bot-b8c2461e-flight.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -27448,19 +27448,30 @@ var __webpack_modules__ = {
let daemon = false;
let proxy = false;
let generate = false;
let oneShot = false;
for (const arg of argv ?? []){
if ([
"--print-startup-config",
"--startup-sign-out",
"help",
"--help",
"-h",
"--version",
"-V"
].includes(arg)) oneShot = true;
if (arg === "app-server") appServer = true;
else if (arg === "daemon") daemon = true;
else if (arg === "proxy") proxy = true;
else if (arg === "generate-ts" || arg === "generate-json-schema") generate = true;
}
return appServer && !daemon && !proxy && !generate;
return appServer && !daemon && !proxy && !generate && !oneShot;
}
function renderWrapperScript({ realPath, bridgePath, wrapperLogPath, codexHome, bridgeLogPath }) {
return `#!/bin/bash
# Installed by \`gbot codex desktop-shim install\`. Rewrites ChatGPT Desktop's
# \`codex ... app-server\` spawn into a stdio<->WebSocket bridge onto the managed
# daemon's control socket. Reversible: \`gbot codex desktop-shim uninstall\`.
# Startup configuration, sign-out, help and version requests use the real CLI.
# The daemon is kept up by the LaunchAgent login script, not here: this hot path
# never starts the daemon itself, so a wedged daemon lock cannot hang
# Desktop. Fail-open runs ONLY while stdio is still pristine (no stdin
Expand Down Expand Up @@ -27493,9 +27504,10 @@ if [[ ! -x "$REAL" ]]; then
fi
fi

has_app_server=0; has_daemon=0; has_proxy=0; has_generate=0
has_app_server=0; has_daemon=0; has_proxy=0; has_generate=0; has_one_shot=0
for a in "$@"; do
case "$a" in
--print-startup-config|--startup-sign-out|help|--help|-h|--version|-V) has_one_shot=1 ;;
app-server) has_app_server=1 ;;
daemon) has_daemon=1 ;;
proxy) has_proxy=1 ;;
Expand All @@ -27509,7 +27521,7 @@ preflight() {
python3 -c 'import os,socket,sys; s=socket.socket(socket.AF_UNIX,socket.SOCK_STREAM); s.settimeout(float(os.environ.get("CODEX_DESKTOP_PREFLIGHT_TIMEOUT","3"))); s.connect(sys.argv[1]); s.close()' "$SOCK" 2>/dev/null
}

if [[ "$has_app_server" -eq 1 && "$has_daemon" -eq 0 && "$has_proxy" -eq 0 && "$has_generate" -eq 0 ]]; then
if [[ "$has_app_server" -eq 1 && "$has_daemon" -eq 0 && "$has_proxy" -eq 0 && "$has_generate" -eq 0 && "$has_one_shot" -eq 0 ]]; then
if [[ -f "$BRIDGE" ]] && command -v python3 >/dev/null 2>&1; then
if preflight; then
echo "$(ts) rewrite -> stdio-ws bridge" >>"$LOG" 2>/dev/null || true
Expand Down
18 changes: 15 additions & 3 deletions artifact/mcp/mcp-grok-bot-b8c2461e.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -64663,19 +64663,30 @@ var __webpack_modules__ = {
let daemon = false;
let proxy = false;
let generate = false;
let oneShot = false;
for (const arg of argv ?? []){
if ([
"--print-startup-config",
"--startup-sign-out",
"help",
"--help",
"-h",
"--version",
"-V"
].includes(arg)) oneShot = true;
if (arg === "app-server") appServer = true;
else if (arg === "daemon") daemon = true;
else if (arg === "proxy") proxy = true;
else if (arg === "generate-ts" || arg === "generate-json-schema") generate = true;
}
return appServer && !daemon && !proxy && !generate;
return appServer && !daemon && !proxy && !generate && !oneShot;
}
function renderWrapperScript({ realPath, bridgePath, wrapperLogPath, codexHome, bridgeLogPath }) {
return `#!/bin/bash
# Installed by \`gbot codex desktop-shim install\`. Rewrites ChatGPT Desktop's
# \`codex ... app-server\` spawn into a stdio<->WebSocket bridge onto the managed
# daemon's control socket. Reversible: \`gbot codex desktop-shim uninstall\`.
# Startup configuration, sign-out, help and version requests use the real CLI.
# The daemon is kept up by the LaunchAgent login script, not here: this hot path
# never starts the daemon itself, so a wedged daemon lock cannot hang
# Desktop. Fail-open runs ONLY while stdio is still pristine (no stdin
Expand Down Expand Up @@ -64708,9 +64719,10 @@ if [[ ! -x "$REAL" ]]; then
fi
fi

has_app_server=0; has_daemon=0; has_proxy=0; has_generate=0
has_app_server=0; has_daemon=0; has_proxy=0; has_generate=0; has_one_shot=0
for a in "$@"; do
case "$a" in
--print-startup-config|--startup-sign-out|help|--help|-h|--version|-V) has_one_shot=1 ;;
app-server) has_app_server=1 ;;
daemon) has_daemon=1 ;;
proxy) has_proxy=1 ;;
Expand All @@ -64724,7 +64736,7 @@ preflight() {
python3 -c 'import os,socket,sys; s=socket.socket(socket.AF_UNIX,socket.SOCK_STREAM); s.settimeout(float(os.environ.get("CODEX_DESKTOP_PREFLIGHT_TIMEOUT","3"))); s.connect(sys.argv[1]); s.close()' "$SOCK" 2>/dev/null
}

if [[ "$has_app_server" -eq 1 && "$has_daemon" -eq 0 && "$has_proxy" -eq 0 && "$has_generate" -eq 0 ]]; then
if [[ "$has_app_server" -eq 1 && "$has_daemon" -eq 0 && "$has_proxy" -eq 0 && "$has_generate" -eq 0 && "$has_one_shot" -eq 0 ]]; then
if [[ -f "$BRIDGE" ]] && command -v python3 >/dev/null 2>&1; then
if preflight; then
echo "$(ts) rewrite -> stdio-ws bridge" >>"$LOG" 2>/dev/null || true
Expand Down
18 changes: 15 additions & 3 deletions artifact/scripts/gbot-relay.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -24638,19 +24638,30 @@ var __webpack_modules__ = {
let daemon = false;
let proxy = false;
let generate = false;
let oneShot = false;
for (const arg of argv ?? []){
if ([
"--print-startup-config",
"--startup-sign-out",
"help",
"--help",
"-h",
"--version",
"-V"
].includes(arg)) oneShot = true;
if (arg === "app-server") appServer = true;
else if (arg === "daemon") daemon = true;
else if (arg === "proxy") proxy = true;
else if (arg === "generate-ts" || arg === "generate-json-schema") generate = true;
}
return appServer && !daemon && !proxy && !generate;
return appServer && !daemon && !proxy && !generate && !oneShot;
}
function renderWrapperScript({ realPath, bridgePath, wrapperLogPath, codexHome, bridgeLogPath }) {
return `#!/bin/bash
# Installed by \`gbot codex desktop-shim install\`. Rewrites ChatGPT Desktop's
# \`codex ... app-server\` spawn into a stdio<->WebSocket bridge onto the managed
# daemon's control socket. Reversible: \`gbot codex desktop-shim uninstall\`.
# Startup configuration, sign-out, help and version requests use the real CLI.
# The daemon is kept up by the LaunchAgent login script, not here: this hot path
# never starts the daemon itself, so a wedged daemon lock cannot hang
# Desktop. Fail-open runs ONLY while stdio is still pristine (no stdin
Expand Down Expand Up @@ -24683,9 +24694,10 @@ if [[ ! -x "$REAL" ]]; then
fi
fi

has_app_server=0; has_daemon=0; has_proxy=0; has_generate=0
has_app_server=0; has_daemon=0; has_proxy=0; has_generate=0; has_one_shot=0
for a in "$@"; do
case "$a" in
--print-startup-config|--startup-sign-out|help|--help|-h|--version|-V) has_one_shot=1 ;;
app-server) has_app_server=1 ;;
daemon) has_daemon=1 ;;
proxy) has_proxy=1 ;;
Expand All @@ -24699,7 +24711,7 @@ preflight() {
python3 -c 'import os,socket,sys; s=socket.socket(socket.AF_UNIX,socket.SOCK_STREAM); s.settimeout(float(os.environ.get("CODEX_DESKTOP_PREFLIGHT_TIMEOUT","3"))); s.connect(sys.argv[1]); s.close()' "$SOCK" 2>/dev/null
}

if [[ "$has_app_server" -eq 1 && "$has_daemon" -eq 0 && "$has_proxy" -eq 0 && "$has_generate" -eq 0 ]]; then
if [[ "$has_app_server" -eq 1 && "$has_daemon" -eq 0 && "$has_proxy" -eq 0 && "$has_generate" -eq 0 && "$has_one_shot" -eq 0 ]]; then
if [[ -f "$BRIDGE" ]] && command -v python3 >/dev/null 2>&1; then
if preflight; then
echo "$(ts) rewrite -> stdio-ws bridge" >>"$LOG" 2>/dev/null || true
Expand Down
2 changes: 1 addition & 1 deletion src/core/desktop-shim-bridge.js

Large diffs are not rendered by default.

12 changes: 8 additions & 4 deletions src/core/desktop-shim.js
Original file line number Diff line number Diff line change
Expand Up @@ -62,27 +62,30 @@ export function defaultPaths({ env = process.env, home = userHomeDir(env) } = {}
/**
* Pure passthrough decision, mirroring the wrapper's argv scan. True only for
* a bare `app-server` spawn: Desktop's private-stdio case. `daemon`, `proxy`,
* and schema generations always exec the real Codex.
* schema generations, and one-shot startup/help/version requests exec real Codex.
*/
export function shouldBridge(argv) {
let appServer = false;
let daemon = false;
let proxy = false;
let generate = false;
let oneShot = false;
for (const arg of argv ?? []) {
if (["--print-startup-config", "--startup-sign-out", "help", "--help", "-h", "--version", "-V"].includes(arg)) oneShot = true;
if (arg === "app-server") appServer = true;
else if (arg === "daemon") daemon = true;
else if (arg === "proxy") proxy = true;
else if (arg === "generate-ts" || arg === "generate-json-schema") generate = true;
}
return appServer && !daemon && !proxy && !generate;
return appServer && !daemon && !proxy && !generate && !oneShot;
}

export function renderWrapperScript({ realPath, bridgePath, wrapperLogPath, codexHome, bridgeLogPath }) {
return `#!/bin/bash
# Installed by \`gbot codex desktop-shim install\`. Rewrites ChatGPT Desktop's
# \`codex ... app-server\` spawn into a stdio<->WebSocket bridge onto the managed
# daemon's control socket. Reversible: \`gbot codex desktop-shim uninstall\`.
# Startup configuration, sign-out, help and version requests use the real CLI.
# The daemon is kept up by the LaunchAgent login script, not here: this hot path
# never starts the daemon itself, so a wedged daemon lock cannot hang
# Desktop. Fail-open runs ONLY while stdio is still pristine (no stdin
Expand Down Expand Up @@ -115,9 +118,10 @@ if [[ ! -x "$REAL" ]]; then
fi
fi

has_app_server=0; has_daemon=0; has_proxy=0; has_generate=0
has_app_server=0; has_daemon=0; has_proxy=0; has_generate=0; has_one_shot=0
for a in "$@"; do
case "$a" in
--print-startup-config|--startup-sign-out|help|--help|-h|--version|-V) has_one_shot=1 ;;
app-server) has_app_server=1 ;;
daemon) has_daemon=1 ;;
proxy) has_proxy=1 ;;
Expand All @@ -131,7 +135,7 @@ preflight() {
python3 -c 'import os,socket,sys; s=socket.socket(socket.AF_UNIX,socket.SOCK_STREAM); s.settimeout(float(os.environ.get("CODEX_DESKTOP_PREFLIGHT_TIMEOUT","3"))); s.connect(sys.argv[1]); s.close()' "$SOCK" 2>/dev/null
}

if [[ "$has_app_server" -eq 1 && "$has_daemon" -eq 0 && "$has_proxy" -eq 0 && "$has_generate" -eq 0 ]]; then
if [[ "$has_app_server" -eq 1 && "$has_daemon" -eq 0 && "$has_proxy" -eq 0 && "$has_generate" -eq 0 && "$has_one_shot" -eq 0 ]]; then
if [[ -f "$BRIDGE" ]] && command -v python3 >/dev/null 2>&1; then
if preflight; then
echo "$(ts) rewrite -> stdio-ws bridge" >>"$LOG" 2>/dev/null || true
Expand Down
Loading