---
type: research
title: Flow A Standalone — Verified Building Blocks
description: Every fact needed to run the attested chat-submode flow outside the desktop app — refresh (oai_is), Node DeviceCheck token, real headers/bodies, conduits, attestation challenge
tags: [openai, flow-a, standalone, device-check, attestation, protocol, verified]
timestamp: 2026-08-26
---

# Flow A Standalone — Verified Building Blocks

> **HISTORICAL / SUPERSEDED — 2026-08-27.** This page describes the A0 ("standalone PKCE") attempt and its building blocks as of 2026-08-26. The validated sequence today is **A1** (session-keeper): [flow-a-manual-step-by-step](flow-a-manual-step-by-step.md) (request by request, with exact headers/bodies) — see [flow-definitions](flow-definitions.md) for the flow map. Keep the earlier claims below only as the historical record of A0; do not present them as the current protocol.

Part of [openai-research](openai-research.md). Historical record: every item below was **verified on 2026-08-26 with zero requests against the chat backend beyond what the app itself did** (auth test + addon load are local/read-only; the refresh is the standard OAuth rotation).

## 1. Auth (full PKCE route for the standalone probe)

- Chosen route for the flow-A standalone probe: **full PKCE** (real browser login) — `auth.openai.com/oauth/authorize` (+ PKCE S256, callback `localhost:1455`, scope `openid profile email offline_access api.connectors.read api.connectors.invoke` (the full login scope), extras `codex_cli_simplified_flow` + `id_token_add_organizations`), exchange form-urlencoded, tokens persisted in a **probe-own file** (never `~/.codex/auth.json`), refresh handled by the probe.
- "**Preemptive refresh**" = **always check the TTL before every request** (parse `exp` of the access token; refresh when `exp - now <= 5min` or `last_refresh > 8d`); never send an expired token. It is NOT a scheduled/cron refresh. (Semantics = codex-rs `should_refresh_proactively`.)
- OAuth refresh (proven standalone): `POST https://auth.openai.com/oauth/token` JSON `{client_id: app_EMoamEEZ73f0CkXaXp7hrann, grant_type: "refresh_token", refresh_token}` → **200** with keys `access_token, token_type, expires_in (864000), scope, id_token, earliest_refresh_at, refresh_token, oai_is` — **`oai_is` is a new field of the OAuth refresh response** (integrity token; NOT used by codex-rs; not observed as a header in the desktop f/* requests).

## 2. Attestation (Node, no app)

`require('/Applications/ChatGPT.app/Contents/Resources/native/devicecheck.node')` in a **plain Node** process → `generateToken()` → `{supported: true, tokenBase64: "AgAA…" (2980), latencyMs}`. No entitlement/signature dependencies (see [attestation-flow](attestation-flow.md)).

## 3. Request surfaces (real captured values)

**UA** (version-pinned, safe to hardcode for THIS build): `Codex Desktop/26.820.60940 (Mac OS; arm64)` — **originator**: `Codex Desktop`.

**`oai-did` — DYNAMIC (application logic, replicable)**: the renderer keeps it in localStorage under key **`codex.chatgpt-conversations.device-id`** (webview; verified persisted in `~/Library/Application Support/Codex/Default/Local Storage/leveldb/…ldb`); `_Nr()` reads it, generates with a UUID (`Ug()` — crypto.randomUUID family) and persists on first run. The standalone probe **must do the same**: `randomUUID()` v4 + persist (own file) — never reuse the app's captured UUID.

**Attestation challenge**: `GET /backend-api/ios/attestation_challenge` with `Authorization: Bearer`, `ChatGPT-Account-Id: aa06cafc-…`, `OAI-Language: en-US`, `oai-did`, `originator: Codex Desktop`, and `x-sentinel-dc: {"token": "<DeviceCheck tokenBase64>"}` → `{attestation_challenge: gAAAAAB…}`.

**Conversation** `POST /backend-api/f/conversation` headers (real, captured): `OAI-Language: en-US`, `oai-did`, `x-conduit-token` (JWT ES256 from the prepare), `Content-Type: application/json`, `Authorization: Bearer`, `ChatGPT-Account-Id`, `originator: Codex Desktop`, `User-Agent: Codex Desktop/26.820.60940 (Mac OS; arm64)`.

**Body (first turn, real)**: `{action:"next", local_function_signatures:[handoff…], messages:[<full user record: author{metadata,name:null,role:"user"}, channel:null, content:{content_type:"text", parts:[text]}, create_time, end_turn:null, id:<uuid>, metadata:{}, recipient:"all", status:"finished_successfully", update_time:null, weight:1>], model:"gpt-5-6-thinking", supported_encodings:["v1"], thinking_effort:"extended", timezone:"America/Sao_Paulo", timezone_offset_min:180, client_prepare_state:"success", app_attest_challenge:"gAAAAAB……"}`.

**Prepare** (typing/warm): `POST /f/conversation/prepare` `{action:"next", client_prepare_state:"sent", [partial_query…], local_function_signatures, model, thinking_effort, timezone, timezone_offset_min}` with header `x-conduit-token: no-token` → returns the **conduit_token** (used as `x-conduit-token` header in the conversation JWT: `conduit_uuid`, `conduit_location: 10.x.x.x:8304`, `cluster: unified-83`).

**Multi-turn**: next turn body adds `conversation_id` + `parent_message_id` (+ the injected handoff result message with `role:"tool"`, `channel:"commentary"`, `content:{content_type:"code", text:'{"call_id":…,"result":{"accepted":true,…}}'}`) and a fresh `app_attest_challenge`.

**SSE** response: delta v1 (see [response-sse-real](response-sse-real.md)) — the model's web search runs **server-side** (the only "http" the probe ever makes is to chatgpt.com backend; the web.run results come back inside the stream).

**NOTE (2026-08-27)**: superseded in part — the VALIDATED end-to-end sequence is [flow-a-manual-step-by-step](flow-a-manual-step-by-step.md) (A1, session-keeper via the running app-server). The lesson additions since this page was written: the `oai-did` is the persisted installation identity (never regenerated; live from the renderer), the `/f/conversation` may carry `x-conduit-token` **or not** — the app alternates; when it is carried, the request must land on the conduit's cluster (a mismatch = 403 "Unusual activity"); the live `authToken` comes from `getAuthStatus {includeToken:true}` (the static `auth.json` can lag).

## 4. What this means (probe constraints)

- Probe is **non-golden**: it runs NOW, with the bezetacil running (it reads the app version + quota surface from the harness; a guard check verifies the harness is online before starting). No `--app-dir` fallback, no atemporality (hardcoded version comes from the harness in this phase).
- The addon used by the probe: **extracted to `~/.bezetacil-chatgpt/scratchpad/devicecheck.node`** (gitignored; not version-dependent, no signature requirement — see [attestation-flow](attestation-flow.md)).
- The whole flow A is reproducible from **Node + the addon + its own tokens**, **app closed** (no Electron, no onboarding state, no browser after the login). The only macOS requirement is the OS (DeviceCheck).
- Quota note: chat submode generation consumes **chat** quota (not codex); the observed snapshot (post-realtime) lives in [work-runtime-trace](work-runtime-trace.md)/the `quota` trigger.
- **Correction**: `X-OAI-IS` is NOT present in any captured desktop request header (f/*, wham/* — 0 occurrences across sessions); it was a claim from the web/vendor analysis. The refresh response field is `oai_is` (integrity data for the OAuth token path). See [chatgpt-desktop-architecture](chatgpt-desktop-architecture.md) note.

## Cross-references

- [attestation-flow](attestation-flow.md) — addon/challenge
- [chat-submode-protocol](chat-submode-protocol.md) — cycle
- [auth-and-link-session](auth-and-link-session.md) — refresh semantics
- [probes](probes.md) — how probes are versioned/disciplined (this probe is the next one, when approved)
