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:
@@ -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(
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 = {
|
||||
|
||||
@@ -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";
|
||||
|
||||
Reference in New Issue
Block a user