Add native ChatGPT Web provider for Codex clients (#8949)

* Bypass proxy compaction for native Codex context

* Add native ChatGPT Web provider pipeline

* Add managed browser and tunnel deployment

* Add ChatGPT Web setup and doctor UI

* Document and test ChatGPT Web integration

* fix(security): register chatgpt-web-codex-doctor in LOCAL_ONLY_API_PATTERNS

The diagnostic route under /api/providers/{id}/chatgpt-web-codex-doctor
was not registered in the spawn-capable route guard. Adding it for
parity with the existing /login pattern.

Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>

* fix(providers): route chatgpt-web-codex admin routes through a service boundary

The provider CRUD/doctor routes imported chatgpt-web-codex helpers
(finalizeValidatedChatGptWebCodexSecrets, encode/decodeChatGptWebCodexSecrets,
getChatGptWebCodexDoctorStatus) directly from open-sse/executors/**, which
no-restricted-imports (EXECUTOR_IMPORT_RESTRICTION) forbids for src/app/**
files — executor implementations must stay behind an open-sse handler or
service boundary.

Add open-sse/services/chatgptWebCodexAdmin.ts as a thin re-export boundary
(mirroring the existing tokenRefresh.ts re-export pattern) and import from
there instead. No behavior change.

Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>

---------

Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
This commit is contained in:
Jan Leon
2026-08-11 14:53:39 +02:00
committed by GitHub
parent 1d33025c70
commit a99c795a67
91 changed files with 10601 additions and 26 deletions

View File

@@ -585,12 +585,18 @@ class ResponsesWsSession {
// preparedContext, but never touches this.upstream/this.upstreamReady; the caller decides
// whether a new upstream socket is needed.
async runPrepare(message, responseBody) {
const prepared = await callInternal(this.fetchImpl, this.baseUrl, this.bridgeSecret, "prepare", {
requestUrl: this.requestUrl,
headers: getAuthHeaders(this.requestUrl, this.requestHeaders),
message,
response: responseBody,
});
const prepared = await callInternal(
this.fetchImpl,
this.baseUrl,
this.bridgeSecret,
"prepare",
{
requestUrl: this.requestUrl,
headers: getAuthHeaders(this.requestUrl, this.requestHeaders),
message,
response: responseBody,
}
);
if (!prepared.ok) {
const message2 =
@@ -602,6 +608,7 @@ class ResponsesWsSession {
const error = new Error(message2);
error.code = code;
error.status = prepared.status;
if (code === "responses_websocket_http_fallback") error.httpFallback = true;
throw error;
}
@@ -716,11 +723,28 @@ class ResponsesWsSession {
// otherwise every turn after the first bypasses the whole pipeline. This reuses
// the already-established upstream transport; it must NOT recreate the socket.
const prepared = await this.runPrepare(message, nextTurnBody);
this.upstream.send(jsonStringifySafe(withPreparedResponseCreate(message, prepared.json.response)));
this.upstream.send(
jsonStringifySafe(withPreparedResponseCreate(message, prepared.json.response))
);
return;
}
this.upstream.send(jsonStringifySafe(message));
} catch (error) {
if (error?.httpFallback) {
const failurePayload = this.sendFailure(
"responses_websocket_http_fallback",
"Retry this request over HTTP/SSE Responses"
);
void this.persistHistory({
status: 426,
success: false,
errorCode: "responses_websocket_http_fallback",
errorMessage: "HTTP/SSE Responses transport required",
terminalMessage: failurePayload,
});
this.close(1013, "http_fallback_required");
return;
}
const code = error?.code || "upstream_websocket_connect_failed";
const messageText = error instanceof Error ? error.message : String(error);
const failurePayload = this.sendFailure(code, messageText);