Provider-level credential discovery ignores auth.order, OAuth preference, and expiry — silently breaks ChatGPT-mode background paths
- OpenClaw version: 2026.7.1 (2d2ddc4)
- Platform: macOS (gateway as LaunchAgent), Node 22.x
- Component: auth storage / model registry / background (non-interactive) credential resolution
Summary
The automatic/background credential path resolves credentials by provider
name from a one-credential-per-provider map, built by iterating the
credential store's profiles in raw store order and keeping the first
credential found for each normalized provider. No OAuth-over-api_key
preference, no auth.order, and no expiry check is applied on this path. The
profile-id-aware resolution used by manual RPCs is not consulted.
Call chain (2026.7.1 dist):
discoverAuthStorage → resolveAgentCredentialMapFromStore
("build one credential per normalized provider") —
agent-model-discovery-Bq74MwWh.js:63-73
- background consumers: context-engine compaction safeguard →
resolveModelAuth → modelRegistry.getApiKeyAndHeaders(model) →
authStorage.getApiKey(model.provider) —
attempt.model-diagnostic-events-Dg8sP6iR.js:2790
- ChatGPT-mode driver JWT-decodes the resolved apiKey to build its
chatgpt-account-id header — openai-chatgpt-responses-DVC4Bk_A.mjs:1015-1017
Expected behavior
Background/automatic paths resolve credentials with the same preference logic
as interactive/profile resolution: OAuth preferred over api_key, auth.order
respected, expired credentials skipped — or, when the session carries an
authProfileOverride, background paths route through profile-id resolution.
Actual behavior
The first credential in raw store order for the provider wins unconditionally.
If that credential is unusable for the driver the session actually runs on,
every background operation for that provider fails, while the identical manual
operation succeeds.
Reproduction steps
- Create two auth profiles for one provider in the credential store, ordered
so an api_key profile precedes an OAuth profile (raw store order).
- Run an agent on a ChatGPT-mode model (codex agent runtime /
openai-chatgpt-responses driver), which authenticates via the OAuth
profile.
- Let a session grow past the compaction budget threshold so the automatic
compaction safeguard fires (any other background/non-interactive model call
for that provider also works as a trigger).
- Observe: automatic compaction fails on every attempt with
Failed to extract accountId from token (the driver JWT-decodes the
api_key, which is not a JWT), with retryAsync ×3 bursts — while a manual
sessions.compact on the same session succeeds via profile resolution.
Impact
Observed on a production single-user deployment (2026-07-15/16): ~24
errors/day from failed automatic compactions; sessions could no longer be
compacted automatically at all. Two secondary hazards:
- The repeated background failures fed auth-profile rotation, which
auto-pinned the dead profile onto live sessions (self-poisoning; the
sessions then failed until the pins were cleared manually).
- An expired OAuth credential also passes conversion on this path and
would be refresh-attempted from an in-memory copy — a refresh-token-reuse
hazard for any deployment holding two profiles of one provider.
Suggested fix
Build the discovery map with the same preference logic as profile resolution
(OAuth preferred, auth.order respected, expired credentials skipped), or
route background paths through profile-id resolution when the session carries
an authProfileOverride.
Provider-level credential discovery ignores auth.order, OAuth preference, and expiry — silently breaks ChatGPT-mode background paths
Summary
The automatic/background credential path resolves credentials by provider
name from a one-credential-per-provider map, built by iterating the
credential store's profiles in raw store order and keeping the first
credential found for each normalized provider. No OAuth-over-api_key
preference, no
auth.order, and no expiry check is applied on this path. Theprofile-id-aware resolution used by manual RPCs is not consulted.
Call chain (2026.7.1 dist):
discoverAuthStorage→resolveAgentCredentialMapFromStore("build one credential per normalized provider") —
agent-model-discovery-Bq74MwWh.js:63-73resolveModelAuth→modelRegistry.getApiKeyAndHeaders(model)→authStorage.getApiKey(model.provider)—attempt.model-diagnostic-events-Dg8sP6iR.js:2790chatgpt-account-idheader —openai-chatgpt-responses-DVC4Bk_A.mjs:1015-1017Expected behavior
Background/automatic paths resolve credentials with the same preference logic
as interactive/profile resolution: OAuth preferred over api_key,
auth.orderrespected, expired credentials skipped — or, when the session carries an
authProfileOverride, background paths route through profile-id resolution.Actual behavior
The first credential in raw store order for the provider wins unconditionally.
If that credential is unusable for the driver the session actually runs on,
every background operation for that provider fails, while the identical manual
operation succeeds.
Reproduction steps
so an api_key profile precedes an OAuth profile (raw store order).
openai-chatgpt-responsesdriver), which authenticates via the OAuthprofile.
compaction safeguard fires (any other background/non-interactive model call
for that provider also works as a trigger).
Failed to extract accountId from token(the driver JWT-decodes theapi_key, which is not a JWT), with
retryAsync×3 bursts — while a manualsessions.compacton the same session succeeds via profile resolution.Impact
Observed on a production single-user deployment (2026-07-15/16): ~24
errors/day from failed automatic compactions; sessions could no longer be
compacted automatically at all. Two secondary hazards:
auto-pinned the dead profile onto live sessions (self-poisoning; the
sessions then failed until the pins were cleared manually).
would be refresh-attempted from an in-memory copy — a refresh-token-reuse
hazard for any deployment holding two profiles of one provider.
Suggested fix
Build the discovery map with the same preference logic as profile resolution
(OAuth preferred,
auth.orderrespected, expired credentials skipped), orroute background paths through profile-id resolution when the session carries
an
authProfileOverride.