Merge 351d778099 into da71eaebd2
This commit is contained in:
commit
68789a6ae7
@ -1,5 +1,11 @@
|
||||
import { readQwenCliCredentialsCached } from "../cli-credentials.js";
|
||||
import {
|
||||
readClaudeCliCredentialsCached,
|
||||
readCodexCliCredentialsCached,
|
||||
readQwenCliCredentialsCached,
|
||||
} from "../cli-credentials.js";
|
||||
import {
|
||||
CLAUDE_CLI_PROFILE_ID,
|
||||
CODEX_CLI_PROFILE_ID,
|
||||
EXTERNAL_CLI_NEAR_EXPIRY_MS,
|
||||
EXTERNAL_CLI_SYNC_TTL_MS,
|
||||
QWEN_CLI_PROFILE_ID,
|
||||
@ -22,16 +28,132 @@ function shallowEqualOAuthCredentials(a: OAuthCredential | undefined, b: OAuthCr
|
||||
);
|
||||
}
|
||||
|
||||
function isExternalProfileFresh(cred: AuthProfileCredential | undefined, now: number): boolean {
|
||||
function isExternalProfileFresh(
|
||||
cred: AuthProfileCredential | undefined,
|
||||
now: number,
|
||||
provider?: string,
|
||||
): boolean {
|
||||
if (!cred) return false;
|
||||
if (cred.type !== "oauth" && cred.type !== "token") return false;
|
||||
if (cred.provider !== "qwen-portal") {
|
||||
if (provider && cred.provider !== provider) {
|
||||
return false;
|
||||
}
|
||||
if (typeof cred.expires !== "number") return true;
|
||||
return cred.expires > now + EXTERNAL_CLI_NEAR_EXPIRY_MS;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sync Claude Code CLI credentials into the store.
|
||||
* Called on refresh failure to attempt recovery.
|
||||
*
|
||||
* Returns the synced credential if successful, null otherwise.
|
||||
*/
|
||||
export function trySyncClaudeCliCredentialsOnRefreshFailure(
|
||||
store: AuthProfileStore,
|
||||
): OAuthCredential | null {
|
||||
const now = Date.now();
|
||||
const creds = readClaudeCliCredentialsCached({ ttlMs: 0 }); // Force fresh read
|
||||
|
||||
if (!creds) {
|
||||
log.debug("no claude cli credentials found for recovery sync");
|
||||
return null;
|
||||
}
|
||||
|
||||
// Only sync if CLI credentials are fresh (not expired)
|
||||
if (creds.expires <= now) {
|
||||
log.debug("claude cli credentials are expired, skipping recovery sync", {
|
||||
expires: new Date(creds.expires).toISOString(),
|
||||
});
|
||||
return null;
|
||||
}
|
||||
|
||||
const existing = store.profiles[CLAUDE_CLI_PROFILE_ID];
|
||||
const existingOAuth = existing?.type === "oauth" ? existing : undefined;
|
||||
|
||||
// Check if CLI credentials are newer than what we have
|
||||
if (existingOAuth && existingOAuth.expires >= creds.expires) {
|
||||
log.debug("stored credentials are not older than claude cli, skipping recovery sync");
|
||||
return null;
|
||||
}
|
||||
|
||||
// Convert Claude CLI credential to OAuthCredential format
|
||||
if (creds.type === "oauth") {
|
||||
const oauthCred: OAuthCredential = {
|
||||
type: "oauth",
|
||||
provider: "anthropic",
|
||||
access: creds.access,
|
||||
refresh: creds.refresh,
|
||||
expires: creds.expires,
|
||||
};
|
||||
|
||||
if (!shallowEqualOAuthCredentials(existingOAuth, oauthCred)) {
|
||||
store.profiles[CLAUDE_CLI_PROFILE_ID] = oauthCred;
|
||||
log.info("synced anthropic credentials from claude cli after refresh failure", {
|
||||
profileId: CLAUDE_CLI_PROFILE_ID,
|
||||
expires: new Date(creds.expires).toISOString(),
|
||||
});
|
||||
return oauthCred;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sync Codex CLI credentials into the store.
|
||||
* Called on refresh failure to attempt recovery.
|
||||
*
|
||||
* Returns the synced credential if successful, null otherwise.
|
||||
*/
|
||||
export function trySyncCodexCliCredentialsOnRefreshFailure(
|
||||
store: AuthProfileStore,
|
||||
): OAuthCredential | null {
|
||||
const now = Date.now();
|
||||
const creds = readCodexCliCredentialsCached({ ttlMs: 0 }); // Force fresh read
|
||||
|
||||
if (!creds) {
|
||||
log.debug("no codex cli credentials found for recovery sync");
|
||||
return null;
|
||||
}
|
||||
|
||||
// Only sync if CLI credentials are fresh (not expired)
|
||||
if (creds.expires <= now) {
|
||||
log.debug("codex cli credentials are expired, skipping recovery sync", {
|
||||
expires: new Date(creds.expires).toISOString(),
|
||||
});
|
||||
return null;
|
||||
}
|
||||
|
||||
const existing = store.profiles[CODEX_CLI_PROFILE_ID];
|
||||
const existingOAuth = existing?.type === "oauth" ? existing : undefined;
|
||||
|
||||
// Check if CLI credentials are newer than what we have
|
||||
if (existingOAuth && existingOAuth.expires >= creds.expires) {
|
||||
log.debug("stored credentials are not older than codex cli, skipping recovery sync");
|
||||
return null;
|
||||
}
|
||||
|
||||
const oauthCred: OAuthCredential = {
|
||||
type: "oauth",
|
||||
provider: creds.provider,
|
||||
access: creds.access,
|
||||
refresh: creds.refresh,
|
||||
expires: creds.expires,
|
||||
accountId: creds.accountId,
|
||||
};
|
||||
|
||||
if (!shallowEqualOAuthCredentials(existingOAuth, oauthCred)) {
|
||||
store.profiles[CODEX_CLI_PROFILE_ID] = oauthCred;
|
||||
log.info("synced openai-codex credentials from codex cli after refresh failure", {
|
||||
profileId: CODEX_CLI_PROFILE_ID,
|
||||
expires: new Date(creds.expires).toISOString(),
|
||||
});
|
||||
return oauthCred;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sync OAuth credentials from external CLI tools (Qwen Code CLI) into the store.
|
||||
*
|
||||
|
||||
@ -4,8 +4,12 @@ import lockfile from "proper-lockfile";
|
||||
import type { OpenClawConfig } from "../../config/config.js";
|
||||
import { refreshChutesTokens } from "../chutes-oauth.js";
|
||||
import { refreshQwenPortalCredentials } from "../../providers/qwen-portal-oauth.js";
|
||||
import { AUTH_STORE_LOCK_OPTIONS, log } from "./constants.js";
|
||||
import { AUTH_STORE_LOCK_OPTIONS, CLAUDE_CLI_PROFILE_ID, CODEX_CLI_PROFILE_ID, log } from "./constants.js";
|
||||
import { formatAuthDoctorHint } from "./doctor.js";
|
||||
import {
|
||||
trySyncClaudeCliCredentialsOnRefreshFailure,
|
||||
trySyncCodexCliCredentialsOnRefreshFailure,
|
||||
} from "./external-cli-sync.js";
|
||||
import { ensureAuthStoreFile, resolveAuthStorePath } from "./paths.js";
|
||||
import { suggestOAuthProfileIdForLegacyDefault } from "./repair.js";
|
||||
import { ensureAuthProfileStore, saveAuthProfileStore } from "./store.js";
|
||||
@ -197,6 +201,52 @@ export async function resolveApiKeyForProfile(params: {
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback: try syncing from external CLI tools (Claude Code CLI, Codex CLI)
|
||||
// This handles the case where another tool refreshed the token and we have stale credentials
|
||||
if (cred.provider === "anthropic" && profileId === CLAUDE_CLI_PROFILE_ID) {
|
||||
try {
|
||||
const synced = trySyncClaudeCliCredentialsOnRefreshFailure(refreshedStore);
|
||||
if (synced && Date.now() < synced.expires) {
|
||||
saveAuthProfileStore(refreshedStore, params.agentDir);
|
||||
log.info("recovered from refresh failure by syncing Claude Code CLI credentials", {
|
||||
profileId,
|
||||
expires: new Date(synced.expires).toISOString(),
|
||||
});
|
||||
return {
|
||||
apiKey: buildOAuthApiKey(synced.provider, synced),
|
||||
provider: synced.provider,
|
||||
email: synced.email,
|
||||
};
|
||||
}
|
||||
} catch (syncError) {
|
||||
log.debug("failed to sync claude cli credentials after refresh failure", {
|
||||
error: syncError instanceof Error ? syncError.message : String(syncError),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (cred.provider === "openai-codex" && profileId === CODEX_CLI_PROFILE_ID) {
|
||||
try {
|
||||
const synced = trySyncCodexCliCredentialsOnRefreshFailure(refreshedStore);
|
||||
if (synced && Date.now() < synced.expires) {
|
||||
saveAuthProfileStore(refreshedStore, params.agentDir);
|
||||
log.info("recovered from refresh failure by syncing Codex CLI credentials", {
|
||||
profileId,
|
||||
expires: new Date(synced.expires).toISOString(),
|
||||
});
|
||||
return {
|
||||
apiKey: buildOAuthApiKey(synced.provider, synced),
|
||||
provider: synced.provider,
|
||||
email: synced.email,
|
||||
};
|
||||
}
|
||||
} catch (syncError) {
|
||||
log.debug("failed to sync codex cli credentials after refresh failure", {
|
||||
error: syncError instanceof Error ? syncError.message : String(syncError),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback: if this is a secondary agent, try using the main agent's credentials
|
||||
if (params.agentDir) {
|
||||
try {
|
||||
|
||||
Loading…
Reference in New Issue
Block a user