---
type: research
title: Flow A 403 Cases — Every Failure and What It Taught
description: The complete catalog of 403s encountered while closing flow A — the exact causes, per-case evidence, and the lessons that led to the working sequence
tags: [openai, flow-a, 403, waf, errors, lessons, sentinel]
timestamp: 2026-08-27
---

# Flow A 403 Cases — Every Failure and What It Taught

Part of [openai-research](openai-research.md). Each case below was observed live with captured evidence; each is a **distinct cause** (or a validated non-cause). The final column is what the failure established.

## The 403 catalog

| # | What was attempted | Response | Root cause (proved by) |
|---|---|---|---|
| 1 | Challenge with a **fresh generated did** (`02987bd6` / `04bd1580`) | 403 | The did is **installation identity** (persisted, never regenerated). Proof: same token, did `a05f9cdd` → 200. The server binds `oai-did`↔device; an unknown did dies at the challenge. |
| 2 | `/f/conversation` with the **PKCE-session access** (any run of the A0 line) | 403 "Unusual activity" | The session created by a manual browser authorize is not recognized by the edge for `f/*`; only the **app-server session** (auth.json/live `authToken`) passes. Proof: A/B (the same request with app-session access → 200). |
| 3 | `/f/conversation` **with `x-conduit-token`** | 403 | The app's own winning requests carry **no** conduit header (the conduit is prepare-only; the cluster pin it carries was the suspected differentiator — and its absence is what the app does). Proof: app request 200 without it; our request without it → 200. |
| 4 | Challenge computed over **node:https** while the conversation goes via the **main** (A-1 step) | 403 | Same-path invariant: challenge+prepare+conv must come from **the same transport/pool** (the app always uses the main). Fix: everything through the `conv` trigger. |
| 5 | `local_function_signatures` **without the handoff** (only the custom tool) in a normal turn | 403 | The server expects the handoff signature in normal conversation turns (the app always sends it; 100% of captured 200s include it). |
| 6 | **Continuation (2nd text turn) without `parent_message_id`** | 403 | The app always supplies `conversation_id` + `parent_message_id` on follow-up turns (verified in captured payloads). |
| 7 | **Re-executing a request** (curl re-run of the same `/run/conv` input) | 403 | The `app_attest_challenge` is one-time; reuse of the same input/request → 403 (and the earlier verified 200 was overwritten by my own re-run). |
| 8 | `/devicecheck` with a **revoked PKCE access** (after the user killed the sessions) | 401 `token_revoked` | The access token had been revoked (the earlier PKCE sessions were logged out); not a protocol issue. |
| 9 | (Non-403, but a lesson)**static `auth.json` after an app-server refresh** | — | The live `getAuthStatus {includeToken:true}` returns the current session token; the disc file can lag (a refresh at 16:11 changed the file and **all requests with the stale access became 403** until the live token was used). |

## The two broken chains that caused most of the damage

1. **"Own session" chain (A0)**: the A0 probe created its own PKCE session, used a regenerated did, and attached the conduit — three deviations from the app in one request → 403 stack (cases 1–3). The system closed it: the session must be the app-server's, the did must be the installation's, and the conduit must not travel.
2. **Mixed-transport chain (A-1)**: the probe fetched the challenge over its own HTTP while the conversation went through the main (case 4) → the challenge↔conversation pairing is source-bound.

## What the failures established (the final set of invariants)

- `oai-did`: persisted installation identity (live renderer storage; never regenerate).
- Session access: the **live app-server `authToken`** (`getAuthStatus` includeToken) — not a fresh login, not the stale file.
- Transport: challenge+prepare+conv through **the same main/electron path**.
- No `x-conduit-token` on `/f/conversation`.
- `local_function_signatures` with the handoff present in normal turns.
- `conversation_id` + `parent_message_id` on every turn after the first.
- `app_attest_challenge` is one-time per request (never reuse/re-run an input).

## Cross-references

- [flow-a-manual-step-by-step](flow-a-manual-step-by-step.md) — the sequence that passes
- [flow-a-tool-protocol](flow-a-tool-protocol.md) — the tool-return contract (equally validated)
- [flow-definitions](flow-definitions.md) — where A0/A1/B sit
- [attestation-flow](attestation-flow.md) — registration/challenge mechanism
- [postmortem-flow-a-2026-08-27](postmortem-flow-a-2026-08-27.md) — the full narrative of the cycle (including the process failures behind these cases)
