Skip to content

fix(cli): key gemini cli auth epoch on google account identity#71076

Merged
openperf merged 2 commits into
openclaw:mainfrom
openperf:fix/gemini-cli-auth-epoch-70973
Apr 25, 2026
Merged

fix(cli): key gemini cli auth epoch on google account identity#71076
openperf merged 2 commits into
openclaw:mainfrom
openperf:fix/gemini-cli-auth-epoch-70973

Conversation

@openperf

@openperf openperf commented Apr 24, 2026

Copy link
Copy Markdown
Member

Summary

  • Problem: getLocalCliCredentialFingerprint in src/agents/cli-auth-epoch.ts:114 dispatches on provider through a switch with explicit branches for claude-cli and codex-cli; google-gemini-cli falls through to the default branch and returns undefined. As a result, the authEpoch value stored alongside a google-gemini-cli CLI session binding never binds to the user's local Gemini OAuth state at ~/.gemini/oauth_creds.json, and the auth-epoch contract that Claude CLI and Codex CLI rely on is unenforceable on Gemini sessions. Reported in [Bug]: CLI sessions: Gemini CLI not covered by Claude CLI session persistence fixes (#69679 / #70106 / #70132) — gateway restart still mints a fresh conversation #70973.
  • Root Cause: The auth-epoch contract implemented by encodeOAuthIdentity keys the epoch on stable, non-secret OAuth identity fields (clientId / email / enterpriseUrl / projectId / accountId) so the stored binding survives access-token rotation and flips on re-authentication under a different account. Gemini CLI's ~/.gemini/oauth_creds.json carries the OpenID id_token JWT alongside the OAuth tokens; its sub and email claims are the identity fields that line up with the encodeOAuthIdentity shape Claude and Codex already feed.
  • Fix:
    1. In src/agents/cli-credentials.ts, decode the openid id_token once at credential-read time and lift subaccountId and emailemail onto GeminiCliCredential. The existing readCachedCliCredential helper continues to provide the 5000 ms TTL and readFileMtimeMs-based source-fingerprinting.
    2. In src/agents/cli-auth-epoch.ts, register google-gemini-cli in getLocalCliCredentialFingerprint and delegate encodeGeminiCredential to the shared encodeOAuthIdentity. With identity fields populated, the encoder produces an identity-keyed hash. When the id_token is absent (older logins, scope omitted), encodeOAuthIdentity collapses to a provider-keyed constant — the same identity-less fallback the Claude CLI OAuth branch produces, preserving backward compatibility for legacy login state.
    3. Bump CLI_AUTH_EPOCH_VERSION from 3 to 4 so existing google-gemini-cli bindings on disk (persisted with authEpoch: undefined, since the local fingerprint returned undefined before this change) hit the existing version-gate at src/agents/cli-session.ts:152 and skip the epoch comparison on the first request after upgrade. Once the next turn writes a fresh binding under v4 with the identity-keyed hash, all subsequent comparisons run normally. This is the same migration mechanism the file's existing "accepts older auth epoch versions for binding upgrades" test already pins.
  • What changed:
    • src/agents/cli-credentials.ts: add GEMINI_CLI_CREDENTIALS_RELATIVE_PATH, a GeminiCliCredential type with optional accountId / email identity fields, a geminiCliCache slot, a resolveGeminiCliCredentialsPath helper, and readGeminiCliCredentials / readGeminiCliCredentialsCached that read the OAuth file directly, decode the id_token JWT through a new private decodeJwtIdentityClaims helper (mirroring the existing decodeJwtExpiryMs), and lift sub / email onto the credential. Extend resetCliCredentialCachesForTest to clear the new cache.
    • src/agents/cli-auth-epoch.ts: import readGeminiCliCredentialsCached and GeminiCliCredential; register the reader on CliAuthEpochDeps and its defaults for test injection; add encodeGeminiCredential that delegates to the shared encodeOAuthIdentity; add the case "google-gemini-cli" branch mirroring the existing Codex and Claude branches; bump CLI_AUTH_EPOCH_VERSION from 3 to 4.
    • src/agents/cli-credentials.test.ts: add two tests — id_token lift produces accountId / email on the credential, and credentials read without an id_token round-trip without identity fields (legacy fallback).
    • src/agents/cli-auth-epoch.test.ts: extend the existing "no local or auth-profile credentials" case to stub the new dep; assert that the Gemini local-credential epoch (a) stays stable across access/refresh rotation when identity is unchanged, (b) flips on email change, (c) flips on accountId change; add a separate test for the id_token-absent case asserting identity-less fallback stability.
    • src/agents/cli-session.test.ts: add "accepts v3 bindings without authEpoch as binding upgrades to v4" mirroring the existing "accepts older auth epoch versions for binding upgrades" case, pinning the version-gate behavior for the v3 → v4 migration.
  • What did NOT change (scope boundary):
    • resolveCliAuthEpoch's public signature, the overall parts ordering, or the hash construction. Existing Claude / Codex callers compute bit-for-bit identical epochs.
    • resolveCliSessionReuse and the cliSessionBindings schema. No new fields, no migrations. Existing bindings on disk keep working through the file's existing authEpochVersion upgrade semantics.
    • The shared encodeOAuthIdentity helper and the Claude / Codex encoders — they keep returning the exact same strings for pre-existing inputs.
    • readPortalCliOauthCredentials and the MiniMax reader — untouched. Gemini gets a dedicated reader because, unlike MiniMax, it carries an id_token.
    • extensions/google/ — no extension code changes. The new reader lives alongside the existing Claude and Codex readers to stay consistent with the current core pattern.
    • Warm-stdio / claude-live-session.ts — not applicable to the Gemini transport (output: "json", input: "arg").
    • No PII hashing — sub and email are non-secret OpenID claims, the same identity shape Claude / Codex already feed into encodeOAuthIdentity.

