mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 14:10:51 +00:00
fix(auth): scope external CLI auth status overlays (#74156)
* fix(auth): scope external CLI auth status overlays * fix: pass external auth config to overlays * fix(auth): keep no-prompt CLI reads file-only * docs: update clawsweeper app wording
This commit is contained in:
committed by
GitHub
parent
8f6c72823e
commit
f79553bef6
@@ -24,12 +24,12 @@ read-only report work read-only unless Peter asked to commit.
|
||||
|
||||
## One Bot, One App
|
||||
|
||||
Use the ClawSweeper repo and the `openclaw-ci` GitHub App. Use only
|
||||
Use the ClawSweeper repo and the `clawsweeper` GitHub App. Use only
|
||||
`CLAWSWEEPER_*` configuration for this automation.
|
||||
|
||||
Required app setup:
|
||||
|
||||
- `CLAWSWEEPER_APP_CLIENT_ID`: public app client ID for `openclaw-ci`.
|
||||
- `CLAWSWEEPER_APP_CLIENT_ID`: public app client ID for `clawsweeper`.
|
||||
- `CLAWSWEEPER_APP_PRIVATE_KEY`: private key used only inside
|
||||
`actions/create-github-app-token` steps.
|
||||
- Target app permissions: read target scan context; write issues and pull
|
||||
|
||||
@@ -91,6 +91,7 @@ Docs: https://docs.openclaw.ai
|
||||
- Agents/Claude CLI: reuse already-cached macOS Keychain credentials for no-prompt Claude credential reads, so doctor/runtime checks do not miss fresh interactive Claude auth. Fixes #73682. Thanks @RyanSandoval.
|
||||
- Agents/Claude CLI doctor: scope workspace and project-dir checks to agents that actually use the Claude CLI runtime, so non-default Claude agents no longer make the default agent look Claude-backed. Fixes #73903. Thanks @bobfreeman1989.
|
||||
- Gateway/sessions: expose effective agent runtime metadata on session rows, `sessions.patch`, and local `openclaw sessions --json`, while keeping Claude CLI-backed rows on the canonical model provider so runtime backend and model identity are no longer conflated. Fixes #73090. Thanks @vishutdhar.
|
||||
- Gateway/auth status: scope external CLI credential overlays to configured providers, runtimes, or profiles and keep status reads off new Keychain prompts, so single-provider Gateway configs no longer probe unrelated Claude/Codex/MiniMax auth on startup. Fixes #73908. Thanks @Ailuras.
|
||||
- Agents/runtime status: expose effective agent runtime metadata in `agents.list`, Control UI agent panels, and `/agents`, and avoid rendering stale or cumulative CLI token totals as live context usage. Fixes #73660, #73578, and #45268. Thanks @spartman, @DashLabsDev, and @xyooz.
|
||||
- Agents/transcripts: strip empty assistant text blocks while preserving valid text, images, and signatures, so Anthropic-style providers no longer reject sanitized transcript turns. Fixes #73640. Thanks @jowhee327.
|
||||
- Providers/Bedrock: omit deprecated `temperature` for Claude Opus 4.7 Bedrock model ids, named and application inference profiles, including dotted `opus-4.7` refs, and classify the nested validation response for failover. Fixes #73663. Thanks @bstanbury.
|
||||
|
||||
@@ -80,6 +80,14 @@ the target agent signs in separately and creates its own local profile.
|
||||
candidate for it, `models status --probe` reports `status: no_model` with
|
||||
`reasonCode: no_model`.
|
||||
|
||||
## External CLI credential discovery
|
||||
|
||||
- Runtime-only credentials owned by external CLIs are discovered only when the
|
||||
provider, runtime, or auth profile is in scope for the current operation, or
|
||||
when a stored local profile for that external source already exists.
|
||||
- Read-only/status paths pass `allowKeychainPrompt: false`; they use file-backed
|
||||
external CLI credentials only and do not read or reuse macOS Keychain results.
|
||||
|
||||
## OAuth SecretRef Policy Guard
|
||||
|
||||
- SecretRef input is for static credentials only.
|
||||
|
||||
@@ -59,7 +59,7 @@ describe("external CLI auth scope", () => {
|
||||
{
|
||||
id: "worker",
|
||||
model: "opencode-go/kimi-k2.6",
|
||||
agentRuntime: { id: "codex" },
|
||||
agentRuntime: { id: "codex-app-server" },
|
||||
subagents: { model: { primary: "z.ai/glm-4.7" } },
|
||||
},
|
||||
],
|
||||
@@ -75,11 +75,12 @@ describe("external CLI auth scope", () => {
|
||||
"openai-codex",
|
||||
"minimax-portal",
|
||||
"claude-cli",
|
||||
"codex",
|
||||
"codex-app-server",
|
||||
"opencode-go",
|
||||
"z.ai",
|
||||
"zai",
|
||||
]),
|
||||
);
|
||||
expect(scope?.profileIds).toContain("openai-codex:default");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -374,6 +374,36 @@ describe("external cli oauth resolution", () => {
|
||||
expect(mocks.readMiniMaxCliCredentialsCached).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("passes non-prompting keychain policy to scoped Codex CLI credential reads", () => {
|
||||
mocks.readCodexCliCredentialsCached.mockReturnValue(
|
||||
makeOAuthCredential({
|
||||
provider: "openai-codex",
|
||||
access: "codex-cli-access",
|
||||
refresh: "codex-cli-refresh",
|
||||
}),
|
||||
);
|
||||
|
||||
const profiles = resolveExternalCliAuthProfiles(makeStore(), {
|
||||
providerIds: ["codex-app-server"],
|
||||
allowKeychainPrompt: false,
|
||||
});
|
||||
|
||||
expect(profiles).toEqual([
|
||||
{
|
||||
profileId: OPENAI_CODEX_DEFAULT_PROFILE_ID,
|
||||
credential: expect.objectContaining({
|
||||
type: "oauth",
|
||||
provider: "openai-codex",
|
||||
}),
|
||||
},
|
||||
]);
|
||||
expect(mocks.readCodexCliCredentialsCached).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ allowKeychainPrompt: false }),
|
||||
);
|
||||
expect(mocks.readClaudeCliCredentialsCached).not.toHaveBeenCalled();
|
||||
expect(mocks.readMiniMaxCliCredentialsCached).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("ignores Claude CLI token credentials", () => {
|
||||
mocks.readClaudeCliCredentialsCached.mockReturnValue({
|
||||
type: "token",
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import type { OpenClawConfig } from "../../config/types.openclaw.js";
|
||||
import type { ProviderExternalAuthProfile } from "../../plugins/provider-external-auth.types.js";
|
||||
import { resolveExternalAuthProfilesWithPlugins } from "../../plugins/provider-runtime.js";
|
||||
import * as externalCliSync from "./external-cli-sync.js";
|
||||
@@ -12,6 +13,7 @@ type ExternalAuthProfileMap = Map<string, ProviderExternalAuthProfile>;
|
||||
type ResolveExternalAuthProfiles = typeof resolveExternalAuthProfilesWithPlugins;
|
||||
type ExternalCliOverlayOptions = {
|
||||
allowKeychainPrompt?: boolean;
|
||||
config?: OpenClawConfig;
|
||||
externalCliProviderIds?: Iterable<string>;
|
||||
externalCliProfileIds?: Iterable<string>;
|
||||
};
|
||||
@@ -50,8 +52,9 @@ function resolveExternalAuthProfileMap(params: {
|
||||
resolveExternalAuthProfilesForRuntime ?? resolveExternalAuthProfilesWithPlugins;
|
||||
const profiles = resolveProfiles({
|
||||
env,
|
||||
config: params.externalCli?.config,
|
||||
context: {
|
||||
config: undefined,
|
||||
config: params.externalCli?.config,
|
||||
agentDir: params.agentDir,
|
||||
workspaceDir: undefined,
|
||||
env,
|
||||
@@ -118,6 +121,7 @@ export function shouldPersistExternalAuthProfile(params: {
|
||||
credential: OAuthCredential;
|
||||
agentDir?: string;
|
||||
env?: NodeJS.ProcessEnv;
|
||||
config?: OpenClawConfig;
|
||||
externalCliProviderIds?: Iterable<string>;
|
||||
externalCliProfileIds?: Iterable<string>;
|
||||
}): boolean {
|
||||
@@ -126,6 +130,7 @@ export function shouldPersistExternalAuthProfile(params: {
|
||||
agentDir: params.agentDir,
|
||||
env: params.env,
|
||||
externalCli: {
|
||||
config: params.config,
|
||||
externalCliProviderIds: params.externalCliProviderIds,
|
||||
externalCliProfileIds: params.externalCliProfileIds,
|
||||
},
|
||||
|
||||
@@ -48,6 +48,7 @@ function addExternalCliRuntimeScope(out: Set<string>, value: string | undefined)
|
||||
normalized === "claude-cli" ||
|
||||
normalized === "codex" ||
|
||||
normalized === "codex-cli" ||
|
||||
normalized === "codex-app-server" ||
|
||||
normalized === "openai-codex" ||
|
||||
normalized === "minimax" ||
|
||||
normalized === "minimax-cli" ||
|
||||
@@ -73,8 +74,14 @@ export function resolveExternalCliAuthScopeFromConfig(
|
||||
}
|
||||
addProviderScopeId(providerIds, profile?.provider);
|
||||
}
|
||||
for (const provider of Object.keys(cfg.auth?.order ?? {})) {
|
||||
for (const [provider, orderedProfileIds] of Object.entries(cfg.auth?.order ?? {})) {
|
||||
addProviderScopeId(providerIds, provider);
|
||||
for (const profileId of orderedProfileIds ?? []) {
|
||||
const normalizedProfileId = profileId.trim();
|
||||
if (normalizedProfileId) {
|
||||
profileIds.add(normalizedProfileId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const defaults = cfg.agents?.defaults;
|
||||
|
||||
@@ -100,8 +100,12 @@ const EXTERNAL_CLI_SYNC_PROVIDERS: ExternalCliSyncProvider[] = [
|
||||
{
|
||||
profileId: OPENAI_CODEX_DEFAULT_PROFILE_ID,
|
||||
provider: "openai-codex",
|
||||
aliases: ["codex", "codex-cli"],
|
||||
readCredentials: () => readCodexCliCredentialsCached({ ttlMs: EXTERNAL_CLI_SYNC_TTL_MS }),
|
||||
aliases: ["codex", "codex-cli", "codex-app-server"],
|
||||
readCredentials: (options) =>
|
||||
readCodexCliCredentialsCached({
|
||||
ttlMs: EXTERNAL_CLI_SYNC_TTL_MS,
|
||||
allowKeychainPrompt: options?.allowKeychainPrompt,
|
||||
}),
|
||||
bootstrapOnly: true,
|
||||
},
|
||||
{
|
||||
|
||||
@@ -11,7 +11,7 @@ const resolveExternalAuthProfilesWithPluginsMock = vi.fn<
|
||||
(params: unknown) => ProviderExternalAuthProfile[]
|
||||
>(() => []);
|
||||
const readCodexCliCredentialsCachedMock = vi.hoisted(() =>
|
||||
vi.fn<() => OAuthCredential | null>(() => null),
|
||||
vi.fn<(_options?: unknown) => OAuthCredential | null>(() => null),
|
||||
);
|
||||
|
||||
vi.mock("../cli-credentials.js", () => ({
|
||||
@@ -70,6 +70,29 @@ describe("auth external oauth helpers", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("passes config and CLI scope through overlay resolution", () => {
|
||||
const cfg = {
|
||||
models: {
|
||||
providers: { "openai-codex": { auth: "oauth" as const, baseUrl: "", models: [] } },
|
||||
},
|
||||
};
|
||||
readCodexCliCredentialsCachedMock.mockReturnValueOnce(createCredential());
|
||||
|
||||
overlayExternalOAuthProfiles(createStore(), {
|
||||
allowKeychainPrompt: false,
|
||||
config: cfg,
|
||||
externalCliProviderIds: ["openai-codex"],
|
||||
});
|
||||
|
||||
expect(resolveExternalAuthProfilesWithPluginsMock).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
config: cfg,
|
||||
context: expect.objectContaining({ config: cfg }),
|
||||
}),
|
||||
);
|
||||
expect(readCodexCliCredentialsCachedMock).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("omits exact runtime-only overlays from persisted store writes", () => {
|
||||
const credential = createCredential();
|
||||
resolveExternalAuthProfilesWithPluginsMock.mockReturnValueOnce([
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import fs from "node:fs";
|
||||
import { isDeepStrictEqual } from "node:util";
|
||||
import type { OpenClawConfig } from "../../config/types.openclaw.js";
|
||||
import { withFileLock } from "../../infra/file-lock.js";
|
||||
import { saveJsonFile } from "../../infra/json-file.js";
|
||||
import {
|
||||
@@ -36,6 +37,7 @@ import type { AuthProfileStore } from "./types.js";
|
||||
|
||||
type LoadAuthProfileStoreOptions = {
|
||||
allowKeychainPrompt?: boolean;
|
||||
config?: OpenClawConfig;
|
||||
readOnly?: boolean;
|
||||
syncExternalCli?: boolean;
|
||||
externalCliProviderIds?: Iterable<string>;
|
||||
@@ -359,6 +361,7 @@ export function loadAuthProfileStoreForRuntime(
|
||||
return overlayExternalAuthProfiles(store, {
|
||||
agentDir,
|
||||
allowKeychainPrompt: options?.allowKeychainPrompt,
|
||||
config: options?.config,
|
||||
externalCliProviderIds: options?.externalCliProviderIds,
|
||||
externalCliProfileIds: options?.externalCliProfileIds,
|
||||
});
|
||||
@@ -368,6 +371,7 @@ export function loadAuthProfileStoreForRuntime(
|
||||
return overlayExternalAuthProfiles(mergeAuthProfileStores(mainStore, store), {
|
||||
agentDir,
|
||||
allowKeychainPrompt: options?.allowKeychainPrompt,
|
||||
config: options?.config,
|
||||
externalCliProviderIds: options?.externalCliProviderIds,
|
||||
externalCliProfileIds: options?.externalCliProfileIds,
|
||||
});
|
||||
@@ -394,6 +398,7 @@ export function ensureAuthProfileStore(
|
||||
agentDir?: string,
|
||||
options?: {
|
||||
allowKeychainPrompt?: boolean;
|
||||
config?: OpenClawConfig;
|
||||
externalCliProviderIds?: Iterable<string>;
|
||||
externalCliProfileIds?: Iterable<string>;
|
||||
},
|
||||
@@ -403,6 +408,7 @@ export function ensureAuthProfileStore(
|
||||
{
|
||||
agentDir,
|
||||
allowKeychainPrompt: options?.allowKeychainPrompt,
|
||||
config: options?.config,
|
||||
externalCliProviderIds: options?.externalCliProviderIds,
|
||||
externalCliProfileIds: options?.externalCliProfileIds,
|
||||
},
|
||||
|
||||
@@ -272,7 +272,7 @@ describe("cli credentials", () => {
|
||||
expect(execSyncMock).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("reuses cached Claude keychain credentials for no-prompt reads", async () => {
|
||||
it("keeps no-prompt Claude reads on the file credential path after a keychain read", async () => {
|
||||
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-claude-cache-"));
|
||||
vi.setSystemTime(new Date("2025-01-01T00:00:00Z"));
|
||||
mockClaudeCliCredentialRead();
|
||||
@@ -292,7 +292,12 @@ describe("cli credentials", () => {
|
||||
execSync: execSyncMock,
|
||||
});
|
||||
|
||||
expect(withoutPrompt).toEqual(withKeychain);
|
||||
expect(withKeychain).toMatchObject({
|
||||
type: "oauth",
|
||||
provider: "anthropic",
|
||||
refresh: "cached-refresh",
|
||||
});
|
||||
expect(withoutPrompt).toBeNull();
|
||||
expect(execSyncMock).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
@@ -361,6 +366,126 @@ describe("cli credentials", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("does not read Codex keychain when keychain prompts are disabled", async () => {
|
||||
const tempHome = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-codex-no-prompt-"));
|
||||
process.env.CODEX_HOME = tempHome;
|
||||
const expSeconds = Math.floor(Date.parse("2026-03-24T12:34:56Z") / 1000);
|
||||
const authPath = path.join(tempHome, "auth.json");
|
||||
fs.mkdirSync(tempHome, { recursive: true, mode: 0o700 });
|
||||
fs.writeFileSync(
|
||||
authPath,
|
||||
JSON.stringify({
|
||||
tokens: {
|
||||
access_token: createJwtWithExp(expSeconds),
|
||||
refresh_token: "file-refresh",
|
||||
},
|
||||
}),
|
||||
"utf8",
|
||||
);
|
||||
|
||||
const creds = readCodexCliCredentialsCached({
|
||||
allowKeychainPrompt: false,
|
||||
ttlMs: CLI_CREDENTIALS_CACHE_TTL_MS,
|
||||
platform: "darwin",
|
||||
execSync: execSyncMock,
|
||||
});
|
||||
|
||||
expect(creds).toMatchObject({
|
||||
access: createJwtWithExp(expSeconds),
|
||||
refresh: "file-refresh",
|
||||
provider: "openai-codex",
|
||||
});
|
||||
expect(execSyncMock).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("does not let no-keychain Codex cache misses poison keychain reads", async () => {
|
||||
const tempHome = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-codex-cache-"));
|
||||
process.env.CODEX_HOME = tempHome;
|
||||
const expSeconds = Math.floor(Date.parse("2026-03-24T12:34:56Z") / 1000);
|
||||
|
||||
const withoutKeychain = readCodexCliCredentialsCached({
|
||||
allowKeychainPrompt: false,
|
||||
ttlMs: CLI_CREDENTIALS_CACHE_TTL_MS,
|
||||
platform: "darwin",
|
||||
execSync: execSyncMock,
|
||||
});
|
||||
expect(withoutKeychain).toBeNull();
|
||||
|
||||
execSyncMock.mockReturnValue(
|
||||
JSON.stringify({
|
||||
tokens: {
|
||||
access_token: createJwtWithExp(expSeconds),
|
||||
refresh_token: "keychain-refresh",
|
||||
},
|
||||
}),
|
||||
);
|
||||
const withKeychain = readCodexCliCredentialsCached({
|
||||
allowKeychainPrompt: true,
|
||||
ttlMs: CLI_CREDENTIALS_CACHE_TTL_MS,
|
||||
platform: "darwin",
|
||||
execSync: execSyncMock,
|
||||
});
|
||||
|
||||
expect(withKeychain).toMatchObject({
|
||||
access: createJwtWithExp(expSeconds),
|
||||
refresh: "keychain-refresh",
|
||||
provider: "openai-codex",
|
||||
});
|
||||
expect(execSyncMock).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("keeps no-prompt Codex reads on auth.json after a keychain read", async () => {
|
||||
const tempHome = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-codex-cache-"));
|
||||
process.env.CODEX_HOME = tempHome;
|
||||
const keychainExpiry = Math.floor(Date.parse("2026-03-24T12:34:56Z") / 1000);
|
||||
const fileExpiry = Math.floor(Date.parse("2026-03-25T12:34:56Z") / 1000);
|
||||
const authPath = path.join(tempHome, "auth.json");
|
||||
fs.mkdirSync(tempHome, { recursive: true, mode: 0o700 });
|
||||
fs.writeFileSync(
|
||||
authPath,
|
||||
JSON.stringify({
|
||||
tokens: {
|
||||
access_token: createJwtWithExp(fileExpiry),
|
||||
refresh_token: "file-refresh",
|
||||
},
|
||||
}),
|
||||
"utf8",
|
||||
);
|
||||
execSyncMock.mockReturnValue(
|
||||
JSON.stringify({
|
||||
tokens: {
|
||||
access_token: createJwtWithExp(keychainExpiry),
|
||||
refresh_token: "keychain-refresh",
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
const withKeychain = readCodexCliCredentialsCached({
|
||||
allowKeychainPrompt: true,
|
||||
ttlMs: CLI_CREDENTIALS_CACHE_TTL_MS,
|
||||
platform: "darwin",
|
||||
execSync: execSyncMock,
|
||||
});
|
||||
const withoutPrompt = readCodexCliCredentialsCached({
|
||||
allowKeychainPrompt: false,
|
||||
ttlMs: CLI_CREDENTIALS_CACHE_TTL_MS,
|
||||
platform: "darwin",
|
||||
execSync: execSyncMock,
|
||||
});
|
||||
|
||||
expect(withKeychain).toMatchObject({
|
||||
refresh: "keychain-refresh",
|
||||
expires: keychainExpiry * 1000,
|
||||
provider: "openai-codex",
|
||||
});
|
||||
expect(withoutPrompt).toMatchObject({
|
||||
refresh: "file-refresh",
|
||||
expires: fileExpiry * 1000,
|
||||
provider: "openai-codex",
|
||||
});
|
||||
expect(execSyncMock).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("invalidates cached Codex credentials when auth.json changes within the TTL window", () => {
|
||||
const tempHome = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-codex-cache-"));
|
||||
process.env.CODEX_HOME = tempHome;
|
||||
|
||||
@@ -249,9 +249,10 @@ function readCodexKeychainAuthRecord(options?: {
|
||||
codexHome?: string;
|
||||
platform?: NodeJS.Platform;
|
||||
execSync?: ExecSyncFn;
|
||||
allowKeychainPrompt?: boolean;
|
||||
}): Record<string, unknown> | null {
|
||||
const { platform, execSyncImpl, codexHome } = resolveCodexKeychainParams(options);
|
||||
if (platform !== "darwin") {
|
||||
if (platform !== "darwin" || options?.allowKeychainPrompt === false) {
|
||||
return null;
|
||||
}
|
||||
const account = computeCodexKeychainAccount(codexHome);
|
||||
@@ -277,6 +278,7 @@ function readCodexKeychainCredentials(options?: {
|
||||
codexHome?: string;
|
||||
platform?: NodeJS.Platform;
|
||||
execSync?: ExecSyncFn;
|
||||
allowKeychainPrompt?: boolean;
|
||||
}): CodexCliCredential | null {
|
||||
const parsed = readCodexKeychainAuthRecord(options);
|
||||
if (!parsed) {
|
||||
@@ -458,17 +460,6 @@ export function readClaudeCliCredentialsCached(options?: {
|
||||
const platform = options?.platform ?? process.platform;
|
||||
const ttlMs = options?.ttlMs ?? 0;
|
||||
const credentialsPath = resolveClaudeCliCredentialsPath(options?.homeDir);
|
||||
const keychainCacheKey = `${credentialsPath}:keychain`;
|
||||
if (
|
||||
ttlMs > 0 &&
|
||||
platform === "darwin" &&
|
||||
options?.allowKeychainPrompt === false &&
|
||||
claudeCliCache?.cacheKey === keychainCacheKey &&
|
||||
claudeCliCache.value &&
|
||||
Date.now() - claudeCliCache.readAt < ttlMs
|
||||
) {
|
||||
return claudeCliCache.value;
|
||||
}
|
||||
const keychainIntent =
|
||||
platform === "darwin" && options?.allowKeychainPrompt !== false ? "keychain" : "file";
|
||||
return readCachedCliCredential({
|
||||
@@ -608,11 +599,13 @@ export function writeClaudeCliCredentials(
|
||||
|
||||
export function readCodexCliCredentials(options?: {
|
||||
codexHome?: string;
|
||||
allowKeychainPrompt?: boolean;
|
||||
platform?: NodeJS.Platform;
|
||||
execSync?: ExecSyncFn;
|
||||
}): CodexCliCredential | null {
|
||||
const keychain = readCodexKeychainCredentials({
|
||||
codexHome: options?.codexHome,
|
||||
allowKeychainPrompt: options?.allowKeychainPrompt,
|
||||
platform: options?.platform,
|
||||
execSync: options?.execSync,
|
||||
});
|
||||
@@ -664,18 +657,24 @@ export function readCodexCliCredentials(options?: {
|
||||
|
||||
export function readCodexCliCredentialsCached(options?: {
|
||||
codexHome?: string;
|
||||
allowKeychainPrompt?: boolean;
|
||||
ttlMs?: number;
|
||||
platform?: NodeJS.Platform;
|
||||
execSync?: ExecSyncFn;
|
||||
}): CodexCliCredential | null {
|
||||
const platform = options?.platform ?? process.platform;
|
||||
const ttlMs = options?.ttlMs ?? 0;
|
||||
const authPath = path.join(resolveCodexHomePath(options?.codexHome), CODEX_CLI_AUTH_FILENAME);
|
||||
const keychainIntent =
|
||||
platform === "darwin" && options?.allowKeychainPrompt !== false ? "keychain" : "file";
|
||||
return readCachedCliCredential({
|
||||
ttlMs: options?.ttlMs ?? 0,
|
||||
ttlMs,
|
||||
cache: codexCliCache,
|
||||
cacheKey: `${options?.platform ?? process.platform}|${authPath}`,
|
||||
cacheKey: `${platform}|${authPath}:${keychainIntent}`,
|
||||
read: () =>
|
||||
readCodexCliCredentials({
|
||||
codexHome: options?.codexHome,
|
||||
allowKeychainPrompt: options?.allowKeychainPrompt,
|
||||
platform: options?.platform,
|
||||
execSync: options?.execSync,
|
||||
}),
|
||||
|
||||
@@ -219,6 +219,8 @@ describe("models.authStatus", () => {
|
||||
expect(mocks.ensureAuthProfileStore).toHaveBeenCalledWith(
|
||||
"/tmp/agent",
|
||||
expect.objectContaining({
|
||||
allowKeychainPrompt: false,
|
||||
config: expect.any(Object),
|
||||
externalCliProviderIds: expect.arrayContaining(["opencode-go"]),
|
||||
externalCliProfileIds: ["opencode-go:default"],
|
||||
}),
|
||||
@@ -232,7 +234,15 @@ describe("models.authStatus", () => {
|
||||
it("keeps the auth store overlay unscoped when config has no provider signal", async () => {
|
||||
await handler(createOptions());
|
||||
|
||||
expect(mocks.ensureAuthProfileStore).toHaveBeenCalledWith("/tmp/agent");
|
||||
expect(mocks.ensureAuthProfileStore).toHaveBeenCalledWith(
|
||||
"/tmp/agent",
|
||||
expect.objectContaining({
|
||||
allowKeychainPrompt: false,
|
||||
config: expect.any(Object),
|
||||
externalCliProviderIds: undefined,
|
||||
externalCliProfileIds: undefined,
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it("still returns providers when usage fetch fails", async () => {
|
||||
|
||||
@@ -294,12 +294,12 @@ export const modelsAuthStatusHandlers: GatewayRequestHandlers = {
|
||||
const cfg = context.getRuntimeConfig();
|
||||
const agentDir = resolveOpenClawAgentDir();
|
||||
const externalCliAuthScope = resolveExternalCliAuthScopeFromConfig(cfg);
|
||||
const store = externalCliAuthScope
|
||||
? ensureAuthProfileStore(agentDir, {
|
||||
externalCliProviderIds: externalCliAuthScope.providerIds,
|
||||
externalCliProfileIds: externalCliAuthScope.profileIds,
|
||||
})
|
||||
: ensureAuthProfileStore(agentDir);
|
||||
const store = ensureAuthProfileStore(agentDir, {
|
||||
allowKeychainPrompt: false,
|
||||
config: cfg,
|
||||
externalCliProviderIds: externalCliAuthScope?.providerIds,
|
||||
externalCliProfileIds: externalCliAuthScope?.profileIds,
|
||||
});
|
||||
const configured = resolveConfiguredProviders(cfg);
|
||||
const authHealth: AuthHealthSummary = buildAuthHealthSummary({
|
||||
store,
|
||||
|
||||
Reference in New Issue
Block a user