WikifitaGitHub live67e8de5
pesquisa · openai-research/sentinel-fallback-validation

Sentinel Fallback Validation — Exercising the PoW Path on macOS

Runtime validation of the sentinel/PoW fallback by flipping the DeviceCheck gate — real fingerprint values, real headers, placeholder confirmation

Baixar raw

Sentinel Fallback Validation — Exercising the PoW Path on macOS

Part of openai-research. Method: bezetacil v4 runtime flip of the isDeviceCheckSupported gate, then observing the resulting network flow.

The gate and its flip

The app's vPr() gate in the preload:

isDeviceCheckSupported: () => process.platform === `darwin` && process.arch === `arm64`,

vPr() = getSentryInitOptions().appVersion !== '0.0.0' && isDeviceCheckSupported().

To force the fallback on macOS, a bezetacil preload added via session.setPreloads flips the check:

// flip preload — registered into the session before the app preload runs
try {
  Object.defineProperty(process, 'platform', { value: 'win32', configurable: true, writable: true });
  Object.defineProperty(process, 'arch', { value: 'x64', configurable: true, writable: true });
} catch (e) {}

Registered via the bezetacil REST eval:

session.setPreloads([...session.getPreloads(), '/Users/alefita/.bezetacil-chatgpt/scripts/flip-devicecheck-preload.js'])
webContents.reloadIgnoringCache()

Result verified: window.electronBridge.isDeviceCheckSupported()false after the reload.

The observed fallback flow (post-flip)

The app switched from the attestation flow to the sentinel flow. Observed requests (real captured traces, 2026-08-26 18:35):

POST https://chatgpt.com/backend-api/sentinel/chat-requirements/prepare   (×2, no attestation_challenge)
POST /backend-api/f/conversation/prepare   (the normal prepare — no attestation present)

Sentinel prepare headers (the fallback surface)

OAI-Language: en-US
oai-did: a05f9cdd-ca51-4a91-9da1-9c374dfe0536
originator: Codex Browser          ← (NOT "Codex Desktop" — the sentinel path uses browser originator!)
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/151.0.0.0 Safari/537.36
sec-ch-ua: "Chromium";v="151", "Google Chrome";v="151", "Not=A?Brand";v="24"
sec-ch-ua-mobile: ?0
sec-ch-ua-platform: "macOS"
Content-Type: application/json
Authorization: Bearer <same access token>
ChatGPT-Account-Id: <account id>

The real requirementsKey fingerprint (decoded base64)

gAAAAAC + base64(JSON(fingerprint)):

0:  "Wed Aug 26 2026 15:35:12 GMT-0300 (Brasilia Standard Time)"   ← current date
1:  0.4…
2:  4395630592                                        ← real heap size
3:  1                                                 ← counter
4:  "Mozilla/5.0 (Macintosh…) Chrome/151.0.0.0 Safari/537.36"
5:  "app://-/assets/index-rLYmMQgS.js"                 ← real webview chunk!
6:  "<!-- PROD_BUILD_TAG_HERE -->"                     ← THE LITERAL PLACEHOLDER — CONFIRMED OUT OF THE CLIENT
7:  "en-US"
8:  "en-US,en"
9:  0.6…
...

Confirmed: the literal placeholder <!-- PROD_BUILD_TAG_HERE --> IS the value that leaves the client in slot 6. The static asar analysis was correct; the runtime trace now proves it. The earlier claim (that a client would never send a placeholder) was disproven — the deployed app runs with the placeholder tag intact.

Server behavior: the /prepare did NOT reject the fingerprint containing the placeholder — the server accepted it (the sentinel prepare succeeded post-flip). This validates that slot 5/6 validation is either lenient for desktop or not enforced for this deployment.

The PoW solver chain (stack captured)

net.fetch ← net.fetch ← net.fetch ← y ← hI.performDesktopFetch ← hI.handleRequest ← kge.handleMessage

The nested net.fetch stack (from the three bezetacil hooks) shows the sentinel call path goes through the same performDesktopFetch, and the fingerprint is built at the call site.

