mirror of
https://github.com/openclaw/openclaw.git
synced 2026-04-18 01:27:27 +00:00
fix: syncs all credential types to agent auth.json
Previously, the synchronization of credentials to the agent's file was limited to OAuth profiles. This prevented other providers and credential types from being correctly registered for agent use. This update expands the synchronization to include , (mappedto ), and credentials for all configured providers. It ensures the agent's accurately reflects available credentials, enabling proper authentication and model discovery. The synchronization now: - Converts all supported credential types. - Skips profiles with empty keys. - Preserves unrelated entries in the target . - Only writes to disk when actual changes are detected.
This commit is contained in:
committed by
Peter Steinberger
parent
12ce358da5
commit
feed570984
@@ -39,4 +39,153 @@ describe("ensurePiAuthJsonFromAuthProfiles", () => {
|
||||
const second = await ensurePiAuthJsonFromAuthProfiles(agentDir);
|
||||
expect(second.wrote).toBe(false);
|
||||
});
|
||||
|
||||
it("writes api_key credentials into auth.json", async () => {
|
||||
const agentDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-agent-"));
|
||||
|
||||
saveAuthProfileStore(
|
||||
{
|
||||
version: 1,
|
||||
profiles: {
|
||||
"openrouter:default": {
|
||||
type: "api_key",
|
||||
provider: "openrouter",
|
||||
key: "sk-or-v1-test-key",
|
||||
},
|
||||
},
|
||||
},
|
||||
agentDir,
|
||||
);
|
||||
|
||||
const result = await ensurePiAuthJsonFromAuthProfiles(agentDir);
|
||||
expect(result.wrote).toBe(true);
|
||||
|
||||
const authPath = path.join(agentDir, "auth.json");
|
||||
const auth = JSON.parse(await fs.readFile(authPath, "utf8")) as Record<string, unknown>;
|
||||
expect(auth["openrouter"]).toMatchObject({
|
||||
type: "api_key",
|
||||
key: "sk-or-v1-test-key",
|
||||
});
|
||||
});
|
||||
|
||||
it("writes token credentials as api_key into auth.json", async () => {
|
||||
const agentDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-agent-"));
|
||||
|
||||
saveAuthProfileStore(
|
||||
{
|
||||
version: 1,
|
||||
profiles: {
|
||||
"anthropic:default": {
|
||||
type: "token",
|
||||
provider: "anthropic",
|
||||
token: "sk-ant-test-token",
|
||||
},
|
||||
},
|
||||
},
|
||||
agentDir,
|
||||
);
|
||||
|
||||
const result = await ensurePiAuthJsonFromAuthProfiles(agentDir);
|
||||
expect(result.wrote).toBe(true);
|
||||
|
||||
const authPath = path.join(agentDir, "auth.json");
|
||||
const auth = JSON.parse(await fs.readFile(authPath, "utf8")) as Record<string, unknown>;
|
||||
expect(auth["anthropic"]).toMatchObject({
|
||||
type: "api_key",
|
||||
key: "sk-ant-test-token",
|
||||
});
|
||||
});
|
||||
|
||||
it("syncs multiple providers at once", async () => {
|
||||
const agentDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-agent-"));
|
||||
|
||||
saveAuthProfileStore(
|
||||
{
|
||||
version: 1,
|
||||
profiles: {
|
||||
"openrouter:default": {
|
||||
type: "api_key",
|
||||
provider: "openrouter",
|
||||
key: "sk-or-key",
|
||||
},
|
||||
"anthropic:default": {
|
||||
type: "token",
|
||||
provider: "anthropic",
|
||||
token: "sk-ant-token",
|
||||
},
|
||||
"openai-codex:default": {
|
||||
type: "oauth",
|
||||
provider: "openai-codex",
|
||||
access: "access",
|
||||
refresh: "refresh",
|
||||
expires: Date.now() + 60_000,
|
||||
},
|
||||
},
|
||||
},
|
||||
agentDir,
|
||||
);
|
||||
|
||||
const result = await ensurePiAuthJsonFromAuthProfiles(agentDir);
|
||||
expect(result.wrote).toBe(true);
|
||||
|
||||
const authPath = path.join(agentDir, "auth.json");
|
||||
const auth = JSON.parse(await fs.readFile(authPath, "utf8")) as Record<string, unknown>;
|
||||
|
||||
expect(auth["openrouter"]).toMatchObject({ type: "api_key", key: "sk-or-key" });
|
||||
expect(auth["anthropic"]).toMatchObject({ type: "api_key", key: "sk-ant-token" });
|
||||
expect(auth["openai-codex"]).toMatchObject({ type: "oauth", access: "access" });
|
||||
});
|
||||
|
||||
it("skips profiles with empty keys", async () => {
|
||||
const agentDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-agent-"));
|
||||
|
||||
saveAuthProfileStore(
|
||||
{
|
||||
version: 1,
|
||||
profiles: {
|
||||
"openrouter:default": {
|
||||
type: "api_key",
|
||||
provider: "openrouter",
|
||||
key: "",
|
||||
},
|
||||
},
|
||||
},
|
||||
agentDir,
|
||||
);
|
||||
|
||||
const result = await ensurePiAuthJsonFromAuthProfiles(agentDir);
|
||||
expect(result.wrote).toBe(false);
|
||||
});
|
||||
|
||||
it("preserves existing auth.json entries not in auth-profiles", async () => {
|
||||
const agentDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-agent-"));
|
||||
const authPath = path.join(agentDir, "auth.json");
|
||||
|
||||
// Pre-populate auth.json with an entry
|
||||
await fs.mkdir(agentDir, { recursive: true });
|
||||
await fs.writeFile(
|
||||
authPath,
|
||||
JSON.stringify({ "legacy-provider": { type: "api_key", key: "legacy-key" } }),
|
||||
);
|
||||
|
||||
saveAuthProfileStore(
|
||||
{
|
||||
version: 1,
|
||||
profiles: {
|
||||
"openrouter:default": {
|
||||
type: "api_key",
|
||||
provider: "openrouter",
|
||||
key: "new-key",
|
||||
},
|
||||
},
|
||||
},
|
||||
agentDir,
|
||||
);
|
||||
|
||||
await ensurePiAuthJsonFromAuthProfiles(agentDir);
|
||||
|
||||
const auth = JSON.parse(await fs.readFile(authPath, "utf8")) as Record<string, unknown>;
|
||||
expect(auth["legacy-provider"]).toMatchObject({ type: "api_key", key: "legacy-key" });
|
||||
expect(auth["openrouter"]).toMatchObject({ type: "api_key", key: "new-key" });
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import fs from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
import { ensureAuthProfileStore, listProfilesForProvider } from "./auth-profiles.js";
|
||||
import type { AuthProfileCredential } from "./auth-profiles/types.js";
|
||||
import { ensureAuthProfileStore } from "./auth-profiles.js";
|
||||
|
||||
type AuthJsonCredential =
|
||||
| {
|
||||
@@ -31,70 +32,119 @@ async function readAuthJson(filePath: string): Promise<AuthJsonShape> {
|
||||
}
|
||||
|
||||
/**
|
||||
* pi-coding-agent's ModelRegistry/AuthStorage expects OAuth credentials in auth.json.
|
||||
* Convert an OpenClaw auth-profiles credential to pi-coding-agent auth.json format.
|
||||
* Returns null if the credential cannot be converted.
|
||||
*/
|
||||
function convertCredential(cred: AuthProfileCredential): AuthJsonCredential | null {
|
||||
if (cred.type === "api_key") {
|
||||
const key = typeof cred.key === "string" ? cred.key.trim() : "";
|
||||
if (!key) {
|
||||
return null;
|
||||
}
|
||||
return { type: "api_key", key };
|
||||
}
|
||||
|
||||
if (cred.type === "token") {
|
||||
// pi-coding-agent treats static tokens as api_key type
|
||||
const token = typeof cred.token === "string" ? cred.token.trim() : "";
|
||||
if (!token) {
|
||||
return null;
|
||||
}
|
||||
return { type: "api_key", key: token };
|
||||
}
|
||||
|
||||
if (cred.type === "oauth") {
|
||||
const accessRaw = (cred as { access?: unknown }).access;
|
||||
const refreshRaw = (cred as { refresh?: unknown }).refresh;
|
||||
const expiresRaw = (cred as { expires?: unknown }).expires;
|
||||
|
||||
const access = typeof accessRaw === "string" ? accessRaw.trim() : "";
|
||||
const refresh = typeof refreshRaw === "string" ? refreshRaw.trim() : "";
|
||||
const expires = typeof expiresRaw === "number" ? expiresRaw : Number.NaN;
|
||||
|
||||
if (!access || !refresh || !Number.isFinite(expires) || expires <= 0) {
|
||||
return null;
|
||||
}
|
||||
return { type: "oauth", access, refresh, expires };
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if two auth.json credentials are equivalent.
|
||||
*/
|
||||
function credentialsEqual(a: AuthJsonCredential | undefined, b: AuthJsonCredential): boolean {
|
||||
if (!a || typeof a !== "object") {
|
||||
return false;
|
||||
}
|
||||
if (a.type !== b.type) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (a.type === "api_key" && b.type === "api_key") {
|
||||
return a.key === b.key;
|
||||
}
|
||||
|
||||
if (a.type === "oauth" && b.type === "oauth") {
|
||||
return a.access === b.access && a.refresh === b.refresh && a.expires === b.expires;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* pi-coding-agent's ModelRegistry/AuthStorage expects credentials in auth.json.
|
||||
*
|
||||
* OpenClaw stores OAuth credentials in auth-profiles.json instead. This helper
|
||||
* bridges a subset of credentials into agentDir/auth.json so pi-coding-agent can
|
||||
* (a) consider the provider authenticated and (b) include built-in models in its
|
||||
* OpenClaw stores credentials in auth-profiles.json instead. This helper
|
||||
* bridges all credentials into agentDir/auth.json so pi-coding-agent can
|
||||
* (a) consider providers authenticated and (b) include built-in models in its
|
||||
* registry/catalog output.
|
||||
*
|
||||
* Currently used for openai-codex.
|
||||
* Syncs all credential types: api_key, token (as api_key), and oauth.
|
||||
*/
|
||||
export async function ensurePiAuthJsonFromAuthProfiles(agentDir: string): Promise<{
|
||||
wrote: boolean;
|
||||
authPath: string;
|
||||
}> {
|
||||
const store = ensureAuthProfileStore(agentDir, { allowKeychainPrompt: false });
|
||||
const codexProfiles = listProfilesForProvider(store, "openai-codex");
|
||||
if (codexProfiles.length === 0) {
|
||||
return { wrote: false, authPath: path.join(agentDir, "auth.json") };
|
||||
}
|
||||
|
||||
const profileId = codexProfiles[0];
|
||||
const cred = profileId ? store.profiles[profileId] : undefined;
|
||||
if (!cred || cred.type !== "oauth") {
|
||||
return { wrote: false, authPath: path.join(agentDir, "auth.json") };
|
||||
}
|
||||
|
||||
const accessRaw = (cred as { access?: unknown }).access;
|
||||
const refreshRaw = (cred as { refresh?: unknown }).refresh;
|
||||
const expiresRaw = (cred as { expires?: unknown }).expires;
|
||||
|
||||
const access = typeof accessRaw === "string" ? accessRaw.trim() : "";
|
||||
const refresh = typeof refreshRaw === "string" ? refreshRaw.trim() : "";
|
||||
const expires = typeof expiresRaw === "number" ? expiresRaw : Number.NaN;
|
||||
|
||||
if (!access || !refresh || !Number.isFinite(expires) || expires <= 0) {
|
||||
return { wrote: false, authPath: path.join(agentDir, "auth.json") };
|
||||
}
|
||||
|
||||
const authPath = path.join(agentDir, "auth.json");
|
||||
const next = await readAuthJson(authPath);
|
||||
|
||||
const existing = next["openai-codex"];
|
||||
const desired: AuthJsonCredential = {
|
||||
type: "oauth",
|
||||
access,
|
||||
refresh,
|
||||
expires,
|
||||
};
|
||||
// Group profiles by provider, taking the first valid profile for each
|
||||
const providerCredentials = new Map<string, AuthJsonCredential>();
|
||||
|
||||
const isSame =
|
||||
existing &&
|
||||
typeof existing === "object" &&
|
||||
(existing as { type?: unknown }).type === "oauth" &&
|
||||
(existing as { access?: unknown }).access === access &&
|
||||
(existing as { refresh?: unknown }).refresh === refresh &&
|
||||
(existing as { expires?: unknown }).expires === expires;
|
||||
for (const [, cred] of Object.entries(store.profiles)) {
|
||||
const provider = cred.provider;
|
||||
if (!provider || providerCredentials.has(provider)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (isSame) {
|
||||
const converted = convertCredential(cred);
|
||||
if (converted) {
|
||||
providerCredentials.set(provider, converted);
|
||||
}
|
||||
}
|
||||
|
||||
if (providerCredentials.size === 0) {
|
||||
return { wrote: false, authPath };
|
||||
}
|
||||
|
||||
next["openai-codex"] = desired;
|
||||
const existing = await readAuthJson(authPath);
|
||||
let changed = false;
|
||||
|
||||
for (const [provider, cred] of providerCredentials) {
|
||||
if (!credentialsEqual(existing[provider], cred)) {
|
||||
existing[provider] = cred;
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (!changed) {
|
||||
return { wrote: false, authPath };
|
||||
}
|
||||
|
||||
await fs.mkdir(agentDir, { recursive: true, mode: 0o700 });
|
||||
await fs.writeFile(authPath, `${JSON.stringify(next, null, 2)}\n`, { mode: 0o600 });
|
||||
await fs.writeFile(authPath, `${JSON.stringify(existing, null, 2)}\n`, { mode: 0o600 });
|
||||
|
||||
return { wrote: true, authPath };
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user