Reproduction

  1. Set up google-gemini-cli via Gemini CLI's own gemini auth login so ~/.gemini/oauth_creds.json exists with real access_token / refresh_token / expiry_date / id_token.
  2. Start OpenClaw with a google-gemini-cli/* default model and have a multi-turn conversation. A cliSessionBindings["google-gemini-cli"] entry is persisted with an identity-keyed authEpoch.
  3. On subsequent turns (including across gateway restarts) the recomputed local authEpoch matches the stored one as long as the Gemini credential file still belongs to the same account, and the session is reused. When the user logs out and re-logs under a different Google account, id_token.sub changes, the epoch flips, resolveCliSessionReuse returns { invalidatedReason: "auth-epoch" }, and OpenClaw mints a fresh binding — matching the behavior Claude and Codex already have.
  4. Migration of existing bindings: a google-gemini-cli session persisted under a prior version stores authEpochVersion: 3 with no authEpoch field. After upgrading to a build with this PR, the first request hits the version-gate (3 !== 4) and skips the epoch comparison, so the stored session is reused without churn. The turn then rewrites the binding under v4 with the identity-keyed hash, and all subsequent comparisons run normally.

Risk / Mitigation

  • Risk: Reading a new file from disk (~/.gemini/oauth_creds.json) on an auth-epoch hot path could regress startup / latency.
  • Mitigation: The read goes through the existing readCachedCliCredential helper with a 5000 ms TTL and readFileMtimeMs-based source-fingerprinting, identical to the Codex reader. When the file is absent the read returns null in a single stat call. No new network I/O. The added id_token decoding is a single base64url + JSON parse on a value the file already contains, mirroring the existing decodeJwtExpiryMs pattern in the same file.
  • Risk: A malformed or tampered id_token could cause the reader to throw on every auth-epoch computation.
  • Mitigation: decodeJwtIdentityClaims wraps the parse in a try/catch and returns {} on any error — the credential round-trips without identity fields and the encoder falls back to the identity-less constant. The credential reader itself is unaffected by id_token malformation.
  • Risk: Rotation of id_token payload contents (e.g., Google adding new claims) shifts the epoch.
  • Mitigation: decodeJwtIdentityClaims only reads two specific string fields (sub, email); unknown additional claims are ignored. The encoder consumes only accountId / email from the lifted shape. Stable.
  • Risk: A user with no openid email scope on their Gemini login (older flows) sees a constant epoch.
  • Mitigation: Intentional and matches the Claude CLI OAuth branch's identity-less fallback exactly. The added "falls back to the identity-less oauth epoch when gemini id_token is absent" test pins this behavior.
  • Risk: The version bump (34) is global; on the first request after upgrade, every CLI provider's existing bindings (Claude / Codex / Gemini) skip the auth-epoch check via the version-gate. A user who switched OAuth accounts in this exact deploy window could reuse a stale binding for one request before the new binding is written under v4.
  • Mitigation: The encoders for Claude and Codex are unchanged in this PR, so the v3 stored hash and the v4 computed hash are bit-for-bit identical for steady-state users — the version-gate skip is a no-op for them in practice. The race window is bounded to a single request per binding because the next turn rewrites the binding at v4 with the current identity hash. The behavior is pinned by the new "accepts v3 bindings without authEpoch as binding upgrades to v4" test alongside the file's existing version-upgrade coverage.

Change Type (select all)

  • Bug fix

Scope (select all touched areas)

  • Agents
  • CLI sessions
  • Provider: Google Gemini CLI
  • Tests

Linked Issue/PR

Fixes #70973

@openperf
openperf requested a review from a team as a code owner April 24, 2026 11:32
@aisle-research-bot

aisle-research-bot Bot commented Apr 24, 2026

Copy link
Copy Markdown

🔒 Aisle Security Analysis

We found 2 potential security issue(s) in this PR:

# Severity Title
1 🟡 Medium Unverified JWT claim decoding used for session/account binding (Gemini CLI credentials)
2 🟡 Medium Potential DoS via unbounded read/parse of Gemini CLI credentials and JWT payload
1. 🟡 Unverified JWT claim decoding used for session/account binding (Gemini CLI credentials)
Property Value
Severity Medium
CWE CWE-287
Location src/agents/cli-credentials.ts:232-246

Description

readGeminiCliCredentials() lifts sub/email from an id_token by base64url-decoding the JWT payload without verifying the JWT signature or validating iss/aud.

This identity material is then used by encodeOAuthIdentity() (via encodeGeminiCredential() in cli-auth-epoch.ts) to derive the auth epoch, which gates whether an existing CLI session binding is reused or invalidated.

Impact:

  • If ~/.gemini/oauth_creds.json can be modified (malicious local process, compromised user account, or misconfiguration), an attacker can forge id_token payload claims to control the derived auth epoch.
  • This can cause incorrect session reuse across Google accounts (session remains bound to a previous account even after re-login) or intentional session invalidation/DoS by flipping the epoch.
  • Because the id_token is not verified, the code treats attacker-controlled JSON as authoritative identity.

Vulnerable code:

const payloadRaw = Buffer.from(parts[1], "base64url").toString("utf8");
const payload = JSON.parse(payloadRaw) as { sub?: unknown; email?: unknown };
...
const identity = typeof idTokenRaw === "string" && idTokenRaw
  ? decodeJwtIdentityClaims(idTokenRaw)
  : {};

Recommendation

Do not trust unverified JWT payloads for identity/session binding.

Preferred fixes (choose one):

  1. Verify the ID token (signature + iss + aud + expiry) using Google’s OIDC JWKS before using sub/email.

  2. If verification is not feasible in this context, do not use id_token claims at all for auth-epoch binding. Instead, derive the epoch from:

    • a verified identity source (e.g., token introspection / tokeninfo endpoint), or
    • a stable identifier obtained during the actual OAuth flow and stored separately with integrity protection.

Example (conceptual) using an ID-token verifier:

import { createRemoteJWKSet, jwtVerify } from 'jose';

const jwks = createRemoteJWKSet(new URL('https://www.googleapis.com/oauth2/v3/certs'));

async function verifyAndExtract(token: string, clientId: string) {
  const { payload } = await jwtVerify(token, jwks, {
    issuer: ['https://accounts.google.com', 'accounts.google.com'],
    audience: clientId,
  });
  return {
    sub: typeof payload.sub === 'string' ? payload.sub : undefined,
    email: typeof payload.email === 'string' ? payload.email : undefined,
  };
}

Additionally, consider keying session invalidation on a hash of the refresh/access token (or a server-issued session identifier) rather than unauthenticated claims.

2. 🟡 Potential DoS via unbounded read/parse of Gemini CLI credentials and JWT payload
Property Value
Severity Medium
CWE CWE-400
Location src/agents/cli-credentials.ts:232-246

Description

readGeminiCliCredentials() loads ~/.gemini/oauth_creds.json via loadJsonFile(), then decodes and parses the id_token JWT payload with no explicit size bounds.

  • Unbounded file read/parse: loadJsonFile() does fs.readFileSync(pathname, "utf8") followed by JSON.parse(raw) with no maximum size check.
  • Unbounded JWT payload decode/parse: decodeJwtIdentityClaims() base64url-decodes parts[1] into a UTF-8 string and then JSON.parses it. A very large id_token (or just a very large oauth_creds.json) can cause excessive CPU/memory usage.
  • Amplification: resolveCliAuthEpoch() can call readGeminiCliCredentialsCached(ttlMs: 5000) repeatedly in a long-running process, re-triggering heavy parsing periodically if caching is disabled (ttl 0 elsewhere) or if the file mtime changes.

Vulnerable code:

const raw = fs.readFileSync(pathname, "utf8");
return JSON.parse(raw) as T;

const payloadRaw = Buffer.from(parts[1], "base64url").toString("utf8");
const payload = JSON.parse(payloadRaw);

Recommendation

Add explicit size limits before reading/parsing JSON files and before decoding/parsing JWT payloads.

Option A (preferred): enforce a maximum JSON file size in loadJsonFile (or add a new helper)

export function loadJsonFile<T = unknown>(pathname: string, opts?: { maxBytes?: number }): T | undefined {
  const maxBytes = opts?.maxBytes ?? 1024 * 1024; // 1 MiB, tune as needed
  try {
    const stat = fs.statSync(pathname);
    if (stat.size > maxBytes) return undefined;
    const raw = fs.readFileSync(pathname, "utf8");
    return JSON.parse(raw) as T;
  } catch {
    return undefined;
  }
}

Option B: additionally bound id_token payload length before decoding/parsing

function decodeJwtIdentityClaims(token: string): { sub?: string; email?: string } {
  const parts = token.split(".");
  if (parts.length < 2) return {};// bound base64url segment length
  if (parts[1].length > 16_384) return {};
  try {
    const payloadRaw = Buffer.from(parts[1], "base64url").toString("utf8");
    if (payloadRaw.length > 16_384) return {};
    const payload = JSON.parse(payloadRaw) as { sub?: unknown; email?: unknown };
    ...
  } catch {
    return {};
  }
}

This prevents a crafted/oversized credentials file or JWT from consuming excessive resources.


Analyzed PR: #71076 at commit 3e1eeb7

Last updated on: 2026-04-25T12:48:00Z

@openclaw-barnacle openclaw-barnacle Bot added agents Agent runtime and tooling size: S maintainer Maintainer-authored PR labels Apr 24, 2026
@greptile-apps

greptile-apps Bot commented Apr 24, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR adds a google-gemini-cli branch to getLocalCliCredentialFingerprint in src/agents/cli-auth-epoch.ts, mirroring the existing Claude and Codex branches. It introduces GeminiCliCredential, readGeminiCliCredentials, and readGeminiCliCredentialsCached in src/agents/cli-credentials.ts using the existing readPortalCliOauthCredentials helper, and adds encodeGeminiCredential which hashes on the refresh token (the only identity-stable field in ~/.gemini/oauth_creds.json). Two new tests cover access-token-stable epochs and refresh-token rotation, matching the coverage for the existing Codex mixed-credential test.

Confidence Score: 5/5

Safe to merge — targeted bug fix following established patterns with no behavior change for existing providers.

The change is minimal, strictly additive, and mirrors the already-tested Codex/Claude code paths. All remaining observations are P2 style notes. No correctness, data-integrity, or security issues found.

No files require special attention.

Reviews (1): Last reviewed commit: "fix(cli): fingerprint gemini cli credent..." | Re-trigger Greptile

@openperf
openperf force-pushed the fix/gemini-cli-auth-epoch-70973 branch from 2a4d5fe to 4fa14b2 Compare April 24, 2026 13:09
@openperf
openperf force-pushed the fix/gemini-cli-auth-epoch-70973 branch from 4fa14b2 to 3b2809a Compare April 24, 2026 13:32
@openperf
openperf force-pushed the fix/gemini-cli-auth-epoch-70973 branch from 3b2809a to 4a8d811 Compare April 24, 2026 14:20
@openperf
openperf force-pushed the fix/gemini-cli-auth-epoch-70973 branch from 4a8d811 to f0399a2 Compare April 25, 2026 08:20
@openperf openperf changed the title fix(cli): fingerprint gemini cli credentials in auth epoch fix(cli): key gemini cli auth epoch on google account identity Apr 25, 2026
@openperf
openperf force-pushed the fix/gemini-cli-auth-epoch-70973 branch 4 times, most recently from afdbc98 to 941270f Compare April 25, 2026 12:31
getLocalCliCredentialFingerprint only switched on claude-cli and
codex-cli, so google-gemini-cli fell through to undefined and the
stored session's authEpoch never bound to the user's local Gemini
OAuth identity.

Read ~/.gemini/oauth_creds.json through the same cached-reader pattern
as Codex/MiniMax, and encode a stable identity fingerprint from the
refresh token (stable across Google's access-token rotation, flips on
re-authentication), matching openclaw#70132's stability contract.

Adds: readGeminiCliCredentials(Cached) in cli-credentials.ts, a
'google-gemini-cli' case in getLocalCliCredentialFingerprint, and
parallel test coverage alongside the existing claude/codex cases.
@openperf
openperf force-pushed the fix/gemini-cli-auth-epoch-70973 branch from 941270f to 3e1eeb7 Compare April 25, 2026 12:45
@openperf
openperf merged commit bc73141 into openclaw:main Apr 25, 2026
52 of 57 checks passed
@openperf

Copy link
Copy Markdown
Member Author

Landed as bc73141.

Angfr95 pushed a commit to Angfr95/openclaw that referenced this pull request Apr 25, 2026
…law#71076)

Fixes openclaw#70973. Adds a \`google-gemini-cli\` branch to \`getLocalCliCredentialFingerprint\` that lifts OpenID \`id_token\` \`sub\`/\`email\` claims from \`~/.gemini/oauth_creds.json\` onto \`GeminiCliCredential\` so the shared \`encodeOAuthIdentity\` produces an identity-keyed auth-epoch matching the Claude/Codex contract, plus bumps \`CLI_AUTH_EPOCH_VERSION\` from 3 to 4 so existing v3 Gemini bindings without an \`authEpoch\` ride the existing \`cli-session.ts\` version-gate instead of forcing a one-time invalidation.
ayesha-aziz123 pushed a commit to ayesha-aziz123/openclaw that referenced this pull request Apr 26, 2026
…law#71076)

Fixes openclaw#70973. Adds a \`google-gemini-cli\` branch to \`getLocalCliCredentialFingerprint\` that lifts OpenID \`id_token\` \`sub\`/\`email\` claims from \`~/.gemini/oauth_creds.json\` onto \`GeminiCliCredential\` so the shared \`encodeOAuthIdentity\` produces an identity-keyed auth-epoch matching the Claude/Codex contract, plus bumps \`CLI_AUTH_EPOCH_VERSION\` from 3 to 4 so existing v3 Gemini bindings without an \`authEpoch\` ride the existing \`cli-session.ts\` version-gate instead of forcing a one-time invalidation.
ogt-redknie pushed a commit to ogt-redknie/OPENX that referenced this pull request May 2, 2026
…law#71076)

Fixes openclaw#70973. Adds a \`google-gemini-cli\` branch to \`getLocalCliCredentialFingerprint\` that lifts OpenID \`id_token\` \`sub\`/\`email\` claims from \`~/.gemini/oauth_creds.json\` onto \`GeminiCliCredential\` so the shared \`encodeOAuthIdentity\` produces an identity-keyed auth-epoch matching the Claude/Codex contract, plus bumps \`CLI_AUTH_EPOCH_VERSION\` from 3 to 4 so existing v3 Gemini bindings without an \`authEpoch\` ride the existing \`cli-session.ts\` version-gate instead of forcing a one-time invalidation.
stainlu added a commit to stainlu/openclaw that referenced this pull request May 7, 2026
…nclaw#74312)

Extend Ayaan's OAuth identity-only hashing pattern (1ff461f,
3eb6edc) and Chunyue's Gemini extension (openclaw#71076) to the static-token
credential branch. Token rotation and transient partial keychain reads
no longer flip the claude-cli auth-epoch.

Before: encodeClaudeCredential token branch hashed credential.token,
and encodeAuthProfileCredential token case did the same. Either
rotation or a partial keychain read (where parseClaudeCliOauthCredential
sees accessToken without refreshToken and falls into the token branch)
flipped the auth-epoch and forced cli session reset: reason=auth-epoch
on every active claude-cli conversation. macOS keychain-only setups
hit the auth-profile path as the dominant trigger.

After: encodeClaudeCredential routes both branches through
encodeOAuthIdentity, collapsing partial reads onto the same
provider-keyed identity hash. encodeAuthProfileCredential token case
drops credential.token from the hash; identity fields (provider,
tokenRef, email, displayName) are the discriminator.

CLI_AUTH_EPOCH_VERSION bumped 4 to 5.

Test updates flip the existing token-flips assertion to stability,
add a regression case proving Claude CLI OAuth and token epochs match
(closing the partial-read race), and add auth-profile token tests for
both rotation stability and identity-change invalidation.
stainlu added a commit to stainlu/openclaw that referenced this pull request May 8, 2026
…nclaw#74312)

Extend Ayaan's OAuth identity-only hashing pattern (1ff461f,
3eb6edc) and Chunyue's Gemini extension (openclaw#71076) to the static-token
credential branch. Token rotation and transient partial keychain reads
no longer flip the claude-cli auth-epoch.

Before: encodeClaudeCredential token branch hashed credential.token,
and encodeAuthProfileCredential token case did the same. Either
rotation or a partial keychain read (where parseClaudeCliOauthCredential
sees accessToken without refreshToken and falls into the token branch)
flipped the auth-epoch and forced cli session reset: reason=auth-epoch
on every active claude-cli conversation. macOS keychain-only setups
hit the auth-profile path as the dominant trigger.

After: encodeClaudeCredential routes both branches through
encodeOAuthIdentity, collapsing partial reads onto the same
provider-keyed identity hash. encodeAuthProfileCredential token case
drops credential.token from the hash; identity fields (provider,
tokenRef, email, displayName) are the discriminator.

CLI_AUTH_EPOCH_VERSION bumped 4 to 5.

Test updates flip the existing token-flips assertion to stability,
add a regression case proving Claude CLI OAuth and token epochs match
(closing the partial-read race), and add auth-profile token tests for
both rotation stability and identity-change invalidation.
github-actions Bot pushed a commit to Desicool/openclaw that referenced this pull request May 9, 2026
…law#71076)

Fixes openclaw#70973. Adds a \`google-gemini-cli\` branch to \`getLocalCliCredentialFingerprint\` that lifts OpenID \`id_token\` \`sub\`/\`email\` claims from \`~/.gemini/oauth_creds.json\` onto \`GeminiCliCredential\` so the shared \`encodeOAuthIdentity\` produces an identity-keyed auth-epoch matching the Claude/Codex contract, plus bumps \`CLI_AUTH_EPOCH_VERSION\` from 3 to 4 so existing v3 Gemini bindings without an \`authEpoch\` ride the existing \`cli-session.ts\` version-gate instead of forcing a one-time invalidation.
stainlu added a commit to stainlu/openclaw that referenced this pull request May 10, 2026
…nclaw#74312)

Extend Ayaan's OAuth identity-only hashing pattern (1ff461f,
3eb6edc) and Chunyue's Gemini extension (openclaw#71076) to the static-token
credential branch. Token rotation and transient partial keychain reads
no longer flip the claude-cli auth-epoch.

Before: encodeClaudeCredential token branch hashed credential.token,
and encodeAuthProfileCredential token case did the same. Either
rotation or a partial keychain read (where parseClaudeCliOauthCredential
sees accessToken without refreshToken and falls into the token branch)
flipped the auth-epoch and forced cli session reset: reason=auth-epoch
on every active claude-cli conversation. macOS keychain-only setups
hit the auth-profile path as the dominant trigger.

After: encodeClaudeCredential routes both branches through
encodeOAuthIdentity, collapsing partial reads onto the same
provider-keyed identity hash. encodeAuthProfileCredential token case
drops credential.token from the hash; identity fields (provider,
tokenRef, email, displayName) are the discriminator.

CLI_AUTH_EPOCH_VERSION bumped 4 to 5.

Test updates flip the existing token-flips assertion to stability,
add a regression case proving Claude CLI OAuth and token epochs match
(closing the partial-read race), and add auth-profile token tests for
both rotation stability and identity-change invalidation.
stainlu added a commit to stainlu/openclaw that referenced this pull request May 10, 2026
…nclaw#74312)

Extend Ayaan's OAuth identity-only hashing pattern (1ff461f,
3eb6edc) and Chunyue's Gemini extension (openclaw#71076) to the static-token
credential branch. Token rotation and transient partial keychain reads
no longer flip the claude-cli auth-epoch.

Before: encodeClaudeCredential token branch hashed credential.token,
and encodeAuthProfileCredential token case did the same. Either
rotation or a partial keychain read (where parseClaudeCliOauthCredential
sees accessToken without refreshToken and falls into the token branch)
flipped the auth-epoch and forced cli session reset: reason=auth-epoch
on every active claude-cli conversation. macOS keychain-only setups
hit the auth-profile path as the dominant trigger.

After: encodeClaudeCredential routes both branches through
encodeOAuthIdentity, collapsing partial reads onto the same
provider-keyed identity hash. encodeAuthProfileCredential token case
drops credential.token from the hash; identity fields (provider,
tokenRef, email, displayName) are the discriminator.

CLI_AUTH_EPOCH_VERSION bumped 4 to 5.

Test updates flip the existing token-flips assertion to stability,
add a regression case proving Claude CLI OAuth and token epochs match
(closing the partial-read race), and add auth-profile token tests for
both rotation stability and identity-change invalidation.
stainlu added a commit to stainlu/openclaw that referenced this pull request May 10, 2026
…nclaw#74312)

Extend Ayaan's OAuth identity-only hashing pattern (1ff461f,
3eb6edc) and Chunyue's Gemini extension (openclaw#71076) to the static-token
credential branch. Token rotation and transient partial keychain reads
no longer flip the claude-cli auth-epoch.

Before: encodeClaudeCredential token branch hashed credential.token,
and encodeAuthProfileCredential token case did the same. Either
rotation or a partial keychain read (where parseClaudeCliOauthCredential
sees accessToken without refreshToken and falls into the token branch)
flipped the auth-epoch and forced cli session reset: reason=auth-epoch
on every active claude-cli conversation. macOS keychain-only setups
hit the auth-profile path as the dominant trigger.

After: encodeClaudeCredential routes both branches through
encodeOAuthIdentity, collapsing partial reads onto the same
provider-keyed identity hash. encodeAuthProfileCredential token case
drops credential.token from the hash; identity fields (provider,
tokenRef, email, displayName) are the discriminator.

CLI_AUTH_EPOCH_VERSION bumped 4 to 5.

Test updates flip the existing token-flips assertion to stability,
add a regression case proving Claude CLI OAuth and token epochs match
(closing the partial-read race), and add auth-profile token tests for
both rotation stability and identity-change invalidation.
stainlu added a commit to stainlu/openclaw that referenced this pull request May 12, 2026
…nclaw#74312)

Extend Ayaan's OAuth identity-only hashing pattern (1ff461f,
3eb6edc) and Chunyue's Gemini extension (openclaw#71076) to the static-token
credential branch. Token rotation and transient partial keychain reads
no longer flip the claude-cli auth-epoch.

Before: encodeClaudeCredential token branch hashed credential.token,
and encodeAuthProfileCredential token case did the same. Either
rotation or a partial keychain read (where parseClaudeCliOauthCredential
sees accessToken without refreshToken and falls into the token branch)
flipped the auth-epoch and forced cli session reset: reason=auth-epoch
on every active claude-cli conversation. macOS keychain-only setups
hit the auth-profile path as the dominant trigger.

After: encodeClaudeCredential routes both branches through
encodeOAuthIdentity, collapsing partial reads onto the same
provider-keyed identity hash. encodeAuthProfileCredential token case
drops credential.token from the hash; identity fields (provider,
tokenRef, email, displayName) are the discriminator.

CLI_AUTH_EPOCH_VERSION bumped 4 to 5.

Test updates flip the existing token-flips assertion to stability,
add a regression case proving Claude CLI OAuth and token epochs match
(closing the partial-read race), and add auth-profile token tests for
both rotation stability and identity-change invalidation.
globalcaos pushed a commit to globalcaos/tinkerclaw that referenced this pull request May 13, 2026
…law#71076)

Fixes openclaw#70973. Adds a \`google-gemini-cli\` branch to \`getLocalCliCredentialFingerprint\` that lifts OpenID \`id_token\` \`sub\`/\`email\` claims from \`~/.gemini/oauth_creds.json\` onto \`GeminiCliCredential\` so the shared \`encodeOAuthIdentity\` produces an identity-keyed auth-epoch matching the Claude/Codex contract, plus bumps \`CLI_AUTH_EPOCH_VERSION\` from 3 to 4 so existing v3 Gemini bindings without an \`authEpoch\` ride the existing \`cli-session.ts\` version-gate instead of forcing a one-time invalidation.
github-actions Bot pushed a commit to Desicool/openclaw that referenced this pull request May 24, 2026
…law#71076)

Fixes openclaw#70973. Adds a \`google-gemini-cli\` branch to \`getLocalCliCredentialFingerprint\` that lifts OpenID \`id_token\` \`sub\`/\`email\` claims from \`~/.gemini/oauth_creds.json\` onto \`GeminiCliCredential\` so the shared \`encodeOAuthIdentity\` produces an identity-keyed auth-epoch matching the Claude/Codex contract, plus bumps \`CLI_AUTH_EPOCH_VERSION\` from 3 to 4 so existing v3 Gemini bindings without an \`authEpoch\` ride the existing \`cli-session.ts\` version-gate instead of forcing a one-time invalidation.
steipete pushed a commit to stainlu/openclaw that referenced this pull request May 31, 2026
…nclaw#74312)

Extend Ayaan's OAuth identity-only hashing pattern (1ff461f,
3eb6edc) and Chunyue's Gemini extension (openclaw#71076) to the static-token
credential branch. Token rotation and transient partial keychain reads
no longer flip the claude-cli auth-epoch.

Before: encodeClaudeCredential token branch hashed credential.token,
and encodeAuthProfileCredential token case did the same. Either
rotation or a partial keychain read (where parseClaudeCliOauthCredential
sees accessToken without refreshToken and falls into the token branch)
flipped the auth-epoch and forced cli session reset: reason=auth-epoch
on every active claude-cli conversation. macOS keychain-only setups
hit the auth-profile path as the dominant trigger.

After: encodeClaudeCredential routes both branches through
encodeOAuthIdentity, collapsing partial reads onto the same
provider-keyed identity hash. encodeAuthProfileCredential token case
drops credential.token from the hash; identity fields (provider,
tokenRef, email, displayName) are the discriminator.

CLI_AUTH_EPOCH_VERSION bumped 4 to 5.

Test updates flip the existing token-flips assertion to stability,
add a regression case proving Claude CLI OAuth and token epochs match
(closing the partial-read race), and add auth-profile token tests for
both rotation stability and identity-change invalidation.
jameslcowan pushed a commit to jameslcowan/openclaw that referenced this pull request Jun 2, 2026
…law#71076)

Fixes openclaw#70973. Adds a \`google-gemini-cli\` branch to \`getLocalCliCredentialFingerprint\` that lifts OpenID \`id_token\` \`sub\`/\`email\` claims from \`~/.gemini/oauth_creds.json\` onto \`GeminiCliCredential\` so the shared \`encodeOAuthIdentity\` produces an identity-keyed auth-epoch matching the Claude/Codex contract, plus bumps \`CLI_AUTH_EPOCH_VERSION\` from 3 to 4 so existing v3 Gemini bindings without an \`authEpoch\` ride the existing \`cli-session.ts\` version-gate instead of forcing a one-time invalidation.
sablehead pushed a commit to sablehead/openclaw that referenced this pull request Jun 10, 2026
…law#71076)

Fixes openclaw#70973. Adds a \`google-gemini-cli\` branch to \`getLocalCliCredentialFingerprint\` that lifts OpenID \`id_token\` \`sub\`/\`email\` claims from \`~/.gemini/oauth_creds.json\` onto \`GeminiCliCredential\` so the shared \`encodeOAuthIdentity\` produces an identity-keyed auth-epoch matching the Claude/Codex contract, plus bumps \`CLI_AUTH_EPOCH_VERSION\` from 3 to 4 so existing v3 Gemini bindings without an \`authEpoch\` ride the existing \`cli-session.ts\` version-gate instead of forcing a one-time invalidation.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

agents Agent runtime and tooling maintainer Maintainer-authored PR size: M

Projects

None yet

1 participant