What this validates

  1. The hierarchical flow confirmed: vPr() true → attestation flow (DeviceCheck); vPr() false → sentinel/PoW flow. Both exist and are exercisable; the flip on macOS selects the sentinel path.
  2. Originator switch: the sentinel path uses originator: Codex Browser (not Codex Desktop), with a browser-like UA + full sec-ch-ua set. The attestation path uses Codex Desktop + Codex Desktop/26.818.61809 (Mac OS; arm64) UA.
  3. The fingerprint real values: heap size 4395630592, chunk app://-/assets/index-rLYmMQgS.js, placeholder literal, current date string, counter — all produced by the real app, matching the sPr() structure.
  4. Data-build placeholder confirmed: slot 6 in the real fingerprint IS <!-- PROD_BUILD_TAG_HERE --> — the client really sends it, and the server accepts it.

The sentinel heartbeat (analyzed — NOT part of the protocol)

POST /backend-api/sentinel/heartbeatno body, originator: Codex Desktop + Bearer, fired every ~60s by the renderer (y_u(){ wb.safePost('/sentinel/heartbeat').catch(b_u) }, interval x_u=6e4), gated on authMethod === 'chatgpt' + config flag. It is fire-and-forget — the response is discarded (empty catch), nothing is stored, no key exchange, no PoW — a presence/telemetry ping only. Not needed by the conversation protocol (prepare/conversation never consume it). The probe/standalone does not need to send heartbeats.

Windows sandbox — what "Finish Windows setup" actually is

The flip made the app run the Windows sandbox onboarding (not a UAC prompt of the app per se — the UAC is the elevation the Windows sandbox setup needs). Evidence chain:

  • App-server (Rust): methods windowsSandbox/setupStart + windowsSandbox/readiness (JSON-RPC, ClientRequest::WindowsSandboxSetupStart / WindowsSandboxReadiness), core/src/windows_sandbox.rs, app-server/src/request_processors/windows_sandbox_processor.rs; features experimental_windows_sandbox, elevated_windows_sandbox; requirements windows.sandbox_private_desktop; metrics codex.windows_sandbox.fallback_prompt_shown, codex.windows_sandbox.elevated_setup_duration_ms
  • Renderer: createWindowsSandboxSetupSessionstartWindowsSandboxSetup (runtime lifecycle → windowsSandbox/setupStart); statsig codex_windows_sandbox_setup_attempted {sandbox_mode}
  • Analytics event: CodexWindowsSandboxStateObserved with readiness: CODEX_WINDOWS_SANDBOX_READINESS_NOT_CONFIGURED (protobuf enum: UNSPECIFIED / READY / NOT_CONFIGURED)
  • Banner strings (i18n codex.windowsSandboxBanner.*): title "Finish Windows setup to continue" with description "Primary status shown when Agent mode requires the Windows sandbox to be enabled"; states: backgroundSetup ("{appName} is creating the sandbox required… may take a few minutes"), backupCta ("Continue without administrator access" — the allowUnelevatedFallback path), readinessError, dismiss
  • What responding to the UAC does: the setup session runs windowsSandbox/setupStart (the elevation creates/hatches the sandbox); the "Continue without administrator access" CTA is the unelevated fallback (allowUnelevatedFallback). The banner only clears when readiness = READY. On macOS (flip) it stays NOT_CONFIGURED — the setup cannot succeed because there is no Windows sandbox — so Agent mode stays blocked. This is a Windows-integration feature of the app-server, not a macOS permission issue (matches the hierarchy: macOS uses its own sandbox/executor; Linux uses bwrap use_linux_sandbox_bwrap).

TypeScript types (the sentinel fallback surface)

// ── the gate and the flip ────────────────────────────────────────────────
export interface PreloadGate {
  isDeviceCheckSupported: () => boolean       // `platform === 'darwin' && arch === 'arm64'`
  vPr: (appVersion: string) => boolean        // isDeviceCheckSupported() && appVersion !== '0.0.0'
}

// ── sentinel prepare request ─────────────────────────────────────────────
export interface SentinelPrepareBody { p: string }       // 'gAAAAAC' + base64(fingerprint JSON)
export interface SentinelPrepareHeaders {
  'OAI-Language': 'en-US'
  'oai-did': string
  originator: 'Codex Browser'                    // NOT 'Codex Desktop' — the fallback originator
  'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/151.0.0.0 Safari/537.36'
  'sec-ch-ua': '"Chromium";v="151", "Google Chrome";v="151", "Not=A?Brand";v="24"'
  'sec-ch-ua-mobile': '?0'
  'sec-ch-ua-platform': '"macOS"'
  'Content-Type': 'application/json'
  Authorization: `Bearer ${string}`
  'ChatGPT-Account-Id': string
}

