Agents: inject pi auth storage from runtime profiles

This commit is contained in:
joshavant
2026-02-21 13:42:03 -08:00
committed by Peter Steinberger
parent 45ec5aaf2b
commit 4c5a2c3c6d
8 changed files with 188 additions and 33 deletions

View File

@@ -31,6 +31,7 @@ export function mockCatalogImportFailThenRecover() {
throw new Error("boom");
}
return {
discoverAuthStorage: () => ({}),
AuthStorage: class {},
ModelRegistry: class {
getAll() {

View File

@@ -38,6 +38,7 @@ describe("loadModelCatalog", () => {
__setModelCatalogImportForTest(
async () =>
({
discoverAuthStorage: () => ({}),
AuthStorage: class {},
ModelRegistry: class {
getAll() {
@@ -69,6 +70,7 @@ describe("loadModelCatalog", () => {
__setModelCatalogImportForTest(
async () =>
({
discoverAuthStorage: () => ({}),
AuthStorage: class {},
ModelRegistry: class {
getAll() {

View File

@@ -154,14 +154,6 @@ export function __setModelCatalogImportForTest(loader?: () => Promise<PiSdkModul
importPiSdk = loader ?? defaultImportPiSdk;
}
function createAuthStorage(AuthStorageLike: unknown, path: string) {
const withFactory = AuthStorageLike as { create?: (path: string) => unknown };
if (typeof withFactory.create === "function") {
return withFactory.create(path);
}
return new (AuthStorageLike as { new (path: string): unknown })(path);
}
export async function loadModelCatalog(params?: {
config?: OpenClawConfig;
useCache?: boolean;
@@ -186,9 +178,6 @@ export async function loadModelCatalog(params?: {
try {
const cfg = params?.config ?? loadConfig();
await ensureOpenClawModelsJson(cfg);
await (
await import("./pi-auth-json.js")
).ensurePiAuthJsonFromAuthProfiles(resolveOpenClawAgentDir());
// IMPORTANT: keep the dynamic import *inside* the try/catch.
// If this fails once (e.g. during a pnpm install that temporarily swaps node_modules),
// we must not poison the cache with a rejected promise (otherwise all channel handlers
@@ -196,7 +185,7 @@ export async function loadModelCatalog(params?: {
const piSdk = await importPiSdk();
const agentDir = resolveOpenClawAgentDir();
const { join } = await import("node:path");
const authStorage = createAuthStorage(piSdk.AuthStorage, join(agentDir, "auth.json"));
const authStorage = piSdk.discoverAuthStorage(agentDir);
const registry = new (piSdk.ModelRegistry as unknown as {
new (
authStorage: unknown,

View File

@@ -0,0 +1,68 @@
import fs from "node:fs/promises";
import os from "node:os";
import path from "node:path";
import { describe, expect, it } from "vitest";
import { saveAuthProfileStore } from "./auth-profiles.js";
import { discoverAuthStorage } from "./pi-model-discovery.js";
async function createAgentDir(): Promise<string> {
return await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-pi-auth-storage-"));
}
async function pathExists(pathname: string): Promise<boolean> {
try {
await fs.stat(pathname);
return true;
} catch {
return false;
}
}
describe("discoverAuthStorage", () => {
it("loads runtime credentials from auth-profiles without writing auth.json", async () => {
const agentDir = await createAgentDir();
try {
saveAuthProfileStore(
{
version: 1,
profiles: {
"openrouter:default": {
type: "api_key",
provider: "openrouter",
key: "sk-or-v1-runtime",
},
"anthropic:default": {
type: "token",
provider: "anthropic",
token: "sk-ant-runtime",
},
"openai-codex:default": {
type: "oauth",
provider: "openai-codex",
access: "oauth-access",
refresh: "oauth-refresh",
expires: Date.now() + 60_000,
},
},
},
agentDir,
);
const authStorage = discoverAuthStorage(agentDir);
expect(authStorage.hasAuth("openrouter")).toBe(true);
expect(authStorage.hasAuth("anthropic")).toBe(true);
expect(authStorage.hasAuth("openai-codex")).toBe(true);
await expect(authStorage.getApiKey("openrouter")).resolves.toBe("sk-or-v1-runtime");
await expect(authStorage.getApiKey("anthropic")).resolves.toBe("sk-ant-runtime");
expect(authStorage.get("openai-codex")).toMatchObject({
type: "oauth",
access: "oauth-access",
});
expect(await pathExists(path.join(agentDir, "auth.json"))).toBe(false);
} finally {
await fs.rm(agentDir, { recursive: true, force: true });
}
});
});

View File

@@ -1,19 +1,125 @@
import path from "node:path";
import { AuthStorage, ModelRegistry } from "@mariozechner/pi-coding-agent";
import {
AuthStorage,
InMemoryAuthStorageBackend,
ModelRegistry,
} from "@mariozechner/pi-coding-agent";
import { ensureAuthProfileStore } from "./auth-profiles.js";
import type { AuthProfileCredential } from "./auth-profiles.js";
import { normalizeProviderId } from "./model-selection.js";
export { AuthStorage, ModelRegistry } from "@mariozechner/pi-coding-agent";
function createAuthStorage(AuthStorageLike: unknown, path: string) {
const withFactory = AuthStorageLike as { create?: (path: string) => unknown };
if (typeof withFactory.create === "function") {
return withFactory.create(path) as AuthStorage;
type PiApiKeyCredential = { type: "api_key"; key: string };
type PiOAuthCredential = {
type: "oauth";
access: string;
refresh: string;
expires: number;
};
type PiCredential = PiApiKeyCredential | PiOAuthCredential;
type PiCredentialMap = Record<string, PiCredential>;
function createAuthStorage(AuthStorageLike: unknown, path: string, creds: PiCredentialMap) {
const withInMemory = AuthStorageLike as { inMemory?: (data?: unknown) => unknown };
if (typeof withInMemory.inMemory === "function") {
return withInMemory.inMemory(creds) as AuthStorage;
}
return new (AuthStorageLike as { new (path: string): unknown })(path) as AuthStorage;
const withFromStorage = AuthStorageLike as {
fromStorage?: (storage: unknown) => unknown;
};
if (typeof withFromStorage.fromStorage === "function") {
const backend = new InMemoryAuthStorageBackend();
backend.withLock(() => ({
result: undefined,
next: JSON.stringify(creds, null, 2),
}));
return withFromStorage.fromStorage(backend) as AuthStorage;
}
const withFactory = AuthStorageLike as { create?: (path: string) => unknown };
const withRuntimeOverride = (
typeof withFactory.create === "function"
? withFactory.create(path)
: new (AuthStorageLike as { new (path: string): unknown })(path)
) as AuthStorage & {
setRuntimeApiKey?: (provider: string, apiKey: string) => void;
};
if (typeof withRuntimeOverride.setRuntimeApiKey === "function") {
for (const [provider, credential] of Object.entries(creds)) {
if (credential.type === "api_key") {
withRuntimeOverride.setRuntimeApiKey(provider, credential.key);
continue;
}
withRuntimeOverride.setRuntimeApiKey(provider, credential.access);
}
}
return withRuntimeOverride;
}
function convertAuthProfileCredential(cred: AuthProfileCredential): PiCredential | 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") {
const token = typeof cred.token === "string" ? cred.token.trim() : "";
if (!token) {
return null;
}
if (
typeof cred.expires === "number" &&
Number.isFinite(cred.expires) &&
Date.now() >= cred.expires
) {
return null;
}
return { type: "api_key", key: token };
}
if (cred.type === "oauth") {
const access = typeof cred.access === "string" ? cred.access.trim() : "";
const refresh = typeof cred.refresh === "string" ? cred.refresh.trim() : "";
if (!access || !refresh || !Number.isFinite(cred.expires) || cred.expires <= 0) {
return null;
}
return {
type: "oauth",
access,
refresh,
expires: cred.expires,
};
}
return null;
}
function resolvePiCredentials(agentDir: string): PiCredentialMap {
const store = ensureAuthProfileStore(agentDir, { allowKeychainPrompt: false });
const credentials: PiCredentialMap = {};
for (const credential of Object.values(store.profiles)) {
const provider = normalizeProviderId(String(credential.provider ?? "")).trim();
if (!provider || credentials[provider]) {
continue;
}
const converted = convertAuthProfileCredential(credential);
if (converted) {
credentials[provider] = converted;
}
}
return credentials;
}
// Compatibility helpers for pi-coding-agent 0.50+ (discover* helpers removed).
export function discoverAuthStorage(agentDir: string): AuthStorage {
return createAuthStorage(AuthStorage, path.join(agentDir, "auth.json"));
const credentials = resolvePiCredentials(agentDir);
return createAuthStorage(AuthStorage, path.join(agentDir, "auth.json"), credentials);
}
export function discoverModels(authStorage: AuthStorage, agentDir: string): ModelRegistry {

View File

@@ -100,7 +100,7 @@ describe("models list auth-profile sync", () => {
const openrouter = await runModelsListAndGetProvider("openrouter/");
expect(openrouter?.available).toBe(true);
expect(await pathExists(authPath)).toBe(true);
expect(await pathExists(authPath)).toBe(false);
});
});

View File

@@ -6,9 +6,6 @@ let toModelRow: typeof import("./models/list.registry.js").toModelRow;
const loadConfig = vi.fn();
const ensureOpenClawModelsJson = vi.fn().mockResolvedValue(undefined);
const ensurePiAuthJsonFromAuthProfiles = vi
.fn()
.mockResolvedValue({ wrote: false, authPath: "/tmp/openclaw-agent/auth.json" });
const resolveOpenClawAgentDir = vi.fn().mockReturnValue("/tmp/openclaw-agent");
const ensureAuthProfileStore = vi.fn().mockReturnValue({ version: 1, profiles: {} });
const listProfilesForProvider = vi.fn().mockReturnValue([]);
@@ -38,10 +35,6 @@ vi.mock("../agents/models-config.js", () => ({
ensureOpenClawModelsJson,
}));
vi.mock("../agents/pi-auth-json.js", () => ({
ensurePiAuthJsonFromAuthProfiles,
}));
vi.mock("../agents/agent-paths.js", () => ({
resolveOpenClawAgentDir,
}));
@@ -121,7 +114,6 @@ beforeEach(() => {
modelRegistryState.getAllError = undefined;
modelRegistryState.getAvailableError = undefined;
listProfilesForProvider.mockReturnValue([]);
ensurePiAuthJsonFromAuthProfiles.mockClear();
});
afterEach(() => {
@@ -223,13 +215,12 @@ describe("models list/status", () => {
({ loadModelRegistry, toModelRow } = await import("./models/list.registry.js"));
});
it("models list syncs auth-profiles into auth.json before availability checks", async () => {
it("models list runs model discovery without auth.json sync", async () => {
setDefaultZaiRegistry();
const runtime = makeRuntime();
await modelsListCommand({ all: true, json: true }, runtime);
expect(ensurePiAuthJsonFromAuthProfiles).toHaveBeenCalledWith("/tmp/openclaw-agent");
expect(runtime.error).not.toHaveBeenCalled();
});
it("models list outputs canonical zai key for configured z.ai model", async () => {

View File

@@ -8,7 +8,6 @@ import {
resolveEnvApiKey,
} from "../../agents/model-auth.js";
import { ensureOpenClawModelsJson } from "../../agents/models-config.js";
import { ensurePiAuthJsonFromAuthProfiles } from "../../agents/pi-auth-json.js";
import type { ModelRegistry } from "../../agents/pi-model-discovery.js";
import { discoverAuthStorage, discoverModels } from "../../agents/pi-model-discovery.js";
import type { OpenClawConfig } from "../../config/config.js";
@@ -98,7 +97,6 @@ function loadAvailableModels(registry: ModelRegistry): Model<Api>[] {
export async function loadModelRegistry(cfg: OpenClawConfig) {
await ensureOpenClawModelsJson(cfg);
const agentDir = resolveOpenClawAgentDir();
await ensurePiAuthJsonFromAuthProfiles(agentDir);
const authStorage = discoverAuthStorage(agentDir);
const registry = discoverModels(authStorage, agentDir);
const models = registry.getAll();