Repository navigation
Caller-owned thread_spawn preview still reads physical main auth.json #4850
Description
Client or integration
Codex App
Area
Authentication and account pool
Summary
A thread_spawn request that authenticates with the caller's own forwardable Codex bearer still opens the operator's physical native-main auth.json during request preview.
The native-main read fence already prevents quota priming, entitlement discovery, denial-cache credential validation, reconciliation, and final-auth selection from reading physical main for this credential domain. Pool eligibility remains outside that fence:
prepareResponsesRequestcallspreviewCodexAccountForRequest.- Priority preemption reaches
getEligiblePoolAccounts. - Main eligibility calls
isMainAccountCredentialUsable, which opens and parsesauth.json. applySubagentModelFallbackinvokes the preview callback and repeats the same read.
Final authentication does not select or send the physical main credential: it uses synthetic liveness and either selects a stored pool account or preserves the caller-owned credential. The preview can nevertheless score physical-main liveness, cached quota, and plan state and use that result when deciding whether to rewrite the subagent model.
Expected: a request authenticated with its own credential performs no physical-main credential reads or scoring.
Actual: request preview reads auth.json twice through pool eligibility.
Severity is bounded. There is no token disclosure, refresh, upstream send, or quota consumption through the observed path, and the credential-substitution hypothesis was checked and falsified. The observable consequence is that operator-main state can influence subagent model selection for a request that owns its own credential.
ADR-0086 records "read the physical main token for identity" as an explicitly rejected alternative, and #4813 states that caller-owned preview "could read the physical main token and score main differently from final authentication" — so this path being outside the fence appears to be an omission rather than a decision.
Reproduction
-
Configure canonical OpenAI forwarding in Pool mode with:
- a physical native-main
auth.json; - one usable stored pool account;
- a subagent fallback chain.
- a physical native-main
-
Send a
/v1/responsesthread_spawnrequest carrying a forwardable caller Codex bearer. -
Instrument
fs.readFileSynccalls whose path ends inauth.json. -
Observe these two stacks before final dispatch:
prepareResponsesRequest -> previewCodexAccountForRequest -> pickPriorityPreemption -> getEligiblePoolAccounts -> isCodexAccountUsable -> isMainAccountCredentialUsableapplySubagentModelFallback -> poolAccountPreview -> previewCodexAccountForRequest -> ... -> isMainAccountCredentialUsable
-
Observe that final authentication still dispatches with the stored pool account or caller-owned bearer rather than the physical main credential.
The behavioural test at tests/responses/responses-preview-main-read-fence.test.ts records these reads; its denial-cache assertions deliberately filter them out because closing pool-liveness was outside that test-only change.
Version
Operating system
Observed in hosted GitHub Actions while converting the read-fence source contract to behavioural assertions. The call path is platform-independent.
Provider and model
openai / gpt-5.6-sol
Logs or error output
prepareResponsesRequest
previewCodexAccountForRequest
pickPriorityPreemption
getEligiblePoolAccounts
isCodexAccountUsable
isMainAccountCredentialUsable
readFileSync(.../auth.json)
applySubagentModelFallback
poolAccountPreview
previewCodexAccountForRequest
pickPriorityPreemption
getEligiblePoolAccounts
isCodexAccountUsable
isMainAccountCredentialUsable
readFileSync(.../auth.json)Screenshots and supporting files
The behavioural evidence is in tests/responses/responses-preview-main-read-fence.test.ts.
A narrow fix exists: use the existing CodexAccountUsabilityOptions.isMainAccountTokenLive seam in both request-preview option objects, returning read-free synthetic liveness for caller-owned requests and preserving the documented healthy main-pin behaviour from #3166. Scoping it to previewRequestScopedMainCredential === true leaves ordinary request selection unchanged.
Redacted configuration
{
"defaultProvider": "openai",
"activeCodexAccountId": "pool-a",
"autoSwitchThreshold": 0,
"subagentModelFallback": ["xai/example-fallback"],
"providers": {
"openai": {
"adapter": "openai-responses",
"baseUrl": "https://chatgpt.com/backend-api/codex",
"authMode": "forward",
"codexAccountMode": "pool"
}
},
"codexAccounts": [
{ "id": "__main__", "isMain": true },
{ "id": "pool-a", "isMain": false }
]
}Checks
- I searched existing issues and documentation.
- I removed secrets, tokens, account details, request credentials, and personal data.
Activity
lidge-jun commentedon Sep 16, 2026
리뷰 · 우선순위 73 / 80
이 이슈는 caller가 자기 Codex bearer로 인증하는
thread_spawn요청의 preview가, 최종 인증은 건드리지 않는 물리 native-mainauth.json을 아직 읽는다는 보고입니다. 지금dev(cfdf5c379) 기준으로 보면 최종 쪽 울타리는 이미 있습니다.src/codex/auth-context.ts는requestScopedMainCredential일 때isMainAccountTokenLive를 합성 값으로 넣어 물리 토큰을 읽지 않습니다.그런데 preview 준비는 다릅니다.
src/server/responses/request-prepare.ts에서previewRequestScopedMainCredential과nativeMainReadsForbidden을 계산하고, quota priming·entitlement·denied-cache 쪽은 그 울타리를 지킵니다. 하지만previewSelectionOptions객체에는nativeMainSelectionOnly와 uploaded-file retention만 들어가고isMainAccountTokenLive는 없습니다. 그 옵션이previewCodexAccountForRequest→getEligiblePoolAccounts→ 계정 사용 가능 판정으로 흘러가면,src/codex/account-usability.ts가 기본값으로isMainAccountCredentialUsable()을 호출하고, 그 함수는readMainAuthJsonCredential()으로 물리auth.json을 엽니다.이슈가 적은 스택 두 줄은 그 경로와 맞습니다. 하나는
prepareResponsesRequest의 첫 preview이고, 다른 하나는applySubagentModelFallback이 preview 콜백을 다시 부를 때입니다. 최종 인증은 synthetic liveness와 pool/caller 자격으로 물리 main을 고르지 않는다고 적혀 있고, 토큰 유출·refresh·upstream 전송·쿼ota 소비는 없다고 한정합니다. 남는 관측 결과는 operator-main의 살아 있음/쿼ota/플랜 상태가 subagent 모델 fallback 점수에 섞일 수 있다는 점입니다. ADR-0086이 물리 main 토큰을 identity로 읽는 대안을 거부했고, #4813도 caller-owned preview가 물리 main을 읽어 최종과 다르게 점수 매길 수 있다고 적어 두었으니, 울타리 누락으로 보는 설명이 설득력 있습니다.제안된 수정 방향도 현재 코드와 잘 맞습니다. 이미 있는
CodexAccountUsabilityOptions.isMainAccountTokenLiveseam을 preview 옵션 두 곳(직접 selection options와 fallback이 넘기는 preview 옵션)에,previewRequestScopedMainCredential === true일 때만 읽기 없는 합성을 넣으면 됩니다. 일반 요청 selection은 그대로 두고,tests/responses/responses-preview-main-read-fence.test.ts가 기록해 둔auth.json읽기를 denial-cache 필터 밖에서도 막히게 단언을 키우면 회귀가 고정됩니다. types.ts/config.ts 대형 분리와는 무관합니다.path request-prepare previewSelectionOptions -
isMainAccountTokenLive가 빠져 caller-owned preview가 pool eligibility에서 물리 main을 읽는다.path account-usability main 분기 - seam이 없으면
isMainAccountCredentialUsable()→auth.json동기 읽기가 기본 경로다.path applySubagentModelFallback / preview 콜백 - 같은 옵션 객체를 다시 써서 읽기가 두 번 반복된다.
path responses-preview-main-read-fence.test.ts - 읽기를 관측하지만 denial-cache 단언이 그 경로를 의도적으로 걸러 회귀를 못 잡는다.
symbol preserveRequestOwnedMainPin / #3166 - 합성 liveness를 넣을 때 healthy main-pin 보존 동작을 깨지 않게 스코프를 previewRequestScoped에만 묶어야 한다.
메인테이너의 판단이 필요한 지점
isMainAccountTokenLive값을 auth-context의preserveRequestOwnedMainPin과 동일 함수로 맞출지, preview 전용 상수 true로 둘지너의 추천
이슈 서술과 현재
dev코드가 일치한다. 좁은 수정 PR을 열어previewSelectionOptions(및 fallback preview가 spread하는 동일 객체)에previewRequestScopedMainCredential일 때만isMainAccountTokenLive합성을 넣고,responses-preview-main-read-fence.test.ts에서 물리auth.json읽기 0건을 단언하는 쪽이 가장 작다. #3166 main-pin 동작을 깨지 않게 스코프를 그 플래그에만 묶으면 된다.이 댓글은 grok-bot이 작성했습니다
fix(codex): close pool eligibility inside the caller-owned preview re…