// ── fingerprint (24 slots, the real values) ──────────────────────────────
export type FingerprintSlot = number | string | null | boolean | unknown[]
export interface Fingerprint {
  0: number                                   // screen.width + screen.height (derived from viewport)
  1: string                                   // new Date().toString()
  2: number                                   // performance.memory.jsHeapSizeLimit (real; 4395630592 observed)
  3: number                                   // counter (solver-mutated)
  4: string                                   // navigator.userAgent (webview UA observed)
  5: string                                   // one of the real webview script srcs ('app://-/assets/index-rLYmMQgS.js')
  6: string                                   // the data-build value — '<!-- PROD_BUILD_TAG_HERE -->' CONFIRMED leaving the client
  7: string                                   // navigator.language ('en-US')
  8: string                                   // navigator.languages ('en-US,en')
  9: number                                   // the time slot (solver-mutated)
  10: unknown                                 // random navigator prototype property + value
  11: unknown                                 // pick(keys(document))
  12: unknown                                 // pick(keys(window))
  13: number                                 // performance.now()
  14: string                                 // uuid
  15: string                                 // search params join ('')
  16: number                                 // hardwareConcurrency
  17: number                                 // performance.timeOrigin
  18-23: unknown[]                           // flags: ai / createPRNG / cache / data / solana / dump / InstallTrigger
}
export type FingerprintArray = Array<FingerprintSlot & { [key: number]: unknown }>

// ── the PoW chain (captured stack) ───────────────────────────────────────
export interface PoWStack {
  // net.fetch ← net.fetch ← net.fetch ← y ← hI.performDesktopFetch ← hI.handleRequest ← kge.handleMessage
  frames: string[]
}

// ── the Windows sandbox (the flip side effect) ───────────────────────────
export type WindowsSandboxReadiness =
  | 'CODEX_WINDOWS_SANDBOX_READINESS_UNSPECIFIED'
  | 'CODEX_WINDOWS_SANDBOX_READINESS_READY'
  | 'CODEX_WINDOWS_SANDBOX_READINESS_NOT_CONFIGURED'
export interface WindowsSandboxEvents {
  setupStart: { mode: string; allowUnelevatedFallback: boolean }
  readiness: { readiness: WindowsSandboxReadiness }
}
export const WINDOWS_SANDBOX_BANNER_IDS = {
  title: 'codex.windowsSandboxBanner.title',        // "Finish Windows setup to continue"
  backgroundSetupTitle: 'codex.windowsSandboxBanner.backgroundSetup.title',
  backgroundSetupDetail: 'codex.windowsSandboxBanner.backgroundSetup.detail',
  backupCta: 'codex.windowsSandboxBanner.backupCta', // "Continue without administrator access"
  readinessError: 'codex.windowsSandboxBanner.readinessError',
} as const

Cross-references

The platform-persisted onboarding side effect

Flipping the process.platform gate (to force the sentinel path on macOS) had a side effect: the app re-ran the "Finish Windows setup" onboarding screen — a User Account Control prompt for ChatGPT and a "Click Yes in the next step to finish setup" confirmation. This proves the app keeps persisted platform state beyond the in-memory gate: the sentinel path is not merely a "non-Apple" fallback — it has its own per-platform onboarding bootstrap.

Correction on "defaults to Windows": the flip preload sets process.platform = 'win32' explicitly — the app did NOT pick Windows by itself. The Windows sandbox behavior is the app-server agent mode following the declared platform (windows_sandbox feature + UAC elevation, see below). A future experiment for the B flow: flip to 'linux' instead (the app-server has use_linux_sandbox_bwrap; the agent mode likely requires bwrap → expected failure on macOS — test carefully under the same discipline).

The state was cleaned by disabling the flip preload (.js.disabled) and relaunching the app; the canonical state returned to the attestation flow (UA Codex Desktop/26.820.60940 (Mac OS; arm64) — the app self-updated during the session, 26.818 → 26.820).