feat: added x-oai-attestation header for ChatGPT-OAuth Codex requests

- Add `CodexAttestationProvider` hook and `getCodexAttestationHeader` resolver that gates on ChatGPT-OAuth credentials.
- Integrate attestation into WebSocket transport, SSE event stream, and OpenAI compaction requests.
- Wire `setCodexAttestationProvider(generateCodexAttestation)` in model-registry init for OAuth sessions.
This commit is contained in:
can1357
2026-07-24 09:17:00 +02:00
parent c1d4e38aa6
commit 17735c7786
8 changed files with 62 additions and 3 deletions
+5
View File
@@ -20,6 +20,7 @@ import { applyCodexResponsesLiteShape } from "@oh-my-pi/pi-ai/providers/openai-c
import {
createOpenAICodexCompactionRequestContext,
createOpenAICodexCompatibilityMetadata,
getCodexAttestationHeader,
} from "@oh-my-pi/pi-ai/providers/openai-codex-responses";
import { parseAzureDeploymentNameMap, parseTextSignature } from "@oh-my-pi/pi-ai/providers/openai-shared";
import { transformMessages } from "@oh-my-pi/pi-ai/providers/transform-messages";
@@ -650,6 +651,10 @@ export async function requestOpenAiRemoteCompaction(
if (accountId) {
headers[OPENAI_HEADERS.ACCOUNT_ID] = accountId;
}
const attestation = await getCodexAttestationHeader(accountId);
if (attestation) {
headers[OPENAI_HEADERS.ATTESTATION] = attestation;
}
headers[OPENAI_HEADERS.BETA] = OPENAI_HEADER_VALUES.BETA_RESPONSES;
headers[OPENAI_HEADERS.ORIGINATOR] = OPENAI_HEADER_VALUES.ORIGINATOR_CODEX;
Object.assign(
+1
View File
@@ -4,6 +4,7 @@
### Added
- Added `setCodexAttestationProvider` API for injecting `x-oai-attestation` headers in ChatGPT-OAuth Codex requests
- Added OAuth account session pinning and active status tracking in storage
- Added OpenAI Responses native computer-use transport, including batched actions and exact `computer_call`/`computer_call_output` replay with pending/acknowledged safety checks and `image_url`/`file_id` output references. Models without native support receive the same action surface as a regular function tool; provider-specific tool-choice forcing is used where supported.
- Added `PI_CODEX_RESPONSES_LITE` to override the catalog-selected Codex Responses transport for diagnostics (`1`/`true` forces Lite; `0`/`false` forces the standard body).
@@ -241,6 +241,42 @@ const CODEX_RETRYABLE_EVENT_CODES = new Set(["model_error", "server_error", "int
const CODEX_RETRYABLE_EVENT_MESSAGE =
/processing your request|retry your request|temporar(?:y|ily)|overloaded|service.?unavailable|internal error|server error/i;
const CODEX_PROVIDER_SESSION_STATE_KEY = "openai-codex-responses";
/**
* Host integration boundary for just-in-time `x-oai-attestation` header
* values (codex-rs `AttestationProvider`). Resolves to the full header value
* — an `{"v":1,"s":0,"t":"v1.…"}` envelope — or `undefined` when no
* attestation should be sent.
*/
export type CodexAttestationProvider = () => Promise<string | undefined>;
let codexAttestationProvider: CodexAttestationProvider | undefined;
/**
* Install the process-wide attestation hook consulted for upstream Codex
* requests (codex-rs stores its provider on `ModelClient` construction). The
* hook is only consulted for ChatGPT-OAuth credentials and runs just-in-time
* per request; WebSocket handshakes resolve once per connection because the
* header is connection-scoped there.
*/
export function setCodexAttestationProvider(provider: CodexAttestationProvider | undefined): void {
codexAttestationProvider = provider;
}
/**
* Resolve the `x-oai-attestation` header value for one upstream request.
* Gated on ChatGPT-OAuth credentials (a Codex JWT carries `chatgpt_account_id`;
* codex-rs gates on `auth.is_chatgpt_auth()`). A throwing hook degrades to no
* header rather than failing the request.
*/
export async function getCodexAttestationHeader(accountId: string | undefined): Promise<string | undefined> {
if (!accountId || !codexAttestationProvider) return undefined;
try {
return await codexAttestationProvider();
} catch {
return undefined;
}
}
const X_CODEX_TURN_STATE_HEADER = "x-codex-turn-state";
const X_MODELS_ETAG_HEADER = "x-models-etag";
/** WebSocket frames cannot carry per-request HTTP headers; codex-rs mirrors the lite marker into `client_metadata` under this key. */
@@ -1535,6 +1571,7 @@ async function openCodexWebSocketTransport(
websocketState,
requestContext.responsesLite,
requestContext.requestMetadata,
await getCodexAttestationHeader(requestContext.accountId),
);
const requestBodyForState = structuredCloneJSON(requestContext.transformedBody);
// `onPayload` may rewrite the outgoing frame (e.g. drop `stream_options`);
@@ -2730,6 +2767,7 @@ export async function prewarmOpenAICodexResponses(
);
const codexClientVersion = CODEX_CLIENT_VERSION;
const requestIdentity = createCodexCompatibilityIdentity(metadataSession);
const attestation = await getCodexAttestationHeader(accountId);
const headers = logger.time(
"prewarmCodex:createHeaders",
createCodexHeaders,
@@ -2742,6 +2780,7 @@ export async function prewarmOpenAICodexResponses(
state,
responsesLite,
requestIdentity,
attestation,
);
await logger.time(
"prewarmCodex:establishWs",
@@ -3888,6 +3927,7 @@ async function openCodexSseEventStream(
state,
responsesLite,
requestMetadata,
await getCodexAttestationHeader(accountId),
);
CODEX_DEBUG &&
logger.debug("[codex] codex request", {
@@ -3961,11 +4001,17 @@ function createCodexHeaders(
state?: CodexWebSocketSessionState,
responsesLite = false,
requestMetadata?: CodexCompatibilityIdentity,
attestation?: string,
): Headers {
const headers = new Headers(initHeaders ?? {});
headers.delete("x-api-key");
headers.set("Authorization", `Bearer ${accessToken}`);
if (accountId) headers.set(OPENAI_HEADERS.ACCOUNT_ID, accountId);
if (attestation) {
headers.set(OPENAI_HEADERS.ATTESTATION, attestation);
} else {
headers.delete(OPENAI_HEADERS.ATTESTATION);
}
const betaHeader =
transport === "websocket"
? OPENAI_HEADER_VALUES.BETA_RESPONSES_WEBSOCKETS_V2
+2
View File
@@ -25,6 +25,8 @@ export const OPENAI_HEADERS = {
SUBAGENT: "x-openai-subagent",
/** Responses Lite transport marker (codex-rs `add_responses_lite_header`); value is always `"true"`. */
RESPONSES_LITE: "x-openai-internal-codex-responses-lite",
/** DeviceCheck attestation envelope (codex-rs `X_OAI_ATTESTATION_HEADER`); sent on ChatGPT-OAuth requests. */
ATTESTATION: "x-oai-attestation",
} as const;
export const OPENAI_HEADER_VALUES = {
+1 -1
View File
@@ -12,7 +12,7 @@
### Fixed
- Fixed live-call attestation depending on the ChatGPT desktop app being installed: `generateLiveAttestation` now mints DeviceCheck tokens in-process through the `@oh-my-pi/pi-natives` `deviceCheckGenerateToken` binding instead of probing `/Applications` for the app's `devicecheck.node` addon, so the `x-oai-attestation` header works on hosts without the desktop app and drops the `createRequire` addon probing.
- Fixed live-call attestation depending on the ChatGPT desktop app being installed: `generateLiveAttestation` now mints DeviceCheck tokens in-process through the `@oh-my-pi/pi-natives` `deviceCheckGenerateToken` binding instead of probing `/Applications` for the app's `devicecheck.node` addon, so the `x-oai-attestation` header works on hosts without the desktop app and drops the `createRequire` addon probing; the attestation provider is now wired up to `@oh-my-pi/pi-ai` for ChatGPT-OAuth Codex requests.
- Fixed `xd://` device execution failures rendering as `write` errors instead of using the mounted tool's own error renderer.
- Fixed custom tools without bespoke renderers losing the default state-tinted card when mounted under `xd://`; dispatched calls now keep their label, arguments, status, output preview, and expansion affordance instead of dumping a bare result line into the transcript.
- Fixed the clipboard image-paste keybind mangling copied URL text into a bogus path error on macOS (e.g. `Image not found at /https/::i.can.ac:CE4Ek3.png` for a copied `https://i.can.ac/CE4Ek3.png`). AppleScript's `the clipboard as «class furl»` coerces plain *text* into a file URL by treating the string as an HFS path (`:`↔`/` swap), so `readMacFileUrlsFromClipboard` returned a garbage path that dead-ended in `handleImagePathPaste` instead of falling through to the text paste. The script now bails early via `clipboard info for «class furl»` unless the pasteboard actually carries a `public.file-url` representation, so URL/text clipboards paste as text.
@@ -65,6 +65,7 @@ const BUILT_IN_DISCOVERY_NON_AUTHORITATIVE_RETRY_MS = 5 * 60 * 1000;
import type { ApiKeyResolver, FetchImpl } from "@oh-my-pi/pi-ai";
import { registerOAuthProvider, unregisterOAuthProviders } from "@oh-my-pi/pi-ai/oauth";
import type { OAuthCredentials, OAuthLoginCallbacks } from "@oh-my-pi/pi-ai/oauth/types";
import { setCodexAttestationProvider } from "@oh-my-pi/pi-ai/providers/openai-codex-responses";
import {
getBundledModelReferenceIndex,
inheritReferenceThinking,
@@ -72,6 +73,7 @@ import {
} from "@oh-my-pi/pi-catalog/identity";
import { isBunTestRuntime, isRecord, logger, wrapFetchForExtraCa } from "@oh-my-pi/pi-utils";
import { parseModelString, resolveProviderModelReference } from "../config/model-resolver";
import { generateCodexAttestation } from "../live/attestation";
import type { AuthStorage, OAuthCredential } from "../session/auth-storage";
import { type ApiKeyResolverModel, type ApiKeyResolverOptions, createApiKeyResolver } from "./api-key-resolver";
import type { ConfigError, ConfigFile } from "./config-file";
@@ -90,6 +92,10 @@ import { ModelsConfigFile, type ProviderValidationModel, validateProviderConfigu
import type { ModelOverride, ModelsConfig, ProviderAuthMode } from "./models-config-schema";
import { settings } from "./settings";
// DeviceCheck attestation (`x-oai-attestation`) for ChatGPT-OAuth Codex
// requests; the pi-ai provider resolves it just-in-time per request.
setCodexAttestationProvider(generateCodexAttestation);
export const kNoAuth = "N/A";
export function isAuthenticated(apiKey: string | undefined | null): apiKey is string {
@@ -2,7 +2,7 @@ import type { Component } from "@oh-my-pi/pi-tui";
import { Box, Container } from "@oh-my-pi/pi-tui";
import type { MessageRenderer } from "../../extensibility/extensions/types";
import { theme } from "../../modes/theme/theme";
import { LIVE_DELEGATION_MESSAGE_TYPE, type CustomMessage } from "../../session/messages";
import { type CustomMessage, LIVE_DELEGATION_MESSAGE_TYPE } from "../../session/messages";
import { renderFramedMessage } from "./message-frame";
/**
@@ -1,5 +1,4 @@
import type { AssistantMessage } from "@oh-my-pi/pi-ai";
import chalk from "chalk";
import { logger } from "@oh-my-pi/pi-utils";
import { LiveSessionController, type LiveTranscript } from "../../live/controller";
import { LIVE_MODEL } from "../../live/protocol";