From a0f4cbfbdc5bc16518c53f6f94c5960c93452455 Mon Sep 17 00:00:00 2001 From: JUN Date: Fri, 2 Oct 2026 01:27:21 +0900 Subject: [PATCH] fix(chatgpt): never parse an over-cap line that arrives whole, and tighten the shim docs Follow-up to #6361 review threads: a complete line over maxLineBytes now passes through raw even when it arrives in one chunk; the guide states restore's partial effect when ChatGPT is not installed and scopes the fallback claim; the structure doc drops the unverified respawn claim and maps src/cli/ and src/codex/. Co-authored-by: lcxhh521 <59329914+lcxhh521@users.noreply.github.com> --- .../src/content/docs/guides/chatgpt-desktop.md | 11 +++++++++-- src/chatgpt/app-server-shim/filter.ts | 9 +++++++-- structure/INDEX.md | 4 ++-- structure/clients/chatgpt-desktop.md | 15 ++++++++------- structure/manifest.json | 4 +++- tests/clients/desktop-app-server-shim.test.ts | 7 +++++++ 6 files changed, 36 insertions(+), 14 deletions(-) diff --git a/docs-site/src/content/docs/guides/chatgpt-desktop.md b/docs-site/src/content/docs/guides/chatgpt-desktop.md index 2a1b0a9d438..dfa3f62725b 100644 --- a/docs-site/src/content/docs/guides/chatgpt-desktop.md +++ b/docs-site/src/content/docs/guides/chatgpt-desktop.md @@ -40,6 +40,9 @@ Restore leaves the config flag as configured. Set `chatgptDesktop.appServerShim` to `false` or remove it to disable future explicit shim launches. Normal launches from Dock or Spotlight do not apply the shim automatically. +If ChatGPT is not installed (no `com.openai.codex` bundle is found), `restore` +only removes the launcher: it cannot relaunch anything and exits with an error. + ## Rewrite boundary Only `account/rateLimits/updated` notifications and responses whose top-level @@ -82,9 +85,13 @@ the running ChatGPT bundle process carries the expected launcher override. ## Failure behavior and known limits -A failed precondition or a failed self-test runs the original binary with untouched stdout. +When the platform is not macOS, the OpenCodex runtime is missing, or the filter +self-test fails, the launcher runs the original binary with untouched stdout. A +missing bundled app-server binary is the exception: there is nothing to fall back +to, so the launcher exits with an error (see below). A filter that passes the self-test and then dies mid-session closes the pipe. -Expected (not yet validated against the bundled app-server): the server gets SIGPIPE or a write error and Desktop respawns it through the same launcher. +What the bundled app-server does after that has not been verified; it may get +SIGPIPE or a write error and be respawned by Desktop through the same launcher. The filter's passthrough mode limits this to an exit/crash case: a rewrite exception passes its line through, and an unexpected rewrite-machinery failure switches the remaining stream to raw bytes. diff --git a/src/chatgpt/app-server-shim/filter.ts b/src/chatgpt/app-server-shim/filter.ts index f806c028d33..df9e88db7fd 100644 --- a/src/chatgpt/app-server-shim/filter.ts +++ b/src/chatgpt/app-server-shim/filter.ts @@ -75,8 +75,13 @@ export function createRpcLineFilter( let heldLength = previousLength; for (let i = chunk.indexOf(NEWLINE, start); i !== -1; i = chunk.indexOf(NEWLINE, start)) { const tail = chunk.subarray(start, i + 1); - const whole = heldLength === 0 ? tail : concatParts([...held, tail], heldLength + tail.length); - out.push(emit(whole.subarray(0, whole.length - 1), whole, true)); + if (heldLength + tail.length - 1 > maxLineBytes) { + // A complete line over the cap is passed through as it arrived, never joined or parsed. + out.push(...held, tail); + } else { + const whole = heldLength === 0 ? tail : concatParts([...held, tail], heldLength + tail.length); + out.push(emit(whole.subarray(0, whole.length - 1), whole, true)); + } held = []; heldLength = 0; start = i + 1; diff --git a/structure/INDEX.md b/structure/INDEX.md index 57565dce129..cec7af313b8 100644 --- a/structure/INDEX.md +++ b/structure/INDEX.md @@ -120,10 +120,10 @@ A source area can be described by more than one doc, because these docs are orga | `src/chatgpt/` | [`clients/chatgpt-desktop.md`](clients/chatgpt-desktop.md) | | `src/claude/` | [`runtime.md`](runtime.md)
[`clients/claude-desktop.md`](clients/claude-desktop.md) | | `src/cli.ts` | [`runtime.md`](runtime.md)
[`ops/docs-and-release.md`](ops/docs-and-release.md) | -| `src/cli/` | [`runtime.md`](runtime.md)
[`config.md`](config.md)
[`clients/integrations.md`](clients/integrations.md)
[`clients/claude-desktop.md`](clients/claude-desktop.md)
[`ops/docs-and-release.md`](ops/docs-and-release.md) | +| `src/cli/` | [`runtime.md`](runtime.md)
[`config.md`](config.md)
[`clients/integrations.md`](clients/integrations.md)
[`clients/chatgpt-desktop.md`](clients/chatgpt-desktop.md)
[`clients/claude-desktop.md`](clients/claude-desktop.md)
[`ops/docs-and-release.md`](ops/docs-and-release.md) | | `src/client/` | [`runtime.md`](runtime.md)
[`clients/claude-desktop.md`](clients/claude-desktop.md) | | `src/clients/` | [`clients/integrations.md`](clients/integrations.md) | -| `src/codex/` | [`runtime.md`](runtime.md)
[`config.md`](config.md)
[`codex-home.md`](codex-home.md)
[`catalog.md`](catalog.md)
[`subagents.md`](subagents.md)
[`transports/responses-failover.md`](transports/responses-failover.md)
[`providers/openai-tiers.md`](providers/openai-tiers.md)
[`providers/openai-accounts.md`](providers/openai-accounts.md)
[`gui-and-management-api.md`](gui-and-management-api.md)
[`dashboard-and-usage.md`](dashboard-and-usage.md)
[`ops/docs-and-release.md`](ops/docs-and-release.md) | +| `src/codex/` | [`runtime.md`](runtime.md)
[`config.md`](config.md)
[`codex-home.md`](codex-home.md)
[`catalog.md`](catalog.md)
[`subagents.md`](subagents.md)
[`transports/responses-failover.md`](transports/responses-failover.md)
[`providers/openai-tiers.md`](providers/openai-tiers.md)
[`providers/openai-accounts.md`](providers/openai-accounts.md)
[`gui-and-management-api.md`](gui-and-management-api.md)
[`dashboard-and-usage.md`](dashboard-and-usage.md)
[`clients/chatgpt-desktop.md`](clients/chatgpt-desktop.md)
[`ops/docs-and-release.md`](ops/docs-and-release.md) | | `src/combos/` | [`runtime.md`](runtime.md)
[`providers-and-adapters.md`](providers-and-adapters.md) | | `src/companion/` | [`overview.md`](overview.md)
[`gui-and-management-api.md`](gui-and-management-api.md)
[`companion.md`](companion.md) | | `src/compatibility/` | [`runtime.md`](runtime.md)
[`adapters/compatibility-contracts.md`](adapters/compatibility-contracts.md) | diff --git a/structure/clients/chatgpt-desktop.md b/structure/clients/chatgpt-desktop.md index 32305d6d4dd..1dc8ef396fc 100644 --- a/structure/clients/chatgpt-desktop.md +++ b/structure/clients/chatgpt-desktop.md @@ -17,10 +17,11 @@ then replaces itself with the bundled app-server using shell exec. Only stdout passes through the filter. Stdin, stderr, process identity and the real server's exit status retain the direct app/server relationship. -A failed precondition or a failed self-test runs the original binary with untouched stdout. -A filter that passes the self-test and then dies mid-session closes the pipe. -Expected (not yet validated against the bundled app-server): the server gets SIGPIPE or a write error and Desktop respawns it through the same launcher. -The filter's passthrough mode limits this to an exit/crash case. +When the platform is not macOS, the runtime is missing, or the filter self-test fails, +the launcher runs the original binary with untouched stdout. A missing bundled binary +exits 127 instead (see below). A filter that passes the self-test and then exits +mid-session closes the server's stdout pipe; the filter's passthrough mode limits this +to an exit/crash case. The pure gate rewrite changes known plain-quota fields only in eligible JSON-RPC rate-limit notifications and top-level rate-limit results. Workspace, credit, @@ -34,9 +35,9 @@ machinery preserves buffered bytes and switches the rest of the stream to raw passthrough. Output-write failures propagate; they are not rewrite failures. A partial line is held as a list of chunks and joined once at its newline, so a long line split across many pipe reads costs linear copying. -A line longer than `MAX_FILTERED_LINE_BYTES` (8 MiB) is never buffered or parsed: the -held bytes and the rest of that line stream through raw, and filtering resumes after -its newline. +A line longer than `MAX_FILTERED_LINE_BYTES` (8 MiB) is never joined or parsed, whether +it arrives across many chunks or whole in one: its bytes stream through raw, and +filtering resumes after its newline. The app is discovered and confirmed by bundle identifier through `darwinDesktopAppAdapter.discover` (`src/codex/desktop-app/darwin.ts`). Launch derives diff --git a/structure/manifest.json b/structure/manifest.json index 74de2a50575..3e65e7ba4be 100644 --- a/structure/manifest.json +++ b/structure/manifest.json @@ -454,7 +454,9 @@ "title": "ChatGPT Desktop", "scope": "Experimental macOS app-server stdout shim, opt-in launch, restore and failure boundaries.", "documents": [ - "src/chatgpt/" + "src/chatgpt/", + "src/cli/", + "src/codex/" ] }, { diff --git a/tests/clients/desktop-app-server-shim.test.ts b/tests/clients/desktop-app-server-shim.test.ts index 7d2f554c8c7..d0974969cb8 100644 --- a/tests/clients/desktop-app-server-shim.test.ts +++ b/tests/clients/desktop-app-server-shim.test.ts @@ -187,6 +187,13 @@ describe("app-server line filter", () => { expect(out[0]).toBe(oversized); expect(JSON.parse(out[1]!).result.ordinaryUsageAllowed).toBe(true); expect(seen.some(line => line.includes("padding"))).toBe(false); + + // The same oversized line arriving whole, newline included, in one chunk is not parsed either. + seen.length = 0; + const single = collect([enc(`${oversized}\n${rpcResult(EXHAUSTED_RATE_LIMITS)}\n`)], createRpcLineFilter(rewrite, 1024)).split("\n"); + expect(single[0]).toBe(oversized); + expect(JSON.parse(single[1]!).result.ordinaryUsageAllowed).toBe(true); + expect(seen.some(line => line.includes("padding"))).toBe(false); }); });